refactor(kv): 整理存档接线并清理迁移冗余实现
This commit is contained in:
@@ -1,31 +0,0 @@
|
|||||||
#if UNITY_EDITOR
|
|
||||||
using System;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Reflection;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
public static class KVGameLoadValidation
|
|
||||||
{
|
|
||||||
// Fixed assembly/type/method and explicit case allow-list; no arbitrary reflection arguments.
|
|
||||||
public static async Task<string> RunAsync(string caseId)
|
|
||||||
{
|
|
||||||
if (caseId != "M08-M23" && caseId != "M21-M22-M24" && caseId != "M09-M10-M12" && caseId != "M14" && caseId != "M07" && caseId != "M20-M21-M22-M24-M28-M29" && caseId != "M16-M17" && caseId != "M06-M13" && caseId != "M15-M26-M27" && caseId != "M30" && caseId != "M19" && caseId != "M11" && caseId != "M25" && caseId != "M01-M02-M21-M22-M24" && caseId != "M04-M21-M22-M24" && caseId != "M05-M21-M22-M24" && caseId != "M03" && caseId != "M18" && caseId != "M31" && caseId != "B2-20" && caseId != "M01-Immediate" && caseId != "Module02-OriginalLoad" && caseId != "M03B-LoginRetry" && caseId != "M03B-LoginRetry-Cleanup" && caseId != "M03C-CustomId" && caseId != "M03D-Destroy")
|
|
||||||
{
|
|
||||||
if (caseId != "P6A-InitializationLastLogin" && caseId != "P6B-OrdinarySave" && caseId != "P6B-OrdinarySave-Cleanup" && caseId != "P8A-Leaderboard" && caseId != "P8B-Friends" && caseId != "P8C-TitleNews" && caseId != "P8C-TitleNews-Cleanup" && caseId != "P8D-Matchmaking" && caseId != "P12A-OrdinaryCadence" && caseId != "P12B-FormalReadAdapters" && caseId != "P12C-StartupSubscriptions" && caseId != "P14-FormalRealtime" && caseId != "P15-FormalMatchmaking" && caseId != "P15-FormalMatchState")
|
|
||||||
{
|
|
||||||
throw new ArgumentException("Unknown fixed validation case.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var assembly = AppDomain.CurrentDomain.GetAssemblies().Single(a => a.GetName().Name == "HotUpdate");
|
|
||||||
var type = assembly.GetType("game.PlayerDataStorage.PlayerKVEditorValidationHost", true);
|
|
||||||
var method = type.GetMethod("RunAsync", BindingFlags.Static | BindingFlags.NonPublic, null, new[] { typeof(string) }, null);
|
|
||||||
if (method == null)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Validation entry unavailable.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return await (Task<string>)method.Invoke(null, new object[] { caseId });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 4ce05f2f22c2492f8a3c51a82a208647
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
#if UNITY_EDITOR
|
|
||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Reflection;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using UnityEditor;
|
|
||||||
using UnityEngine;
|
|
||||||
|
|
||||||
// Fixed CLI helpers; no arbitrary method names or process-marker deletion.
|
|
||||||
public static class KVGameplayLoopSetup
|
|
||||||
{
|
|
||||||
static string Root => Path.GetFullPath(Path.Combine(Application.dataPath,"../../CodexOutput/upserver/KVGameplayLoopTests"));
|
|
||||||
static Type Find(string name) => AppDomain.CurrentDomain.GetAssemblies().Select(a=>a.GetType("game.PlayerDataStorage."+name)).FirstOrDefault(t=>t!=null)
|
|
||||||
?? throw new InvalidOperationException("Gameplay assembly not loaded.");
|
|
||||||
static object ReadProperty(Type type,string name) => type.GetProperty(name,BindingFlags.Static|BindingFlags.NonPublic)?.GetValue(null);
|
|
||||||
|
|
||||||
public static async System.Threading.Tasks.Task<string> RunValidationAsync(string caseId)
|
|
||||||
{
|
|
||||||
string methodName;
|
|
||||||
switch (caseId)
|
|
||||||
{
|
|
||||||
case "equipment-chain":
|
|
||||||
methodName = "VerifyEquipmentForEditorAsync";
|
|
||||||
break;
|
|
||||||
case "fishing-consume":
|
|
||||||
methodName = "VerifyFishingForEditorAsync";
|
|
||||||
break;
|
|
||||||
case "fishing-reward":
|
|
||||||
methodName = "VerifyFishingRewardForEditorAsync";
|
|
||||||
break;
|
|
||||||
case "fishing-original-reward":
|
|
||||||
methodName = "VerifyOriginalFishingRewardForEditorAsync";
|
|
||||||
break;
|
|
||||||
case "fishing-original-consume":
|
|
||||||
methodName = "VerifyOriginalFishingForEditorAsync";
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new ArgumentException("Unknown fixed validation case.");
|
|
||||||
}
|
|
||||||
if(EditorApplication.isPlayingOrWillChangePlaymode || EditorApplication.isCompiling || EditorApplication.isUpdating)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Idle editor required.");
|
|
||||||
}
|
|
||||||
var method = Find("PlayerKVGameplayLoop").GetMethod(methodName, BindingFlags.Static | BindingFlags.NonPublic);
|
|
||||||
if (method == null)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Fixed equipment validation entry unavailable.");
|
|
||||||
}
|
|
||||||
return await (System.Threading.Tasks.Task<string>)method.Invoke(null,null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string State()
|
|
||||||
{
|
|
||||||
var type=Find("PlayerKVGameplayLoop");
|
|
||||||
var history=Path.Combine(Root,"run-control");
|
|
||||||
return JsonConvert.SerializeObject(new{project=Application.dataPath,playing=EditorApplication.isPlaying,
|
|
||||||
enabled=ReadProperty(type,"Enabled"),ready=ReadProperty(type,"Ready"),error=ReadProperty(type,"Error"),run=ReadProperty(type,"RunPath"),
|
|
||||||
permitExists=File.Exists(Path.Combine(Root,"permit.json")),
|
|
||||||
processMarkers=Directory.Exists(history)?Directory.GetFiles(history,"*.claimed").Select(Path.GetFileName).OrderBy(x=>x).ToArray():Array.Empty<string>()});
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string InspectPFBackup(string path)
|
|
||||||
{
|
|
||||||
if(EditorApplication.isPlayingOrWillChangePlaymode || EditorApplication.isCompiling || EditorApplication.isUpdating)
|
|
||||||
throw new InvalidOperationException("Idle editor required.");
|
|
||||||
var method=Find("PlayerKVGameplayLoop").GetMethod("InspectPFBackupForEditor",BindingFlags.Static|BindingFlags.NonPublic);
|
|
||||||
try { return (string)method.Invoke(null,new object[]{path}); }
|
|
||||||
catch(TargetInvocationException e) { throw e.InnerException ?? e; }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: ff2728a480374b609aaed9ea0742215e
|
|
||||||
@@ -1,632 +0,0 @@
|
|||||||
#if UNITY_EDITOR
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Net;
|
|
||||||
using System.Net.Http;
|
|
||||||
using System.Reflection;
|
|
||||||
using System.Runtime.ExceptionServices;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using game;
|
|
||||||
using game.Account;
|
|
||||||
using game.PlayerDataStorage;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using UnityEditor;
|
|
||||||
using UnityEngine;
|
|
||||||
|
|
||||||
// Fixed B01 synthetic cases only. No global container replacement, permission UI or Play mode.
|
|
||||||
public static class KVPromptStateValidation
|
|
||||||
{
|
|
||||||
private static bool running;
|
|
||||||
private static object failedCleanup;
|
|
||||||
private static readonly BindingFlags PrivateInstance = BindingFlags.Instance | BindingFlags.NonPublic;
|
|
||||||
|
|
||||||
public static async Task<string> RunAsync(string caseId)
|
|
||||||
{
|
|
||||||
if ((caseId != "B01" && caseId != "CP3A" && caseId != "P6D") || EditorApplication.isPlayingOrWillChangePlaymode || running || failedCleanup != null)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("B01 requires an idle Editor and completed prior cleanup.");
|
|
||||||
}
|
|
||||||
running = true;
|
|
||||||
var run = new FixedRun();
|
|
||||||
var staticField = typeof(RedPointManager).GetField("instance", BindingFlags.Static | BindingFlags.NonPublic);
|
|
||||||
object previous = null;
|
|
||||||
var isolated = false;
|
|
||||||
var errors = new List<Exception>();
|
|
||||||
string result = null;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
previous = staticField.GetValue(null);
|
|
||||||
staticField.SetValue(null, new RedPointManager());
|
|
||||||
isolated = true;
|
|
||||||
result = caseId == "P6D" ? await run.ExecuteP6D() : caseId == "B01" ? await run.Execute() : await run.ExecuteCP3A();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
errors.Add(ex);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await run.Close();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
failedCleanup = run;
|
|
||||||
errors.Add(ex);
|
|
||||||
}
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (isolated)
|
|
||||||
{
|
|
||||||
staticField.SetValue(null, previous);
|
|
||||||
if (!ReferenceEquals(previous, staticField.GetValue(null)))
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Red point instance was not restored.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
failedCleanup = run;
|
|
||||||
errors.Add(ex);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
running = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (errors.Count != 0)
|
|
||||||
{
|
|
||||||
throw new AggregateException(errors);
|
|
||||||
}
|
|
||||||
var report = Newtonsoft.Json.Linq.JObject.Parse(result);
|
|
||||||
report["restored"] = true;
|
|
||||||
report["remainingTestObjects"] = 0;
|
|
||||||
return report.ToString(Formatting.None);
|
|
||||||
}
|
|
||||||
|
|
||||||
sealed class Handler : HttpMessageHandler
|
|
||||||
{
|
|
||||||
readonly Func<HttpRequestMessage, HttpResponseMessage> send;
|
|
||||||
internal Handler(Func<HttpRequestMessage, HttpResponseMessage> send) { this.send = send; }
|
|
||||||
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage r, CancellationToken t) => Task.FromResult(send(r));
|
|
||||||
}
|
|
||||||
static HttpResponseMessage Json(object value) => new HttpResponseMessage(HttpStatusCode.OK)
|
|
||||||
{ Content = new StringContent(JsonConvert.SerializeObject(value)) };
|
|
||||||
|
|
||||||
sealed class FixedRun
|
|
||||||
{
|
|
||||||
private readonly List<FixedRun> children = new List<FixedRun>();
|
|
||||||
private readonly List<Action> restoreFaults = new List<Action>();
|
|
||||||
readonly Uri api = new Uri("http://b01-editor.invalid/");
|
|
||||||
readonly string cache = Path.GetFullPath(Path.Combine(Application.dataPath,
|
|
||||||
"../../CodexOutput/upserver/B3BatchMigration/B01Tests/unity-runs", Guid.NewGuid().ToString("N")));
|
|
||||||
readonly List<S101AccountSession> sessions = new List<S101AccountSession>();
|
|
||||||
readonly List<GameObject> objects = new List<GameObject>();
|
|
||||||
PlayerKVGameSessionBridge bridge;
|
|
||||||
long epoch = 1;
|
|
||||||
int assertions, requests;
|
|
||||||
void Check(bool value, string label) { assertions++; if (!value) throw new InvalidOperationException("B01: " + label); }
|
|
||||||
static object Call(object target, string method, params object[] args)
|
|
||||||
{
|
|
||||||
try { return target.GetType().GetMethod(method, PrivateInstance | BindingFlags.Public).Invoke(target, args); }
|
|
||||||
catch (TargetInvocationException ex) { ExceptionDispatchInfo.Capture(ex.InnerException).Throw(); throw; }
|
|
||||||
}
|
|
||||||
static void Field(object target, string name, object value) => target.GetType().GetField(name, PrivateInstance).SetValue(target, value);
|
|
||||||
static T Bound<T>(PlayerKVGameSessionHandle handle) => (T)typeof(T).GetConstructor(PrivateInstance,
|
|
||||||
null, new[] { typeof(PlayerKVGameSessionHandle) }, null).Invoke(new object[] { handle });
|
|
||||||
void Reject(Action action)
|
|
||||||
{
|
|
||||||
try { action(); }
|
|
||||||
catch (InvalidOperationException) { assertions++; return; }
|
|
||||||
throw new InvalidOperationException("B01: expected owner rejection");
|
|
||||||
}
|
|
||||||
async Task<PlayerKVGameSessionHandle> Open()
|
|
||||||
{
|
|
||||||
var session = new S101AccountSession(api, jar => new Handler(r =>
|
|
||||||
{
|
|
||||||
Check(r.RequestUri == new Uri(api, "account/Login"), "synthetic login only");
|
|
||||||
jar.Add(api, new Cookie("sid", "101"));
|
|
||||||
return Json(new { state = 0, internalId = 101, newCreate = false,
|
|
||||||
createTime = "2026-01-01T00:00:00Z", svrTime = "2026-01-01T00:00:00Z" });
|
|
||||||
}));
|
|
||||||
sessions.Add(session);
|
|
||||||
Check((await session.LoginAsync("101")).Success, "synthetic login");
|
|
||||||
var opened = await bridge.OpenAsync(session, cache, "101", api, epoch, e => e == epoch);
|
|
||||||
Check(opened.Success, "synthetic owner open");
|
|
||||||
return opened.Handle;
|
|
||||||
}
|
|
||||||
T Panel<T>() where T : Component
|
|
||||||
{
|
|
||||||
var go = new GameObject("B01-" + typeof(T).Name) { hideFlags = HideFlags.HideAndDontSave };
|
|
||||||
go.SetActive(false); objects.Add(go);
|
|
||||||
return go.AddComponent<T>();
|
|
||||||
}
|
|
||||||
internal async Task<string> Execute()
|
|
||||||
{
|
|
||||||
Func<CookieContainer, HttpMessageHandler> factory = jar => new Handler(r =>
|
|
||||||
{
|
|
||||||
Check(r.RequestUri == new Uri(api, "playerdata/Get"), "no write/no real transport");
|
|
||||||
requests++;
|
|
||||||
return Json(new { data = new Dictionary<string, string>() });
|
|
||||||
});
|
|
||||||
bridge = (PlayerKVGameSessionBridge)typeof(PlayerKVGameSessionBridge).GetConstructor(PrivateInstance,
|
|
||||||
null, new[] { typeof(Func<CookieContainer, HttpMessageHandler>), typeof(bool) }, null).Invoke(new object[] { factory, false });
|
|
||||||
var handle = await Open();
|
|
||||||
var settings = Bound<DefaultSettingService>(handle);
|
|
||||||
var club = Bound<ClubService>(handle);
|
|
||||||
Check(!settings.ShowNotice, "valid missing key");
|
|
||||||
settings.ShowNotice = true;
|
|
||||||
Check(settings.ShowNotice && requests == 1, "accepted local only");
|
|
||||||
var original = settings.GetType().GetProperty("LastPromptEdit", PrivateInstance).GetValue(settings);
|
|
||||||
settings.ShowNotice = true;
|
|
||||||
Check(ReferenceEquals(original, settings.GetType().GetProperty("LastPromptEdit", PrivateInstance).GetValue(settings)), "same value no duplicate");
|
|
||||||
// A fresh real ClubService retains its original initialized flag.
|
|
||||||
Check(club.IsOpenClubRed, "fresh real club retains original flag");
|
|
||||||
await bridge.CloseAsync();
|
|
||||||
Reject(() => settings.ShowNotice = false);
|
|
||||||
Reject(club.SetOpenClubRed);
|
|
||||||
Check(club.IsOpenClubRed, "stale real club preserves flag");
|
|
||||||
|
|
||||||
// Notification constructor validates the current owner. Create a fresh fixed instance, then revoke it.
|
|
||||||
epoch++;
|
|
||||||
var nextHandle = await Open();
|
|
||||||
var nextSettings = Bound<DefaultSettingService>(nextHandle);
|
|
||||||
var tables = new cfg.Tables(name => SimpleJSON.JSON.Parse(File.ReadAllText(
|
|
||||||
Path.Combine(Application.dataPath, "../../cfgData", name + ".json"))));
|
|
||||||
var notifications = (LocalNotificationService)typeof(LocalNotificationService).GetConstructor(PrivateInstance,
|
|
||||||
null, new[] { typeof(DefaultSettingService), typeof(cfg.Tables) }, null).Invoke(new object[] { nextSettings, tables });
|
|
||||||
var pendingInit = notifications.Init();
|
|
||||||
await bridge.CloseAsync();
|
|
||||||
try { await pendingInit; throw new InvalidOperationException("B01: stale init succeeded"); }
|
|
||||||
catch (InvalidOperationException ex) { Check(!ex.Message.Contains("stale init succeeded"), "stale Editor permission continuation rejected"); }
|
|
||||||
|
|
||||||
var grade = Panel<SettingNotificationsGradePopupPanel>();
|
|
||||||
Field(grade, "settings", nextSettings); Field(grade, "notifications", notifications);
|
|
||||||
var popup = Panel<SettingNotificationsPopupPanel>(); Field(popup, "notifications", notifications);
|
|
||||||
var settingPanel = Panel<SettingPopupPanel>(); Field(settingPanel, "settingService", nextSettings); Field(settingPanel, "notificationService", notifications);
|
|
||||||
var gift = Panel<GiftNoticePanel>(); Field(gift, "notificationService", notifications);
|
|
||||||
var social = Panel<FishingSocialPanel>(); Field(social, "_socialData", club);
|
|
||||||
var errors = new List<string>();
|
|
||||||
void Capture(string message, string stack, LogType type) { if (type == LogType.Error || type == LogType.Exception) errors.Add(message); }
|
|
||||||
Application.logMessageReceived += Capture;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Reject(() => Call(grade, "DestroyUI"));
|
|
||||||
Reject(() => Call(grade, "OnBtnOk"));
|
|
||||||
Reject(() => Call(popup, "OnBtnOk"));
|
|
||||||
Reject(() => Call(popup, "DestroyUI"));
|
|
||||||
Reject(() => Call(settingPanel, "OnToggleNotification", true));
|
|
||||||
Reject(() => Call(settingPanel, "OnToggleNotification", false));
|
|
||||||
Reject(() => Call(gift, "OnClickYES"));
|
|
||||||
Call(social, "Start");
|
|
||||||
Check(errors.Count == 1, "only unchanged social entry logs its owner rejection");
|
|
||||||
Check(errors.TrueForAll(e => e.Contains("ShowNotice owner=") || e.Contains("IsOpenClubRed owner=")), "no unexpected exception");
|
|
||||||
foreach (var go in objects) Check(go != null, "stale callback did not destroy another UI");
|
|
||||||
}
|
|
||||||
finally { Application.logMessageReceived -= Capture; }
|
|
||||||
Check(requests == 2, "read-only synthetic transport");
|
|
||||||
return JsonConvert.SerializeObject(new { success = true, assertions, requests, formalRequests = 0,
|
|
||||||
playing = EditorApplication.isPlaying, globalContainerChanged = false, caseId = "B01", cache });
|
|
||||||
}
|
|
||||||
internal async Task<string> ExecuteP6D()
|
|
||||||
{
|
|
||||||
foreach (var value in new[] { null, "0", "1", "" })
|
|
||||||
{
|
|
||||||
var fixture = new FixedRun();
|
|
||||||
children.Add(fixture);
|
|
||||||
await fixture.HealthyPrompt(value);
|
|
||||||
}
|
|
||||||
foreach (var club in new[] { false, true })
|
|
||||||
{
|
|
||||||
foreach (var stage in new[] { "BeforeWrite", "AfterReplace" })
|
|
||||||
{
|
|
||||||
var fixture = new FixedRun();
|
|
||||||
children.Add(fixture);
|
|
||||||
await fixture.FaultPrompt(club, stage);
|
|
||||||
}
|
|
||||||
var revoked = new FixedRun();
|
|
||||||
children.Add(revoked);
|
|
||||||
await revoked.RevokedDuringPrompt(club);
|
|
||||||
}
|
|
||||||
var callback = new FixedRun();
|
|
||||||
children.Add(callback);
|
|
||||||
await callback.RevokedDuringClubCallback();
|
|
||||||
foreach (var fixture in children)
|
|
||||||
{
|
|
||||||
assertions += fixture.assertions;
|
|
||||||
requests += fixture.requests;
|
|
||||||
}
|
|
||||||
return JsonConvert.SerializeObject(new { success = true, caseId = "P6D", assertions,
|
|
||||||
cases = children.Count, requests, formalRequests = 0, playing = EditorApplication.isPlaying,
|
|
||||||
globalContainerChanged = false });
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<PlayerKVGameSessionHandle> OpenPrompt()
|
|
||||||
{
|
|
||||||
Func<CookieContainer, HttpMessageHandler> factory = jar => new Handler(r =>
|
|
||||||
{
|
|
||||||
Check(r.RequestUri == new Uri(api, "playerdata/Get"), "P6D synthetic Get only");
|
|
||||||
requests++;
|
|
||||||
return Json(new { data = new Dictionary<string, string>() });
|
|
||||||
});
|
|
||||||
bridge = (PlayerKVGameSessionBridge)typeof(PlayerKVGameSessionBridge).GetConstructor(PrivateInstance,
|
|
||||||
null, new[] { typeof(Func<CookieContainer, HttpMessageHandler>), typeof(bool) }, null).Invoke(new object[] { factory, false });
|
|
||||||
return await Open();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static PlayerKVEditResult Edit(object service)
|
|
||||||
{
|
|
||||||
return (PlayerKVEditResult)service.GetType().GetProperty("LastPromptEdit", PrivateInstance).GetValue(service);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool? Cached(DefaultSettingService settings)
|
|
||||||
{
|
|
||||||
return (bool?)typeof(DefaultSettingService).GetField("_showNotice", PrivateInstance).GetValue(settings);
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class RedProbe : IUIRedPoint
|
|
||||||
{
|
|
||||||
private readonly Action<bool> callback;
|
|
||||||
internal RedProbe(Action<bool> callback)
|
|
||||||
{
|
|
||||||
this.callback = callback;
|
|
||||||
}
|
|
||||||
public void SetRedPointState(bool state)
|
|
||||||
{
|
|
||||||
callback(state);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ObserveRed(Action<bool> callback)
|
|
||||||
{
|
|
||||||
RedPointManager.Instance.RemoveRedPoint(RedPointName.Home_Club);
|
|
||||||
RedPointManager.Instance.AddRedPoint(RedPointName.Home_Club, new RedProbe(callback));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Delegate FaultDelegate<T>(Action<string> callback)
|
|
||||||
{
|
|
||||||
return new Action<T>(stage => callback(stage.ToString()));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void InjectFault(PlayerKVGameSessionHandle handle, Action<string> callback)
|
|
||||||
{
|
|
||||||
var owner = handle.GetType().GetProperty("Owner", PrivateInstance).GetValue(handle);
|
|
||||||
var state = owner.GetType().GetField("state", PrivateInstance).GetValue(owner);
|
|
||||||
var store = state.GetType().GetField("store", PrivateInstance).GetValue(state);
|
|
||||||
var field = store.GetType().GetField("fault", PrivateInstance);
|
|
||||||
var previous = field.GetValue(store);
|
|
||||||
var method = typeof(FixedRun).GetMethod("FaultDelegate", BindingFlags.Static | BindingFlags.NonPublic)
|
|
||||||
.MakeGenericMethod(field.FieldType.GetGenericArguments()[0]);
|
|
||||||
var fault = method.Invoke(null, new object[] { callback });
|
|
||||||
restoreFaults.Add(() => field.SetValue(store, previous));
|
|
||||||
field.SetValue(store, fault);
|
|
||||||
Check(ReferenceEquals(field.GetValue(store), fault), "actual cache fault installed");
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task HealthyPrompt(string initial)
|
|
||||||
{
|
|
||||||
var handle = await OpenPrompt();
|
|
||||||
if (initial != null)
|
|
||||||
{
|
|
||||||
Check(handle.Update("ShowNotice", initial).Accepted, "seed initial value");
|
|
||||||
}
|
|
||||||
var settings = Bound<DefaultSettingService>(handle);
|
|
||||||
var expected = initial == "1";
|
|
||||||
Check(settings.ShowNotice == expected, "first read follows original string comparison");
|
|
||||||
Check(handle.Update("ShowNotice", expected ? "0" : "1").Accepted, "other consumer updates actual cache");
|
|
||||||
Check(settings.ShowNotice == expected, "first-read nullable cache survives other consumer writes");
|
|
||||||
var fresh = Bound<DefaultSettingService>(handle);
|
|
||||||
var target = !expected;
|
|
||||||
var observedWrites = 0;
|
|
||||||
InjectFault(handle, stage =>
|
|
||||||
{
|
|
||||||
if (stage == "BeforeWrite")
|
|
||||||
{
|
|
||||||
observedWrites++;
|
|
||||||
Check(Cached(fresh) == target, "nullable cache assigned before actual disk write");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
fresh.ShowNotice = target;
|
|
||||||
Check(Edit(fresh)?.Accepted == true && observedWrites == 1, "setter before getter writes even when stored value matches");
|
|
||||||
var receipt = Edit(fresh);
|
|
||||||
fresh.ShowNotice = target;
|
|
||||||
Check(ReferenceEquals(receipt, Edit(fresh)) && observedWrites == 1, "same cached value does not write");
|
|
||||||
target = !target;
|
|
||||||
fresh.ShowNotice = target;
|
|
||||||
Check(observedWrites == 2 && !ReferenceEquals(receipt, Edit(fresh)), "changed cached value writes");
|
|
||||||
RestoreFaults();
|
|
||||||
|
|
||||||
Check(handle.Update("IsOpenClubRed", "0").Accepted, "seed present club value");
|
|
||||||
var club = Bound<ClubService>(handle);
|
|
||||||
var calls = 0;
|
|
||||||
ObserveRed(red =>
|
|
||||||
{
|
|
||||||
calls++;
|
|
||||||
Check(!red && !club.IsOpenClubRed, "flag clears before red callback");
|
|
||||||
Check(handle.ReadLocalSnapshot().Data["IsOpenClubRed"] == (calls == 1 ? "0" : "1"), "red callback precedes write");
|
|
||||||
});
|
|
||||||
club.SetOpenClubRed();
|
|
||||||
Check(calls == 1 && Edit(club)?.Accepted == true && handle.ReadLocalSnapshot().Data["IsOpenClubRed"] == "1", "present key does not replace original flag gate");
|
|
||||||
var clubReceipt = Edit(club);
|
|
||||||
club.SetOpenClubRed();
|
|
||||||
Check(calls == 1 && ReferenceEquals(clubReceipt, Edit(club)), "false flag skips red and write");
|
|
||||||
club.IsOpenClubRed = true;
|
|
||||||
club.SetOpenClubRed();
|
|
||||||
Check(calls == 2 && !ReferenceEquals(clubReceipt, Edit(club)), "true flag writes existing value");
|
|
||||||
|
|
||||||
// A diagnostic result is not a second owner state. Do not unlock a real failed owner.
|
|
||||||
var rejected = handle.Update(null, "1");
|
|
||||||
Check(!rejected.Accepted, "actual rejected diagnostic");
|
|
||||||
fresh.GetType().GetProperty("LastPromptEdit", PrivateInstance).SetValue(fresh, rejected);
|
|
||||||
club.GetType().GetProperty("LastPromptEdit", PrivateInstance).SetValue(club, rejected);
|
|
||||||
Check(fresh.ShowNotice == target, "healthy owner ignores old rejected diagnostic");
|
|
||||||
club.SetOpenClubRed();
|
|
||||||
Check(calls == 2, "healthy false club ignores diagnostic without business effects");
|
|
||||||
await bridge.CloseAsync();
|
|
||||||
Reject(() =>
|
|
||||||
{
|
|
||||||
_ = fresh.ShowNotice;
|
|
||||||
});
|
|
||||||
Reject(() => fresh.ShowNotice = target);
|
|
||||||
Reject(club.SetOpenClubRed);
|
|
||||||
Check(calls == 2 && Cached(fresh) == target && !club.IsOpenClubRed, "stale cache-hit entries cause no effects");
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task FaultPrompt(bool useClub, string failureStage)
|
|
||||||
{
|
|
||||||
var handle = await OpenPrompt();
|
|
||||||
var settings = Bound<DefaultSettingService>(handle);
|
|
||||||
var club = Bound<ClubService>(handle);
|
|
||||||
var redCalls = 0;
|
|
||||||
ObserveRed(red =>
|
|
||||||
{
|
|
||||||
redCalls++;
|
|
||||||
Check(!red && !club.IsOpenClubRed, "failed write still follows original red order");
|
|
||||||
});
|
|
||||||
var faults = 0;
|
|
||||||
InjectFault(handle, stage =>
|
|
||||||
{
|
|
||||||
if (stage == failureStage)
|
|
||||||
{
|
|
||||||
faults++;
|
|
||||||
Check(useClub ? !club.IsOpenClubRed && redCalls == 1 : Cached(settings) == true,
|
|
||||||
"business state precedes actual storage failure");
|
|
||||||
throw new IOException("P6D synthetic " + failureStage);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
Reject(() =>
|
|
||||||
{
|
|
||||||
if (useClub)
|
|
||||||
{
|
|
||||||
club.SetOpenClubRed();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
settings.ShowNotice = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
var edit = Edit(useClub ? (object)club : settings);
|
|
||||||
Check(faults == 1 && edit != null && !edit.Accepted, "actual rejected write preserved");
|
|
||||||
Check(edit.StateResult.Error == PlayerKVChangeError.StorageError &&
|
|
||||||
edit.StateResult.LocalCommitMayHaveSucceeded == (failureStage == "AfterReplace"), "actual uncertainty preserved");
|
|
||||||
Check(handle.Status == PlayerKVSyncStatus.StorageError, "actual storage terminal state retained");
|
|
||||||
Reject(() =>
|
|
||||||
{
|
|
||||||
_ = settings.ShowNotice;
|
|
||||||
});
|
|
||||||
Reject(() => settings.ShowNotice = true);
|
|
||||||
Reject(club.SetOpenClubRed);
|
|
||||||
Check(faults == 1 && redCalls == (useClub ? 1 : 0), "actual unreadable owner blocks repeat effects");
|
|
||||||
Check(useClub ? !club.IsOpenClubRed : Cached(settings) == true, "no added rollback");
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task RevokedDuringPrompt(bool useClub)
|
|
||||||
{
|
|
||||||
var handle = await OpenPrompt();
|
|
||||||
var settings = Bound<DefaultSettingService>(handle);
|
|
||||||
var club = Bound<ClubService>(handle);
|
|
||||||
var redCalls = 0;
|
|
||||||
ObserveRed(red =>
|
|
||||||
{
|
|
||||||
redCalls++;
|
|
||||||
});
|
|
||||||
InjectFault(handle, stage =>
|
|
||||||
{
|
|
||||||
if (stage == "AfterReplace")
|
|
||||||
{
|
|
||||||
epoch++;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
Reject(() =>
|
|
||||||
{
|
|
||||||
if (useClub)
|
|
||||||
{
|
|
||||||
club.SetOpenClubRed();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
settings.ShowNotice = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
Check(Edit(useClub ? (object)club : settings)?.Accepted == true, "completed disk write not disguised as unaccepted");
|
|
||||||
Check(useClub ? !club.IsOpenClubRed && redCalls == 1 : Cached(settings) == true, "revocation does not roll back original front effects");
|
|
||||||
Check(!handle.ReadLocalSnapshot().Success && requests == 1, "revoked old owner has no replay or transport write");
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task RevokedDuringClubCallback()
|
|
||||||
{
|
|
||||||
var handle = await OpenPrompt();
|
|
||||||
var club = Bound<ClubService>(handle);
|
|
||||||
var redCalls = 0;
|
|
||||||
ObserveRed(red =>
|
|
||||||
{
|
|
||||||
redCalls++;
|
|
||||||
epoch++;
|
|
||||||
});
|
|
||||||
var writes = 0;
|
|
||||||
InjectFault(handle, stage =>
|
|
||||||
{
|
|
||||||
writes++;
|
|
||||||
});
|
|
||||||
Reject(club.SetOpenClubRed);
|
|
||||||
Check(!club.IsOpenClubRed && redCalls == 1, "callback revocation preserves front effects");
|
|
||||||
Check(Edit(club)?.Accepted == false && writes == 0 && requests == 1, "callback revocation refuses old write without replay");
|
|
||||||
}
|
|
||||||
internal async Task<string> ExecuteCP3A()
|
|
||||||
{
|
|
||||||
Func<CookieContainer, HttpMessageHandler> factory = jar => new Handler(r =>
|
|
||||||
{
|
|
||||||
Check(r.RequestUri == new Uri(api, "playerdata/Get"), "CP3A synthetic Get only");
|
|
||||||
requests++;
|
|
||||||
return Json(new { data = new Dictionary<string, string>() });
|
|
||||||
});
|
|
||||||
bridge = (PlayerKVGameSessionBridge)typeof(PlayerKVGameSessionBridge).GetConstructor(PrivateInstance,
|
|
||||||
null, new[] { typeof(Func<CookieContainer, HttpMessageHandler>), typeof(bool) }, null).Invoke(new object[] { factory, false });
|
|
||||||
var tables = new cfg.Tables(name => SimpleJSON.JSON.Parse(File.ReadAllText(
|
|
||||||
Path.Combine(Application.dataPath, "../../cfgData", name + ".json"))));
|
|
||||||
var settingCtor = typeof(DefaultSettingService).GetConstructor(PrivateInstance,
|
|
||||||
null, new[] { typeof(bool) }, null);
|
|
||||||
var clubCtor = typeof(ClubService).GetConstructor(PrivateInstance,
|
|
||||||
null, new[] { typeof(bool) }, null);
|
|
||||||
var notificationCtor = typeof(LocalNotificationService).GetConstructor(PrivateInstance,
|
|
||||||
null, new[] { typeof(cfg.Tables) }, null);
|
|
||||||
Check(settingCtor != null && clubCtor != null && notificationCtor != null,
|
|
||||||
"formal service constructors exist");
|
|
||||||
var settings = (DefaultSettingService)settingCtor.Invoke(new object[] { true });
|
|
||||||
var club = (ClubService)clubCtor.Invoke(new object[] { true });
|
|
||||||
var notifications = (LocalNotificationService)notificationCtor.Invoke(new object[] { tables });
|
|
||||||
var pendingNotifications = (LocalNotificationService)notificationCtor.Invoke(new object[] { tables });
|
|
||||||
var pendingInit = pendingNotifications.Init();
|
|
||||||
Reject(() => Call(pendingNotifications, "PreflightFormalPromptSettings", settings));
|
|
||||||
await pendingInit;
|
|
||||||
await notifications.Init();
|
|
||||||
Reject(() => { _ = settings.ShowNotice; });
|
|
||||||
Reject(() => settings.ShowNotice = true);
|
|
||||||
Reject(club.SetOpenClubRed);
|
|
||||||
var handle = await Open();
|
|
||||||
Call(settings, "BindFormalPromptOwner", handle);
|
|
||||||
Call(club, "BindFormalPromptOwner", handle);
|
|
||||||
Call(notifications, "BindFormalPromptSettings", settings);
|
|
||||||
Call(settings, "BindFormalPromptOwner", handle);
|
|
||||||
Call(club, "BindFormalPromptOwner", handle);
|
|
||||||
Call(notifications, "BindFormalPromptSettings", settings);
|
|
||||||
Check(!settings.ShowNotice, "bound missing ShowNotice");
|
|
||||||
settings.ShowNotice = true;
|
|
||||||
club.SetOpenClubRed();
|
|
||||||
Check(settings.ShowNotice && !club.IsOpenClubRed, "bound prompt effects");
|
|
||||||
Check(requests == 1, "one synthetic KV Get and no transport write");
|
|
||||||
await bridge.CloseAsync();
|
|
||||||
Reject(() => { _ = settings.ShowNotice; });
|
|
||||||
Reject(club.SetOpenClubRed);
|
|
||||||
epoch++;
|
|
||||||
var nextHandle = await Open();
|
|
||||||
Reject(() => Call(settings, "BindFormalPromptOwner", nextHandle));
|
|
||||||
Reject(() => Call(club, "BindFormalPromptOwner", nextHandle));
|
|
||||||
var partialSetting = (DefaultSettingService)settingCtor.Invoke(new object[] { true });
|
|
||||||
var partialClub = (ClubService)clubCtor.Invoke(new object[] { true });
|
|
||||||
Call(partialSetting, "BindFormalPromptOwner", nextHandle);
|
|
||||||
await bridge.CloseAsync();
|
|
||||||
Reject(() => Call(partialClub, "BindFormalPromptOwner", nextHandle));
|
|
||||||
Reject(() => { _ = partialSetting.ShowNotice; });
|
|
||||||
epoch++;
|
|
||||||
var thirdHandle = await Open();
|
|
||||||
Reject(() => Call(partialSetting, "BindFormalPromptOwner", thirdHandle));
|
|
||||||
Check(requests == 3, "same owner binds and partial failure add no transport requests");
|
|
||||||
return JsonConvert.SerializeObject(new { success = true, caseId = "CP3A", assertions,
|
|
||||||
requests, formalRequests = 0, globalContainerChanged = false });
|
|
||||||
}
|
|
||||||
internal async Task Close()
|
|
||||||
{
|
|
||||||
var errors = new List<Exception>();
|
|
||||||
foreach (var child in children)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await child.Close();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
errors.Add(ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
try
|
|
||||||
{
|
|
||||||
RestoreFaults();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
errors.Add(ex);
|
|
||||||
}
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (bridge != null)
|
|
||||||
{
|
|
||||||
await bridge.CloseAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
errors.Add(ex);
|
|
||||||
}
|
|
||||||
foreach (var session in sessions)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
session.Dispose();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
errors.Add(ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
foreach (var go in objects)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (go != null)
|
|
||||||
{
|
|
||||||
UnityEngine.Object.DestroyImmediate(go);
|
|
||||||
}
|
|
||||||
if (go != null)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Test object survived cleanup.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
errors.Add(ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (errors.Count != 0)
|
|
||||||
{
|
|
||||||
throw new AggregateException(errors);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RestoreFaults()
|
|
||||||
{
|
|
||||||
var errors = new List<Exception>();
|
|
||||||
foreach (var restore in restoreFaults)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
restore();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
errors.Add(ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (errors.Count != 0)
|
|
||||||
{
|
|
||||||
throw new AggregateException(errors);
|
|
||||||
}
|
|
||||||
restoreFaults.Clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 67d5bde18c99423aa2168a5af1c2b0da
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Threading;
|
|
||||||
using game.Account;
|
|
||||||
using UnityEditor;
|
|
||||||
using UnityEngine;
|
|
||||||
|
|
||||||
public sealed class S101LoginWindow : EditorWindow
|
|
||||||
{
|
|
||||||
string playerId = "101";
|
|
||||||
string status = "输入涂游测试 ID,验证 s101 认证及 Cookie。";
|
|
||||||
bool busy;
|
|
||||||
S101AccountSession session;
|
|
||||||
CancellationTokenSource cancellation;
|
|
||||||
|
|
||||||
[MenuItem("Tools/Debug/S101 Minimal Login")]
|
|
||||||
static void Open() => GetWindow<S101LoginWindow>("S101 最小登录");
|
|
||||||
|
|
||||||
void OnGUI()
|
|
||||||
{
|
|
||||||
EditorGUILayout.LabelField("API", S101AccountSession.DefaultApiUrl);
|
|
||||||
using (new EditorGUI.DisabledScope(busy))
|
|
||||||
{
|
|
||||||
playerId = EditorGUILayout.TextField("涂游测试 ID", playerId);
|
|
||||||
if (GUILayout.Button("登录并验证 Cookie")) Login();
|
|
||||||
}
|
|
||||||
EditorGUILayout.HelpBox(status, MessageType.Info);
|
|
||||||
EditorGUILayout.LabelField("此入口只验证认证,不初始化玩法或写入存档。");
|
|
||||||
}
|
|
||||||
|
|
||||||
async void Login()
|
|
||||||
{
|
|
||||||
busy = true;
|
|
||||||
session?.Dispose();
|
|
||||||
cancellation?.Dispose();
|
|
||||||
cancellation = new CancellationTokenSource();
|
|
||||||
var attempt = new S101AccountSession();
|
|
||||||
session = attempt;
|
|
||||||
var token = cancellation.Token;
|
|
||||||
status = "正在登录…";
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var login = await attempt.LoginAsync(playerId, token);
|
|
||||||
if (token.IsCancellationRequested) return;
|
|
||||||
if (!login.Success)
|
|
||||||
{
|
|
||||||
status = $"登录失败:{login.Error},HTTP {login.StatusCode}";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var probe = await attempt.CheckAuthorizationAsync(token);
|
|
||||||
if (token.IsCancellationRequested) return;
|
|
||||||
status = probe.Success
|
|
||||||
? $"认证通过:涂游 ID {attempt.PlayerId},内部 ID {attempt.InternalId};Cookie 授权读取通过。"
|
|
||||||
: $"登录成功,但授权读取未通过:{probe.Error},HTTP {probe.StatusCode}";
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
status = "验证异常:" + ex.GetType().Name;
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
busy = false;
|
|
||||||
if (this != null) Repaint();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void OnDisable()
|
|
||||||
{
|
|
||||||
cancellation?.Cancel();
|
|
||||||
session?.Dispose();
|
|
||||||
cancellation?.Dispose();
|
|
||||||
cancellation = null;
|
|
||||||
session = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -9,8 +9,8 @@ namespace Activity.Data
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class EventDataRepository : IEventDataRepository
|
public class EventDataRepository : IEventDataRepository
|
||||||
{
|
{
|
||||||
private readonly game.PlayerDataStorage.IPlayerSaveStore store;
|
private readonly Core.KV.IPlayerSaveStore store;
|
||||||
public EventDataRepository(game.PlayerDataStorage.IPlayerSaveStore store)
|
public EventDataRepository(Core.KV.IPlayerSaveStore store)
|
||||||
{ this.store = store ?? throw new ArgumentNullException(nameof(store)); }
|
{ this.store = store ?? throw new ArgumentNullException(nameof(store)); }
|
||||||
|
|
||||||
public void Save<T>(T data) where T : IEventData
|
public void Save<T>(T data) where T : IEventData
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ namespace Activity.Services
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class EventTokenService : IEventTokenService
|
public class EventTokenService : IEventTokenService
|
||||||
{
|
{
|
||||||
private readonly game.PlayerDataStorage.IPlayerSaveStore store;
|
private readonly Core.KV.IPlayerSaveStore store;
|
||||||
public EventTokenService(game.PlayerDataStorage.IPlayerSaveStore store)
|
public EventTokenService(Core.KV.IPlayerSaveStore store)
|
||||||
{ this.store = store ?? throw new ArgumentNullException(nameof(store)); }
|
{ this.store = store ?? throw new ArgumentNullException(nameof(store)); }
|
||||||
|
|
||||||
#region 常量与字段
|
#region 常量与字段
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ using UnityEngine;
|
|||||||
public class AquariumManager
|
public class AquariumManager
|
||||||
{
|
{
|
||||||
public const string AquariumEventDataNewKey = "AquariumEventDataNewKey";
|
public const string AquariumEventDataNewKey = "AquariumEventDataNewKey";
|
||||||
readonly game.PlayerDataStorage.IPlayerSaveStore saveStore;
|
readonly Core.KV.IPlayerSaveStore saveStore;
|
||||||
readonly IUserService loginUser;
|
readonly IUserService loginUser;
|
||||||
private cfg.Tables _tables;
|
private cfg.Tables _tables;
|
||||||
private ICustomServerMgr customServerMgr;
|
private ICustomServerMgr customServerMgr;
|
||||||
@@ -23,14 +23,14 @@ public class AquariumManager
|
|||||||
public List<AquariumSlotData> sellList;
|
public List<AquariumSlotData> sellList;
|
||||||
public AquariumManager()
|
public AquariumManager()
|
||||||
{
|
{
|
||||||
saveStore = GContext.container.Resolve<game.PlayerDataStorage.IPlayerSaveStore>();
|
saveStore = GContext.container.Resolve<Core.KV.IPlayerSaveStore>();
|
||||||
loginUser = GContext.container.Resolve<IUserService>();
|
loginUser = GContext.container.Resolve<IUserService>();
|
||||||
_tables = GContext.container.Resolve<Tables>();
|
_tables = GContext.container.Resolve<Tables>();
|
||||||
customServerMgr = GContext.container.Resolve<ICustomServerMgr>();
|
customServerMgr = GContext.container.Resolve<ICustomServerMgr>();
|
||||||
}
|
}
|
||||||
public static AquariumManagerData GetAquariumManagerData(game.PlayerDataStorage.IPlayerSaveStore store = null)
|
public static AquariumManagerData GetAquariumManagerData(Core.KV.IPlayerSaveStore store = null)
|
||||||
{
|
{
|
||||||
var source = store ?? GContext.container.Resolve<game.PlayerDataStorage.IPlayerSaveStore>();
|
var source = store ?? GContext.container.Resolve<Core.KV.IPlayerSaveStore>();
|
||||||
string dataStr = source.Read(AquariumEventDataNewKey).ValueOrThrow();
|
string dataStr = source.Read(AquariumEventDataNewKey).ValueOrThrow();
|
||||||
if (!string.IsNullOrEmpty(dataStr))
|
if (!string.IsNullOrEmpty(dataStr))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ using UnityEngine;
|
|||||||
|
|
||||||
public class CampDataMM
|
public class CampDataMM
|
||||||
{
|
{
|
||||||
[asap.core.Inject] public game.PlayerDataStorage.IPlayerSaveStore SaveStore { get; set; }
|
[asap.core.Inject] public Core.KV.IPlayerSaveStore SaveStore { get; set; }
|
||||||
|
|
||||||
private cfg.Tables _tables;
|
private cfg.Tables _tables;
|
||||||
MMTService _MMService;
|
MMTService _MMService;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ using System.Threading;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using game.Account;
|
using game.Account;
|
||||||
using game.PlayerDataStorage;
|
using game.KV;
|
||||||
|
|
||||||
public interface ICustomServerMgr
|
public interface ICustomServerMgr
|
||||||
{
|
{
|
||||||
@@ -27,12 +27,14 @@ public interface ICustomServerMgr
|
|||||||
Uri ApiBaseUri { get; }
|
Uri ApiBaseUri { get; }
|
||||||
int SessionVersion { get; }
|
int SessionVersion { get; }
|
||||||
event Action SessionChanged;
|
event Action SessionChanged;
|
||||||
void UseAccountSession(S101AccountSession session);
|
void UseAccountSession(GameServerAccountSession session);
|
||||||
void ClearAccountSession();
|
void ClearAccountSession();
|
||||||
}
|
}
|
||||||
|
|
||||||
public class CustomServerMgr : ICustomServerMgr
|
public class CustomServerMgr : ICustomServerMgr
|
||||||
{
|
{
|
||||||
|
public const string DefaultApiUrl = "http://s101.local:18080/";
|
||||||
|
|
||||||
IConfig config { get; set; }
|
IConfig config { get; set; }
|
||||||
|
|
||||||
//网络客户端
|
//网络客户端
|
||||||
@@ -43,34 +45,31 @@ public class CustomServerMgr : ICustomServerMgr
|
|||||||
private readonly SynchronizationContext ownerContext;
|
private readonly SynchronizationContext ownerContext;
|
||||||
public int SessionVersion { get; private set; }
|
public int SessionVersion { get; private set; }
|
||||||
public event Action SessionChanged;
|
public event Action SessionChanged;
|
||||||
PlayerKVAccountLease kvLease;
|
|
||||||
bool kvOnly;
|
private KvAccountLease kvLease;
|
||||||
bool kvBusinessReady;
|
private bool kvOnly;
|
||||||
bool kvBusinessPublished;
|
private bool kvBusinessReady;
|
||||||
int pfRequests;
|
private bool kvBusinessPublished;
|
||||||
|
private int pfRequests;
|
||||||
internal event Action<int> KVUnauthorized;
|
internal event Action<int> KVUnauthorized;
|
||||||
internal bool IsFreshForKVLoad => pfRequests==0 && kvLease==null;
|
internal bool IsFreshForKVLoad => pfRequests==0 && kvLease==null;
|
||||||
internal void BindKVForValidation(PlayerKVAccountLease lease)
|
internal void BindKVForGame(KvAccountLease lease)
|
||||||
{
|
{
|
||||||
if(lease==null || lease.IsFormal || !lease.IsCurrent || lease.ApiBaseUri!=ApiBaseUri || !IsFreshForKVLoad)
|
if (lease == null || !lease.IsCurrent ||
|
||||||
throw new InvalidOperationException("Fresh matching KV lease required.");
|
lease.ApiBaseUri.AbsoluteUri != ApiBaseUri.AbsoluteUri || !IsFreshForKVLoad)
|
||||||
kvOnly=true; kvBusinessReady=false; kvBusinessPublished=false; SessionVersion++; kvLease=lease; _cookie=null;
|
{
|
||||||
var previous=client; client=null; previous?.Dispose(); SessionChanged?.Invoke();
|
|
||||||
}
|
|
||||||
internal void BindKVForGame(PlayerKVAccountLease lease)
|
|
||||||
{
|
|
||||||
if(lease==null || !lease.IsFormal || !lease.IsCurrent || lease.ApiBaseUri.AbsoluteUri!=PlayerKVRuntimeSelection.FormalApiUrl ||
|
|
||||||
ApiBaseUri.AbsoluteUri!=PlayerKVRuntimeSelection.FormalApiUrl || !IsFreshForKVLoad)
|
|
||||||
throw new InvalidOperationException("Fresh formal KV lease required.");
|
throw new InvalidOperationException("Fresh formal KV lease required.");
|
||||||
|
}
|
||||||
kvOnly=true; kvBusinessReady=false; kvBusinessPublished=false; SessionVersion++; kvLease=lease; _cookie=null;
|
kvOnly=true; kvBusinessReady=false; kvBusinessPublished=false; SessionVersion++; kvLease=lease; _cookie=null;
|
||||||
var previous=client; client=null; previous?.Dispose(); SessionChanged?.Invoke();
|
var previous=client; client=null; previous?.Dispose(); SessionChanged?.Invoke();
|
||||||
}
|
}
|
||||||
internal void EnableKVBusiness(long epoch)
|
internal void EnableKVBusiness(long epoch)
|
||||||
{
|
{
|
||||||
if(!kvOnly || kvBusinessReady || kvBusinessPublished || kvLease==null || !kvLease.IsFormal || !kvLease.IsCurrent ||
|
if (!kvOnly || kvBusinessReady || kvBusinessPublished || kvLease == null || !kvLease.IsCurrent ||
|
||||||
kvLease.Epoch!=epoch || kvLease.ApiBaseUri.AbsoluteUri!=PlayerKVRuntimeSelection.FormalApiUrl ||
|
kvLease.Epoch != epoch || kvLease.ApiBaseUri.AbsoluteUri != ApiBaseUri.AbsoluteUri)
|
||||||
ApiBaseUri.AbsoluteUri!=PlayerKVRuntimeSelection.FormalApiUrl)
|
{
|
||||||
throw new InvalidOperationException("Current formal KV lease required for business transport.");
|
throw new InvalidOperationException("Current formal KV lease required for business transport.");
|
||||||
|
}
|
||||||
kvBusinessPublished=true; kvBusinessReady=true;
|
kvBusinessPublished=true; kvBusinessReady=true;
|
||||||
}
|
}
|
||||||
internal void DisableKVBusiness()
|
internal void DisableKVBusiness()
|
||||||
@@ -80,30 +79,21 @@ public class CustomServerMgr : ICustomServerMgr
|
|||||||
try { SessionChanged?.Invoke(); }
|
try { SessionChanged?.Invoke(); }
|
||||||
catch(Exception ex) { Debug.LogError($"KV session change failed: {ex.GetType().Name}"); }
|
catch(Exception ex) { Debug.LogError($"KV session change failed: {ex.GetType().Name}"); }
|
||||||
}
|
}
|
||||||
internal Task ClearKVForGame() => ClearKVForValidation();
|
internal Task ClearKVForGame()
|
||||||
internal Task ClearKVForValidation()
|
|
||||||
{
|
{
|
||||||
kvBusinessReady=false; SessionVersion++; var previous=kvLease; kvLease=null; _cookie=null;
|
kvBusinessReady = false;
|
||||||
var closing=previous?.CloseAsync() ?? Task.CompletedTask;
|
SessionVersion++;
|
||||||
SessionChanged?.Invoke(); return closing;
|
var previous = kvLease;
|
||||||
}
|
kvLease = null;
|
||||||
internal async Task<PlayerKVAccountResponse> SendKVForValidationAsync(string route)
|
_cookie = null;
|
||||||
{
|
var closing = previous?.CloseAsync() ?? Task.CompletedTask;
|
||||||
var lease=kvLease; int version=SessionVersion;
|
SessionChanged?.Invoke();
|
||||||
if(!kvOnly || lease==null) return new PlayerKVAccountResponse(false);
|
return closing;
|
||||||
var response=await lease.SendAsync(HttpMethod.Get,route);
|
|
||||||
if(version!=SessionVersion || !ReferenceEquals(lease,kvLease) || !response.IsCurrent) return new PlayerKVAccountResponse(false);
|
|
||||||
if(response.StatusCode==401 || response.StatusCode==403)
|
|
||||||
{
|
|
||||||
KVUnauthorized?.Invoke(version);
|
|
||||||
return new PlayerKVAccountResponse(false,response.StatusCode);
|
|
||||||
}
|
|
||||||
return response;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public CookieContainer Cookie => kvOnly
|
public CookieContainer Cookie => kvOnly
|
||||||
? (kvBusinessReady && kvLease?.IsFormal==true ? kvLease.FormalCookies : null) ?? throw new NullReferenceException()
|
? (kvBusinessReady && kvLease != null ? kvLease.FormalCookies : null) ?? throw new NullReferenceException()
|
||||||
: _cookie ?? throw new NullReferenceException();
|
: _cookie ?? throw new NullReferenceException();
|
||||||
public Uri ApiBaseUri { get; }
|
public Uri ApiBaseUri { get; }
|
||||||
|
|
||||||
@@ -118,13 +108,16 @@ public class CustomServerMgr : ICustomServerMgr
|
|||||||
});
|
});
|
||||||
this.config = config;
|
this.config = config;
|
||||||
|
|
||||||
var url = config.Get<string>(GConstant.K_Event_API_URL, S101AccountSession.DefaultApiUrl);
|
var url = config.Get<string>(GConstant.K_Event_API_URL, DefaultApiUrl);
|
||||||
if (string.IsNullOrWhiteSpace(url)) url = S101AccountSession.DefaultApiUrl;
|
if (string.IsNullOrWhiteSpace(url))
|
||||||
|
{
|
||||||
|
url = DefaultApiUrl;
|
||||||
|
}
|
||||||
ApiBaseUri = new Uri(url.TrimEnd('/') + "/");
|
ApiBaseUri = new Uri(url.TrimEnd('/') + "/");
|
||||||
ClearAccountSession();
|
ClearAccountSession();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UseAccountSession(S101AccountSession session)
|
public void UseAccountSession(GameServerAccountSession session)
|
||||||
{
|
{
|
||||||
if(kvOnly) throw new InvalidOperationException("Controlled container cannot switch to PlayFab.");
|
if(kvOnly) throw new InvalidOperationException("Controlled container cannot switch to PlayFab.");
|
||||||
if (session == null || !session.IsAuthenticated || session.ApiBaseUri != ApiBaseUri)
|
if (session == null || !session.IsAuthenticated || session.ApiBaseUri != ApiBaseUri)
|
||||||
@@ -134,7 +127,11 @@ public class CustomServerMgr : ICustomServerMgr
|
|||||||
|
|
||||||
public void ClearAccountSession()
|
public void ClearAccountSession()
|
||||||
{
|
{
|
||||||
if(kvOnly) { PlayerKVShutdownRegistry.Process.Register(ClearKVForValidation()); return; }
|
if (kvOnly)
|
||||||
|
{
|
||||||
|
KvShutdownRegistry.Process.Register(ClearKVForGame());
|
||||||
|
return;
|
||||||
|
}
|
||||||
ReplaceTransport(new CookieContainer());
|
ReplaceTransport(new CookieContainer());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,7 +167,10 @@ public class CustomServerMgr : ICustomServerMgr
|
|||||||
}
|
}
|
||||||
if(!kvBusinessReady) throw new InvalidOperationException("Formal KV business transport is not ready.");
|
if(!kvBusinessReady) throw new InvalidOperationException("Formal KV business transport is not ready.");
|
||||||
var lease=kvLease; var kvVersion=SessionVersion;
|
var lease=kvLease; var kvVersion=SessionVersion;
|
||||||
if(lease==null || !lease.IsFormal || !lease.IsCurrent) return null;
|
if (lease == null || !lease.IsCurrent)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var response=await lease.SendAsync(method,url,body,contentType);
|
var response=await lease.SendAsync(method,url,body,contentType);
|
||||||
@@ -286,3 +286,4 @@ public class CustomServerMgr : ICustomServerMgr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,54 +11,101 @@ using Newtonsoft.Json.Linq;
|
|||||||
|
|
||||||
namespace game.Account
|
namespace game.Account
|
||||||
{
|
{
|
||||||
public enum AccountLoginError { None, Validation, Http, Rejected, Protocol, Network, Timeout, Cancelled }
|
public enum AccountLoginError
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
Validation,
|
||||||
|
Http,
|
||||||
|
Rejected,
|
||||||
|
Protocol,
|
||||||
|
Network,
|
||||||
|
Timeout,
|
||||||
|
Cancelled
|
||||||
|
}
|
||||||
|
|
||||||
public sealed class AccountLoginResult
|
public sealed class AccountLoginResult
|
||||||
{
|
{
|
||||||
public bool Success => Error == AccountLoginError.None;
|
public bool Success => Error == AccountLoginError.None;
|
||||||
public AccountLoginError Error { get; }
|
public AccountLoginError Error
|
||||||
public int? StatusCode { get; }
|
{
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
public int? StatusCode
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
}
|
||||||
internal AccountLoginResult(AccountLoginError error, int? status = null)
|
internal AccountLoginResult(AccountLoginError error, int? status = null)
|
||||||
{ Error = error; StatusCode = status; }
|
{
|
||||||
|
Error = error;
|
||||||
|
StatusCode = status;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Minimal Tuyou-ID authentication, independent of gameplay and PlayFab storage.
|
/// Minimal Tuyou-ID authentication, independent of gameplay and PlayFab storage.
|
||||||
/// Each login owns a fresh cookie jar; dispose the session on logout.
|
/// Each login owns a fresh cookie jar; dispose the session on logout.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class S101AccountSession : IDisposable
|
public sealed class GameServerAccountSession : IDisposable
|
||||||
{
|
{
|
||||||
public const string DefaultApiUrl = "http://s101.local:18080/";
|
private readonly object gate = new object();
|
||||||
readonly object gate = new object();
|
private readonly Uri api;
|
||||||
readonly Uri api;
|
private readonly Func<CookieContainer, HttpMessageHandler> handlerFactory;
|
||||||
readonly Func<CookieContainer, HttpMessageHandler> handlerFactory;
|
private HttpClient http;
|
||||||
HttpClient http;
|
private int generation;
|
||||||
int generation;
|
private bool disposed;
|
||||||
bool disposed;
|
|
||||||
|
|
||||||
public bool IsAuthenticated { get; private set; }
|
public bool IsAuthenticated
|
||||||
public string PlayerId { get; private set; }
|
{
|
||||||
public uint InternalId { get; private set; }
|
get;
|
||||||
public DateTime CreateTimeUtc { get; private set; }
|
private set;
|
||||||
public DateTime ServerTimeUtc { get; private set; }
|
}
|
||||||
public bool NewlyCreated { get; private set; }
|
public string PlayerId
|
||||||
public CookieContainer Cookies { get; private set; }
|
{
|
||||||
|
get;
|
||||||
|
private set;
|
||||||
|
}
|
||||||
|
public uint InternalId
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
private set;
|
||||||
|
}
|
||||||
|
public DateTime CreateTimeUtc
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
private set;
|
||||||
|
}
|
||||||
|
public DateTime ServerTimeUtc
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
private set;
|
||||||
|
}
|
||||||
|
public bool NewlyCreated
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
private set;
|
||||||
|
}
|
||||||
|
public CookieContainer Cookies
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
private set;
|
||||||
|
}
|
||||||
public Uri ApiBaseUri => api;
|
public Uri ApiBaseUri => api;
|
||||||
|
|
||||||
public S101AccountSession() : this(new Uri(DefaultApiUrl)) { }
|
// KV supplies its owned transport; the PF path uses the default handler.
|
||||||
|
public GameServerAccountSession(Uri api, Func<CookieContainer, HttpMessageHandler> handlerFactory = null)
|
||||||
// The factory is optional for transport tests. Returned handlers are owned by this session.
|
|
||||||
public S101AccountSession(Uri api, Func<CookieContainer, HttpMessageHandler> handlerFactory = null)
|
|
||||||
{
|
{
|
||||||
if (api == null || !api.IsAbsoluteUri ||
|
if (api == null || !api.IsAbsoluteUri ||
|
||||||
(api.Scheme != "http" && api.Scheme != "https") ||
|
(api.Scheme != "http" && api.Scheme != "https") ||
|
||||||
!string.IsNullOrEmpty(api.Query) || !string.IsNullOrEmpty(api.Fragment) ||
|
!string.IsNullOrEmpty(api.Query) || !string.IsNullOrEmpty(api.Fragment) ||
|
||||||
!string.IsNullOrEmpty(api.UserInfo))
|
!string.IsNullOrEmpty(api.UserInfo))
|
||||||
|
{
|
||||||
throw new ArgumentException("An absolute HTTP(S) API base URI is required.", nameof(api));
|
throw new ArgumentException("An absolute HTTP(S) API base URI is required.", nameof(api));
|
||||||
|
}
|
||||||
this.api = new Uri(api.AbsoluteUri.TrimEnd('/') + "/");
|
this.api = new Uri(api.AbsoluteUri.TrimEnd('/') + "/");
|
||||||
this.handlerFactory = handlerFactory ?? (jar => new HttpClientHandler
|
this.handlerFactory = handlerFactory ?? (jar => new HttpClientHandler
|
||||||
{
|
{
|
||||||
CookieContainer = jar, UseCookies = true, AllowAutoRedirect = false
|
CookieContainer = jar,
|
||||||
|
UseCookies = true,
|
||||||
|
AllowAutoRedirect = false
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,7 +117,10 @@ namespace game.Account
|
|||||||
string id;
|
string id;
|
||||||
lock (gate)
|
lock (gate)
|
||||||
{
|
{
|
||||||
if (disposed) return Result(AccountLoginError.Cancelled);
|
if (disposed)
|
||||||
|
{
|
||||||
|
return Result(AccountLoginError.Cancelled);
|
||||||
|
}
|
||||||
generation++;
|
generation++;
|
||||||
ClearIdentity();
|
ClearIdentity();
|
||||||
http?.Dispose();
|
http?.Dispose();
|
||||||
@@ -78,11 +128,19 @@ namespace game.Account
|
|||||||
Cookies = null;
|
Cookies = null;
|
||||||
if (string.IsNullOrEmpty(tuyooId) ||
|
if (string.IsNullOrEmpty(tuyooId) ||
|
||||||
!long.TryParse(tuyooId, NumberStyles.None, CultureInfo.InvariantCulture, out var numericId) || numericId <= 0)
|
!long.TryParse(tuyooId, NumberStyles.None, CultureInfo.InvariantCulture, out var numericId) || numericId <= 0)
|
||||||
|
{
|
||||||
return Result(AccountLoginError.Validation);
|
return Result(AccountLoginError.Validation);
|
||||||
if (token.IsCancellationRequested) return Result(AccountLoginError.Cancelled);
|
}
|
||||||
|
if (token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
return Result(AccountLoginError.Cancelled);
|
||||||
|
}
|
||||||
id = numericId.ToString(CultureInfo.InvariantCulture);
|
id = numericId.ToString(CultureInfo.InvariantCulture);
|
||||||
cookies = new CookieContainer();
|
cookies = new CookieContainer();
|
||||||
client = new HttpClient(handlerFactory(cookies)) { Timeout = TimeSpan.FromSeconds(15) };
|
client = new HttpClient(handlerFactory(cookies))
|
||||||
|
{
|
||||||
|
Timeout = TimeSpan.FromSeconds(15)
|
||||||
|
};
|
||||||
http = client;
|
http = client;
|
||||||
Cookies = cookies;
|
Cookies = cookies;
|
||||||
requestGeneration = generation;
|
requestGeneration = generation;
|
||||||
@@ -91,24 +149,40 @@ namespace game.Account
|
|||||||
int? status = null;
|
int? status = null;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using (var request = Request("account/Login", new JObject { ["playerId"] = id }))
|
using (var request = Request("account/Login", new JObject
|
||||||
|
{
|
||||||
|
["playerId"] = id
|
||||||
|
}))
|
||||||
using (var response = await client.SendAsync(request, token))
|
using (var response = await client.SendAsync(request, token))
|
||||||
{
|
{
|
||||||
status = (int)response.StatusCode;
|
status = (int)response.StatusCode;
|
||||||
if (response.StatusCode != HttpStatusCode.OK) return Result(AccountLoginError.Http, status);
|
if (response.StatusCode != HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
return Result(AccountLoginError.Http, status);
|
||||||
|
}
|
||||||
var body = Parse(await response.Content.ReadAsStringAsync());
|
var body = Parse(await response.Content.ReadAsStringAsync());
|
||||||
if (body["state"]?.Type != JTokenType.Integer) return Result(AccountLoginError.Protocol, status);
|
if (body["state"]?.Type != JTokenType.Integer)
|
||||||
if (body["state"].Value<long>() != 0) return Result(AccountLoginError.Rejected, status);
|
{
|
||||||
|
return Result(AccountLoginError.Protocol, status);
|
||||||
|
}
|
||||||
|
if (body["state"].Value<long>() != 0)
|
||||||
|
{
|
||||||
|
return Result(AccountLoginError.Rejected, status);
|
||||||
|
}
|
||||||
if (body["internalId"]?.Type != JTokenType.Integer ||
|
if (body["internalId"]?.Type != JTokenType.Integer ||
|
||||||
!uint.TryParse(body["internalId"].ToString(), out var internalId) || internalId == 0 ||
|
!uint.TryParse(body["internalId"].ToString(), out var internalId) || internalId == 0 ||
|
||||||
body["newCreate"]?.Type != JTokenType.Boolean ||
|
body["newCreate"]?.Type != JTokenType.Boolean ||
|
||||||
!ReadUtc(body["createTime"], out var created) || !ReadUtc(body["svrTime"], out var serverTime) ||
|
!ReadUtc(body["createTime"], out var created) || !ReadUtc(body["svrTime"], out var serverTime) ||
|
||||||
cookies.GetCookies(api).Count == 0)
|
cookies.GetCookies(api).Count == 0)
|
||||||
|
{
|
||||||
return Result(AccountLoginError.Protocol, status);
|
return Result(AccountLoginError.Protocol, status);
|
||||||
|
}
|
||||||
lock (gate)
|
lock (gate)
|
||||||
{
|
{
|
||||||
if (disposed || generation != requestGeneration || token.IsCancellationRequested)
|
if (disposed || generation != requestGeneration || token.IsCancellationRequested)
|
||||||
|
{
|
||||||
return Result(AccountLoginError.Cancelled, status);
|
return Result(AccountLoginError.Cancelled, status);
|
||||||
|
}
|
||||||
PlayerId = id;
|
PlayerId = id;
|
||||||
InternalId = internalId;
|
InternalId = internalId;
|
||||||
CreateTimeUtc = created;
|
CreateTimeUtc = created;
|
||||||
@@ -122,82 +196,59 @@ namespace game.Account
|
|||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
lock (gate)
|
lock (gate)
|
||||||
return Result(disposed || generation != requestGeneration || token.IsCancellationRequested
|
|
||||||
? AccountLoginError.Cancelled : AccountLoginError.Timeout, status);
|
|
||||||
}
|
|
||||||
catch (ObjectDisposedException) { return Result(AccountLoginError.Cancelled, status); }
|
|
||||||
catch (HttpRequestException) { return Result(AccountLoginError.Network, status); }
|
|
||||||
catch (JsonException) { return Result(AccountLoginError.Protocol, status); }
|
|
||||||
catch (OverflowException) { return Result(AccountLoginError.Protocol, status); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Read-only probe: verifies that the server accepts this session cookie; never writes KV.</summary>
|
|
||||||
public async Task<AccountLoginResult> CheckAuthorizationAsync(CancellationToken token = default)
|
|
||||||
{
|
|
||||||
HttpClient client;
|
|
||||||
int requestGeneration;
|
|
||||||
lock (gate)
|
|
||||||
{
|
|
||||||
if (disposed || !IsAuthenticated) return Result(AccountLoginError.Rejected);
|
|
||||||
client = http;
|
|
||||||
requestGeneration = generation;
|
|
||||||
}
|
|
||||||
int? status = null;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using (var request = Request("playerdata/Get", new JObject()))
|
|
||||||
using (var response = await client.SendAsync(request, token))
|
|
||||||
{
|
{
|
||||||
status = (int)response.StatusCode;
|
return Result(disposed || generation != requestGeneration || token.IsCancellationRequested
|
||||||
lock (gate)
|
? AccountLoginError.Cancelled : AccountLoginError.Timeout, status);
|
||||||
{
|
|
||||||
if (disposed || generation != requestGeneration || token.IsCancellationRequested)
|
|
||||||
return Result(AccountLoginError.Cancelled, status);
|
|
||||||
if (status == 401 || status == 403) ClearIdentity();
|
|
||||||
}
|
|
||||||
if (response.StatusCode != HttpStatusCode.OK) return Result(AccountLoginError.Http, status);
|
|
||||||
var body = Parse(await response.Content.ReadAsStringAsync());
|
|
||||||
if (!(body["data"] is JObject data)) return Result(AccountLoginError.Protocol, status);
|
|
||||||
foreach (var item in data.Properties())
|
|
||||||
if (item.Value.Type != JTokenType.String) return Result(AccountLoginError.Protocol, status);
|
|
||||||
lock (gate)
|
|
||||||
return Result(disposed || generation != requestGeneration || token.IsCancellationRequested
|
|
||||||
? AccountLoginError.Cancelled : AccountLoginError.None, status);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (ObjectDisposedException)
|
||||||
{
|
{
|
||||||
lock (gate)
|
return Result(AccountLoginError.Cancelled, status);
|
||||||
return Result(disposed || generation != requestGeneration || token.IsCancellationRequested
|
}
|
||||||
? AccountLoginError.Cancelled : AccountLoginError.Timeout, status);
|
catch (HttpRequestException)
|
||||||
|
{
|
||||||
|
return Result(AccountLoginError.Network, status);
|
||||||
|
}
|
||||||
|
catch (JsonException)
|
||||||
|
{
|
||||||
|
return Result(AccountLoginError.Protocol, status);
|
||||||
|
}
|
||||||
|
catch (OverflowException)
|
||||||
|
{
|
||||||
|
return Result(AccountLoginError.Protocol, status);
|
||||||
}
|
}
|
||||||
catch (ObjectDisposedException) { return Result(AccountLoginError.Cancelled, status); }
|
|
||||||
catch (HttpRequestException) { return Result(AccountLoginError.Network, status); }
|
|
||||||
catch (JsonException) { return Result(AccountLoginError.Protocol, status); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
HttpRequestMessage Request(string path, JObject body) => new HttpRequestMessage(HttpMethod.Post, new Uri(api, path))
|
private HttpRequestMessage Request(string path, JObject body) => new HttpRequestMessage(HttpMethod.Post, new Uri(api, path))
|
||||||
{ Content = new StringContent(body.ToString(Formatting.None), Encoding.UTF8, "application/json") };
|
|
||||||
|
|
||||||
static JObject Parse(string text)
|
|
||||||
{
|
{
|
||||||
using (var reader = new JsonTextReader(new StringReader(text)) { DateParseHandling = DateParseHandling.None })
|
Content = new StringContent(body.ToString(Formatting.None), Encoding.UTF8, "application/json")
|
||||||
|
};
|
||||||
|
|
||||||
|
private static JObject Parse(string text)
|
||||||
|
{
|
||||||
|
using (var reader = new JsonTextReader(new StringReader(text))
|
||||||
|
{
|
||||||
|
DateParseHandling = DateParseHandling.None
|
||||||
|
})
|
||||||
{
|
{
|
||||||
var value = JObject.Load(reader);
|
var value = JObject.Load(reader);
|
||||||
if (reader.Read()) throw new JsonReaderException("Trailing JSON content.");
|
if (reader.Read())
|
||||||
|
{
|
||||||
|
throw new JsonReaderException("Trailing JSON content.");
|
||||||
|
}
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool ReadUtc(JToken value, out DateTime date)
|
private static bool ReadUtc(JToken value, out DateTime date)
|
||||||
{
|
{
|
||||||
date = default;
|
date = default;
|
||||||
return value?.Type == JTokenType.String && DateTime.TryParse(value.Value<string>(), CultureInfo.InvariantCulture,
|
return value?.Type == JTokenType.String && DateTime.TryParse(value.Value<string>(), CultureInfo.InvariantCulture,
|
||||||
DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out date);
|
DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out date);
|
||||||
}
|
}
|
||||||
|
|
||||||
static AccountLoginResult Result(AccountLoginError error, int? status = null) => new AccountLoginResult(error, status);
|
private static AccountLoginResult Result(AccountLoginError error, int? status = null) => new AccountLoginResult(error, status);
|
||||||
void ClearIdentity()
|
private void ClearIdentity()
|
||||||
{
|
{
|
||||||
IsAuthenticated = false;
|
IsAuthenticated = false;
|
||||||
PlayerId = null;
|
PlayerId = null;
|
||||||
@@ -211,7 +262,10 @@ namespace game.Account
|
|||||||
{
|
{
|
||||||
lock (gate)
|
lock (gate)
|
||||||
{
|
{
|
||||||
if (disposed) return;
|
if (disposed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
disposed = true;
|
disposed = true;
|
||||||
generation++;
|
generation++;
|
||||||
ClearIdentity();
|
ClearIdentity();
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: a8f49f7440c141e89d8519ff3aa1c034
|
guid: 47cad15649b346799afbe5e5cafb4bb3
|
||||||
MonoImporter:
|
MonoImporter:
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: 4930c4a417ba4c908416717d26695848
|
guid: f3f810de2c56488e979e3e585c94b35a
|
||||||
folderAsset: yes
|
folderAsset: yes
|
||||||
DefaultImporter:
|
DefaultImporter:
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
76
Client/Assets/Scripts/Core/KV/IPlayerSaveStore.cs
Normal file
76
Client/Assets/Scripts/Core/KV/IPlayerSaveStore.cs
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Core.KV
|
||||||
|
{
|
||||||
|
// A store belongs to one login. Callers retain this instance across awaits.
|
||||||
|
public interface IPlayerSaveStore
|
||||||
|
{
|
||||||
|
bool IsCurrent
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
PlayerSaveReadResult Read(string key);
|
||||||
|
PlayerSaveWriteResult Write(string key, string value, bool remoteStore = true);
|
||||||
|
Task<bool> SyncDataImmidiate(string key, string value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class PlayerSaveReadResult
|
||||||
|
{
|
||||||
|
public bool Success => Error == null;
|
||||||
|
public bool? Found
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
public string Value
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
public string Error
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
internal PlayerSaveReadResult(bool found, string value)
|
||||||
|
{
|
||||||
|
Found = found;
|
||||||
|
Value = value;
|
||||||
|
}
|
||||||
|
internal PlayerSaveReadResult(string error)
|
||||||
|
{
|
||||||
|
Error = error ?? "ReadFailed";
|
||||||
|
}
|
||||||
|
public string ValueOrThrow()
|
||||||
|
{
|
||||||
|
if (!Success)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Player save read failed: " + Error);
|
||||||
|
}
|
||||||
|
return Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class PlayerSaveWriteResult
|
||||||
|
{
|
||||||
|
public bool Accepted => Error == null;
|
||||||
|
public string Error
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
internal PlayerSaveWriteResult()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
internal PlayerSaveWriteResult(string error)
|
||||||
|
{
|
||||||
|
Error = error ?? "WriteRejected";
|
||||||
|
}
|
||||||
|
public void RequireAccepted()
|
||||||
|
{
|
||||||
|
if (!Accepted)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Player save write rejected: " + Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
132
Client/Assets/Scripts/Core/KV/KvCacheStore.cs
Normal file
132
Client/Assets/Scripts/Core/KV/KvCacheStore.cs
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Security;
|
||||||
|
using System.Text;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace Core.KV
|
||||||
|
{
|
||||||
|
internal sealed class KvCacheStore : IDisposable
|
||||||
|
{
|
||||||
|
private const long MaxBytes = 64L * 1024 * 1024;
|
||||||
|
private readonly KvIdentity identity;
|
||||||
|
private readonly FileStream lease;
|
||||||
|
private readonly string main, temporary, backup;
|
||||||
|
private bool disposed;
|
||||||
|
|
||||||
|
private KvCacheStore(KvIdentity identity, FileStream lease)
|
||||||
|
{
|
||||||
|
this.identity = identity;
|
||||||
|
this.lease = lease;
|
||||||
|
main = Path.Combine(identity.DirectoryPath, "state.json");
|
||||||
|
temporary = main + ".tmp";
|
||||||
|
backup = main + ".bak";
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static KVOpenError Open(KvIdentity identity,
|
||||||
|
out KvCacheStore store, out KvSnapshot snapshot)
|
||||||
|
{
|
||||||
|
store = null;
|
||||||
|
snapshot = null;
|
||||||
|
FileStream lease = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(identity.DirectoryPath);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
lease = new FileStream(Path.Combine(identity.DirectoryPath, "state.lock"), FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None);
|
||||||
|
}
|
||||||
|
catch (IOException ex) when ((ex.HResult & 0xffff) == 32 || (ex.HResult & 0xffff) == 33)
|
||||||
|
{
|
||||||
|
return KVOpenError.Busy;
|
||||||
|
}
|
||||||
|
store = new KvCacheStore(identity, lease);
|
||||||
|
if (!File.Exists(store.main))
|
||||||
|
{
|
||||||
|
if (File.Exists(store.backup) || File.Exists(store.temporary))
|
||||||
|
{
|
||||||
|
throw KvSnapshot.Invalid();
|
||||||
|
}
|
||||||
|
snapshot = new KvSnapshot();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (new FileInfo(store.main).Length > MaxBytes)
|
||||||
|
{
|
||||||
|
throw KvSnapshot.Invalid();
|
||||||
|
}
|
||||||
|
snapshot = KvSnapshot.Decode(File.ReadAllText(store.main, new UTF8Encoding(false, true)), identity);
|
||||||
|
}
|
||||||
|
return KVOpenError.None;
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex is JsonException || ex is OverflowException || ex is FormatException || ex is DecoderFallbackException)
|
||||||
|
{
|
||||||
|
store?.Dispose();
|
||||||
|
lease?.Dispose();
|
||||||
|
store = null;
|
||||||
|
return KVOpenError.RecoveryRequired;
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (DiskError(ex))
|
||||||
|
{
|
||||||
|
store?.Dispose();
|
||||||
|
lease?.Dispose();
|
||||||
|
store = null;
|
||||||
|
return KVOpenError.StorageError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal bool Save(KvSnapshot snapshot, out bool mayHaveCommitted)
|
||||||
|
{
|
||||||
|
mayHaveCommitted = false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (disposed)
|
||||||
|
{
|
||||||
|
// Preserve the existing diagnostic ObjectName when renaming the implementation.
|
||||||
|
throw new ObjectDisposedException("PlayerKVCacheStore");
|
||||||
|
}
|
||||||
|
snapshot.Validate();
|
||||||
|
var bytes = new UTF8Encoding(false, true).GetBytes(snapshot.Encode(identity));
|
||||||
|
if (bytes.LongLength > MaxBytes)
|
||||||
|
{
|
||||||
|
throw new IOException("KV cache capacity exceeded.");
|
||||||
|
}
|
||||||
|
using (var stream = new FileStream(temporary, FileMode.Create, FileAccess.Write, FileShare.None))
|
||||||
|
{
|
||||||
|
stream.Write(bytes, 0, bytes.Length);
|
||||||
|
stream.Flush(true);
|
||||||
|
}
|
||||||
|
mayHaveCommitted = true;
|
||||||
|
// The temp file is on the same filesystem. Never delete the main file first.
|
||||||
|
if (File.Exists(main))
|
||||||
|
{
|
||||||
|
File.Replace(temporary, main, backup);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
File.Move(temporary, main);
|
||||||
|
}
|
||||||
|
mayHaveCommitted = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (DiskError(ex) || ex is JsonException || ex is EncoderFallbackException)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool DiskError(Exception ex) => ex is IOException || ex is UnauthorizedAccessException ||
|
||||||
|
ex is SecurityException || ex is NotSupportedException || ex is ObjectDisposedException;
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (disposed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
disposed = true;
|
||||||
|
lease.Dispose();
|
||||||
|
// Keep files, including failed temporary writes, for explicit recovery diagnosis.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
@@ -10,7 +11,7 @@ using System.Threading.Tasks;
|
|||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
namespace game.PlayerDataStorage
|
namespace Core.KV
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Private KV protocol for one fixed authenticated account/environment.
|
/// Private KV protocol for one fixed authenticated account/environment.
|
||||||
@@ -18,67 +19,85 @@ namespace game.PlayerDataStorage
|
|||||||
/// that transport for another account. This class neither retries nor logs payloads.
|
/// that transport for another account. This class neither retries nor logs payloads.
|
||||||
/// Production login/storage integration is deliberately outside this adapter.
|
/// Production login/storage integration is deliberately outside this adapter.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class PlayerKVClient
|
public sealed class KvClient
|
||||||
{
|
{
|
||||||
readonly HttpClient http;
|
private readonly HttpClient http;
|
||||||
readonly Uri api;
|
private readonly Uri api;
|
||||||
|
|
||||||
public PlayerKVClient(HttpClient http, Uri apiBaseUri)
|
public KvClient(HttpClient http, Uri apiBaseUri)
|
||||||
{
|
{
|
||||||
this.http = http ?? throw new ArgumentNullException(nameof(http));
|
this.http = http ?? throw new ArgumentNullException(nameof(http));
|
||||||
if (apiBaseUri == null || !apiBaseUri.IsAbsoluteUri ||
|
if (apiBaseUri == null || !apiBaseUri.IsAbsoluteUri ||
|
||||||
(apiBaseUri.Scheme != "http" && apiBaseUri.Scheme != "https") ||
|
(apiBaseUri.Scheme != "http" && apiBaseUri.Scheme != "https") ||
|
||||||
!string.IsNullOrEmpty(apiBaseUri.UserInfo) ||
|
!string.IsNullOrEmpty(apiBaseUri.UserInfo) ||
|
||||||
!string.IsNullOrEmpty(apiBaseUri.Query) || !string.IsNullOrEmpty(apiBaseUri.Fragment))
|
!string.IsNullOrEmpty(apiBaseUri.Query) || !string.IsNullOrEmpty(apiBaseUri.Fragment))
|
||||||
|
{
|
||||||
throw new ArgumentException("An absolute HTTP(S) API base URI is required.", nameof(apiBaseUri));
|
throw new ArgumentException("An absolute HTTP(S) API base URI is required.", nameof(apiBaseUri));
|
||||||
|
}
|
||||||
api = new Uri(apiBaseUri.AbsoluteUri.TrimEnd('/') + "/");
|
api = new Uri(apiBaseUri.AbsoluteUri.TrimEnd('/') + "/");
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<PlayerKVResult> GetAsync(IEnumerable<string> keys = null, CancellationToken token = default)
|
public Task<KvResult> GetAsync(CancellationToken token = default)
|
||||||
{
|
{
|
||||||
if (!CopyKeys(keys, true, out var snapshot)) return Invalid();
|
|
||||||
var body = new JObject();
|
var body = new JObject();
|
||||||
if (snapshot.Count > 0) body["keys"] = new JArray(snapshot);
|
|
||||||
return SendAsync("Get", body, false, token);
|
return SendAsync("Get", body, false, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<PlayerKVResult> SetAsync(IDictionary<string, string> data, CancellationToken token = default)
|
public Task<KvResult> SetAsync(IDictionary<string, string> data, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
if (data == null) return Invalid();
|
if (data == null)
|
||||||
|
{
|
||||||
|
return Invalid();
|
||||||
|
}
|
||||||
var snapshot = new JObject();
|
var snapshot = new JObject();
|
||||||
foreach (var pair in data)
|
foreach (var pair in data)
|
||||||
{
|
{
|
||||||
if (!ValidKey(pair.Key) || pair.Value == null) return Invalid();
|
if (!ValidKey(pair.Key) || pair.Value == null)
|
||||||
|
{
|
||||||
|
return Invalid();
|
||||||
|
}
|
||||||
snapshot.Add(pair.Key, pair.Value);
|
snapshot.Add(pair.Key, pair.Value);
|
||||||
}
|
}
|
||||||
return SendAsync("Set", new JObject { ["data"] = snapshot }, true, token);
|
return SendAsync("Set", new JObject { ["data"] = snapshot }, true, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<PlayerKVResult> DeleteAsync(IEnumerable<string> keys, CancellationToken token = default)
|
public Task<KvResult> DeleteAsync(IEnumerable<string> keys, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
if (!CopyKeys(keys, false, out var snapshot)) return Invalid();
|
if (!CopyKeys(keys, out var snapshot))
|
||||||
|
{
|
||||||
|
return Invalid();
|
||||||
|
}
|
||||||
return SendAsync("Delete", new JObject { ["keys"] = new JArray(snapshot) }, true, token);
|
return SendAsync("Delete", new JObject { ["keys"] = new JArray(snapshot) }, true, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool ValidKey(string key) => !string.IsNullOrEmpty(key) && key.Length <= 128;
|
private static bool ValidKey(string key) => !string.IsNullOrEmpty(key) && key.Length <= 128;
|
||||||
|
|
||||||
static bool CopyKeys(IEnumerable<string> keys, bool allowNull, out List<string> snapshot)
|
private static bool CopyKeys(IEnumerable<string> keys, out List<string> snapshot)
|
||||||
{
|
{
|
||||||
snapshot = new List<string>();
|
snapshot = new List<string>();
|
||||||
if (keys == null) return allowNull;
|
if (keys == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
foreach (var key in keys)
|
foreach (var key in keys)
|
||||||
{
|
{
|
||||||
if (!ValidKey(key)) return false;
|
if (!ValidKey(key))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
snapshot.Add(key);
|
snapshot.Add(key);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Task<PlayerKVResult> Invalid() => Task.FromResult(new PlayerKVResult(PlayerKVError.Validation));
|
private static Task<KvResult> Invalid() => Task.FromResult(new KvResult(KVError.Validation));
|
||||||
|
|
||||||
async Task<PlayerKVResult> SendAsync(string action, JObject body, bool mutation, CancellationToken token)
|
private async Task<KvResult> SendAsync(string action, JObject body, bool mutation, CancellationToken token)
|
||||||
{
|
{
|
||||||
if (token.IsCancellationRequested) return new PlayerKVResult(PlayerKVError.Cancelled);
|
if (token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
return new KvResult(KVError.Cancelled);
|
||||||
|
}
|
||||||
int? status = null;
|
int? status = null;
|
||||||
bool enteredTransport = false;
|
bool enteredTransport = false;
|
||||||
try
|
try
|
||||||
@@ -93,37 +112,40 @@ namespace game.PlayerDataStorage
|
|||||||
status = (int)response.StatusCode;
|
status = (int)response.StatusCode;
|
||||||
token.ThrowIfCancellationRequested();
|
token.ThrowIfCancellationRequested();
|
||||||
if (response.StatusCode != HttpStatusCode.OK)
|
if (response.StatusCode != HttpStatusCode.OK)
|
||||||
return new PlayerKVResult(status == 401 || status == 403
|
{
|
||||||
? PlayerKVError.Unauthorized : PlayerKVError.Http, status,
|
return new KvResult(status == 401 || status == 403
|
||||||
mayHaveCommitted: mutation);
|
? KVError.Unauthorized : KVError.Http, mayHaveCommitted: mutation);
|
||||||
if (mutation) return new PlayerKVResult(PlayerKVError.None, status);
|
}
|
||||||
|
if (mutation)
|
||||||
|
{
|
||||||
|
return new KvResult(KVError.None);
|
||||||
|
}
|
||||||
var text = await response.Content.ReadAsStringAsync();
|
var text = await response.Content.ReadAsStringAsync();
|
||||||
token.ThrowIfCancellationRequested();
|
token.ThrowIfCancellationRequested();
|
||||||
var data = ParseData(text, out var updatedAtUtc);
|
var data = ParseData(text, out var updatedAtUtc);
|
||||||
return new PlayerKVResult(PlayerKVError.None, status, data, updatedAtUtc: updatedAtUtc);
|
return new KvResult(KVError.None, data, updatedAtUtc: updatedAtUtc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
return new PlayerKVResult(token.IsCancellationRequested ? PlayerKVError.Cancelled : PlayerKVError.Timeout,
|
return new KvResult(token.IsCancellationRequested ? KVError.Cancelled : KVError.Timeout, mayHaveCommitted: mutation && enteredTransport);
|
||||||
status, mayHaveCommitted: mutation && enteredTransport);
|
|
||||||
}
|
}
|
||||||
catch (ObjectDisposedException)
|
catch (ObjectDisposedException)
|
||||||
{
|
{
|
||||||
return new PlayerKVResult(PlayerKVError.Cancelled, status, mayHaveCommitted: mutation && enteredTransport);
|
return new KvResult(KVError.Cancelled, mayHaveCommitted: mutation && enteredTransport);
|
||||||
}
|
}
|
||||||
catch (HttpRequestException)
|
catch (HttpRequestException)
|
||||||
{
|
{
|
||||||
return new PlayerKVResult(PlayerKVError.Network, status, mayHaveCommitted: mutation && enteredTransport);
|
return new KvResult(KVError.Network, mayHaveCommitted: mutation && enteredTransport);
|
||||||
}
|
}
|
||||||
catch (IOException)
|
catch (IOException)
|
||||||
{
|
{
|
||||||
return new PlayerKVResult(PlayerKVError.Network, status, mayHaveCommitted: mutation && enteredTransport);
|
return new KvResult(KVError.Network, mayHaveCommitted: mutation && enteredTransport);
|
||||||
}
|
}
|
||||||
catch (JsonException)
|
catch (JsonException)
|
||||||
{
|
{
|
||||||
return new PlayerKVResult(PlayerKVError.Protocol, status, mayHaveCommitted: mutation && enteredTransport);
|
return new KvResult(KVError.Protocol, mayHaveCommitted: mutation && enteredTransport);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,4 +197,51 @@ namespace game.PlayerDataStorage
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum KVError
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
Validation,
|
||||||
|
Unauthorized,
|
||||||
|
Http,
|
||||||
|
Network,
|
||||||
|
Timeout,
|
||||||
|
Cancelled,
|
||||||
|
Protocol
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class KvResult
|
||||||
|
{
|
||||||
|
public bool Success => Error == KVError.None;
|
||||||
|
public KVError Error
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
// Only a successful Get has Data. Failure must never look like an empty save.
|
||||||
|
public IReadOnlyDictionary<string, string> Data
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
// Null means the response did not supply timestamp metadata; never substitute read time.
|
||||||
|
public IReadOnlyDictionary<string, DateTime> UpdatedAtUtc
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
// A failed write that entered the transport may already have reached the server.
|
||||||
|
// This is conservative, including HTTP failures. Do not blindly replay the write.
|
||||||
|
public bool MayHaveCommitted
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal KvResult(KVError error,
|
||||||
|
Dictionary<string, string> data = null, bool mayHaveCommitted = false,
|
||||||
|
Dictionary<string, DateTime> updatedAtUtc = null)
|
||||||
|
{
|
||||||
|
Error = error;
|
||||||
|
Data = data == null ? null : new ReadOnlyDictionary<string, string>(data);
|
||||||
|
UpdatedAtUtc = updatedAtUtc == null ? null : new ReadOnlyDictionary<string, DateTime>(updatedAtUtc);
|
||||||
|
MayHaveCommitted = mayHaveCommitted;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
461
Client/Assets/Scripts/Core/KV/KvSessionOwner.cs
Normal file
461
Client/Assets/Scripts/Core/KV/KvSessionOwner.cs
Normal file
@@ -0,0 +1,461 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using game.Account;
|
||||||
|
|
||||||
|
namespace Core.KV
|
||||||
|
{
|
||||||
|
public enum KVSessionOpenError
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
Validation,
|
||||||
|
Unauthenticated,
|
||||||
|
Busy,
|
||||||
|
RecoveryRequired,
|
||||||
|
StorageError,
|
||||||
|
ReadFailed,
|
||||||
|
Unauthorized,
|
||||||
|
Stale,
|
||||||
|
Cancelled
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class KvSessionOpenResult
|
||||||
|
{
|
||||||
|
public bool Success => Error == KVSessionOpenError.None;
|
||||||
|
public KVSessionOpenError Error
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
public KvSessionOwner Owner
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
internal KvSessionOpenResult(KVSessionOpenError error, KvSessionOwner owner = null)
|
||||||
|
{
|
||||||
|
Error = error;
|
||||||
|
Owner = owner;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Owns one immutable account/epoch/cache/transport tuple.</summary>
|
||||||
|
public sealed class KvSessionOwner
|
||||||
|
{
|
||||||
|
private sealed class OperationLease : IDisposable
|
||||||
|
{
|
||||||
|
private KvSessionOwner owner;
|
||||||
|
internal OperationLease(KvSessionOwner owner)
|
||||||
|
{
|
||||||
|
this.owner = owner;
|
||||||
|
}
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Interlocked.Exchange(ref owner, null)?.ExitOperation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly object admission = new object();
|
||||||
|
private readonly GameServerAccountSession session;
|
||||||
|
private readonly CookieContainer cookies;
|
||||||
|
private readonly CancellationTokenSource lifetime;
|
||||||
|
private readonly Func<long, bool> isCurrent;
|
||||||
|
private readonly KvSyncState state;
|
||||||
|
|
||||||
|
private readonly KvSyncCoordinator coordinator;
|
||||||
|
private readonly TaskCompletionSource<bool> drained =
|
||||||
|
new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
private readonly string ownerId = Guid.NewGuid().ToString("N");
|
||||||
|
private int activeOperations;
|
||||||
|
private bool closing, closed;
|
||||||
|
private TaskCompletionSource<bool> closeCompletion;
|
||||||
|
|
||||||
|
public string OwnerInstanceId => ownerId;
|
||||||
|
public Uri ApiBaseUri
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
public string PlayerId
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
public long Epoch
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
public KVSyncStatus Status => state.Status;
|
||||||
|
|
||||||
|
private KvSessionOwner(GameServerAccountSession session, CookieContainer cookies, Uri api, string playerId,
|
||||||
|
long epoch, Func<long, bool> isCurrent, CancellationTokenSource lifetime,
|
||||||
|
KvSyncState state, HttpClient http)
|
||||||
|
{
|
||||||
|
this.session = session;
|
||||||
|
this.cookies = cookies;
|
||||||
|
ApiBaseUri = api;
|
||||||
|
PlayerId = playerId;
|
||||||
|
Epoch = epoch;
|
||||||
|
this.isCurrent = isCurrent;
|
||||||
|
this.lifetime = lifetime;
|
||||||
|
this.state = state;
|
||||||
|
coordinator = new KvSyncCoordinator(state, new KvClient(http, api), http,
|
||||||
|
IsCapturedOwnerCurrent, lifetime.Token);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static async Task<KvSessionOpenResult> CreateAsync(GameServerAccountSession session, string cacheRoot,
|
||||||
|
string expectedPlayerId, Uri expectedApiBaseUri, long epoch, Func<long, bool> isCurrent,
|
||||||
|
CancellationToken lifetimeToken, Func<CookieContainer, HttpMessageHandler> handlerFactory)
|
||||||
|
{
|
||||||
|
if (session == null || expectedApiBaseUri == null || isCurrent == null || handlerFactory == null || epoch < 0 ||
|
||||||
|
!long.TryParse(expectedPlayerId, NumberStyles.None, CultureInfo.InvariantCulture, out var numericId) || numericId <= 0)
|
||||||
|
{
|
||||||
|
return new KvSessionOpenResult(KVSessionOpenError.Validation);
|
||||||
|
}
|
||||||
|
var playerId = numericId.ToString(CultureInfo.InvariantCulture);
|
||||||
|
Uri expectedApi;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
expectedApi = NormalizeApi(expectedApiBaseUri);
|
||||||
|
}
|
||||||
|
catch (ArgumentException)
|
||||||
|
{
|
||||||
|
return new KvSessionOpenResult(KVSessionOpenError.Validation);
|
||||||
|
}
|
||||||
|
if (lifetimeToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
return new KvSessionOpenResult(KVSessionOpenError.Cancelled);
|
||||||
|
}
|
||||||
|
if (!session.IsAuthenticated || session.Cookies == null)
|
||||||
|
{
|
||||||
|
return new KvSessionOpenResult(KVSessionOpenError.Unauthenticated);
|
||||||
|
}
|
||||||
|
var capturedCookies = session.Cookies;
|
||||||
|
var capturedApi = NormalizeApi(session.ApiBaseUri);
|
||||||
|
if (session.PlayerId != playerId || capturedApi != expectedApi || !isCurrent(epoch))
|
||||||
|
{
|
||||||
|
return new KvSessionOpenResult(KVSessionOpenError.Stale);
|
||||||
|
}
|
||||||
|
|
||||||
|
var opened = KvSyncState.Open(cacheRoot, capturedApi, playerId);
|
||||||
|
if (!opened.Success)
|
||||||
|
{
|
||||||
|
return new KvSessionOpenResult(MapOpen(opened.Error));
|
||||||
|
}
|
||||||
|
HttpClient http = null;
|
||||||
|
CancellationTokenSource lifetime = null;
|
||||||
|
KvSessionOwner owner = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var handler = handlerFactory(capturedCookies);
|
||||||
|
if (handler == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Transport factory returned null.");
|
||||||
|
}
|
||||||
|
http = new HttpClient(handler)
|
||||||
|
{
|
||||||
|
Timeout = TimeSpan.FromSeconds(15)
|
||||||
|
};
|
||||||
|
lifetime = CancellationTokenSource.CreateLinkedTokenSource(lifetimeToken);
|
||||||
|
owner = new KvSessionOwner(session, capturedCookies, capturedApi, playerId, epoch,
|
||||||
|
isCurrent, lifetime, opened.State, http);
|
||||||
|
http = null;
|
||||||
|
lifetime = null;
|
||||||
|
KvResult read;
|
||||||
|
using (var lease = owner.TryEnter())
|
||||||
|
{
|
||||||
|
if (lease == null)
|
||||||
|
{
|
||||||
|
return await owner.FailCreate(KVSessionOpenError.Cancelled);
|
||||||
|
}
|
||||||
|
read = await owner.coordinator.InitializeAsync();
|
||||||
|
}
|
||||||
|
if (!read.Success)
|
||||||
|
{
|
||||||
|
return await owner.FailCreate(read.Error == KVError.Unauthorized
|
||||||
|
? KVSessionOpenError.Unauthorized
|
||||||
|
: read.Error == KVError.Cancelled ? KVSessionOpenError.Cancelled
|
||||||
|
: KVSessionOpenError.ReadFailed);
|
||||||
|
}
|
||||||
|
if (!owner.IsCapturedOwnerCurrent())
|
||||||
|
{
|
||||||
|
return await owner.FailCreate(KVSessionOpenError.Stale);
|
||||||
|
}
|
||||||
|
return new KvSessionOpenResult(KVSessionOpenError.None, owner: owner);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
if (owner != null)
|
||||||
|
{
|
||||||
|
return await owner.FailCreate(KVSessionOpenError.Cancelled);
|
||||||
|
}
|
||||||
|
opened.State.Dispose();
|
||||||
|
return new KvSessionOpenResult(KVSessionOpenError.Cancelled);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
if (owner != null)
|
||||||
|
{
|
||||||
|
return await owner.FailCreate(KVSessionOpenError.Validation);
|
||||||
|
}
|
||||||
|
http?.Dispose();
|
||||||
|
lifetime?.Dispose();
|
||||||
|
opened.State.Dispose();
|
||||||
|
return new KvSessionOpenResult(KVSessionOpenError.Validation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Reads the existing state under an operation lease; does not open another cache.</summary>
|
||||||
|
public KvLocalReadResult ReadLocalSnapshot()
|
||||||
|
{
|
||||||
|
using (var lease = TryEnter())
|
||||||
|
{
|
||||||
|
if (lease == null)
|
||||||
|
{
|
||||||
|
return ReadFailure(KVLocalReadError.StaleOrClosing);
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var data = state.ReadLocal();
|
||||||
|
lock (admission)
|
||||||
|
{
|
||||||
|
if (!IsCapturedOwnerCurrent() || closing || closed)
|
||||||
|
{
|
||||||
|
return ReadFailure(KVLocalReadError.StaleOrClosing);
|
||||||
|
}
|
||||||
|
return new KvLocalReadResult(KVLocalReadError.None, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException)
|
||||||
|
{
|
||||||
|
lock (admission)
|
||||||
|
return ReadFailure(!IsCapturedOwnerCurrent() || closing || closed
|
||||||
|
? KVLocalReadError.StaleOrClosing : KVLocalReadError.NotReady);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private KvLocalReadResult ReadFailure(KVLocalReadError error) =>
|
||||||
|
new KvLocalReadResult(error);
|
||||||
|
|
||||||
|
internal KvStateResult CompareDatas(long nowTicks)
|
||||||
|
{
|
||||||
|
using (var lease = TryEnter())
|
||||||
|
{
|
||||||
|
return lease == null ? new KvStateResult(KVChangeError.Blocked) : coordinator.CompareDatas(nowTicks);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal KvStateResult Update(string key, string value, bool remoteStore, long updateTicks)
|
||||||
|
{
|
||||||
|
using (var lease = TryEnter())
|
||||||
|
{
|
||||||
|
return lease == null ? ClosedEdit() : coordinator.Update(key, value, remoteStore, updateTicks);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<KvFlushResult> SyncDataAsync()
|
||||||
|
{
|
||||||
|
var lease = TryEnter();
|
||||||
|
if (lease == null)
|
||||||
|
{
|
||||||
|
return Task.FromResult(new KvFlushResult(1, 0, KVError.Cancelled));
|
||||||
|
}
|
||||||
|
return RunOrdinarySyncAsync(lease);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<KvFlushResult> RunOrdinarySyncAsync(OperationLease lease)
|
||||||
|
{
|
||||||
|
using (lease)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await coordinator.SyncDataAsync();
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
return new KvFlushResult(1, 0, KVError.Protocol);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> SyncDataImmidiate(string key, string value)
|
||||||
|
{
|
||||||
|
using (var lease = TryEnter())
|
||||||
|
{
|
||||||
|
if (lease == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return await coordinator.SyncDataImmidiate(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal async Task<bool> SyncDataImmidiate(IReadOnlyDictionary<string, string> values)
|
||||||
|
{
|
||||||
|
using (var lease = TryEnter())
|
||||||
|
{
|
||||||
|
if (lease == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return await coordinator.SyncDataImmidiate(values);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private OperationLease TryEnter()
|
||||||
|
{
|
||||||
|
lock (admission)
|
||||||
|
{
|
||||||
|
if (closing || closed || !IsCapturedOwnerCurrent())
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
activeOperations++;
|
||||||
|
return new OperationLease(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ExitOperation()
|
||||||
|
{
|
||||||
|
lock (admission)
|
||||||
|
{
|
||||||
|
activeOperations--;
|
||||||
|
if (closing && activeOperations == 0)
|
||||||
|
{
|
||||||
|
drained.TrySetResult(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task CloseAsync()
|
||||||
|
{
|
||||||
|
TaskCompletionSource<bool> completion;
|
||||||
|
lock (admission)
|
||||||
|
{
|
||||||
|
if (closeCompletion != null)
|
||||||
|
{
|
||||||
|
return closeCompletion.Task;
|
||||||
|
}
|
||||||
|
closing = true;
|
||||||
|
completion = closeCompletion = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
if (activeOperations == 0)
|
||||||
|
{
|
||||||
|
drained.TrySetResult(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = CloseCoreAsync(completion);
|
||||||
|
return completion.Task;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CloseCoreAsync(TaskCompletionSource<bool> completion)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
lifetime.Cancel();
|
||||||
|
coordinator.CloseTickets();
|
||||||
|
await drained.Task;
|
||||||
|
coordinator.Dispose();
|
||||||
|
state.Dispose();
|
||||||
|
lifetime.Dispose();
|
||||||
|
lock (admission)
|
||||||
|
closed = true;
|
||||||
|
completion.TrySetResult(true);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
completion.TrySetException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<KvSessionOpenResult> FailCreate(KVSessionOpenError error)
|
||||||
|
{
|
||||||
|
await CloseAsync();
|
||||||
|
return new KvSessionOpenResult(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsCapturedOwnerCurrent()
|
||||||
|
{
|
||||||
|
if (lifetime.IsCancellationRequested || !isCurrent(Epoch) || state.AuthenticationInvalid)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return session.IsAuthenticated && ReferenceEquals(session.Cookies, cookies) &&
|
||||||
|
session.PlayerId == PlayerId && NormalizeApi(session.ApiBaseUri) == ApiBaseUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Uri NormalizeApi(Uri api)
|
||||||
|
{
|
||||||
|
if (api == null || !api.IsAbsoluteUri || (api.Scheme != "http" && api.Scheme != "https") ||
|
||||||
|
api.UserInfo.Length != 0 || api.Query.Length != 0 || api.Fragment.Length != 0)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Invalid API base URI.");
|
||||||
|
}
|
||||||
|
return new Uri(api.AbsoluteUri.TrimEnd('/') + "/");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static KVSessionOpenError MapOpen(KVOpenError error)
|
||||||
|
{
|
||||||
|
if (error == KVOpenError.Busy)
|
||||||
|
{
|
||||||
|
return KVSessionOpenError.Busy;
|
||||||
|
}
|
||||||
|
if (error == KVOpenError.RecoveryRequired)
|
||||||
|
{
|
||||||
|
return KVSessionOpenError.RecoveryRequired;
|
||||||
|
}
|
||||||
|
if (error == KVOpenError.StorageError)
|
||||||
|
{
|
||||||
|
return KVSessionOpenError.StorageError;
|
||||||
|
}
|
||||||
|
return KVSessionOpenError.Validation;
|
||||||
|
}
|
||||||
|
|
||||||
|
private KvStateResult ClosedEdit()
|
||||||
|
{
|
||||||
|
return new KvStateResult(KVChangeError.Blocked);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum KVLocalReadError
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
StaleOrClosing,
|
||||||
|
NotReady
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>A detached snapshot. Failed reads have no data, never a pretend empty save.</summary>
|
||||||
|
public sealed class KvLocalReadResult
|
||||||
|
{
|
||||||
|
public bool Success => Error == KVLocalReadError.None;
|
||||||
|
public KVLocalReadError Error
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
public IReadOnlyDictionary<string, string> Data
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal KvLocalReadResult(KVLocalReadError error, IReadOnlyDictionary<string, string> data = null)
|
||||||
|
{
|
||||||
|
Error = error;
|
||||||
|
if (error != KVLocalReadError.None)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(data));
|
||||||
|
}
|
||||||
|
var copy = new Dictionary<string, string>(StringComparer.Ordinal);
|
||||||
|
foreach (var item in data)
|
||||||
|
{
|
||||||
|
copy.Add(item.Key, item.Value);
|
||||||
|
}
|
||||||
|
Data = new ReadOnlyDictionary<string, string>(copy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,123 +4,128 @@ using System.Linq;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace game.PlayerDataStorage
|
namespace Core.KV
|
||||||
{
|
{
|
||||||
public enum PlayerKVTicketStatus { Pending, Confirmed, Superseded, Blocked, Unknown, Cancelled }
|
public enum KVTicketStatus
|
||||||
|
|
||||||
public sealed class PlayerKVSyncTicket
|
|
||||||
{
|
{
|
||||||
public string OwnerInstanceId { get; }
|
Pending,
|
||||||
public long Epoch { get; }
|
Confirmed,
|
||||||
public string Key { get; }
|
Superseded,
|
||||||
public long Sequence { get; }
|
Blocked,
|
||||||
internal PlayerKVSyncTicket(string ownerId, long epoch, string key, long sequence)
|
Unknown,
|
||||||
{ OwnerInstanceId = ownerId; Epoch = epoch; Key = key; Sequence = sequence; }
|
Cancelled
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class PlayerKVTicketResult
|
public sealed class KvTicketResult
|
||||||
{
|
{
|
||||||
public PlayerKVTicketStatus Status { get; }
|
public KVTicketStatus Status
|
||||||
public PlayerKVError TransportError { get; }
|
|
||||||
public PlayerKVSyncStatus CacheStatus { get; }
|
|
||||||
internal PlayerKVTicketResult(PlayerKVTicketStatus status, PlayerKVError transportError, PlayerKVSyncStatus cacheStatus)
|
|
||||||
{ Status = status; TransportError = transportError; CacheStatus = cacheStatus; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class PlayerKVEditResult
|
|
||||||
{
|
|
||||||
public bool Accepted => StateResult != null && StateResult.Success;
|
|
||||||
public PlayerKVStateResult StateResult { get; }
|
|
||||||
public PlayerKVSyncTicket Ticket { get; }
|
|
||||||
internal PlayerKVEditResult(PlayerKVStateResult result, PlayerKVSyncTicket ticket = null)
|
|
||||||
{ StateResult = result; Ticket = ticket; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class PlayerKVFlushResult
|
|
||||||
{
|
|
||||||
public int Confirmed { get; }
|
|
||||||
public int Superseded { get; }
|
|
||||||
public int Blocked { get; }
|
|
||||||
public int Unknown { get; }
|
|
||||||
public int Buffered
|
|
||||||
{
|
{
|
||||||
get;
|
get;
|
||||||
}
|
}
|
||||||
public PlayerKVError TransportError { get; }
|
public KVError TransportError
|
||||||
public PlayerKVSyncStatus CacheStatus { get; }
|
|
||||||
internal PlayerKVFlushResult(int confirmed, int superseded, int blocked, int unknown,
|
|
||||||
PlayerKVError transportError, PlayerKVSyncStatus cacheStatus, int buffered = 0)
|
|
||||||
{
|
{
|
||||||
Buffered = buffered;
|
get;
|
||||||
Confirmed = confirmed; Superseded = superseded; Blocked = blocked; Unknown = unknown;
|
}
|
||||||
TransportError = transportError; CacheStatus = cacheStatus;
|
internal KvTicketResult(KVTicketStatus status, KVError transportError)
|
||||||
|
{
|
||||||
|
Status = status;
|
||||||
|
TransportError = transportError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class KvFlushResult
|
||||||
|
{
|
||||||
|
public int Blocked
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
public int Unknown
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
public KVError TransportError
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
internal KvFlushResult(int blocked, int unknown, KVError transportError)
|
||||||
|
{
|
||||||
|
Blocked = blocked;
|
||||||
|
Unknown = unknown;
|
||||||
|
TransportError = transportError;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>One fixed owner's serialized Get/Set/Delete channel. No timers or automatic retries.</summary>
|
/// <summary>One fixed owner's serialized Get/Set/Delete channel. No timers or automatic retries.</summary>
|
||||||
public sealed class PlayerKVSyncCoordinator : IDisposable
|
public sealed class KvSyncCoordinator : IDisposable
|
||||||
{
|
{
|
||||||
sealed class TicketRecord
|
private sealed class TicketRecord
|
||||||
{
|
{
|
||||||
internal readonly PlayerKVSyncTicket Ticket;
|
internal KvTicketResult Result;
|
||||||
internal readonly TaskCompletionSource<PlayerKVTicketResult> Completion =
|
internal bool Dispatched;
|
||||||
new TaskCompletionSource<PlayerKVTicketResult>(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
||||||
internal bool Dispatched, ClaimedByFlush;
|
|
||||||
internal TicketRecord(PlayerKVSyncTicket ticket) { Ticket = ticket; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly object ticketGate = new object();
|
private readonly object ticketGate = new object();
|
||||||
readonly Dictionary<long, TicketRecord> tickets = new Dictionary<long, TicketRecord>();
|
private readonly Dictionary<long, TicketRecord> tickets = new Dictionary<long, TicketRecord>();
|
||||||
readonly SemaphoreSlim dispatch = new SemaphoreSlim(1, 1);
|
private readonly SemaphoreSlim dispatch = new SemaphoreSlim(1, 1);
|
||||||
readonly PlayerKVSyncState state;
|
private readonly KvSyncState state;
|
||||||
readonly PlayerKVClient client;
|
private readonly KvClient client;
|
||||||
readonly System.Net.Http.HttpClient http;
|
private readonly System.Net.Http.HttpClient http;
|
||||||
readonly string ownerId;
|
private readonly Func<bool> isOwnerCurrent;
|
||||||
readonly long epoch;
|
private readonly CancellationToken ownerToken;
|
||||||
readonly Func<bool> isOwnerCurrent;
|
|
||||||
readonly CancellationToken ownerToken;
|
|
||||||
private bool disposed;
|
private bool disposed;
|
||||||
private bool ordinaryDraining;
|
private bool ordinaryDraining;
|
||||||
private TaskCompletionSource<(PlayerKVError Error, Dictionary<long, PlayerKVTicketResult> Results)> ordinaryDrainCompletion;
|
private TaskCompletionSource<(KVError Error, Dictionary<long, KvTicketResult> Results)> ordinaryDrainCompletion;
|
||||||
|
|
||||||
internal PlayerKVSyncCoordinator(PlayerKVSyncState state, PlayerKVClient client,
|
internal KvSyncCoordinator(KvSyncState state, KvClient client,
|
||||||
System.Net.Http.HttpClient http, string ownerId, long epoch, Func<bool> isOwnerCurrent,
|
System.Net.Http.HttpClient http, Func<bool> isOwnerCurrent,
|
||||||
CancellationToken ownerToken)
|
CancellationToken ownerToken)
|
||||||
{
|
{
|
||||||
this.state = state; this.client = client; this.http = http; this.ownerId = ownerId;
|
this.state = state;
|
||||||
this.epoch = epoch; this.isOwnerCurrent = isOwnerCurrent; this.ownerToken = ownerToken;
|
this.client = client;
|
||||||
|
this.http = http;
|
||||||
|
this.isOwnerCurrent = isOwnerCurrent;
|
||||||
|
this.ownerToken = ownerToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal async Task<PlayerKVResult> InitializeAsync(bool prepareOriginalComparison = false)
|
internal async Task<KvResult> InitializeAsync()
|
||||||
{
|
{
|
||||||
await dispatch.WaitAsync(ownerToken);
|
await dispatch.WaitAsync(ownerToken);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!Current()) return CancelledResult();
|
if (!Current())
|
||||||
|
{
|
||||||
|
return CancelledResult();
|
||||||
|
}
|
||||||
if (state.HasPendingComparison)
|
if (state.HasPendingComparison)
|
||||||
{
|
{
|
||||||
return new PlayerKVResult(PlayerKVError.Protocol);
|
return new KvResult(KVError.Protocol);
|
||||||
|
}
|
||||||
|
var result = await client.GetAsync(ownerToken);
|
||||||
|
if (!Current())
|
||||||
|
{
|
||||||
|
return CancelledResult();
|
||||||
}
|
}
|
||||||
var result = await client.GetAsync(null, ownerToken);
|
|
||||||
if (!Current()) return CancelledResult();
|
|
||||||
if (result.Success)
|
if (result.Success)
|
||||||
{
|
{
|
||||||
var reconciled = state.Reconcile(result.Data, result.UpdatedAtUtc, prepareOriginalComparison);
|
var reconciled = state.Reconcile(result.Data, result.UpdatedAtUtc);
|
||||||
return reconciled.Success ? result : new PlayerKVResult(PlayerKVError.Protocol);
|
return reconciled.Success ? result : new KvResult(KVError.Protocol);
|
||||||
}
|
}
|
||||||
state.RecordReadFailure(result.Error == PlayerKVError.Unauthorized);
|
state.RecordReadFailure(result.Error == KVError.Unauthorized);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
finally { dispatch.Release(); }
|
finally
|
||||||
|
{
|
||||||
|
dispatch.Release();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal PlayerKVStateResult CompareDatas(long nowTicks)
|
internal KvStateResult CompareDatas(long nowTicks)
|
||||||
{
|
{
|
||||||
lock (ticketGate)
|
lock (ticketGate)
|
||||||
{
|
{
|
||||||
if (!Current())
|
if (!Current())
|
||||||
{
|
{
|
||||||
return new PlayerKVStateResult(PlayerKVChangeError.Blocked);
|
return new KvStateResult(KVChangeError.Blocked);
|
||||||
}
|
}
|
||||||
var previous = Unsent(state.Inspect()).Select(item => item.Sequence).ToArray();
|
var previous = Unsent(state.Inspect()).Select(item => item.Sequence).ToArray();
|
||||||
var result = state.CompareDatas(nowTicks);
|
var result = state.CompareDatas(nowTicks);
|
||||||
@@ -132,7 +137,7 @@ namespace game.PlayerDataStorage
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal PlayerKVEditResult Update(string key, string value, bool remoteStore, long? updateTicks = null)
|
internal KvStateResult Update(string key, string value, bool remoteStore, long updateTicks)
|
||||||
{
|
{
|
||||||
lock (ticketGate)
|
lock (ticketGate)
|
||||||
{
|
{
|
||||||
@@ -140,37 +145,19 @@ namespace game.PlayerDataStorage
|
|||||||
var result = state.Update(key, value, remoteStore, updateTicks);
|
var result = state.Update(key, value, remoteStore, updateTicks);
|
||||||
if (!result.Success)
|
if (!result.Success)
|
||||||
{
|
{
|
||||||
return new PlayerKVEditResult(result);
|
return result;
|
||||||
}
|
}
|
||||||
CompleteRemovedSources(previous);
|
CompleteRemovedSources(previous);
|
||||||
if (!remoteStore || !Unsent(state.Inspect()).Any(item => item.Sequence == result.AttemptSequence))
|
if (!remoteStore || !Unsent(state.Inspect()).Any(item => item.Sequence == result.AttemptSequence))
|
||||||
{
|
{
|
||||||
return new PlayerKVEditResult(result);
|
return result;
|
||||||
}
|
}
|
||||||
var ticket = new PlayerKVSyncTicket(ownerId, epoch, key, result.AttemptSequence);
|
tickets.Add(result.AttemptSequence, new TicketRecord());
|
||||||
tickets.Add(ticket.Sequence, new TicketRecord(ticket));
|
return result;
|
||||||
return new PlayerKVEditResult(result, ticket);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal PlayerKVEditResult Delete(string key)
|
private static IEnumerable<KvChange> Unsent(KvDiagnosticSnapshot value)
|
||||||
{
|
|
||||||
lock (ticketGate)
|
|
||||||
{
|
|
||||||
var previous = Unsent(state.Inspect()).Select(item => item.Sequence).ToArray();
|
|
||||||
var result = state.Delete(key);
|
|
||||||
if (!result.Success)
|
|
||||||
{
|
|
||||||
return new PlayerKVEditResult(result);
|
|
||||||
}
|
|
||||||
CompleteRemovedSources(previous);
|
|
||||||
var ticket = new PlayerKVSyncTicket(ownerId, epoch, key, result.AttemptSequence);
|
|
||||||
tickets.Add(ticket.Sequence, new TicketRecord(ticket));
|
|
||||||
return new PlayerKVEditResult(result, ticket);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static IEnumerable<PlayerKVChange> Unsent(PlayerKVDiagnosticSnapshot value)
|
|
||||||
{
|
{
|
||||||
return value.OrdinaryBuffers.SelectMany(bucket => bucket.Values)
|
return value.OrdinaryBuffers.SelectMany(bucket => bucket.Values)
|
||||||
.Concat(value.LegacyPending.Values).Concat(value.ExplicitDeletes.Values);
|
.Concat(value.LegacyPending.Values).Concat(value.ExplicitDeletes.Values);
|
||||||
@@ -182,16 +169,16 @@ namespace game.PlayerDataStorage
|
|||||||
foreach (var sequence in previous)
|
foreach (var sequence in previous)
|
||||||
{
|
{
|
||||||
if (!remaining.Contains(sequence) && tickets.TryGetValue(sequence, out var record) &&
|
if (!remaining.Contains(sequence) && tickets.TryGetValue(sequence, out var record) &&
|
||||||
!record.Dispatched && !record.Completion.Task.IsCompleted)
|
!record.Dispatched && record.Result == null)
|
||||||
{
|
{
|
||||||
Complete(record, PlayerKVTicketStatus.Superseded, PlayerKVError.None);
|
Complete(record, KVTicketStatus.Superseded, KVError.None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal Task<bool> SyncDataImmidiate(string key, string value)
|
internal Task<bool> SyncDataImmidiate(string key, string value)
|
||||||
{
|
{
|
||||||
if (!KVSnapshot.ValidKey(key) || value == null)
|
if (!KvSnapshot.ValidKey(key) || value == null)
|
||||||
{
|
{
|
||||||
return Task.FromResult(false);
|
return Task.FromResult(false);
|
||||||
}
|
}
|
||||||
@@ -205,7 +192,7 @@ namespace game.PlayerDataStorage
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
var data = values.ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.Ordinal);
|
var data = values.ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.Ordinal);
|
||||||
IReadOnlyList<PlayerKVChange> operations;
|
IReadOnlyList<KvChange> operations;
|
||||||
lock (ticketGate)
|
lock (ticketGate)
|
||||||
{
|
{
|
||||||
var previous = Unsent(state.Inspect()).Select(item => item.Sequence).ToArray();
|
var previous = Unsent(state.Inspect()).Select(item => item.Sequence).ToArray();
|
||||||
@@ -221,7 +208,7 @@ namespace game.PlayerDataStorage
|
|||||||
{
|
{
|
||||||
await dispatch.WaitAsync(ownerToken);
|
await dispatch.WaitAsync(ownerToken);
|
||||||
acquired = true;
|
acquired = true;
|
||||||
if (!Current() || state.Status != PlayerKVSyncStatus.Ready)
|
if (!Current() || state.Status != KVSyncStatus.Ready)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -235,7 +222,7 @@ namespace game.PlayerDataStorage
|
|||||||
{
|
{
|
||||||
return state.CompleteImmediate(operations).Success;
|
return state.CompleteImmediate(operations).Success;
|
||||||
}
|
}
|
||||||
if (result.Error == PlayerKVError.Unauthorized)
|
if (result.Error == KVError.Unauthorized)
|
||||||
{
|
{
|
||||||
state.RecordReadFailure(true);
|
state.RecordReadFailure(true);
|
||||||
}
|
}
|
||||||
@@ -258,53 +245,32 @@ namespace game.PlayerDataStorage
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal long CaptureWatermark(out long[] targetSequences)
|
private static IEnumerable<KvChange> Outstanding(KvDiagnosticSnapshot snapshot)
|
||||||
{
|
|
||||||
lock (ticketGate)
|
|
||||||
{
|
|
||||||
var snapshot = state.Inspect();
|
|
||||||
var watermark = snapshot.NextSequence <= 1 ? 0 : snapshot.NextSequence - 1;
|
|
||||||
var target = new HashSet<long>(Outstanding(snapshot).Where(item => item.Sequence <= watermark).Select(item => item.Sequence));
|
|
||||||
foreach (var record in tickets.Values)
|
|
||||||
{
|
|
||||||
if (record.Ticket.Sequence <= watermark && !record.ClaimedByFlush)
|
|
||||||
{
|
|
||||||
record.ClaimedByFlush = true;
|
|
||||||
target.Add(record.Ticket.Sequence);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
targetSequences = target.ToArray();
|
|
||||||
return watermark;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static IEnumerable<PlayerKVChange> Outstanding(PlayerKVDiagnosticSnapshot snapshot)
|
|
||||||
{
|
{
|
||||||
return Unsent(snapshot).Concat(snapshot.PreparedOrdinaryBatches.SelectMany(batch => batch.Items))
|
return Unsent(snapshot).Concat(snapshot.PreparedOrdinaryBatches.SelectMany(batch => batch.Items))
|
||||||
.Concat(snapshot.OrdinaryUnknownBatches.SelectMany(batch => batch.Items))
|
.Concat(snapshot.OrdinaryUnknownBatches.SelectMany(batch => batch.Items))
|
||||||
.Concat(snapshot.ActiveBatch == null ? Enumerable.Empty<PlayerKVChange>() : snapshot.ActiveBatch.Items)
|
.Concat(snapshot.ActiveBatch == null ? Enumerable.Empty<KvChange>() : snapshot.ActiveBatch.Items)
|
||||||
.Concat(snapshot.UnknownBatch == null ? Enumerable.Empty<PlayerKVChange>() : snapshot.UnknownBatch.Items);
|
.Concat(snapshot.UnknownBatch == null ? Enumerable.Empty<KvChange>() : snapshot.UnknownBatch.Items);
|
||||||
}
|
}
|
||||||
|
|
||||||
// This explicit channel only sends imported legacy intents and explicit deletes, never ordinary buckets.
|
// This channel sends one imported legacy batch, never ordinary buckets.
|
||||||
internal async Task<PlayerKVFlushResult> FlushThroughAsync(long watermark, long[] targetSequences,
|
internal async Task<KvFlushResult> FlushThroughAsync(long watermark, long[] targetSequences)
|
||||||
bool legacyOnly = false, int maximumBatches = int.MaxValue)
|
|
||||||
{
|
{
|
||||||
var error = PlayerKVError.None;
|
var error = KVError.None;
|
||||||
var observed = new Dictionary<long, PlayerKVTicketStatus>();
|
var observed = new Dictionary<long, KVTicketStatus>();
|
||||||
var acquired = false;
|
var acquired = false;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await dispatch.WaitAsync(ownerToken);
|
await dispatch.WaitAsync(ownerToken);
|
||||||
acquired = true;
|
acquired = true;
|
||||||
var sent = 0;
|
var sent = 0;
|
||||||
while (Current() && sent < maximumBatches)
|
while (Current() && sent < 1)
|
||||||
{
|
{
|
||||||
PlayerKVStateResult begin;
|
KvStateResult begin;
|
||||||
PlayerKVBatch batch;
|
KvBatch batch;
|
||||||
lock (ticketGate)
|
lock (ticketGate)
|
||||||
{
|
{
|
||||||
begin = state.BeginBatch(watermark, legacyOnly, out batch);
|
begin = state.BeginBatch(watermark, out batch);
|
||||||
if (begin.Success && batch != null)
|
if (begin.Success && batch != null)
|
||||||
{
|
{
|
||||||
MarkDispatched(batch);
|
MarkDispatched(batch);
|
||||||
@@ -312,7 +278,7 @@ namespace game.PlayerDataStorage
|
|||||||
}
|
}
|
||||||
if (!begin.Success)
|
if (!begin.Success)
|
||||||
{
|
{
|
||||||
MarkExplicitBlocked(watermark, legacyOnly, PlayerKVError.None, observed);
|
MarkExplicitBlocked(watermark, KVError.None, observed);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (batch == null)
|
if (batch == null)
|
||||||
@@ -323,13 +289,13 @@ namespace game.PlayerDataStorage
|
|||||||
var result = await SendAsync(batch);
|
var result = await SendAsync(batch);
|
||||||
if (!Current() && result.Success)
|
if (!Current() && result.Success)
|
||||||
{
|
{
|
||||||
result = new PlayerKVResult(PlayerKVError.Cancelled, mayHaveCommitted: true);
|
result = new KvResult(KVError.Cancelled, mayHaveCommitted: true);
|
||||||
}
|
}
|
||||||
if (result.Success)
|
if (result.Success)
|
||||||
{
|
{
|
||||||
var confirmed = state.ConfirmBatch(batch.Id);
|
var confirmed = state.ConfirmBatch(batch.Id);
|
||||||
var outcome = confirmed.Success ? PlayerKVTicketStatus.Confirmed : PlayerKVTicketStatus.Unknown;
|
var outcome = confirmed.Success ? KVTicketStatus.Confirmed : KVTicketStatus.Unknown;
|
||||||
RecordBatch(batch, outcome, PlayerKVError.None, observed);
|
RecordBatch(batch, outcome, KVError.None, observed);
|
||||||
if (!confirmed.Success)
|
if (!confirmed.Success)
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
@@ -337,27 +303,27 @@ namespace game.PlayerDataStorage
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
error = result.Error;
|
error = result.Error;
|
||||||
state.FailBatch(batch.Id, result.MayHaveCommitted, result.Error == PlayerKVError.Unauthorized);
|
state.FailBatch(batch.Id, result.MayHaveCommitted, result.Error == KVError.Unauthorized);
|
||||||
var failedStatus = result.MayHaveCommitted ? PlayerKVTicketStatus.Unknown :
|
var failedStatus = result.MayHaveCommitted ? KVTicketStatus.Unknown :
|
||||||
ownerToken.IsCancellationRequested ? PlayerKVTicketStatus.Cancelled : PlayerKVTicketStatus.Blocked;
|
ownerToken.IsCancellationRequested ? KVTicketStatus.Cancelled : KVTicketStatus.Blocked;
|
||||||
RecordBatch(batch, failedStatus, result.Error, observed);
|
RecordBatch(batch, failedStatus, result.Error, observed);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (!Current())
|
if (!Current())
|
||||||
{
|
{
|
||||||
error = PlayerKVError.Cancelled;
|
error = KVError.Cancelled;
|
||||||
CloseTickets();
|
CloseTickets();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
error = PlayerKVError.Cancelled;
|
error = KVError.Cancelled;
|
||||||
CloseTickets();
|
CloseTickets();
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
error = PlayerKVError.Protocol;
|
error = KVError.Protocol;
|
||||||
MarkExplicitBlocked(watermark, legacyOnly, error, observed);
|
MarkExplicitBlocked(watermark, error, observed);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -369,9 +335,9 @@ namespace game.PlayerDataStorage
|
|||||||
return Summary(targetSequences ?? Array.Empty<long>(), error, observed);
|
return Summary(targetSequences ?? Array.Empty<long>(), error, observed);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal Task<PlayerKVFlushResult> SyncDataAsync()
|
internal Task<KvFlushResult> SyncDataAsync()
|
||||||
{
|
{
|
||||||
TaskCompletionSource<(PlayerKVError Error, Dictionary<long, PlayerKVTicketResult> Results)> completion;
|
TaskCompletionSource<(KVError Error, Dictionary<long, KvTicketResult> Results)> completion;
|
||||||
bool start;
|
bool start;
|
||||||
long[] targets;
|
long[] targets;
|
||||||
lock (ticketGate)
|
lock (ticketGate)
|
||||||
@@ -379,12 +345,12 @@ namespace game.PlayerDataStorage
|
|||||||
var snapshot = state.Inspect();
|
var snapshot = state.Inspect();
|
||||||
if (!Current())
|
if (!Current())
|
||||||
{
|
{
|
||||||
return Task.FromResult(new PlayerKVFlushResult(0, 0, 1, 0, PlayerKVError.Cancelled, state.Status));
|
return Task.FromResult(new KvFlushResult(1, 0, KVError.Cancelled));
|
||||||
}
|
}
|
||||||
if (snapshot.LegacyPending.Count != 0)
|
if (snapshot.LegacyPending.Count != 0)
|
||||||
{
|
{
|
||||||
var watermark = snapshot.NextSequence - 1;
|
var watermark = snapshot.NextSequence - 1;
|
||||||
return FlushThroughAsync(watermark, snapshot.LegacyPending.Values.Select(item => item.Sequence).ToArray(), true, 1);
|
return FlushThroughAsync(watermark, snapshot.LegacyPending.Values.Select(item => item.Sequence).ToArray());
|
||||||
}
|
}
|
||||||
var captured = state.CaptureOrdinaryBatch(out var batch);
|
var captured = state.CaptureOrdinaryBatch(out var batch);
|
||||||
if (!captured.Success)
|
if (!captured.Success)
|
||||||
@@ -393,28 +359,28 @@ namespace game.PlayerDataStorage
|
|||||||
{
|
{
|
||||||
if (tickets.TryGetValue(item.Sequence, out var record))
|
if (tickets.TryGetValue(item.Sequence, out var record))
|
||||||
{
|
{
|
||||||
Complete(record, PlayerKVTicketStatus.Blocked, PlayerKVError.None);
|
Complete(record, KVTicketStatus.Blocked, KVError.None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Task.FromResult(new PlayerKVFlushResult(0, 0, 1, 0, PlayerKVError.None, state.Status));
|
return Task.FromResult(new KvFlushResult(1, 0, KVError.None));
|
||||||
}
|
}
|
||||||
snapshot = state.Inspect();
|
snapshot = state.Inspect();
|
||||||
if (batch == null && ordinaryDraining)
|
if (batch == null && ordinaryDraining)
|
||||||
{
|
{
|
||||||
// The existing caller owns the queued work; an empty tick has no new result to observe.
|
// The existing caller owns the queued work; an empty tick has no new result to observe.
|
||||||
return Task.FromResult(Summary(Array.Empty<long>(), PlayerKVError.None));
|
return Task.FromResult(Summary(Array.Empty<long>(), KVError.None));
|
||||||
}
|
}
|
||||||
targets = batch != null ? batch.Items.Select(item => item.Sequence).ToArray() :
|
targets = batch != null ? batch.Items.Select(item => item.Sequence).ToArray() :
|
||||||
snapshot.PreparedOrdinaryBatches.SelectMany(item => item.Items).Select(item => item.Sequence).ToArray();
|
snapshot.PreparedOrdinaryBatches.SelectMany(item => item.Items).Select(item => item.Sequence).ToArray();
|
||||||
if (snapshot.PreparedOrdinaryBatches.Count == 0)
|
if (snapshot.PreparedOrdinaryBatches.Count == 0)
|
||||||
{
|
{
|
||||||
return Task.FromResult(Summary(targets, PlayerKVError.None));
|
return Task.FromResult(Summary(targets, KVError.None));
|
||||||
}
|
}
|
||||||
start = !ordinaryDraining;
|
start = !ordinaryDraining;
|
||||||
if (start)
|
if (start)
|
||||||
{
|
{
|
||||||
ordinaryDraining = true;
|
ordinaryDraining = true;
|
||||||
ordinaryDrainCompletion = new TaskCompletionSource<(PlayerKVError Error, Dictionary<long, PlayerKVTicketResult> Results)>(TaskCreationOptions.RunContinuationsAsynchronously);
|
ordinaryDrainCompletion = new TaskCompletionSource<(KVError Error, Dictionary<long, KvTicketResult> Results)>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
}
|
}
|
||||||
completion = ordinaryDrainCompletion;
|
completion = ordinaryDrainCompletion;
|
||||||
}
|
}
|
||||||
@@ -426,31 +392,31 @@ namespace game.PlayerDataStorage
|
|||||||
return ObserveOrdinaryAsync(completion.Task, targets);
|
return ObserveOrdinaryAsync(completion.Task, targets);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<PlayerKVFlushResult> ObserveOrdinaryAsync(Task<(PlayerKVError Error, Dictionary<long, PlayerKVTicketResult> Results)> worker, long[] targets)
|
private async Task<KvFlushResult> ObserveOrdinaryAsync(Task<(KVError Error, Dictionary<long, KvTicketResult> Results)> worker, long[] targets)
|
||||||
{
|
{
|
||||||
var completed = await worker;
|
var completed = await worker;
|
||||||
var error = targets.Where(completed.Results.ContainsKey).Select(sequence => completed.Results[sequence].TransportError)
|
var error = targets.Where(completed.Results.ContainsKey).Select(sequence => completed.Results[sequence].TransportError)
|
||||||
.FirstOrDefault(value => value != PlayerKVError.None);
|
.FirstOrDefault(value => value != KVError.None);
|
||||||
if (error == PlayerKVError.None && targets.Any(sequence => !completed.Results.ContainsKey(sequence)))
|
if (error == KVError.None && targets.Any(sequence => !completed.Results.ContainsKey(sequence)))
|
||||||
{
|
{
|
||||||
error = completed.Error;
|
error = completed.Error;
|
||||||
}
|
}
|
||||||
return Summary(targets, error, completed.Results.ToDictionary(pair => pair.Key, pair => pair.Value.Status));
|
return Summary(targets, error, completed.Results.ToDictionary(pair => pair.Key, pair => pair.Value.Status));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task DrainOrdinaryAsync(TaskCompletionSource<(PlayerKVError Error, Dictionary<long, PlayerKVTicketResult> Results)> completion)
|
private async Task DrainOrdinaryAsync(TaskCompletionSource<(KVError Error, Dictionary<long, KvTicketResult> Results)> completion)
|
||||||
{
|
{
|
||||||
var error = PlayerKVError.None;
|
var error = KVError.None;
|
||||||
var observed = new Dictionary<long, PlayerKVTicketResult>();
|
var observed = new Dictionary<long, KvTicketResult>();
|
||||||
void RecordOrdinaryBatch(PlayerKVBatch batch, PlayerKVTicketStatus status, PlayerKVError transportError)
|
void RecordOrdinaryBatch(KvBatch batch, KVTicketStatus status, KVError transportError)
|
||||||
{
|
{
|
||||||
CompleteBatch(batch, status, transportError);
|
CompleteBatch(batch, status, transportError);
|
||||||
foreach (var item in batch.Items)
|
foreach (var item in batch.Items)
|
||||||
{
|
{
|
||||||
observed[item.Sequence] = new PlayerKVTicketResult(status, transportError, state.Status);
|
observed[item.Sequence] = new KvTicketResult(status, transportError);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
PlayerKVBatch active = null;
|
KvBatch active = null;
|
||||||
var acquired = false;
|
var acquired = false;
|
||||||
var dispatched = false;
|
var dispatched = false;
|
||||||
try
|
try
|
||||||
@@ -459,7 +425,7 @@ namespace game.PlayerDataStorage
|
|||||||
{
|
{
|
||||||
await dispatch.WaitAsync(ownerToken);
|
await dispatch.WaitAsync(ownerToken);
|
||||||
acquired = true;
|
acquired = true;
|
||||||
PlayerKVStateResult begin;
|
KvStateResult begin;
|
||||||
lock (ticketGate)
|
lock (ticketGate)
|
||||||
{
|
{
|
||||||
begin = state.BeginOrdinaryBatch(out active);
|
begin = state.BeginOrdinaryBatch(out active);
|
||||||
@@ -473,7 +439,7 @@ namespace game.PlayerDataStorage
|
|||||||
{
|
{
|
||||||
if (tickets.TryGetValue(item.Sequence, out var record))
|
if (tickets.TryGetValue(item.Sequence, out var record))
|
||||||
{
|
{
|
||||||
Complete(record, PlayerKVTicketStatus.Blocked, PlayerKVError.None);
|
Complete(record, KVTicketStatus.Blocked, KVError.None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -485,12 +451,12 @@ namespace game.PlayerDataStorage
|
|||||||
var result = await SendAsync(active);
|
var result = await SendAsync(active);
|
||||||
if (!Current() && result.Success)
|
if (!Current() && result.Success)
|
||||||
{
|
{
|
||||||
result = new PlayerKVResult(PlayerKVError.Cancelled, mayHaveCommitted: true);
|
result = new KvResult(KVError.Cancelled, mayHaveCommitted: true);
|
||||||
}
|
}
|
||||||
if (result.Success)
|
if (result.Success)
|
||||||
{
|
{
|
||||||
var confirmed = state.ConfirmBatch(active.Id);
|
var confirmed = state.ConfirmBatch(active.Id);
|
||||||
RecordOrdinaryBatch(active, confirmed.Success ? PlayerKVTicketStatus.Confirmed : PlayerKVTicketStatus.Unknown, PlayerKVError.None);
|
RecordOrdinaryBatch(active, confirmed.Success ? KVTicketStatus.Confirmed : KVTicketStatus.Unknown, KVError.None);
|
||||||
if (!confirmed.Success)
|
if (!confirmed.Success)
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
@@ -499,10 +465,10 @@ namespace game.PlayerDataStorage
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
error = result.Error;
|
error = result.Error;
|
||||||
var failed = state.FailBatch(active.Id, result.MayHaveCommitted, result.Error == PlayerKVError.Unauthorized);
|
var failed = state.FailBatch(active.Id, result.MayHaveCommitted, result.Error == KVError.Unauthorized);
|
||||||
RecordOrdinaryBatch(active, result.MayHaveCommitted ? PlayerKVTicketStatus.Unknown :
|
RecordOrdinaryBatch(active, result.MayHaveCommitted ? KVTicketStatus.Unknown :
|
||||||
ownerToken.IsCancellationRequested ? PlayerKVTicketStatus.Cancelled : PlayerKVTicketStatus.Blocked, result.Error);
|
ownerToken.IsCancellationRequested ? KVTicketStatus.Cancelled : KVTicketStatus.Blocked, result.Error);
|
||||||
if (!failed.Success || state.Status != PlayerKVSyncStatus.Ready)
|
if (!failed.Success || state.Status != KVSyncStatus.Ready)
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -515,20 +481,20 @@ namespace game.PlayerDataStorage
|
|||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
error = PlayerKVError.Cancelled;
|
error = KVError.Cancelled;
|
||||||
if (active != null)
|
if (active != null)
|
||||||
{
|
{
|
||||||
state.FailBatch(active.Id, dispatched);
|
state.FailBatch(active.Id, dispatched);
|
||||||
RecordOrdinaryBatch(active, dispatched ? PlayerKVTicketStatus.Unknown : PlayerKVTicketStatus.Cancelled, error);
|
RecordOrdinaryBatch(active, dispatched ? KVTicketStatus.Unknown : KVTicketStatus.Cancelled, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
error = PlayerKVError.Protocol;
|
error = KVError.Protocol;
|
||||||
if (active != null)
|
if (active != null)
|
||||||
{
|
{
|
||||||
state.FailBatch(active.Id, dispatched);
|
state.FailBatch(active.Id, dispatched);
|
||||||
RecordOrdinaryBatch(active, dispatched ? PlayerKVTicketStatus.Unknown : PlayerKVTicketStatus.Blocked, error);
|
RecordOrdinaryBatch(active, dispatched ? KVTicketStatus.Unknown : KVTicketStatus.Blocked, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@@ -553,14 +519,14 @@ namespace game.PlayerDataStorage
|
|||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
error = PlayerKVError.Protocol;
|
error = KVError.Protocol;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
completion.TrySetResult((error, observed));
|
completion.TrySetResult((error, observed));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task<PlayerKVResult> SendAsync(PlayerKVBatch batch)
|
private Task<KvResult> SendAsync(KvBatch batch)
|
||||||
{
|
{
|
||||||
if (batch.IsDelete)
|
if (batch.IsDelete)
|
||||||
{
|
{
|
||||||
@@ -569,8 +535,8 @@ namespace game.PlayerDataStorage
|
|||||||
return client.SetAsync(batch.Items.ToDictionary(item => item.Key, item => item.Target.Value, StringComparer.Ordinal), ownerToken);
|
return client.SetAsync(batch.Items.ToDictionary(item => item.Key, item => item.Target.Value, StringComparer.Ordinal), ownerToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RecordBatch(PlayerKVBatch batch, PlayerKVTicketStatus status, PlayerKVError error,
|
private void RecordBatch(KvBatch batch, KVTicketStatus status, KVError error,
|
||||||
Dictionary<long, PlayerKVTicketStatus> observed)
|
Dictionary<long, KVTicketStatus> observed)
|
||||||
{
|
{
|
||||||
CompleteBatch(batch, status, error);
|
CompleteBatch(batch, status, error);
|
||||||
foreach (var item in batch.Items)
|
foreach (var item in batch.Items)
|
||||||
@@ -579,19 +545,19 @@ namespace game.PlayerDataStorage
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void MarkExplicitBlocked(long watermark, bool legacyOnly, PlayerKVError error,
|
private void MarkExplicitBlocked(long watermark, KVError error,
|
||||||
Dictionary<long, PlayerKVTicketStatus> observed)
|
Dictionary<long, KVTicketStatus> observed)
|
||||||
{
|
{
|
||||||
lock (ticketGate)
|
lock (ticketGate)
|
||||||
{
|
{
|
||||||
var snapshot = state.Inspect();
|
var snapshot = state.Inspect();
|
||||||
var source = legacyOnly ? snapshot.LegacyPending.Values : snapshot.LegacyPending.Values.Concat(snapshot.ExplicitDeletes.Values);
|
var source = snapshot.LegacyPending.Values;
|
||||||
foreach (var item in source.Where(item => item.Sequence <= watermark))
|
foreach (var item in source.Where(item => item.Sequence <= watermark))
|
||||||
{
|
{
|
||||||
var outcome = PlayerKVTicketStatus.Blocked;
|
var outcome = KVTicketStatus.Blocked;
|
||||||
if (tickets.TryGetValue(item.Sequence, out var record))
|
if (tickets.TryGetValue(item.Sequence, out var record))
|
||||||
{
|
{
|
||||||
outcome = record.Dispatched ? PlayerKVTicketStatus.Unknown : PlayerKVTicketStatus.Blocked;
|
outcome = record.Dispatched ? KVTicketStatus.Unknown : KVTicketStatus.Blocked;
|
||||||
Complete(record, outcome, error);
|
Complete(record, outcome, error);
|
||||||
}
|
}
|
||||||
observed[item.Sequence] = outcome;
|
observed[item.Sequence] = outcome;
|
||||||
@@ -599,58 +565,55 @@ namespace game.PlayerDataStorage
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal async Task<PlayerKVTicketResult> WaitForTicketAsync(PlayerKVSyncTicket ticket, CancellationToken token)
|
|
||||||
{
|
|
||||||
if (ticket == null || ticket.OwnerInstanceId != ownerId || ticket.Epoch != epoch)
|
|
||||||
return new PlayerKVTicketResult(PlayerKVTicketStatus.Blocked, PlayerKVError.Validation, state.Status);
|
|
||||||
TicketRecord record;
|
|
||||||
lock (ticketGate)
|
|
||||||
if (!tickets.TryGetValue(ticket.Sequence, out record) || record.Ticket.Key != ticket.Key)
|
|
||||||
return new PlayerKVTicketResult(PlayerKVTicketStatus.Blocked, PlayerKVError.Validation, state.Status);
|
|
||||||
if (!token.CanBeCanceled) return await record.Completion.Task;
|
|
||||||
if (token.IsCancellationRequested)
|
|
||||||
return new PlayerKVTicketResult(PlayerKVTicketStatus.Cancelled, PlayerKVError.Cancelled, state.Status);
|
|
||||||
var cancelled = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
||||||
using (token.Register(() => cancelled.TrySetResult(true)))
|
|
||||||
{
|
|
||||||
if (await Task.WhenAny(record.Completion.Task, cancelled.Task) == record.Completion.Task)
|
|
||||||
return await record.Completion.Task;
|
|
||||||
}
|
|
||||||
return new PlayerKVTicketResult(PlayerKVTicketStatus.Cancelled, PlayerKVError.Cancelled, state.Status);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void CloseTickets()
|
internal void CloseTickets()
|
||||||
{
|
{
|
||||||
lock (ticketGate)
|
lock (ticketGate)
|
||||||
|
{
|
||||||
foreach (var record in tickets.Values)
|
foreach (var record in tickets.Values)
|
||||||
if (!record.Completion.Task.IsCompleted)
|
{
|
||||||
Complete(record, record.Dispatched ? PlayerKVTicketStatus.Unknown : PlayerKVTicketStatus.Cancelled,
|
if (record.Result == null)
|
||||||
PlayerKVError.Cancelled);
|
{
|
||||||
|
Complete(record, record.Dispatched ? KVTicketStatus.Unknown : KVTicketStatus.Cancelled,
|
||||||
|
KVError.Cancelled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void MarkDispatched(PlayerKVBatch batch)
|
private void MarkDispatched(KvBatch batch)
|
||||||
{
|
{
|
||||||
lock (ticketGate)
|
lock (ticketGate)
|
||||||
foreach (var item in batch.Items)
|
foreach (var item in batch.Items)
|
||||||
if (tickets.TryGetValue(item.Sequence, out var record)) record.Dispatched = true;
|
if (tickets.TryGetValue(item.Sequence, out var record))
|
||||||
|
{
|
||||||
|
record.Dispatched = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void CompleteBatch(PlayerKVBatch batch, PlayerKVTicketStatus status, PlayerKVError error)
|
private void CompleteBatch(KvBatch batch, KVTicketStatus status, KVError error)
|
||||||
{
|
{
|
||||||
lock (ticketGate)
|
lock (ticketGate)
|
||||||
foreach (var item in batch.Items)
|
foreach (var item in batch.Items)
|
||||||
if (tickets.TryGetValue(item.Sequence, out var record)) Complete(record, status, error);
|
if (tickets.TryGetValue(item.Sequence, out var record))
|
||||||
|
{
|
||||||
|
Complete(record, status, error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Complete(TicketRecord record, PlayerKVTicketStatus status, PlayerKVError error) =>
|
private void Complete(TicketRecord record, KVTicketStatus status, KVError error)
|
||||||
record.Completion.TrySetResult(new PlayerKVTicketResult(status, error, state.Status));
|
{
|
||||||
|
if (record.Result == null)
|
||||||
|
{
|
||||||
|
record.Result = new KvTicketResult(status, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private PlayerKVFlushResult Summary(IEnumerable<long> targetSequences, PlayerKVError error,
|
private KvFlushResult Summary(IEnumerable<long> targetSequences, KVError error,
|
||||||
Dictionary<long, PlayerKVTicketStatus> observed = null)
|
Dictionary<long, KVTicketStatus> observed = null)
|
||||||
{
|
{
|
||||||
lock (ticketGate)
|
lock (ticketGate)
|
||||||
{
|
{
|
||||||
var counts = new Dictionary<PlayerKVTicketStatus, int>();
|
var counts = new Dictionary<KVTicketStatus, int>();
|
||||||
var snapshot = state.Inspect();
|
var snapshot = state.Inspect();
|
||||||
var unknown = new HashSet<long>(snapshot.OrdinaryUnknownBatches.SelectMany(batch => batch.Items).Select(item => item.Sequence));
|
var unknown = new HashSet<long>(snapshot.OrdinaryUnknownBatches.SelectMany(batch => batch.Items).Select(item => item.Sequence));
|
||||||
if (snapshot.UnknownBatch != null)
|
if (snapshot.UnknownBatch != null)
|
||||||
@@ -660,10 +623,10 @@ namespace game.PlayerDataStorage
|
|||||||
var outstanding = new HashSet<long>(Outstanding(snapshot).Select(item => item.Sequence));
|
var outstanding = new HashSet<long>(Outstanding(snapshot).Select(item => item.Sequence));
|
||||||
foreach (var sequence in targetSequences.Distinct())
|
foreach (var sequence in targetSequences.Distinct())
|
||||||
{
|
{
|
||||||
PlayerKVTicketStatus outcome;
|
KVTicketStatus outcome;
|
||||||
if (tickets.TryGetValue(sequence, out var record) && record.Completion.Task.IsCompleted)
|
if (tickets.TryGetValue(sequence, out var record) && record.Result != null)
|
||||||
{
|
{
|
||||||
outcome = record.Completion.Task.GetAwaiter().GetResult().Status;
|
outcome = record.Result.Status;
|
||||||
}
|
}
|
||||||
else if (observed != null && observed.TryGetValue(sequence, out var actual))
|
else if (observed != null && observed.TryGetValue(sequence, out var actual))
|
||||||
{
|
{
|
||||||
@@ -671,11 +634,11 @@ namespace game.PlayerDataStorage
|
|||||||
}
|
}
|
||||||
else if (unknown.Contains(sequence))
|
else if (unknown.Contains(sequence))
|
||||||
{
|
{
|
||||||
outcome = PlayerKVTicketStatus.Unknown;
|
outcome = KVTicketStatus.Unknown;
|
||||||
}
|
}
|
||||||
else if (outstanding.Contains(sequence) || tickets.ContainsKey(sequence))
|
else if (outstanding.Contains(sequence) || tickets.ContainsKey(sequence))
|
||||||
{
|
{
|
||||||
outcome = PlayerKVTicketStatus.Pending;
|
outcome = KVTicketStatus.Pending;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -683,22 +646,23 @@ namespace game.PlayerDataStorage
|
|||||||
}
|
}
|
||||||
counts[outcome] = counts.TryGetValue(outcome, out var count) ? count + 1 : 1;
|
counts[outcome] = counts.TryGetValue(outcome, out var count) ? count + 1 : 1;
|
||||||
}
|
}
|
||||||
int Count(PlayerKVTicketStatus status)
|
int Count(KVTicketStatus status)
|
||||||
{
|
{
|
||||||
return counts.TryGetValue(status, out var count) ? count : 0;
|
return counts.TryGetValue(status, out var count) ? count : 0;
|
||||||
}
|
}
|
||||||
return new PlayerKVFlushResult(Count(PlayerKVTicketStatus.Confirmed), Count(PlayerKVTicketStatus.Superseded),
|
return new KvFlushResult(Count(KVTicketStatus.Blocked) + Count(KVTicketStatus.Cancelled), Count(KVTicketStatus.Unknown), error);
|
||||||
Count(PlayerKVTicketStatus.Blocked) + Count(PlayerKVTicketStatus.Cancelled), Count(PlayerKVTicketStatus.Unknown),
|
|
||||||
error, state.Status, Count(PlayerKVTicketStatus.Pending));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Current() => !ownerToken.IsCancellationRequested && isOwnerCurrent();
|
private bool Current() => !ownerToken.IsCancellationRequested && isOwnerCurrent();
|
||||||
static PlayerKVResult CancelledResult() => new PlayerKVResult(PlayerKVError.Cancelled);
|
private static KvResult CancelledResult() => new KvResult(KVError.Cancelled);
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
if (disposed) return;
|
if (disposed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
disposed = true;
|
disposed = true;
|
||||||
http.Dispose();
|
http.Dispose();
|
||||||
dispatch.Dispose();
|
dispatch.Dispose();
|
||||||
@@ -9,67 +9,145 @@ using System.Text;
|
|||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
namespace game.PlayerDataStorage
|
namespace Core.KV
|
||||||
{
|
{
|
||||||
public enum PlayerKVSyncStatus { NotLoaded, Ready, ReadFailed, Conflict, UnknownOutcome, Unauthorized, StorageError, Closed }
|
public enum KVSyncStatus
|
||||||
public enum PlayerKVOpenError { None, Validation, Busy, StorageError, RecoveryRequired }
|
|
||||||
public enum PlayerKVChangeError { None, Validation, Blocked, StorageError }
|
|
||||||
|
|
||||||
public sealed class PlayerKVStateResult
|
|
||||||
{
|
{
|
||||||
public bool Success => Error == PlayerKVChangeError.None;
|
NotLoaded,
|
||||||
public PlayerKVChangeError Error { get; }
|
Ready,
|
||||||
public long AttemptSequence { get; }
|
ReadFailed,
|
||||||
public long Revision { get; }
|
Conflict,
|
||||||
public bool LocalCommitMayHaveSucceeded { get; }
|
UnknownOutcome,
|
||||||
internal PlayerKVStateResult(PlayerKVChangeError error, long sequence = 0, long revision = 0, bool uncertain = false)
|
Unauthorized,
|
||||||
{ Error = error; AttemptSequence = sequence; Revision = revision; LocalCommitMayHaveSucceeded = uncertain; }
|
StorageError,
|
||||||
|
Closed
|
||||||
|
}
|
||||||
|
public enum KVOpenError
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
Validation,
|
||||||
|
Busy,
|
||||||
|
StorageError,
|
||||||
|
RecoveryRequired
|
||||||
|
}
|
||||||
|
public enum KVChangeError
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
Validation,
|
||||||
|
Blocked,
|
||||||
|
StorageError
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class PlayerKVOpenResult
|
public sealed class KvStateResult
|
||||||
{
|
{
|
||||||
public bool Success => Error == PlayerKVOpenError.None;
|
public bool Success => Error == KVChangeError.None;
|
||||||
public PlayerKVOpenError Error { get; }
|
public KVChangeError Error
|
||||||
public PlayerKVSyncState State { get; }
|
{
|
||||||
internal PlayerKVOpenResult(PlayerKVOpenError error, PlayerKVSyncState state = null) { Error = error; State = state; }
|
get;
|
||||||
|
}
|
||||||
|
public long AttemptSequence
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
public bool LocalCommitMayHaveSucceeded
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
internal KvStateResult(KVChangeError error, long sequence = 0, bool uncertain = false)
|
||||||
|
{
|
||||||
|
Error = error;
|
||||||
|
AttemptSequence = sequence;
|
||||||
|
LocalCommitMayHaveSucceeded = uncertain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class KvOpenResult
|
||||||
|
{
|
||||||
|
public bool Success => Error == KVOpenError.None;
|
||||||
|
public KVOpenError Error
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
public KvSyncState State
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
internal KvOpenResult(KVOpenError error, KvSyncState state = null)
|
||||||
|
{
|
||||||
|
Error = error;
|
||||||
|
State = state;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Existence is separate from value: an empty string is not a deletion.
|
// Existence is separate from value: an empty string is not a deletion.
|
||||||
public sealed class PlayerKVValue : IEquatable<PlayerKVValue>
|
public sealed class KvValue : IEquatable<KvValue>
|
||||||
{
|
{
|
||||||
public static readonly PlayerKVValue Missing = new PlayerKVValue(false, null);
|
public static readonly KvValue Missing = new KvValue(false, null);
|
||||||
public bool Exists { get; }
|
public bool Exists
|
||||||
public string Value { get; }
|
{
|
||||||
PlayerKVValue(bool exists, string value) { Exists = exists; Value = value; }
|
get;
|
||||||
public static PlayerKVValue Present(string value) => new PlayerKVValue(true, value ?? throw new ArgumentNullException(nameof(value)));
|
}
|
||||||
public bool Equals(PlayerKVValue other) => other != null && Exists == other.Exists && Value == other.Value;
|
public string Value
|
||||||
public override bool Equals(object obj) => Equals(obj as PlayerKVValue);
|
{
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
private KvValue(bool exists, string value)
|
||||||
|
{
|
||||||
|
Exists = exists;
|
||||||
|
Value = value;
|
||||||
|
}
|
||||||
|
public static KvValue Present(string value) => new KvValue(true, value ?? throw new ArgumentNullException(nameof(value)));
|
||||||
|
public bool Equals(KvValue other) => other != null && Exists == other.Exists && Value == other.Value;
|
||||||
|
public override bool Equals(object obj) => Equals(obj as KvValue);
|
||||||
public override int GetHashCode() => Exists ? (Value.GetHashCode() ^ 397) : 0;
|
public override int GetHashCode() => Exists ? (Value.GetHashCode() ^ 397) : 0;
|
||||||
internal static PlayerKVValue At(IReadOnlyDictionary<string, string> data, string key) =>
|
internal static KvValue At(IReadOnlyDictionary<string, string> data, string key) =>
|
||||||
data.TryGetValue(key, out var value) ? Present(value) : Missing;
|
data.TryGetValue(key, out var value) ? Present(value) : Missing;
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class PlayerKVChange
|
public sealed class KvChange
|
||||||
{
|
{
|
||||||
public string Key { get; }
|
public string Key
|
||||||
public long Sequence { get; }
|
{
|
||||||
public PlayerKVValue BaseValue { get; }
|
get;
|
||||||
public PlayerKVValue Target { get; }
|
}
|
||||||
internal PlayerKVChange(string key, long sequence, PlayerKVValue baseline, PlayerKVValue target)
|
public long Sequence
|
||||||
{ Key = key; Sequence = sequence; BaseValue = baseline; Target = target; }
|
{
|
||||||
internal PlayerKVChange WithBase(PlayerKVValue baseline) => new PlayerKVChange(Key, Sequence, baseline, Target);
|
get;
|
||||||
|
}
|
||||||
|
public KvValue BaseValue
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
public KvValue Target
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
internal KvChange(string key, long sequence, KvValue baseline, KvValue target)
|
||||||
|
{
|
||||||
|
Key = key;
|
||||||
|
Sequence = sequence;
|
||||||
|
BaseValue = baseline;
|
||||||
|
Target = target;
|
||||||
|
}
|
||||||
|
internal KvChange WithBase(KvValue baseline) => new KvChange(Key, Sequence, baseline, Target);
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class PlayerKVBatch
|
public sealed class KvBatch
|
||||||
{
|
{
|
||||||
public string Id { get; }
|
public string Id
|
||||||
public IReadOnlyList<PlayerKVChange> Items { get; }
|
{
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
public IReadOnlyList<KvChange> Items
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
}
|
||||||
public bool IsDelete => !Items[0].Target.Exists;
|
public bool IsDelete => !Items[0].Target.Exists;
|
||||||
public bool IsOrdinary
|
public bool IsOrdinary
|
||||||
{
|
{
|
||||||
get;
|
get;
|
||||||
}
|
}
|
||||||
internal PlayerKVBatch(string id, IEnumerable<PlayerKVChange> items, bool ordinary = false)
|
internal KvBatch(string id, IEnumerable<KvChange> items, bool ordinary = false)
|
||||||
{
|
{
|
||||||
Id = id;
|
Id = id;
|
||||||
Items = Array.AsReadOnly(items.ToArray());
|
Items = Array.AsReadOnly(items.ToArray());
|
||||||
@@ -77,74 +155,66 @@ namespace game.PlayerDataStorage
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class PlayerKVDiagnosticSnapshot
|
public sealed class KvDiagnosticSnapshot
|
||||||
{
|
{
|
||||||
public long Revision { get; }
|
public long NextSequence
|
||||||
public long NextSequence { get; }
|
|
||||||
public bool HasBaseline { get; }
|
|
||||||
public bool AuthenticationInvalid { get; }
|
|
||||||
public IReadOnlyDictionary<string, string> Baseline { get; }
|
|
||||||
public IReadOnlyDictionary<string, string> LocalOnly { get; }
|
|
||||||
public IReadOnlyDictionary<string, PlayerKVChange> Pending { get; }
|
|
||||||
public IReadOnlyDictionary<string, PlayerKVValue> ConflictObservations { get; }
|
|
||||||
public PlayerKVBatch ActiveBatch { get; }
|
|
||||||
public PlayerKVBatch UnknownBatch { get; }
|
|
||||||
public IReadOnlyDictionary<string, string> LocalData
|
|
||||||
{
|
{
|
||||||
get;
|
get;
|
||||||
}
|
}
|
||||||
public IReadOnlyDictionary<string, long> LocalUpdateTicks
|
public KvBatch ActiveBatch
|
||||||
{
|
{
|
||||||
get;
|
get;
|
||||||
}
|
}
|
||||||
public IReadOnlyList<IReadOnlyDictionary<string, PlayerKVChange>> OrdinaryBuffers
|
public KvBatch UnknownBatch
|
||||||
{
|
{
|
||||||
get;
|
get;
|
||||||
}
|
}
|
||||||
public IReadOnlyList<PlayerKVBatch> PreparedOrdinaryBatches
|
public IReadOnlyList<IReadOnlyDictionary<string, KvChange>> OrdinaryBuffers
|
||||||
{
|
{
|
||||||
get;
|
get;
|
||||||
}
|
}
|
||||||
public IReadOnlyList<PlayerKVBatch> OrdinaryUnknownBatches
|
public IReadOnlyList<KvBatch> PreparedOrdinaryBatches
|
||||||
{
|
{
|
||||||
get;
|
get;
|
||||||
}
|
}
|
||||||
public IReadOnlyDictionary<string, PlayerKVChange> LegacyPending
|
public IReadOnlyList<KvBatch> OrdinaryUnknownBatches
|
||||||
{
|
{
|
||||||
get;
|
get;
|
||||||
}
|
}
|
||||||
public IReadOnlyDictionary<string, PlayerKVChange> ExplicitDeletes
|
public IReadOnlyDictionary<string, KvChange> LegacyPending
|
||||||
{
|
{
|
||||||
get;
|
get;
|
||||||
}
|
}
|
||||||
internal PlayerKVDiagnosticSnapshot(KVSnapshot source)
|
public IReadOnlyDictionary<string, KvChange> ExplicitDeletes
|
||||||
{
|
{
|
||||||
Revision = source.Revision; NextSequence = source.NextSequence; HasBaseline = source.HasBaseline;
|
get;
|
||||||
AuthenticationInvalid = source.AuthenticationInvalid;
|
}
|
||||||
Baseline = ReadOnly(source.Baseline); LocalOnly = ReadOnly(source.LocalOnly);
|
internal KvDiagnosticSnapshot(KvSnapshot source)
|
||||||
Pending = ReadOnly(source.Pending); ConflictObservations = ReadOnly(source.Conflicts);
|
{
|
||||||
ActiveBatch = source.Active; UnknownBatch = source.Unknown;
|
NextSequence = source.NextSequence;
|
||||||
LocalData = ReadOnly(source.LocalData);
|
ActiveBatch = source.Active;
|
||||||
LocalUpdateTicks = ReadOnly(source.LocalUpdateTicks);
|
UnknownBatch = source.Unknown;
|
||||||
OrdinaryBuffers = Array.AsReadOnly(source.OrdinaryBuffers.Select(bucket => ReadOnly(bucket)).ToArray());
|
OrdinaryBuffers = Array.AsReadOnly(source.OrdinaryBuffers.Select(bucket => ReadOnly(bucket)).ToArray());
|
||||||
PreparedOrdinaryBatches = Array.AsReadOnly(source.PreparedOrdinaryBatches.ToArray());
|
PreparedOrdinaryBatches = Array.AsReadOnly(source.PreparedOrdinaryBatches.ToArray());
|
||||||
OrdinaryUnknownBatches = Array.AsReadOnly(source.OrdinaryUnknownBatches.ToArray());
|
OrdinaryUnknownBatches = Array.AsReadOnly(source.OrdinaryUnknownBatches.ToArray());
|
||||||
LegacyPending = ReadOnly(source.LegacyPending);
|
LegacyPending = ReadOnly(source.LegacyPending);
|
||||||
ExplicitDeletes = ReadOnly(source.ExplicitDeletes);
|
ExplicitDeletes = ReadOnly(source.ExplicitDeletes);
|
||||||
}
|
}
|
||||||
static IReadOnlyDictionary<string, T> ReadOnly<T>(Dictionary<string, T> source) =>
|
private static IReadOnlyDictionary<string, T> ReadOnly<T>(Dictionary<string, T> source) =>
|
||||||
new ReadOnlyDictionary<string, T>(new Dictionary<string, T>(source, StringComparer.Ordinal));
|
new ReadOnlyDictionary<string, T>(new Dictionary<string, T>(source, StringComparer.Ordinal));
|
||||||
}
|
}
|
||||||
|
|
||||||
internal sealed class KVIdentity
|
internal sealed class KvIdentity
|
||||||
{
|
{
|
||||||
internal readonly string Api, PlayerId, DirectoryPath;
|
internal readonly string Api, PlayerId, DirectoryPath;
|
||||||
internal KVIdentity(string root, Uri api, string playerId)
|
internal KvIdentity(string root, Uri api, string playerId)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(root) || !Path.IsPathFullyQualified(root) || api == null || !api.IsAbsoluteUri ||
|
if (string.IsNullOrWhiteSpace(root) || !Path.IsPathFullyQualified(root) || api == null || !api.IsAbsoluteUri ||
|
||||||
(api.Scheme != "http" && api.Scheme != "https") || api.UserInfo.Length != 0 || api.Query.Length != 0 || api.Fragment.Length != 0 ||
|
(api.Scheme != "http" && api.Scheme != "https") || api.UserInfo.Length != 0 || api.Query.Length != 0 || api.Fragment.Length != 0 ||
|
||||||
!long.TryParse(playerId, NumberStyles.None, CultureInfo.InvariantCulture, out var id) || id <= 0)
|
!long.TryParse(playerId, NumberStyles.None, CultureInfo.InvariantCulture, out var id) || id <= 0)
|
||||||
|
{
|
||||||
throw new ArgumentException("Invalid KV cache identity.");
|
throw new ArgumentException("Invalid KV cache identity.");
|
||||||
|
}
|
||||||
Api = api.AbsoluteUri.TrimEnd('/') + "/";
|
Api = api.AbsoluteUri.TrimEnd('/') + "/";
|
||||||
PlayerId = id.ToString(CultureInfo.InvariantCulture);
|
PlayerId = id.ToString(CultureInfo.InvariantCulture);
|
||||||
using (var hash = SHA256.Create())
|
using (var hash = SHA256.Create())
|
||||||
@@ -156,7 +226,7 @@ namespace game.PlayerDataStorage
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal sealed class KVSnapshot
|
internal sealed class KvSnapshot
|
||||||
{
|
{
|
||||||
internal long Revision, NextSequence = 1;
|
internal long Revision, NextSequence = 1;
|
||||||
internal bool HasBaseline, AuthenticationInvalid;
|
internal bool HasBaseline, AuthenticationInvalid;
|
||||||
@@ -169,34 +239,23 @@ namespace game.PlayerDataStorage
|
|||||||
internal HashSet<string> ImmediatePreservedKeys = new HashSet<string>(StringComparer.Ordinal);
|
internal HashSet<string> ImmediatePreservedKeys = new HashSet<string>(StringComparer.Ordinal);
|
||||||
// Only v1/v2 conversion can populate this lower layer; the first successful Get removes it.
|
// Only v1/v2 conversion can populate this lower layer; the first successful Get removes it.
|
||||||
internal Dictionary<string, string> LegacyLocalOnlyUnderImmediate = new Dictionary<string, string>(StringComparer.Ordinal);
|
internal Dictionary<string, string> LegacyLocalOnlyUnderImmediate = new Dictionary<string, string>(StringComparer.Ordinal);
|
||||||
internal List<Dictionary<string, PlayerKVChange>> OrdinaryBuffers = Enumerable.Range(0, 5)
|
internal List<Dictionary<string, KvChange>> OrdinaryBuffers = Enumerable.Range(0, 5)
|
||||||
.Select(_ => new Dictionary<string, PlayerKVChange>(StringComparer.Ordinal)).ToList();
|
.Select(_ => new Dictionary<string, KvChange>(StringComparer.Ordinal)).ToList();
|
||||||
internal List<PlayerKVBatch> PreparedOrdinaryBatches = new List<PlayerKVBatch>();
|
internal List<KvBatch> PreparedOrdinaryBatches = new List<KvBatch>();
|
||||||
internal List<PlayerKVBatch> OrdinaryUnknownBatches = new List<PlayerKVBatch>();
|
internal List<KvBatch> OrdinaryUnknownBatches = new List<KvBatch>();
|
||||||
internal Dictionary<string, PlayerKVChange> LegacyPending = new Dictionary<string, PlayerKVChange>(StringComparer.Ordinal);
|
internal Dictionary<string, KvChange> LegacyPending = new Dictionary<string, KvChange>(StringComparer.Ordinal);
|
||||||
internal Dictionary<string, PlayerKVChange> ExplicitDeletes = new Dictionary<string, PlayerKVChange>(StringComparer.Ordinal);
|
internal Dictionary<string, KvChange> ExplicitDeletes = new Dictionary<string, KvChange>(StringComparer.Ordinal);
|
||||||
internal Dictionary<string, PlayerKVValue> Conflicts = new Dictionary<string, PlayerKVValue>(StringComparer.Ordinal);
|
internal Dictionary<string, KvValue> Conflicts = new Dictionary<string, KvValue>(StringComparer.Ordinal);
|
||||||
internal Dictionary<long, PlayerKVChange> RemoteOnlyWrites = new Dictionary<long, PlayerKVChange>();
|
internal Dictionary<long, KvChange> RemoteOnlyWrites = new Dictionary<long, KvChange>();
|
||||||
internal PlayerKVBatch Active, Unknown;
|
internal KvBatch Active, Unknown;
|
||||||
|
|
||||||
// Compatibility diagnostics only. A key projection loses cross-bucket sequence history.
|
internal IEnumerable<KvChange> Unsent()
|
||||||
internal Dictionary<string, PlayerKVChange> Pending => Unsent().GroupBy(item => item.Key, StringComparer.Ordinal)
|
|
||||||
.ToDictionary(group => group.Key, group => group.OrderBy(item => item.Sequence).Last(), StringComparer.Ordinal);
|
|
||||||
internal Dictionary<string, string> LocalOnly => LocalOrigins.Where(pair => pair.Value.LocalOnly && LocalData.ContainsKey(pair.Key))
|
|
||||||
.ToDictionary(pair => pair.Key, pair => LocalData[pair.Key], StringComparer.Ordinal);
|
|
||||||
internal IEnumerable<PlayerKVChange> Unsent()
|
|
||||||
{
|
{
|
||||||
return OrdinaryBuffers.SelectMany(bucket => bucket.Values).Concat(LegacyPending.Values).Concat(ExplicitDeletes.Values);
|
return OrdinaryBuffers.SelectMany(bucket => bucket.Values).Concat(LegacyPending.Values).Concat(ExplicitDeletes.Values);
|
||||||
}
|
}
|
||||||
internal IEnumerable<PlayerKVChange> OrdinaryOutstanding()
|
internal KvSnapshot Clone()
|
||||||
{
|
{
|
||||||
return OrdinaryBuffers.SelectMany(bucket => bucket.Values)
|
return new KvSnapshot
|
||||||
.Concat(PreparedOrdinaryBatches.SelectMany(batch => batch.Items))
|
|
||||||
.Concat(Active != null && Active.IsOrdinary ? Active.Items : Enumerable.Empty<PlayerKVChange>());
|
|
||||||
}
|
|
||||||
internal KVSnapshot Clone()
|
|
||||||
{
|
|
||||||
return new KVSnapshot
|
|
||||||
{
|
{
|
||||||
Revision = Revision, NextSequence = NextSequence, HasBaseline = HasBaseline, AuthenticationInvalid = AuthenticationInvalid,
|
Revision = Revision, NextSequence = NextSequence, HasBaseline = HasBaseline, AuthenticationInvalid = AuthenticationInvalid,
|
||||||
Baseline = new Dictionary<string, string>(Baseline, StringComparer.Ordinal),
|
Baseline = new Dictionary<string, string>(Baseline, StringComparer.Ordinal),
|
||||||
@@ -205,19 +264,19 @@ namespace game.PlayerDataStorage
|
|||||||
LocalOrigins = new Dictionary<string, (bool LocalOnly, long Sequence)>(LocalOrigins, StringComparer.Ordinal),
|
LocalOrigins = new Dictionary<string, (bool LocalOnly, long Sequence)>(LocalOrigins, StringComparer.Ordinal),
|
||||||
ImmediatePreservedKeys = new HashSet<string>(ImmediatePreservedKeys, StringComparer.Ordinal),
|
ImmediatePreservedKeys = new HashSet<string>(ImmediatePreservedKeys, StringComparer.Ordinal),
|
||||||
LegacyLocalOnlyUnderImmediate = new Dictionary<string, string>(LegacyLocalOnlyUnderImmediate, StringComparer.Ordinal),
|
LegacyLocalOnlyUnderImmediate = new Dictionary<string, string>(LegacyLocalOnlyUnderImmediate, StringComparer.Ordinal),
|
||||||
OrdinaryBuffers = OrdinaryBuffers.Select(bucket => new Dictionary<string, PlayerKVChange>(bucket, StringComparer.Ordinal)).ToList(),
|
OrdinaryBuffers = OrdinaryBuffers.Select(bucket => new Dictionary<string, KvChange>(bucket, StringComparer.Ordinal)).ToList(),
|
||||||
PreparedOrdinaryBatches = new List<PlayerKVBatch>(PreparedOrdinaryBatches),
|
PreparedOrdinaryBatches = new List<KvBatch>(PreparedOrdinaryBatches),
|
||||||
OrdinaryUnknownBatches = new List<PlayerKVBatch>(OrdinaryUnknownBatches),
|
OrdinaryUnknownBatches = new List<KvBatch>(OrdinaryUnknownBatches),
|
||||||
LegacyPending = new Dictionary<string, PlayerKVChange>(LegacyPending, StringComparer.Ordinal),
|
LegacyPending = new Dictionary<string, KvChange>(LegacyPending, StringComparer.Ordinal),
|
||||||
ExplicitDeletes = new Dictionary<string, PlayerKVChange>(ExplicitDeletes, StringComparer.Ordinal),
|
ExplicitDeletes = new Dictionary<string, KvChange>(ExplicitDeletes, StringComparer.Ordinal),
|
||||||
Conflicts = new Dictionary<string, PlayerKVValue>(Conflicts, StringComparer.Ordinal),
|
Conflicts = new Dictionary<string, KvValue>(Conflicts, StringComparer.Ordinal),
|
||||||
Active = Active, Unknown = Unknown,
|
Active = Active, Unknown = Unknown,
|
||||||
RemoteOnlyWrites = new Dictionary<long, PlayerKVChange>(RemoteOnlyWrites)
|
RemoteOnlyWrites = new Dictionary<long, KvChange>(RemoteOnlyWrites)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
internal static bool ValidKey(string key) => !string.IsNullOrEmpty(key) && key.Length <= 128;
|
internal static bool ValidKey(string key) => !string.IsNullOrEmpty(key) && key.Length <= 128;
|
||||||
|
|
||||||
internal string Encode(KVIdentity identity)
|
internal string Encode(KvIdentity identity)
|
||||||
{
|
{
|
||||||
var conflicts = new JObject();
|
var conflicts = new JObject();
|
||||||
foreach (var pair in Conflicts)
|
foreach (var pair in Conflicts)
|
||||||
@@ -245,20 +304,20 @@ namespace game.PlayerDataStorage
|
|||||||
["remoteOnlyWrites"] = ChangesJson(RemoteOnlyWrites.Values)
|
["remoteOnlyWrites"] = ChangesJson(RemoteOnlyWrites.Values)
|
||||||
}.ToString(Formatting.None);
|
}.ToString(Formatting.None);
|
||||||
}
|
}
|
||||||
private static JArray ChangesJson(IEnumerable<PlayerKVChange> values)
|
private static JArray ChangesJson(IEnumerable<KvChange> values)
|
||||||
{
|
{
|
||||||
return new JArray(values.OrderBy(item => item.Sequence).Select(ChangeJson));
|
return new JArray(values.OrderBy(item => item.Sequence).Select(ChangeJson));
|
||||||
}
|
}
|
||||||
private static JObject ValueJson(PlayerKVValue value)
|
private static JObject ValueJson(KvValue value)
|
||||||
{
|
{
|
||||||
return new JObject { ["exists"] = value.Exists, ["value"] = value.Value };
|
return new JObject { ["exists"] = value.Exists, ["value"] = value.Value };
|
||||||
}
|
}
|
||||||
private static JObject ChangeJson(PlayerKVChange item)
|
private static JObject ChangeJson(KvChange item)
|
||||||
{
|
{
|
||||||
return new JObject { ["key"] = item.Key, ["sequence"] = item.Sequence,
|
return new JObject { ["key"] = item.Key, ["sequence"] = item.Sequence,
|
||||||
["base"] = ValueJson(item.BaseValue), ["target"] = ValueJson(item.Target) };
|
["base"] = ValueJson(item.BaseValue), ["target"] = ValueJson(item.Target) };
|
||||||
}
|
}
|
||||||
private static JToken BatchJson(PlayerKVBatch batch)
|
private static JToken BatchJson(KvBatch batch)
|
||||||
{
|
{
|
||||||
if (batch == null)
|
if (batch == null)
|
||||||
{
|
{
|
||||||
@@ -267,7 +326,7 @@ namespace game.PlayerDataStorage
|
|||||||
return new JObject { ["id"] = batch.Id, ["ordinary"] = batch.IsOrdinary, ["items"] = ChangesJson(batch.Items) };
|
return new JObject { ["id"] = batch.Id, ["ordinary"] = batch.IsOrdinary, ["items"] = ChangesJson(batch.Items) };
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static KVSnapshot Decode(string text, KVIdentity identity)
|
internal static KvSnapshot Decode(string text, KvIdentity identity)
|
||||||
{
|
{
|
||||||
JObject root;
|
JObject root;
|
||||||
using (var reader = new JsonTextReader(new StringReader(text)) { DateParseHandling = DateParseHandling.None })
|
using (var reader = new JsonTextReader(new StringReader(text)) { DateParseHandling = DateParseHandling.None })
|
||||||
@@ -283,7 +342,7 @@ namespace game.PlayerDataStorage
|
|||||||
{
|
{
|
||||||
throw Invalid();
|
throw Invalid();
|
||||||
}
|
}
|
||||||
var result = new KVSnapshot
|
var result = new KvSnapshot
|
||||||
{
|
{
|
||||||
Revision = Integer(root, "revision"), NextSequence = Integer(root, "nextSequence"),
|
Revision = Integer(root, "revision"), NextSequence = Integer(root, "nextSequence"),
|
||||||
HasBaseline = Boolean(root, "hasBaseline"), AuthenticationInvalid = Boolean(root, "authenticationInvalid"),
|
HasBaseline = Boolean(root, "hasBaseline"), AuthenticationInvalid = Boolean(root, "authenticationInvalid"),
|
||||||
@@ -350,13 +409,13 @@ namespace game.PlayerDataStorage
|
|||||||
result.Validate();
|
result.Validate();
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
private static Dictionary<string, PlayerKVChange> Changes(JToken array)
|
private static Dictionary<string, KvChange> Changes(JToken array)
|
||||||
{
|
{
|
||||||
if (array.Type != JTokenType.Array)
|
if (array.Type != JTokenType.Array)
|
||||||
{
|
{
|
||||||
throw Invalid();
|
throw Invalid();
|
||||||
}
|
}
|
||||||
var values = new Dictionary<string, PlayerKVChange>(StringComparer.Ordinal);
|
var values = new Dictionary<string, KvChange>(StringComparer.Ordinal);
|
||||||
foreach (var token in array)
|
foreach (var token in array)
|
||||||
{
|
{
|
||||||
var item = Change(token);
|
var item = Change(token);
|
||||||
@@ -368,9 +427,9 @@ namespace game.PlayerDataStorage
|
|||||||
}
|
}
|
||||||
return values;
|
return values;
|
||||||
}
|
}
|
||||||
private static List<PlayerKVBatch> ReadBatches(JToken root, string name)
|
private static List<KvBatch> ReadBatches(JToken root, string name)
|
||||||
{
|
{
|
||||||
var result = new List<PlayerKVBatch>();
|
var result = new List<KvBatch>();
|
||||||
foreach (var item in Field(root, name, JTokenType.Array))
|
foreach (var item in Field(root, name, JTokenType.Array))
|
||||||
{
|
{
|
||||||
var wrapper = new JObject { ["batch"] = item.DeepClone() };
|
var wrapper = new JObject { ["batch"] = item.DeepClone() };
|
||||||
@@ -384,11 +443,11 @@ namespace game.PlayerDataStorage
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void DecodeLegacy(JObject root, long version, KVSnapshot result)
|
private static void DecodeLegacy(JObject root, long version, KvSnapshot result)
|
||||||
{
|
{
|
||||||
var pending = Changes(Field(root, "pending", JTokenType.Array));
|
var pending = Changes(Field(root, "pending", JTokenType.Array));
|
||||||
var localOnly = Strings(Field(root, "localOnly", JTokenType.Object));
|
var localOnly = Strings(Field(root, "localOnly", JTokenType.Object));
|
||||||
var overrides = new Dictionary<string, PlayerKVValue>(StringComparer.Ordinal);
|
var overrides = new Dictionary<string, KvValue>(StringComparer.Ordinal);
|
||||||
if (version == 2)
|
if (version == 2)
|
||||||
{
|
{
|
||||||
foreach (var property in ((JObject)Field(root, "remoteLocalOverrides", JTokenType.Object)).Properties())
|
foreach (var property in ((JObject)Field(root, "remoteLocalOverrides", JTokenType.Object)).Properties())
|
||||||
@@ -459,7 +518,7 @@ namespace game.PlayerDataStorage
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private static void ApplyValue(Dictionary<string, string> values, string key, PlayerKVValue value)
|
private static void ApplyValue(Dictionary<string, string> values, string key, KvValue value)
|
||||||
{
|
{
|
||||||
if (value.Exists)
|
if (value.Exists)
|
||||||
{
|
{
|
||||||
@@ -483,8 +542,8 @@ namespace game.PlayerDataStorage
|
|||||||
{
|
{
|
||||||
throw Invalid();
|
throw Invalid();
|
||||||
}
|
}
|
||||||
var sequences = new Dictionary<long, PlayerKVChange>();
|
var sequences = new Dictionary<long, KvChange>();
|
||||||
void Add(PlayerKVChange item, bool sourceReference = false)
|
void Add(KvChange item, bool sourceReference = false)
|
||||||
{
|
{
|
||||||
if (!ValidKey(item.Key) || item.Sequence < 1 || item.Sequence >= NextSequence)
|
if (!ValidKey(item.Key) || item.Sequence < 1 || item.Sequence >= NextSequence)
|
||||||
{
|
{
|
||||||
@@ -537,49 +596,70 @@ namespace game.PlayerDataStorage
|
|||||||
throw Invalid();
|
throw Invalid();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
static Dictionary<string, string> Strings(JToken value)
|
private static Dictionary<string, string> Strings(JToken value)
|
||||||
{
|
{
|
||||||
var result = new Dictionary<string, string>(StringComparer.Ordinal);
|
var result = new Dictionary<string, string>(StringComparer.Ordinal);
|
||||||
foreach (var property in ((JObject)value).Properties())
|
foreach (var property in ((JObject)value).Properties())
|
||||||
{
|
{
|
||||||
if (!ValidKey(property.Name) || property.Value.Type != JTokenType.String) throw Invalid();
|
if (!ValidKey(property.Name) || property.Value.Type != JTokenType.String)
|
||||||
|
{
|
||||||
|
throw Invalid();
|
||||||
|
}
|
||||||
result.Add(property.Name, (string)property.Value);
|
result.Add(property.Name, (string)property.Value);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
static PlayerKVChange Change(JToken item)
|
private static KvChange Change(JToken item)
|
||||||
{
|
{
|
||||||
var key = String(item, "key");
|
var key = String(item, "key");
|
||||||
if (!ValidKey(key)) throw Invalid();
|
if (!ValidKey(key))
|
||||||
return new PlayerKVChange(key, Integer(item, "sequence"), Value(Field(item, "base", JTokenType.Object)), Value(Field(item, "target", JTokenType.Object)));
|
{
|
||||||
|
throw Invalid();
|
||||||
|
}
|
||||||
|
return new KvChange(key, Integer(item, "sequence"), Value(Field(item, "base", JTokenType.Object)), Value(Field(item, "target", JTokenType.Object)));
|
||||||
}
|
}
|
||||||
static PlayerKVValue Value(JToken item)
|
private static KvValue Value(JToken item)
|
||||||
{
|
{
|
||||||
var exists = Boolean(item, "exists");
|
var exists = Boolean(item, "exists");
|
||||||
var value = Field(item, "value", exists ? JTokenType.String : JTokenType.Null);
|
var value = Field(item, "value", exists ? JTokenType.String : JTokenType.Null);
|
||||||
return exists ? PlayerKVValue.Present((string)value) : PlayerKVValue.Missing;
|
return exists ? KvValue.Present((string)value) : KvValue.Missing;
|
||||||
}
|
}
|
||||||
static PlayerKVBatch Batch(JToken parent, string name, bool withKind = false)
|
private static KvBatch Batch(JToken parent, string name, bool withKind = false)
|
||||||
{
|
{
|
||||||
var node = parent[name];
|
var node = parent[name];
|
||||||
if (node == null) throw Invalid();
|
if (node == null)
|
||||||
if (node.Type == JTokenType.Null) return null;
|
{
|
||||||
|
throw Invalid();
|
||||||
|
}
|
||||||
|
if (node.Type == JTokenType.Null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
var id = String(node, "id");
|
var id = String(node, "id");
|
||||||
if (!Guid.TryParseExact(id, "N", out _)) throw Invalid();
|
if (!Guid.TryParseExact(id, "N", out _))
|
||||||
|
{
|
||||||
|
throw Invalid();
|
||||||
|
}
|
||||||
var items = Field(node, "items", JTokenType.Array).Select(Change).ToArray();
|
var items = Field(node, "items", JTokenType.Array).Select(Change).ToArray();
|
||||||
if (items.Length == 0 || items.Length > 10 || items.Select(i => i.Key).Distinct(StringComparer.Ordinal).Count() != items.Length ||
|
if (items.Length == 0 || items.Length > 10 || items.Select(i => i.Key).Distinct(StringComparer.Ordinal).Count() != items.Length ||
|
||||||
items.Select(i => i.Sequence).Distinct().Count() != items.Length || items.Any(i => i.Target.Exists != items[0].Target.Exists) ||
|
items.Select(i => i.Sequence).Distinct().Count() != items.Length || items.Any(i => i.Target.Exists != items[0].Target.Exists) ||
|
||||||
!items.Select(i => i.Sequence).SequenceEqual(items.Select(i => i.Sequence).OrderBy(s => s))) throw Invalid();
|
!items.Select(i => i.Sequence).SequenceEqual(items.Select(i => i.Sequence).OrderBy(s => s)))
|
||||||
return new PlayerKVBatch(id, items, withKind && Boolean(node, "ordinary"));
|
{
|
||||||
|
throw Invalid();
|
||||||
|
}
|
||||||
|
return new KvBatch(id, items, withKind && Boolean(node, "ordinary"));
|
||||||
}
|
}
|
||||||
static JToken Field(JToken parent, string name, JTokenType type)
|
private static JToken Field(JToken parent, string name, JTokenType type)
|
||||||
{
|
{
|
||||||
if (!(parent is JObject obj) || obj[name] == null || obj[name].Type != type) throw Invalid();
|
if (!(parent is JObject obj) || obj[name] == null || obj[name].Type != type)
|
||||||
|
{
|
||||||
|
throw Invalid();
|
||||||
|
}
|
||||||
return obj[name];
|
return obj[name];
|
||||||
}
|
}
|
||||||
static string String(JToken parent, string name) => (string)Field(parent, name, JTokenType.String);
|
private static string String(JToken parent, string name) => (string)Field(parent, name, JTokenType.String);
|
||||||
static long Integer(JToken parent, string name) => (long)Field(parent, name, JTokenType.Integer);
|
private static long Integer(JToken parent, string name) => (long)Field(parent, name, JTokenType.Integer);
|
||||||
static bool Boolean(JToken parent, string name) => (bool)Field(parent, name, JTokenType.Boolean);
|
private static bool Boolean(JToken parent, string name) => (bool)Field(parent, name, JTokenType.Boolean);
|
||||||
internal static JsonException Invalid() => new JsonException("Invalid KV cache snapshot.");
|
internal static JsonException Invalid() => new JsonException("Invalid KV cache snapshot.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,23 +4,23 @@ using System.Collections.ObjectModel;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
namespace game.PlayerDataStorage
|
namespace Core.KV
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Synchronous, fixed-identity cache state. No HTTP, gameplay initialization or timers.
|
/// Synchronous, fixed-identity cache state. No HTTP, gameplay initialization or timers.
|
||||||
/// All transitions and disk writes share a gate. The phase-3B owner must drain its
|
/// All transitions and disk writes share a gate. The phase-3B owner must drain its
|
||||||
/// admitted network operations before disposing this state and its filesystem lease.
|
/// admitted network operations before disposing this state and its filesystem lease.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class PlayerKVSyncState : IDisposable
|
public sealed class KvSyncState : IDisposable
|
||||||
{
|
{
|
||||||
private readonly object gate = new object();
|
private readonly object gate = new object();
|
||||||
private readonly PlayerKVCacheStore store;
|
private readonly KvCacheStore store;
|
||||||
private KVSnapshot snapshot;
|
private KvSnapshot snapshot;
|
||||||
private PlayerKVSyncStatus status;
|
private KVSyncStatus status;
|
||||||
private readonly HashSet<long> recoveredImmediate;
|
private readonly HashSet<long> recoveredImmediate;
|
||||||
private bool recoverImmediateView = true;
|
private bool recoverImmediateView = true;
|
||||||
private bool recoveredOrdinaryUnknown;
|
private bool recoveredOrdinaryUnknown;
|
||||||
private KVSnapshot comparisonLocal;
|
private KvSnapshot comparisonLocal;
|
||||||
private Dictionary<string, string> comparisonRemote;
|
private Dictionary<string, string> comparisonRemote;
|
||||||
private Dictionary<string, DateTime> comparisonTimes;
|
private Dictionary<string, DateTime> comparisonTimes;
|
||||||
private Dictionary<string, string> comparisonView;
|
private Dictionary<string, string> comparisonView;
|
||||||
@@ -45,13 +45,19 @@ namespace game.PlayerDataStorage
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public PlayerKVSyncStatus Status { get { lock (gate) return status; } }
|
public KVSyncStatus Status
|
||||||
public string ApiBaseUri { get; }
|
|
||||||
public string PlayerId { get; }
|
|
||||||
|
|
||||||
PlayerKVSyncState(KVIdentity identity, PlayerKVCacheStore store, KVSnapshot snapshot)
|
|
||||||
{
|
{
|
||||||
ApiBaseUri = identity.Api; PlayerId = identity.PlayerId; this.store = store; this.snapshot = snapshot;
|
get
|
||||||
|
{
|
||||||
|
lock (gate)
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private KvSyncState(KvCacheStore store, KvSnapshot snapshot)
|
||||||
|
{
|
||||||
|
this.store = store;
|
||||||
|
this.snapshot = snapshot;
|
||||||
recoveredImmediate = new HashSet<long>(snapshot.RemoteOnlyWrites.Keys);
|
recoveredImmediate = new HashSet<long>(snapshot.RemoteOnlyWrites.Keys);
|
||||||
// Authentication belongs to the old owner. A new owner must obtain a fresh
|
// Authentication belongs to the old owner. A new owner must obtain a fresh
|
||||||
// successful Get before Ready; this never clears a persisted unknown batch.
|
// successful Get before Ready; this never clears a persisted unknown batch.
|
||||||
@@ -71,39 +77,46 @@ namespace game.PlayerDataStorage
|
|||||||
}
|
}
|
||||||
recoveredOrdinaryUnknown = snapshot.OrdinaryUnknownBatches.Count != 0;
|
recoveredOrdinaryUnknown = snapshot.OrdinaryUnknownBatches.Count != 0;
|
||||||
status = snapshot.Unknown != null || recoveredOrdinaryUnknown
|
status = snapshot.Unknown != null || recoveredOrdinaryUnknown
|
||||||
? PlayerKVSyncStatus.UnknownOutcome : PlayerKVSyncStatus.NotLoaded;
|
? KVSyncStatus.UnknownOutcome : KVSyncStatus.NotLoaded;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static PlayerKVOpenResult Open(string root, Uri api, string playerId) => Open(root, api, playerId, null);
|
public static KvOpenResult Open(string root, Uri api, string playerId)
|
||||||
internal static PlayerKVOpenResult Open(string root, Uri api, string playerId, Action<KVCacheWriteStage> fault)
|
|
||||||
{
|
{
|
||||||
KVIdentity identity;
|
KvIdentity identity;
|
||||||
try { identity = new KVIdentity(root, api, playerId); }
|
try
|
||||||
|
{
|
||||||
|
identity = new KvIdentity(root, api, playerId);
|
||||||
|
}
|
||||||
catch (Exception ex) when (ex is ArgumentException || ex is NotSupportedException || ex is PathTooLongException)
|
catch (Exception ex) when (ex is ArgumentException || ex is NotSupportedException || ex is PathTooLongException)
|
||||||
{ return new PlayerKVOpenResult(PlayerKVOpenError.Validation); }
|
{
|
||||||
var error = PlayerKVCacheStore.Open(identity, fault, out var store, out var snapshot);
|
return new KvOpenResult(KVOpenError.Validation);
|
||||||
return error == PlayerKVOpenError.None ? new PlayerKVOpenResult(error, new PlayerKVSyncState(identity, store, snapshot)) : new PlayerKVOpenResult(error);
|
}
|
||||||
|
var error = KvCacheStore.Open(identity, out var store, out var snapshot);
|
||||||
|
return error == KVOpenError.None ? new KvOpenResult(error, new KvSyncState(store, snapshot)) : new KvOpenResult(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
public PlayerKVDiagnosticSnapshot Inspect() { lock (gate) return new PlayerKVDiagnosticSnapshot(snapshot); }
|
public KvDiagnosticSnapshot Inspect()
|
||||||
|
{
|
||||||
|
lock (gate)
|
||||||
|
return new KvDiagnosticSnapshot(snapshot);
|
||||||
|
}
|
||||||
|
|
||||||
public IReadOnlyDictionary<string, string> ReadLocal()
|
public IReadOnlyDictionary<string, string> ReadLocal()
|
||||||
{
|
{
|
||||||
lock (gate)
|
lock (gate)
|
||||||
{
|
{
|
||||||
if (status != PlayerKVSyncStatus.Ready) throw new InvalidOperationException("KV data is not ready.");
|
if (status != KVSyncStatus.Ready)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("KV data is not ready.");
|
||||||
|
}
|
||||||
return new ReadOnlyDictionary<string, string>(ReadView());
|
return new ReadOnlyDictionary<string, string>(ReadView());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The caller must supply a successful complete Get. Failed reads must use RecordReadFailure.
|
// The caller must supply a successful complete Get. Failed reads must use RecordReadFailure.
|
||||||
public PlayerKVStateResult Reconcile(IReadOnlyDictionary<string, string> remote)
|
|
||||||
{
|
|
||||||
return Reconcile(remote, null, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal PlayerKVStateResult Reconcile(IReadOnlyDictionary<string, string> remote,
|
internal KvStateResult Reconcile(IReadOnlyDictionary<string, string> remote,
|
||||||
IReadOnlyDictionary<string, DateTime> updatedAtUtc, bool prepareOriginalComparison)
|
IReadOnlyDictionary<string, DateTime> updatedAtUtc)
|
||||||
{
|
{
|
||||||
lock (gate)
|
lock (gate)
|
||||||
{
|
{
|
||||||
@@ -111,55 +124,42 @@ namespace game.PlayerDataStorage
|
|||||||
{
|
{
|
||||||
return Blocked();
|
return Blocked();
|
||||||
}
|
}
|
||||||
if (remote == null || remote.Any(pair => !KVSnapshot.ValidKey(pair.Key) || pair.Value == null))
|
if (remote == null || remote.Any(pair => !KvSnapshot.ValidKey(pair.Key) || pair.Value == null))
|
||||||
{
|
{
|
||||||
return Invalid();
|
return Invalid();
|
||||||
}
|
}
|
||||||
if (prepareOriginalComparison && (!recoverImmediateView || updatedAtUtc == null ||
|
if (!recoverImmediateView || updatedAtUtc == null ||
|
||||||
updatedAtUtc.Count != remote.Count || remote.Keys.Any(key => !updatedAtUtc.ContainsKey(key)) ||
|
updatedAtUtc.Count != remote.Count || remote.Keys.Any(key => !updatedAtUtc.ContainsKey(key)) ||
|
||||||
updatedAtUtc.Values.Any(time => time.Kind != DateTimeKind.Utc)))
|
updatedAtUtc.Values.Any(time => time.Kind != DateTimeKind.Utc))
|
||||||
{
|
{
|
||||||
return Invalid();
|
return Invalid();
|
||||||
}
|
}
|
||||||
var original = snapshot;
|
var original = snapshot;
|
||||||
var copy = snapshot.Clone();
|
var copy = snapshot.Clone();
|
||||||
copy.Conflicts.Clear();
|
copy.Conflicts.Clear();
|
||||||
if (copy.Unknown != null || (recoveredOrdinaryUnknown && !prepareOriginalComparison))
|
if (copy.Unknown != null)
|
||||||
{
|
{
|
||||||
foreach (var item in copy.Unsent())
|
foreach (var item in copy.Unsent())
|
||||||
{
|
{
|
||||||
copy.Conflicts[item.Key] = PlayerKVValue.At(remote, item.Key);
|
copy.Conflicts[item.Key] = KvValue.At(remote, item.Key);
|
||||||
}
|
}
|
||||||
return Commit(copy, PlayerKVSyncStatus.UnknownOutcome);
|
return Commit(copy, KVSyncStatus.UnknownOutcome);
|
||||||
}
|
}
|
||||||
var recoveringKeys = recoverImmediateView
|
var recoveringKeys = new HashSet<string>(copy.ImmediatePreservedKeys, StringComparer.Ordinal);
|
||||||
? new HashSet<string>(copy.ImmediatePreservedKeys, StringComparer.Ordinal)
|
foreach (var sequence in recoveredImmediate)
|
||||||
: new HashSet<string>(StringComparer.Ordinal);
|
|
||||||
if (recoverImmediateView)
|
|
||||||
{
|
{
|
||||||
foreach (var sequence in recoveredImmediate)
|
var source = copy.RemoteOnlyWrites[sequence];
|
||||||
{
|
Rebase(copy, source.Key, KvValue.At(remote, source.Key));
|
||||||
var source = copy.RemoteOnlyWrites[sequence];
|
|
||||||
Rebase(copy, source.Key, PlayerKVValue.At(remote, source.Key));
|
|
||||||
if (!prepareOriginalComparison)
|
|
||||||
{
|
|
||||||
copy.RemoteOnlyWrites.Remove(sequence);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!prepareOriginalComparison)
|
|
||||||
{
|
|
||||||
copy.ImmediatePreservedKeys.Clear();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
foreach (var source in Sources(copy))
|
foreach (var source in Sources(copy))
|
||||||
{
|
{
|
||||||
if (prepareOriginalComparison && copy.OrdinaryBuffers.Contains(source))
|
if (copy.OrdinaryBuffers.Contains(source))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
foreach (var item in source.Values.ToArray())
|
foreach (var item in source.Values.ToArray())
|
||||||
{
|
{
|
||||||
var observed = PlayerKVValue.At(remote, item.Key);
|
var observed = KvValue.At(remote, item.Key);
|
||||||
if (observed.Equals(item.Target))
|
if (observed.Equals(item.Target))
|
||||||
{
|
{
|
||||||
source.Remove(item.Key);
|
source.Remove(item.Key);
|
||||||
@@ -172,7 +172,7 @@ namespace game.PlayerDataStorage
|
|||||||
}
|
}
|
||||||
foreach (var item in copy.PreparedOrdinaryBatches.SelectMany(batch => batch.Items))
|
foreach (var item in copy.PreparedOrdinaryBatches.SelectMany(batch => batch.Items))
|
||||||
{
|
{
|
||||||
var observed = PlayerKVValue.At(remote, item.Key);
|
var observed = KvValue.At(remote, item.Key);
|
||||||
if (!observed.Equals(item.Target) && !observed.Equals(item.BaseValue))
|
if (!observed.Equals(item.Target) && !observed.Equals(item.BaseValue))
|
||||||
{
|
{
|
||||||
copy.Conflicts[item.Key] = observed;
|
copy.Conflicts[item.Key] = observed;
|
||||||
@@ -183,13 +183,13 @@ namespace game.PlayerDataStorage
|
|||||||
{
|
{
|
||||||
if (pair.Value.LocalOnly)
|
if (pair.Value.LocalOnly)
|
||||||
{
|
{
|
||||||
if (recoverImmediateView && copy.LegacyLocalOnlyUnderImmediate.TryGetValue(pair.Key, out var lower))
|
if (copy.LegacyLocalOnlyUnderImmediate.TryGetValue(pair.Key, out var lower))
|
||||||
{
|
{
|
||||||
visible[pair.Key] = lower;
|
visible[pair.Key] = lower;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Apply(visible, pair.Key, PlayerKVValue.At(copy.LocalData, pair.Key));
|
Apply(visible, pair.Key, KvValue.At(copy.LocalData, pair.Key));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -197,14 +197,10 @@ namespace game.PlayerDataStorage
|
|||||||
var source = copy.Unsent().Concat(copy.PreparedOrdinaryBatches.SelectMany(batch => batch.Items))
|
var source = copy.Unsent().Concat(copy.PreparedOrdinaryBatches.SelectMany(batch => batch.Items))
|
||||||
.Concat(copy.OrdinaryUnknownBatches.SelectMany(batch => batch.Items))
|
.Concat(copy.OrdinaryUnknownBatches.SelectMany(batch => batch.Items))
|
||||||
.FirstOrDefault(item => item.Sequence == pair.Value.Sequence);
|
.FirstOrDefault(item => item.Sequence == pair.Value.Sequence);
|
||||||
if (!recoverImmediateView)
|
if (source != null)
|
||||||
{
|
|
||||||
Apply(visible, pair.Key, PlayerKVValue.At(copy.LocalData, pair.Key));
|
|
||||||
}
|
|
||||||
else if (source != null)
|
|
||||||
{
|
{
|
||||||
Apply(visible, pair.Key, recoveringKeys.Contains(pair.Key)
|
Apply(visible, pair.Key, recoveringKeys.Contains(pair.Key)
|
||||||
? source.Target : PlayerKVValue.At(copy.LocalData, pair.Key));
|
? source.Target : KvValue.At(copy.LocalData, pair.Key));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -212,41 +208,17 @@ namespace game.PlayerDataStorage
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// An immediate write preserves its view until a new owner performs its initial Get.
|
|
||||||
foreach (var key in copy.ImmediatePreservedKeys)
|
|
||||||
{
|
|
||||||
if (prepareOriginalComparison && recoveringKeys.Contains(key))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
Apply(visible, key, PlayerKVValue.At(copy.LocalData, key));
|
|
||||||
}
|
|
||||||
if (recoverImmediateView && !prepareOriginalComparison)
|
|
||||||
{
|
|
||||||
copy.LegacyLocalOnlyUnderImmediate.Clear();
|
|
||||||
}
|
|
||||||
if (!prepareOriginalComparison)
|
|
||||||
{
|
|
||||||
copy.LocalData = visible;
|
|
||||||
}
|
|
||||||
copy.Baseline = remote.ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.Ordinal);
|
copy.Baseline = remote.ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.Ordinal);
|
||||||
copy.HasBaseline = true;
|
copy.HasBaseline = true;
|
||||||
var reconciled = Commit(copy, copy.Conflicts.Count == 0 ? PlayerKVSyncStatus.Ready : PlayerKVSyncStatus.Conflict);
|
var reconciled = Commit(copy, copy.Conflicts.Count == 0 ? KVSyncStatus.Ready : KVSyncStatus.Conflict);
|
||||||
if (reconciled.Success)
|
if (reconciled.Success)
|
||||||
{
|
{
|
||||||
recoverImmediateView = false;
|
recoverImmediateView = false;
|
||||||
if (prepareOriginalComparison)
|
comparisonLocal = original;
|
||||||
{
|
comparisonRemote = remote.ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.Ordinal);
|
||||||
comparisonLocal = original;
|
comparisonTimes = updatedAtUtc.ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.Ordinal);
|
||||||
comparisonRemote = remote.ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.Ordinal);
|
comparisonView = visible;
|
||||||
comparisonTimes = updatedAtUtc.ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.Ordinal);
|
comparisonImmediateKeys = new HashSet<string>(StringComparer.Ordinal);
|
||||||
comparisonView = visible;
|
|
||||||
comparisonImmediateKeys = new HashSet<string>(StringComparer.Ordinal);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
recoveredImmediate.Clear();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return reconciled;
|
return reconciled;
|
||||||
}
|
}
|
||||||
@@ -263,17 +235,17 @@ namespace game.PlayerDataStorage
|
|||||||
{
|
{
|
||||||
if (pair.Value.Sequence >= comparisonLocal.NextSequence)
|
if (pair.Value.Sequence >= comparisonLocal.NextSequence)
|
||||||
{
|
{
|
||||||
Apply(view, pair.Key, PlayerKVValue.At(snapshot.LocalData, pair.Key));
|
Apply(view, pair.Key, KvValue.At(snapshot.LocalData, pair.Key));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal PlayerKVStateResult CompareDatas(long nowTicks)
|
internal KvStateResult CompareDatas(long nowTicks)
|
||||||
{
|
{
|
||||||
lock (gate)
|
lock (gate)
|
||||||
{
|
{
|
||||||
if (status != PlayerKVSyncStatus.Ready || snapshot.AuthenticationInvalid || snapshot.Active != null ||
|
if (status != KVSyncStatus.Ready || snapshot.AuthenticationInvalid || snapshot.Active != null ||
|
||||||
snapshot.Unknown != null || comparisonLocal == null)
|
snapshot.Unknown != null || comparisonLocal == null)
|
||||||
{
|
{
|
||||||
return Blocked();
|
return Blocked();
|
||||||
@@ -314,7 +286,7 @@ namespace game.PlayerDataStorage
|
|||||||
var visible = ReadView();
|
var visible = ReadView();
|
||||||
foreach (var key in protectedKeys)
|
foreach (var key in protectedKeys)
|
||||||
{
|
{
|
||||||
Apply(copy.LocalData, key, PlayerKVValue.At(visible, key));
|
Apply(copy.LocalData, key, KvValue.At(visible, key));
|
||||||
}
|
}
|
||||||
foreach (var key in comparisonLocal.LocalData.Keys.Concat(comparisonRemote.Keys).Distinct(StringComparer.Ordinal))
|
foreach (var key in comparisonLocal.LocalData.Keys.Concat(comparisonRemote.Keys).Distinct(StringComparer.Ordinal))
|
||||||
{
|
{
|
||||||
@@ -373,7 +345,7 @@ namespace game.PlayerDataStorage
|
|||||||
copy.ImmediatePreservedKeys.Remove(key);
|
copy.ImmediatePreservedKeys.Remove(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var result = Commit(copy, PlayerKVSyncStatus.Ready);
|
var result = Commit(copy, KVSyncStatus.Ready);
|
||||||
if (result.Success)
|
if (result.Success)
|
||||||
{
|
{
|
||||||
recoveredOrdinaryUnknown = false;
|
recoveredOrdinaryUnknown = false;
|
||||||
@@ -393,59 +365,51 @@ namespace game.PlayerDataStorage
|
|||||||
comparisonImmediateKeys = null;
|
comparisonImmediateKeys = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public PlayerKVStateResult RecordReadFailure(bool unauthorized = false)
|
public KvStateResult RecordReadFailure(bool unauthorized = false)
|
||||||
{
|
{
|
||||||
lock (gate)
|
lock (gate)
|
||||||
{
|
{
|
||||||
if (Terminal || snapshot.Active != null) return Blocked();
|
if (Terminal || snapshot.Active != null)
|
||||||
if (unauthorized)
|
|
||||||
{
|
|
||||||
var copy = snapshot.Clone(); copy.AuthenticationInvalid = true;
|
|
||||||
return Commit(copy, copy.Unknown == null ? PlayerKVSyncStatus.Unauthorized : PlayerKVSyncStatus.UnknownOutcome);
|
|
||||||
}
|
|
||||||
status = snapshot.Unknown != null ? PlayerKVSyncStatus.UnknownOutcome : PlayerKVSyncStatus.ReadFailed;
|
|
||||||
return new PlayerKVStateResult(PlayerKVChangeError.None, revision: snapshot.Revision);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public PlayerKVStateResult Update(string key, string value, bool remoteStore = true)
|
|
||||||
{
|
|
||||||
return Update(key, value, remoteStore, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal PlayerKVStateResult Update(string key, string value, bool remoteStore, long? updateTicks)
|
|
||||||
{
|
|
||||||
lock (gate)
|
|
||||||
{
|
|
||||||
if (status != PlayerKVSyncStatus.Ready || snapshot.AuthenticationInvalid)
|
|
||||||
{
|
{
|
||||||
return Blocked();
|
return Blocked();
|
||||||
}
|
}
|
||||||
if (!KVSnapshot.ValidKey(key) || value == null || snapshot.NextSequence == long.MaxValue ||
|
if (unauthorized)
|
||||||
(updateTicks.HasValue && (updateTicks.Value < 0 || updateTicks.Value > DateTime.MaxValue.Ticks)))
|
{
|
||||||
|
var copy = snapshot.Clone();
|
||||||
|
copy.AuthenticationInvalid = true;
|
||||||
|
return Commit(copy, copy.Unknown == null ? KVSyncStatus.Unauthorized : KVSyncStatus.UnknownOutcome);
|
||||||
|
}
|
||||||
|
status = snapshot.Unknown != null ? KVSyncStatus.UnknownOutcome : KVSyncStatus.ReadFailed;
|
||||||
|
return new KvStateResult(KVChangeError.None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal KvStateResult Update(string key, string value, bool remoteStore, long updateTicks)
|
||||||
|
{
|
||||||
|
lock (gate)
|
||||||
|
{
|
||||||
|
if (status != KVSyncStatus.Ready || snapshot.AuthenticationInvalid)
|
||||||
|
{
|
||||||
|
return Blocked();
|
||||||
|
}
|
||||||
|
if (!KvSnapshot.ValidKey(key) || value == null || snapshot.NextSequence == long.MaxValue ||
|
||||||
|
(updateTicks < 0 || updateTicks > DateTime.MaxValue.Ticks))
|
||||||
{
|
{
|
||||||
return Invalid();
|
return Invalid();
|
||||||
}
|
}
|
||||||
var copy = snapshot.Clone();
|
var copy = snapshot.Clone();
|
||||||
var sequence = ApplyUpdate(copy, key, value, remoteStore, updateTicks);
|
var sequence = ApplyUpdate(copy, key, value, remoteStore, updateTicks);
|
||||||
return Commit(copy, PlayerKVSyncStatus.Ready, sequence);
|
return Commit(copy, KVSyncStatus.Ready, sequence);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static long ApplyUpdate(KVSnapshot copy, string key, string value, bool remoteStore, long? updateTicks)
|
private static long ApplyUpdate(KvSnapshot copy, string key, string value, bool remoteStore, long updateTicks)
|
||||||
{
|
{
|
||||||
var sequence = copy.NextSequence++;
|
var sequence = copy.NextSequence++;
|
||||||
copy.LocalData[key] = value;
|
copy.LocalData[key] = value;
|
||||||
copy.ImmediatePreservedKeys.Remove(key);
|
copy.ImmediatePreservedKeys.Remove(key);
|
||||||
copy.LegacyLocalOnlyUnderImmediate.Remove(key);
|
copy.LegacyLocalOnlyUnderImmediate.Remove(key);
|
||||||
if (updateTicks.HasValue)
|
copy.LocalUpdateTicks[key] = updateTicks;
|
||||||
{
|
|
||||||
copy.LocalUpdateTicks[key] = updateTicks.Value;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
copy.LocalUpdateTicks.Remove(key);
|
|
||||||
}
|
|
||||||
var acceptedRemote = false;
|
var acceptedRemote = false;
|
||||||
if (remoteStore)
|
if (remoteStore)
|
||||||
{
|
{
|
||||||
@@ -456,8 +420,8 @@ namespace game.PlayerDataStorage
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
var baseline = bucket.TryGetValue(key, out var previous)
|
var baseline = bucket.TryGetValue(key, out var previous)
|
||||||
? previous.BaseValue : PlayerKVValue.At(copy.Baseline, key);
|
? previous.BaseValue : KvValue.At(copy.Baseline, key);
|
||||||
bucket[key] = new PlayerKVChange(key, sequence, baseline, PlayerKVValue.Present(value));
|
bucket[key] = new KvChange(key, sequence, baseline, KvValue.Present(value));
|
||||||
copy.LegacyPending.Remove(key);
|
copy.LegacyPending.Remove(key);
|
||||||
copy.ExplicitDeletes.Remove(key);
|
copy.ExplicitDeletes.Remove(key);
|
||||||
acceptedRemote = true;
|
acceptedRemote = true;
|
||||||
@@ -468,52 +432,26 @@ namespace game.PlayerDataStorage
|
|||||||
return sequence;
|
return sequence;
|
||||||
}
|
}
|
||||||
|
|
||||||
public PlayerKVStateResult Delete(string key)
|
internal KvStateResult CaptureOrdinaryBatch(out KvBatch batch)
|
||||||
{
|
|
||||||
lock (gate)
|
|
||||||
{
|
|
||||||
if (status != PlayerKVSyncStatus.Ready || snapshot.AuthenticationInvalid)
|
|
||||||
{
|
|
||||||
return Blocked();
|
|
||||||
}
|
|
||||||
if (!KVSnapshot.ValidKey(key) || snapshot.NextSequence == long.MaxValue)
|
|
||||||
{
|
|
||||||
return Invalid();
|
|
||||||
}
|
|
||||||
var copy = snapshot.Clone();
|
|
||||||
var sequence = copy.NextSequence++;
|
|
||||||
var prior = copy.ExplicitDeletes.TryGetValue(key, out var previous) ? previous.BaseValue : PlayerKVValue.At(copy.Baseline, key);
|
|
||||||
RemoveSources(copy, key);
|
|
||||||
copy.ExplicitDeletes[key] = new PlayerKVChange(key, sequence, prior, PlayerKVValue.Missing);
|
|
||||||
copy.LocalData.Remove(key);
|
|
||||||
copy.LocalUpdateTicks.Remove(key);
|
|
||||||
copy.LocalOrigins[key] = (false, sequence);
|
|
||||||
copy.ImmediatePreservedKeys.Remove(key);
|
|
||||||
copy.LegacyLocalOnlyUnderImmediate.Remove(key);
|
|
||||||
return Commit(copy, PlayerKVSyncStatus.Ready, sequence);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal PlayerKVStateResult CaptureOrdinaryBatch(out PlayerKVBatch batch)
|
|
||||||
{
|
{
|
||||||
lock (gate)
|
lock (gate)
|
||||||
{
|
{
|
||||||
batch = null;
|
batch = null;
|
||||||
if (status != PlayerKVSyncStatus.Ready || snapshot.AuthenticationInvalid)
|
if (status != KVSyncStatus.Ready || snapshot.AuthenticationInvalid)
|
||||||
{
|
{
|
||||||
return Blocked();
|
return Blocked();
|
||||||
}
|
}
|
||||||
if (snapshot.OrdinaryBuffers[0].Count == 0)
|
if (snapshot.OrdinaryBuffers[0].Count == 0)
|
||||||
{
|
{
|
||||||
return new PlayerKVStateResult(PlayerKVChangeError.None, revision: snapshot.Revision);
|
return new KvStateResult(KVChangeError.None);
|
||||||
}
|
}
|
||||||
var copy = snapshot.Clone();
|
var copy = snapshot.Clone();
|
||||||
var first = copy.OrdinaryBuffers[0];
|
var first = copy.OrdinaryBuffers[0];
|
||||||
var captured = new PlayerKVBatch(Guid.NewGuid().ToString("N"), first.Values.OrderBy(item => item.Sequence), true);
|
var captured = new KvBatch(Guid.NewGuid().ToString("N"), first.Values.OrderBy(item => item.Sequence), true);
|
||||||
copy.PreparedOrdinaryBatches.Add(captured);
|
copy.PreparedOrdinaryBatches.Add(captured);
|
||||||
copy.OrdinaryBuffers.RemoveAt(0);
|
copy.OrdinaryBuffers.RemoveAt(0);
|
||||||
copy.OrdinaryBuffers.Add(new Dictionary<string, PlayerKVChange>(StringComparer.Ordinal));
|
copy.OrdinaryBuffers.Add(new Dictionary<string, KvChange>(StringComparer.Ordinal));
|
||||||
var result = Commit(copy, PlayerKVSyncStatus.Ready);
|
var result = Commit(copy, KVSyncStatus.Ready);
|
||||||
if (result.Success)
|
if (result.Success)
|
||||||
{
|
{
|
||||||
batch = captured;
|
batch = captured;
|
||||||
@@ -522,23 +460,23 @@ namespace game.PlayerDataStorage
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal PlayerKVStateResult BeginOrdinaryBatch(out PlayerKVBatch batch)
|
internal KvStateResult BeginOrdinaryBatch(out KvBatch batch)
|
||||||
{
|
{
|
||||||
lock (gate)
|
lock (gate)
|
||||||
{
|
{
|
||||||
batch = null;
|
batch = null;
|
||||||
if (status != PlayerKVSyncStatus.Ready || snapshot.Active != null || snapshot.Unknown != null || snapshot.AuthenticationInvalid)
|
if (status != KVSyncStatus.Ready || snapshot.Active != null || snapshot.Unknown != null || snapshot.AuthenticationInvalid)
|
||||||
{
|
{
|
||||||
return Blocked();
|
return Blocked();
|
||||||
}
|
}
|
||||||
if (snapshot.PreparedOrdinaryBatches.Count == 0)
|
if (snapshot.PreparedOrdinaryBatches.Count == 0)
|
||||||
{
|
{
|
||||||
return new PlayerKVStateResult(PlayerKVChangeError.None, revision: snapshot.Revision);
|
return new KvStateResult(KVChangeError.None);
|
||||||
}
|
}
|
||||||
var copy = snapshot.Clone();
|
var copy = snapshot.Clone();
|
||||||
copy.Active = copy.PreparedOrdinaryBatches[0];
|
copy.Active = copy.PreparedOrdinaryBatches[0];
|
||||||
copy.PreparedOrdinaryBatches.RemoveAt(0);
|
copy.PreparedOrdinaryBatches.RemoveAt(0);
|
||||||
var result = Commit(copy, PlayerKVSyncStatus.Ready);
|
var result = Commit(copy, KVSyncStatus.Ready);
|
||||||
if (result.Success)
|
if (result.Success)
|
||||||
{
|
{
|
||||||
batch = snapshot.Active;
|
batch = snapshot.Active;
|
||||||
@@ -548,17 +486,13 @@ namespace game.PlayerDataStorage
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Explicit requests retain their original source until the outcome is known. Ordinary buckets are never selected here.
|
// Explicit requests retain their original source until the outcome is known. Ordinary buckets are never selected here.
|
||||||
public PlayerKVStateResult BeginBatch(long throughSequence, out PlayerKVBatch batch)
|
|
||||||
{
|
|
||||||
return BeginBatch(throughSequence, false, out batch);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal PlayerKVStateResult BeginBatch(long throughSequence, bool legacyOnly, out PlayerKVBatch batch)
|
internal KvStateResult BeginBatch(long throughSequence, out KvBatch batch)
|
||||||
{
|
{
|
||||||
lock (gate)
|
lock (gate)
|
||||||
{
|
{
|
||||||
batch = null;
|
batch = null;
|
||||||
if (status != PlayerKVSyncStatus.Ready || snapshot.Active != null || snapshot.Unknown != null || snapshot.AuthenticationInvalid)
|
if (status != KVSyncStatus.Ready || snapshot.Active != null || snapshot.Unknown != null || snapshot.AuthenticationInvalid)
|
||||||
{
|
{
|
||||||
return Blocked();
|
return Blocked();
|
||||||
}
|
}
|
||||||
@@ -566,15 +500,15 @@ namespace game.PlayerDataStorage
|
|||||||
{
|
{
|
||||||
return Invalid();
|
return Invalid();
|
||||||
}
|
}
|
||||||
var source = legacyOnly ? snapshot.LegacyPending.Values : snapshot.LegacyPending.Values.Concat(snapshot.ExplicitDeletes.Values);
|
var source = snapshot.LegacyPending.Values;
|
||||||
var ordered = source.Where(item => item.Sequence <= throughSequence).OrderBy(item => item.Sequence).ToArray();
|
var ordered = source.Where(item => item.Sequence <= throughSequence).OrderBy(item => item.Sequence).ToArray();
|
||||||
if (ordered.Length == 0)
|
if (ordered.Length == 0)
|
||||||
{
|
{
|
||||||
return new PlayerKVStateResult(PlayerKVChangeError.None, revision: snapshot.Revision);
|
return new KvStateResult(KVChangeError.None);
|
||||||
}
|
}
|
||||||
var copy = snapshot.Clone();
|
var copy = snapshot.Clone();
|
||||||
copy.Active = new PlayerKVBatch(Guid.NewGuid().ToString("N"), ordered.TakeWhile(item => item.Target.Exists == ordered[0].Target.Exists).Take(10));
|
copy.Active = new KvBatch(Guid.NewGuid().ToString("N"), ordered.TakeWhile(item => item.Target.Exists == ordered[0].Target.Exists).Take(10));
|
||||||
var result = Commit(copy, PlayerKVSyncStatus.Ready);
|
var result = Commit(copy, KVSyncStatus.Ready);
|
||||||
if (result.Success)
|
if (result.Success)
|
||||||
{
|
{
|
||||||
batch = snapshot.Active;
|
batch = snapshot.Active;
|
||||||
@@ -583,11 +517,11 @@ namespace game.PlayerDataStorage
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public PlayerKVStateResult ConfirmBatch(string batchId)
|
public KvStateResult ConfirmBatch(string batchId)
|
||||||
{
|
{
|
||||||
lock (gate)
|
lock (gate)
|
||||||
{
|
{
|
||||||
if (status != PlayerKVSyncStatus.Ready || snapshot.Active == null || snapshot.Active.Id != batchId)
|
if (status != KVSyncStatus.Ready || snapshot.Active == null || snapshot.Active.Id != batchId)
|
||||||
{
|
{
|
||||||
return Blocked();
|
return Blocked();
|
||||||
}
|
}
|
||||||
@@ -608,12 +542,12 @@ namespace game.PlayerDataStorage
|
|||||||
Rebase(copy, item.Key, item.Target);
|
Rebase(copy, item.Key, item.Target);
|
||||||
}
|
}
|
||||||
copy.Active = null;
|
copy.Active = null;
|
||||||
return Commit(copy, PlayerKVSyncStatus.Ready);
|
return Commit(copy, KVSyncStatus.Ready);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// A definitely undispatched ordinary batch retires, as the original SDK call did; explicit source references stay.
|
// A definitely undispatched ordinary batch retires, as the original SDK call did; explicit source references stay.
|
||||||
public PlayerKVStateResult FailBatch(string batchId, bool mayHaveCommitted, bool unauthorized = false)
|
public KvStateResult FailBatch(string batchId, bool mayHaveCommitted, bool unauthorized = false)
|
||||||
{
|
{
|
||||||
lock (gate)
|
lock (gate)
|
||||||
{
|
{
|
||||||
@@ -636,27 +570,27 @@ namespace game.PlayerDataStorage
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
copy.Active = null;
|
copy.Active = null;
|
||||||
var next = copy.AuthenticationInvalid ? PlayerKVSyncStatus.Unauthorized : ordinary ? PlayerKVSyncStatus.Ready : PlayerKVSyncStatus.ReadFailed;
|
var next = copy.AuthenticationInvalid ? KVSyncStatus.Unauthorized : ordinary ? KVSyncStatus.Ready : KVSyncStatus.ReadFailed;
|
||||||
if (copy.Unknown != null)
|
if (copy.Unknown != null)
|
||||||
{
|
{
|
||||||
next = PlayerKVSyncStatus.UnknownOutcome;
|
next = KVSyncStatus.UnknownOutcome;
|
||||||
}
|
}
|
||||||
return Commit(copy, next);
|
return Commit(copy, next);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IEnumerable<Dictionary<string, PlayerKVChange>> Sources(KVSnapshot value)
|
private static IEnumerable<Dictionary<string, KvChange>> Sources(KvSnapshot value)
|
||||||
{
|
{
|
||||||
return value.OrdinaryBuffers.Concat(new[] { value.LegacyPending, value.ExplicitDeletes });
|
return value.OrdinaryBuffers.Concat(new[] { value.LegacyPending, value.ExplicitDeletes });
|
||||||
}
|
}
|
||||||
private static void RemoveSources(KVSnapshot value, string key)
|
private static void RemoveSources(KvSnapshot value, string key)
|
||||||
{
|
{
|
||||||
foreach (var source in Sources(value))
|
foreach (var source in Sources(value))
|
||||||
{
|
{
|
||||||
source.Remove(key);
|
source.Remove(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private static void Rebase(KVSnapshot value, string key, PlayerKVValue baseline)
|
private static void Rebase(KvSnapshot value, string key, KvValue baseline)
|
||||||
{
|
{
|
||||||
foreach (var source in Sources(value))
|
foreach (var source in Sources(value))
|
||||||
{
|
{
|
||||||
@@ -667,52 +601,33 @@ namespace game.PlayerDataStorage
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal PlayerKVStateResult PrepareImmediate(string key, string value, out PlayerKVChange operation)
|
internal KvStateResult PrepareImmediate(IReadOnlyDictionary<string, string> data,
|
||||||
{
|
out IReadOnlyList<KvChange> operations)
|
||||||
operation = null;
|
|
||||||
if (!KVSnapshot.ValidKey(key) || value == null)
|
|
||||||
{
|
|
||||||
// Preserve the single-key state admission order and validation result.
|
|
||||||
lock (gate)
|
|
||||||
{
|
|
||||||
return status != PlayerKVSyncStatus.Ready || snapshot.AuthenticationInvalid ? Blocked() : Invalid();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var result = PrepareImmediate(new Dictionary<string, string> { [key] = value }, out var operations);
|
|
||||||
if (result.Success)
|
|
||||||
{
|
|
||||||
operation = operations[0];
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal PlayerKVStateResult PrepareImmediate(IReadOnlyDictionary<string, string> data,
|
|
||||||
out IReadOnlyList<PlayerKVChange> operations)
|
|
||||||
{
|
{
|
||||||
lock (gate)
|
lock (gate)
|
||||||
{
|
{
|
||||||
operations = null;
|
operations = null;
|
||||||
if (status != PlayerKVSyncStatus.Ready || snapshot.AuthenticationInvalid)
|
if (status != KVSyncStatus.Ready || snapshot.AuthenticationInvalid)
|
||||||
{
|
{
|
||||||
return Blocked();
|
return Blocked();
|
||||||
}
|
}
|
||||||
if (data == null || data.Count == 0 || data.Count > long.MaxValue - snapshot.NextSequence ||
|
if (data == null || data.Count == 0 || data.Count > long.MaxValue - snapshot.NextSequence ||
|
||||||
data.Any(pair => !KVSnapshot.ValidKey(pair.Key) || pair.Value == null))
|
data.Any(pair => !KvSnapshot.ValidKey(pair.Key) || pair.Value == null))
|
||||||
{
|
{
|
||||||
return Invalid();
|
return Invalid();
|
||||||
}
|
}
|
||||||
var copy = snapshot.Clone();
|
var copy = snapshot.Clone();
|
||||||
var prepared = new List<PlayerKVChange>();
|
var prepared = new List<KvChange>();
|
||||||
foreach (var pair in data)
|
foreach (var pair in data)
|
||||||
{
|
{
|
||||||
copy.ImmediatePreservedKeys.Add(pair.Key);
|
copy.ImmediatePreservedKeys.Add(pair.Key);
|
||||||
var operation = new PlayerKVChange(pair.Key, copy.NextSequence++,
|
var operation = new KvChange(pair.Key, copy.NextSequence++,
|
||||||
PlayerKVValue.At(copy.Baseline, pair.Key), PlayerKVValue.Present(pair.Value));
|
KvValue.At(copy.Baseline, pair.Key), KvValue.Present(pair.Value));
|
||||||
copy.RemoteOnlyWrites.Add(operation.Sequence, operation);
|
copy.RemoteOnlyWrites.Add(operation.Sequence, operation);
|
||||||
RemoveSources(copy, pair.Key);
|
RemoveSources(copy, pair.Key);
|
||||||
prepared.Add(operation);
|
prepared.Add(operation);
|
||||||
}
|
}
|
||||||
var result = Commit(copy, PlayerKVSyncStatus.Ready, prepared[0].Sequence);
|
var result = Commit(copy, KVSyncStatus.Ready, prepared[0].Sequence);
|
||||||
if (result.Success)
|
if (result.Success)
|
||||||
{
|
{
|
||||||
operations = prepared.AsReadOnly();
|
operations = prepared.AsReadOnly();
|
||||||
@@ -725,16 +640,11 @@ namespace game.PlayerDataStorage
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal PlayerKVStateResult CompleteImmediate(PlayerKVChange operation)
|
internal KvStateResult CompleteImmediate(IReadOnlyList<KvChange> operations)
|
||||||
{
|
|
||||||
return CompleteImmediate(new[] { operation });
|
|
||||||
}
|
|
||||||
|
|
||||||
internal PlayerKVStateResult CompleteImmediate(IReadOnlyList<PlayerKVChange> operations)
|
|
||||||
{
|
{
|
||||||
lock (gate)
|
lock (gate)
|
||||||
{
|
{
|
||||||
if (status != PlayerKVSyncStatus.Ready || snapshot.AuthenticationInvalid || snapshot.Active != null ||
|
if (status != KVSyncStatus.Ready || snapshot.AuthenticationInvalid || snapshot.Active != null ||
|
||||||
snapshot.Unknown != null || operations.Any(operation => !snapshot.RemoteOnlyWrites.ContainsKey(operation.Sequence)))
|
snapshot.Unknown != null || operations.Any(operation => !snapshot.RemoteOnlyWrites.ContainsKey(operation.Sequence)))
|
||||||
{
|
{
|
||||||
return Blocked();
|
return Blocked();
|
||||||
@@ -747,16 +657,11 @@ namespace game.PlayerDataStorage
|
|||||||
copy.RemoteOnlyWrites.Remove(operation.Sequence);
|
copy.RemoteOnlyWrites.Remove(operation.Sequence);
|
||||||
copy.ImmediatePreservedKeys.Add(operation.Key);
|
copy.ImmediatePreservedKeys.Add(operation.Key);
|
||||||
}
|
}
|
||||||
return Commit(copy, PlayerKVSyncStatus.Ready);
|
return Commit(copy, KVSyncStatus.Ready);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void CancelUndispatchedImmediate(PlayerKVChange operation)
|
internal void CancelUndispatchedImmediate(IReadOnlyList<KvChange> operations)
|
||||||
{
|
|
||||||
CancelUndispatchedImmediate(new[] { operation });
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void CancelUndispatchedImmediate(IReadOnlyList<PlayerKVChange> operations)
|
|
||||||
{
|
{
|
||||||
lock (gate)
|
lock (gate)
|
||||||
{
|
{
|
||||||
@@ -780,36 +685,52 @@ namespace game.PlayerDataStorage
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Dictionary<string, string> LocalView(KVSnapshot source)
|
private static Dictionary<string, string> LocalView(KvSnapshot source)
|
||||||
{
|
{
|
||||||
return new Dictionary<string, string>(source.LocalData, StringComparer.Ordinal);
|
return new Dictionary<string, string>(source.LocalData, StringComparer.Ordinal);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Terminal => status == PlayerKVSyncStatus.Closed || status == PlayerKVSyncStatus.StorageError;
|
private bool Terminal => status == KVSyncStatus.Closed || status == KVSyncStatus.StorageError;
|
||||||
PlayerKVStateResult Commit(KVSnapshot copy, PlayerKVSyncStatus nextStatus, long sequence = 0)
|
private KvStateResult Commit(KvSnapshot copy, KVSyncStatus nextStatus, long sequence = 0)
|
||||||
{
|
{
|
||||||
if (snapshot.Revision == long.MaxValue)
|
if (snapshot.Revision == long.MaxValue)
|
||||||
{ status = PlayerKVSyncStatus.StorageError; return new PlayerKVStateResult(PlayerKVChangeError.StorageError, sequence, snapshot.Revision); }
|
{
|
||||||
|
status = KVSyncStatus.StorageError;
|
||||||
|
return new KvStateResult(KVChangeError.StorageError, sequence);
|
||||||
|
}
|
||||||
copy.Revision = snapshot.Revision + 1;
|
copy.Revision = snapshot.Revision + 1;
|
||||||
if (!store.Save(copy, out var uncertain))
|
if (!store.Save(copy, out var uncertain))
|
||||||
{
|
{
|
||||||
status = PlayerKVSyncStatus.StorageError;
|
status = KVSyncStatus.StorageError;
|
||||||
return new PlayerKVStateResult(PlayerKVChangeError.StorageError, sequence, copy.Revision, uncertain);
|
return new KvStateResult(KVChangeError.StorageError, sequence, uncertain);
|
||||||
}
|
}
|
||||||
snapshot = copy; status = nextStatus;
|
snapshot = copy;
|
||||||
return new PlayerKVStateResult(PlayerKVChangeError.None, sequence, copy.Revision);
|
status = nextStatus;
|
||||||
|
return new KvStateResult(KVChangeError.None, sequence);
|
||||||
}
|
}
|
||||||
static void Apply(Dictionary<string, string> data, string key, PlayerKVValue value)
|
private static void Apply(Dictionary<string, string> data, string key, KvValue value)
|
||||||
{ if (value.Exists) data[key] = value.Value; else data.Remove(key); }
|
{
|
||||||
PlayerKVStateResult Blocked() => new PlayerKVStateResult(PlayerKVChangeError.Blocked, revision: snapshot.Revision);
|
if (value.Exists)
|
||||||
PlayerKVStateResult Invalid() => new PlayerKVStateResult(PlayerKVChangeError.Validation, revision: snapshot.Revision);
|
{
|
||||||
|
data[key] = value.Value;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
data.Remove(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private KvStateResult Blocked() => new KvStateResult(KVChangeError.Blocked);
|
||||||
|
private KvStateResult Invalid() => new KvStateResult(KVChangeError.Validation);
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
lock (gate)
|
lock (gate)
|
||||||
{
|
{
|
||||||
if (status == PlayerKVSyncStatus.Closed) return;
|
if (status == KVSyncStatus.Closed)
|
||||||
status = PlayerKVSyncStatus.Closed;
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
status = KVSyncStatus.Closed;
|
||||||
ClearComparison();
|
ClearComparison();
|
||||||
store.Dispose();
|
store.Dispose();
|
||||||
}
|
}
|
||||||
@@ -1,231 +0,0 @@
|
|||||||
using System;
|
|
||||||
using UnityEngine;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using game.PlayerDataStorage;
|
|
||||||
|
|
||||||
internal class KVMgr : MonoBehaviour
|
|
||||||
{
|
|
||||||
private static KVMgr instance;
|
|
||||||
private KVPlayerSaveStore store;
|
|
||||||
private CancellationTokenSource tokenSource;
|
|
||||||
|
|
||||||
private void Awake()
|
|
||||||
{
|
|
||||||
instance = this;
|
|
||||||
tokenSource = new CancellationTokenSource();
|
|
||||||
UpdateUserDataValue(tokenSource.Token);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static void Bind(KVPlayerSaveStore original)
|
|
||||||
{
|
|
||||||
if (instance != null)
|
|
||||||
{
|
|
||||||
instance.store = original;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static void Unbind(KVPlayerSaveStore original)
|
|
||||||
{
|
|
||||||
if (instance != null && ReferenceEquals(instance.store, original))
|
|
||||||
{
|
|
||||||
instance.store = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static void Stop(KVPlayerSaveStore original)
|
|
||||||
{
|
|
||||||
if (instance != null && ReferenceEquals(instance.store, original))
|
|
||||||
{
|
|
||||||
instance.Stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Stop()
|
|
||||||
{
|
|
||||||
tokenSource?.Cancel();
|
|
||||||
tokenSource?.Dispose();
|
|
||||||
tokenSource = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void UpdateUserDataValue(CancellationToken token)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
while (!token.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
SyncData();
|
|
||||||
await Task.Delay(40000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Debug.LogError("KV save schedule failed: " + ex.GetType().Name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SyncData()
|
|
||||||
{
|
|
||||||
var original = store;
|
|
||||||
if (original != null && original.IsCurrent)
|
|
||||||
{
|
|
||||||
ObserveSync(original.SyncDataAsync());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async void ObserveSync(Task<PlayerKVFlushResult> task)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var result = await task;
|
|
||||||
if (result.TransportError != PlayerKVError.None || result.Blocked != 0 || result.Unknown != 0)
|
|
||||||
{
|
|
||||||
Debug.LogError("KV ordinary save failed: " + result.TransportError);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Debug.LogError("KV ordinary save failed: " + ex.GetType().Name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnApplicationPause(bool paused)
|
|
||||||
{
|
|
||||||
if (paused)
|
|
||||||
{
|
|
||||||
Stop();
|
|
||||||
SyncData();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
tokenSource = new CancellationTokenSource();
|
|
||||||
UpdateUserDataValue(tokenSource.Token);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnApplicationQuit()
|
|
||||||
{
|
|
||||||
Stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnDestroy()
|
|
||||||
{
|
|
||||||
Stop();
|
|
||||||
SyncData();
|
|
||||||
store = null;
|
|
||||||
if (ReferenceEquals(instance, this))
|
|
||||||
{
|
|
||||||
instance = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static Task<PlayerKVFlushResult> SyncKVAsync(PlayerKVGameSessionHandle handle,
|
|
||||||
CancellationToken waitToken = default)
|
|
||||||
{
|
|
||||||
if (PlayerKVGameLoadGuard.Reject(PlayerKVGuardPoint.Sync))
|
|
||||||
{
|
|
||||||
return Task.FromResult(new PlayerKVFlushResult(0, 0, 1, 0, PlayerKVError.Cancelled,
|
|
||||||
handle == null ? PlayerKVSyncStatus.NotLoaded : handle.Status));
|
|
||||||
}
|
|
||||||
if (handle == null)
|
|
||||||
{
|
|
||||||
return Task.FromResult(new PlayerKVFlushResult(0, 0, 1, 0, PlayerKVError.Validation, PlayerKVSyncStatus.NotLoaded));
|
|
||||||
}
|
|
||||||
return handle.SyncDataAsync(waitToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static PlayerKVKeyReadResult ReadKV(PlayerKVGameSessionHandle handle, string key)
|
|
||||||
{
|
|
||||||
if (PlayerKVGameLoadGuard.Reject(PlayerKVGuardPoint.ReadLocal))
|
|
||||||
{
|
|
||||||
return new PlayerKVKeyReadResult(PlayerKVKeyReadError.LoadGuardActive, handle);
|
|
||||||
}
|
|
||||||
if (handle == null || !KVSnapshot.ValidKey(key))
|
|
||||||
{
|
|
||||||
return new PlayerKVKeyReadResult(PlayerKVKeyReadError.InvalidInput, handle);
|
|
||||||
}
|
|
||||||
var read = handle.ReadLocalSnapshot();
|
|
||||||
if (!read.Success)
|
|
||||||
{
|
|
||||||
return new PlayerKVKeyReadResult(read.Error == PlayerKVLocalReadError.StaleOrClosing
|
|
||||||
? PlayerKVKeyReadError.StaleOrClosing : PlayerKVKeyReadError.NotReady, handle);
|
|
||||||
}
|
|
||||||
var found = read.Data.TryGetValue(key, out var value);
|
|
||||||
return new PlayerKVKeyReadResult(PlayerKVKeyReadError.None, handle, found, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static PlayerKVEditResult WriteKV(PlayerKVGameSessionHandle handle,
|
|
||||||
string key, string value, bool remoteStore = true)
|
|
||||||
{
|
|
||||||
if (PlayerKVGameLoadGuard.Reject(PlayerKVGuardPoint.Update))
|
|
||||||
{
|
|
||||||
return new PlayerKVEditResult(new PlayerKVStateResult(PlayerKVChangeError.Blocked));
|
|
||||||
}
|
|
||||||
if (handle == null)
|
|
||||||
{
|
|
||||||
return new PlayerKVEditResult(new PlayerKVStateResult(PlayerKVChangeError.Validation));
|
|
||||||
}
|
|
||||||
return handle.Update(key, value, remoteStore, ZZTimeHelper.UtcNow().Ticks);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static async Task<bool> SyncDataImmidiate(PlayerKVGameSessionHandle handle, string key, string value)
|
|
||||||
{
|
|
||||||
var success = false;
|
|
||||||
if (!PlayerKVGameLoadGuard.Reject(PlayerKVGuardPoint.Immediate) && handle != null &&
|
|
||||||
KVSnapshot.ValidKey(key) && value != null)
|
|
||||||
{
|
|
||||||
success = await handle.SyncDataImmidiate(key, value);
|
|
||||||
}
|
|
||||||
if (!success)
|
|
||||||
{
|
|
||||||
UnityEngine.Debug.LogError("[KVMgr::SyncDataImmidiate] Save failed.");
|
|
||||||
}
|
|
||||||
return success;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static async Task<bool> SyncDataImmidiate(PlayerKVGameSessionHandle handle,
|
|
||||||
IReadOnlyDictionary<string, string> values)
|
|
||||||
{
|
|
||||||
var success = false;
|
|
||||||
if (!PlayerKVGameLoadGuard.Reject(PlayerKVGuardPoint.Immediate) && handle != null && values != null && values.Count > 0)
|
|
||||||
{
|
|
||||||
success = await handle.SyncDataImmidiate(values);
|
|
||||||
}
|
|
||||||
if (!success)
|
|
||||||
{
|
|
||||||
UnityEngine.Debug.LogError("[KVMgr::SyncDataImmidiate] Save failed.");
|
|
||||||
}
|
|
||||||
return success;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static Task<PlayerKVFlushResult> FlushKVAsync(PlayerKVGameSessionHandle handle,
|
|
||||||
CancellationToken waitToken = default)
|
|
||||||
{
|
|
||||||
if (PlayerKVGameLoadGuard.Reject(PlayerKVGuardPoint.Sync))
|
|
||||||
{
|
|
||||||
return Task.FromResult(new PlayerKVFlushResult(0, 0, 1, 0, PlayerKVError.Cancelled,
|
|
||||||
handle == null ? PlayerKVSyncStatus.NotLoaded : handle.Status));
|
|
||||||
}
|
|
||||||
if (handle == null)
|
|
||||||
{
|
|
||||||
return Task.FromResult(new PlayerKVFlushResult(0, 0, 1, 0, PlayerKVError.Validation, PlayerKVSyncStatus.NotLoaded));
|
|
||||||
}
|
|
||||||
return handle.FlushAsync(waitToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static Task<PlayerKVTicketResult> WaitForKVTicketAsync(PlayerKVGameSessionHandle handle,
|
|
||||||
PlayerKVSyncTicket ticket, CancellationToken waitToken = default)
|
|
||||||
{
|
|
||||||
if (PlayerKVGameLoadGuard.Reject(PlayerKVGuardPoint.Sync))
|
|
||||||
{
|
|
||||||
return Task.FromResult(new PlayerKVTicketResult(PlayerKVTicketStatus.Blocked, PlayerKVError.Cancelled,
|
|
||||||
handle == null ? PlayerKVSyncStatus.NotLoaded : handle.Status));
|
|
||||||
}
|
|
||||||
if (handle == null)
|
|
||||||
{
|
|
||||||
return Task.FromResult(new PlayerKVTicketResult(PlayerKVTicketStatus.Blocked, PlayerKVError.Validation, PlayerKVSyncStatus.NotLoaded));
|
|
||||||
}
|
|
||||||
return handle.WaitForTicketAsync(ticket, waitToken);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
329
Client/Assets/Scripts/Core/KvMgr.cs
Normal file
329
Client/Assets/Scripts/Core/KvMgr.cs
Normal file
@@ -0,0 +1,329 @@
|
|||||||
|
using Core.KV;
|
||||||
|
using game.KV;
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using UnityEngine;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
internal class KvMgr : MonoBehaviour
|
||||||
|
{
|
||||||
|
private static KvMgr instance;
|
||||||
|
private KvSaveStore store;
|
||||||
|
private CancellationTokenSource tokenSource;
|
||||||
|
|
||||||
|
private void Awake()
|
||||||
|
{
|
||||||
|
instance = this;
|
||||||
|
tokenSource = new CancellationTokenSource();
|
||||||
|
UpdateUserDataValue(tokenSource.Token);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void Bind(KvSaveStore original)
|
||||||
|
{
|
||||||
|
if (instance != null)
|
||||||
|
{
|
||||||
|
instance.store = original;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void Unbind(KvSaveStore original)
|
||||||
|
{
|
||||||
|
if (instance != null && ReferenceEquals(instance.store, original))
|
||||||
|
{
|
||||||
|
instance.store = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void Stop(KvSaveStore original)
|
||||||
|
{
|
||||||
|
if (instance != null && ReferenceEquals(instance.store, original))
|
||||||
|
{
|
||||||
|
instance.Stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Stop()
|
||||||
|
{
|
||||||
|
tokenSource?.Cancel();
|
||||||
|
tokenSource?.Dispose();
|
||||||
|
tokenSource = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void UpdateUserDataValue(CancellationToken token)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (!token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
SyncData();
|
||||||
|
await Task.Delay(40000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Debug.LogError("KV save schedule failed: " + ex.GetType().Name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SyncData()
|
||||||
|
{
|
||||||
|
var original = store;
|
||||||
|
if (original != null && original.IsCurrent)
|
||||||
|
{
|
||||||
|
ObserveSync(original.SyncDataAsync());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async void ObserveSync(Task<KvFlushResult> task)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await task;
|
||||||
|
if (result.TransportError != KVError.None || result.Blocked != 0 || result.Unknown != 0)
|
||||||
|
{
|
||||||
|
Debug.LogError("KV ordinary save failed: " + result.TransportError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Debug.LogError("KV ordinary save failed: " + ex.GetType().Name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnApplicationPause(bool paused)
|
||||||
|
{
|
||||||
|
if (paused)
|
||||||
|
{
|
||||||
|
Stop();
|
||||||
|
SyncData();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
tokenSource = new CancellationTokenSource();
|
||||||
|
UpdateUserDataValue(tokenSource.Token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnApplicationQuit()
|
||||||
|
{
|
||||||
|
Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDestroy()
|
||||||
|
{
|
||||||
|
Stop();
|
||||||
|
SyncData();
|
||||||
|
store = null;
|
||||||
|
if (ReferenceEquals(instance, this))
|
||||||
|
{
|
||||||
|
instance = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace game.KV
|
||||||
|
{
|
||||||
|
public sealed class KvSaveStore : IPlayerSaveStore, IDisposable
|
||||||
|
{
|
||||||
|
private KvSessionOwner handle;
|
||||||
|
private CancellationToken lifetime;
|
||||||
|
private bool bound, disposed;
|
||||||
|
private readonly object receiptsGate = new object();
|
||||||
|
public bool IsCurrent => bound && !disposed && !lifetime.IsCancellationRequested && handle.Status != KVSyncStatus.Closed;
|
||||||
|
|
||||||
|
internal void Bind(KvSessionOwner original, CancellationToken loginLifetime)
|
||||||
|
{
|
||||||
|
if (bound || disposed || original == null || loginLifetime.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Player save store cannot be rebound.");
|
||||||
|
}
|
||||||
|
if (!ReadKV(original, "LastLoginTimeUtc").Success)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Player save handle is not ready.");
|
||||||
|
}
|
||||||
|
handle = original;
|
||||||
|
lifetime = loginLifetime;
|
||||||
|
bound = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void ReleaseFailedBinding(KvSessionOwner original)
|
||||||
|
{
|
||||||
|
lock (receiptsGate)
|
||||||
|
{
|
||||||
|
if (!bound)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (disposed || !ReferenceEquals(handle, original) || original.Status != KVSyncStatus.Closed)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Failed save binding has not closed.");
|
||||||
|
}
|
||||||
|
handle = null;
|
||||||
|
bound = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public PlayerSaveReadResult Read(string key)
|
||||||
|
{
|
||||||
|
if (!IsCurrent)
|
||||||
|
{
|
||||||
|
return new PlayerSaveReadResult("StaleOrNotReady");
|
||||||
|
}
|
||||||
|
return ReadKV(handle, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PlayerSaveWriteResult Write(string key, string value, bool remoteStore = true)
|
||||||
|
{
|
||||||
|
lock (receiptsGate)
|
||||||
|
{
|
||||||
|
if (!IsCurrent)
|
||||||
|
{
|
||||||
|
var rejected = new PlayerSaveWriteResult("StaleOrNotReady");
|
||||||
|
return rejected;
|
||||||
|
}
|
||||||
|
var result = WriteKV(handle, key, value, remoteStore);
|
||||||
|
var write = result.Success ? new PlayerSaveWriteResult() :
|
||||||
|
new PlayerSaveWriteResult(result.Error.ToString());
|
||||||
|
return write;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> SyncDataImmidiate(string key, string value)
|
||||||
|
{
|
||||||
|
KvSessionOwner original;
|
||||||
|
CancellationToken originalLifetime;
|
||||||
|
Task<bool> save;
|
||||||
|
lock (receiptsGate)
|
||||||
|
{
|
||||||
|
if (!IsCurrent)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Preserve the former current-store dictionary lookup's null-key behavior.
|
||||||
|
if (key == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(key));
|
||||||
|
}
|
||||||
|
original = handle;
|
||||||
|
originalLifetime = lifetime;
|
||||||
|
save = SyncDataImmidiate(original, key, value);
|
||||||
|
}
|
||||||
|
var success = await save;
|
||||||
|
lock (receiptsGate)
|
||||||
|
{
|
||||||
|
return success && IsCurrent && !originalLifetime.IsCancellationRequested && ReferenceEquals(handle, original);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal async Task<bool> SyncDataImmidiate(IReadOnlyDictionary<string, string> values)
|
||||||
|
{
|
||||||
|
KvSessionOwner original;
|
||||||
|
CancellationToken originalLifetime;
|
||||||
|
Task<bool> save;
|
||||||
|
lock (receiptsGate)
|
||||||
|
{
|
||||||
|
if (!IsCurrent || values == null || values.Count == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var data = values.ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.Ordinal);
|
||||||
|
original = handle;
|
||||||
|
originalLifetime = lifetime;
|
||||||
|
save = SyncDataImmidiate(original, data);
|
||||||
|
}
|
||||||
|
var success = await save;
|
||||||
|
lock (receiptsGate)
|
||||||
|
{
|
||||||
|
return success && IsCurrent && !originalLifetime.IsCancellationRequested && ReferenceEquals(handle, original);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal Task<KvFlushResult> SyncDataAsync()
|
||||||
|
{
|
||||||
|
lock (receiptsGate)
|
||||||
|
{
|
||||||
|
var original = handle;
|
||||||
|
if (!IsCurrent)
|
||||||
|
{
|
||||||
|
return Task.FromResult(new KvFlushResult(1, 0, KVError.Cancelled));
|
||||||
|
}
|
||||||
|
if (original == null)
|
||||||
|
{
|
||||||
|
return Task.FromResult(new KvFlushResult(1, 0, KVError.Validation));
|
||||||
|
}
|
||||||
|
return original.SyncDataAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
lock (receiptsGate)
|
||||||
|
{
|
||||||
|
disposed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static PlayerSaveReadResult ReadKV(KvSessionOwner handle, string key)
|
||||||
|
{
|
||||||
|
if (handle == null || !KvSnapshot.ValidKey(key))
|
||||||
|
{
|
||||||
|
return new PlayerSaveReadResult("InvalidInput");
|
||||||
|
}
|
||||||
|
var read = handle.ReadLocalSnapshot();
|
||||||
|
if (!read.Success)
|
||||||
|
{
|
||||||
|
return new PlayerSaveReadResult(read.Error == KVLocalReadError.StaleOrClosing
|
||||||
|
? "StaleOrClosing" : "NotReady");
|
||||||
|
}
|
||||||
|
var found = read.Data.TryGetValue(key, out var value);
|
||||||
|
if (found && value == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("A successful key observation requires an existence verdict.");
|
||||||
|
}
|
||||||
|
return new PlayerSaveReadResult(found, found ? value : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static KvStateResult WriteKV(KvSessionOwner handle,
|
||||||
|
string key, string value, bool remoteStore = true)
|
||||||
|
{
|
||||||
|
if (handle == null)
|
||||||
|
{
|
||||||
|
return new KvStateResult(KVChangeError.Validation);
|
||||||
|
}
|
||||||
|
return handle.Update(key, value, remoteStore, ZZTimeHelper.UtcNow().Ticks);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static async Task<bool> SyncDataImmidiate(KvSessionOwner handle, string key, string value)
|
||||||
|
{
|
||||||
|
var success = false;
|
||||||
|
if (handle != null &&
|
||||||
|
KvSnapshot.ValidKey(key) && value != null)
|
||||||
|
{
|
||||||
|
success = await handle.SyncDataImmidiate(key, value);
|
||||||
|
}
|
||||||
|
if (!success)
|
||||||
|
{
|
||||||
|
UnityEngine.Debug.LogError("[KVMgr::SyncDataImmidiate] Save failed.");
|
||||||
|
}
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static async Task<bool> SyncDataImmidiate(KvSessionOwner handle,
|
||||||
|
IReadOnlyDictionary<string, string> values)
|
||||||
|
{
|
||||||
|
var success = false;
|
||||||
|
if (handle != null && values != null && values.Count > 0)
|
||||||
|
{
|
||||||
|
success = await handle.SyncDataImmidiate(values);
|
||||||
|
}
|
||||||
|
if (!success)
|
||||||
|
{
|
||||||
|
UnityEngine.Debug.LogError("[KVMgr::SyncDataImmidiate] Save failed.");
|
||||||
|
}
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,7 +7,6 @@ using System.Linq;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using game.PlayerDataStorage;
|
|
||||||
|
|
||||||
public class PlayFabMgr : MonoBehaviour
|
public class PlayFabMgr : MonoBehaviour
|
||||||
{
|
{
|
||||||
@@ -43,7 +42,6 @@ public class PlayFabMgr : MonoBehaviour
|
|||||||
|
|
||||||
public Dictionary<string, string> CompareDatas(Dictionary<string, UserDataRecord> remoteDatas, string userId)
|
public Dictionary<string, string> CompareDatas(Dictionary<string, UserDataRecord> remoteDatas, string userId)
|
||||||
{
|
{
|
||||||
PlayerKVGameLoadGuard.RequireLegacyAllowed(PlayerKVGuardPoint.Compare);
|
|
||||||
Last_Update_Time_Key = $"{Last_Update_Time_Key_Prefix}_{userId}";
|
Last_Update_Time_Key = $"{Last_Update_Time_Key_Prefix}_{userId}";
|
||||||
Local_Data_Key = $"{Local_Data_Key_prefix}_{userId}";
|
Local_Data_Key = $"{Local_Data_Key_prefix}_{userId}";
|
||||||
var lastUpdateTimeStr = PlayerPrefs.GetString(Last_Update_Time_Key, string.Empty);
|
var lastUpdateTimeStr = PlayerPrefs.GetString(Last_Update_Time_Key, string.Empty);
|
||||||
@@ -117,7 +115,6 @@ public class PlayFabMgr : MonoBehaviour
|
|||||||
|
|
||||||
void Awake()
|
void Awake()
|
||||||
{
|
{
|
||||||
if(PlayerKVGameLoadGuard.Reject(PlayerKVGuardPoint.Lifecycle)) return;
|
|
||||||
_instance = this;
|
_instance = this;
|
||||||
InitBuffer();
|
InitBuffer();
|
||||||
tokenSource = new CancellationTokenSource();
|
tokenSource = new CancellationTokenSource();
|
||||||
@@ -136,7 +133,6 @@ public class PlayFabMgr : MonoBehaviour
|
|||||||
|
|
||||||
void OnApplicationPause(bool is_pause)
|
void OnApplicationPause(bool is_pause)
|
||||||
{
|
{
|
||||||
if(PlayerKVGameLoadGuard.Reject(PlayerKVGuardPoint.Lifecycle)) return;
|
|
||||||
if (is_pause)
|
if (is_pause)
|
||||||
{
|
{
|
||||||
tokenSource.Cancel();
|
tokenSource.Cancel();
|
||||||
@@ -152,7 +148,6 @@ public class PlayFabMgr : MonoBehaviour
|
|||||||
|
|
||||||
void OnApplicationQuit()
|
void OnApplicationQuit()
|
||||||
{
|
{
|
||||||
if(PlayerKVGameLoadGuard.Reject(PlayerKVGuardPoint.Lifecycle)) return;
|
|
||||||
tokenSource.Cancel();
|
tokenSource.Cancel();
|
||||||
tokenSource = null;
|
tokenSource = null;
|
||||||
SyncData();
|
SyncData();
|
||||||
@@ -160,16 +155,14 @@ public class PlayFabMgr : MonoBehaviour
|
|||||||
|
|
||||||
void OnDestroy()
|
void OnDestroy()
|
||||||
{
|
{
|
||||||
if(PlayerKVGameLoadGuard.Reject(PlayerKVGuardPoint.Lifecycle)) return;
|
|
||||||
tokenSource?.Cancel();
|
tokenSource?.Cancel();
|
||||||
tokenSource = null;
|
tokenSource = null;
|
||||||
if(!PlayerKVGameLoadGuard.Reject(PlayerKVGuardPoint.PrefsSave)) PlayerPrefs.Save();
|
PlayerPrefs.Save();
|
||||||
SyncData();
|
SyncData();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void UpdateUserDataValue(CancellationToken token)
|
private async void UpdateUserDataValue(CancellationToken token)
|
||||||
{
|
{
|
||||||
if(PlayerKVGameLoadGuard.Reject(PlayerKVGuardPoint.Lifecycle)) return;
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
while (!token.IsCancellationRequested)
|
while (!token.IsCancellationRequested)
|
||||||
@@ -183,7 +176,6 @@ public class PlayFabMgr : MonoBehaviour
|
|||||||
|
|
||||||
public void UpdateUserDataValue(string key, string data, bool remoteStore = true)
|
public void UpdateUserDataValue(string key, string data, bool remoteStore = true)
|
||||||
{
|
{
|
||||||
PlayerKVGameLoadGuard.RequireLegacyAllowed(PlayerKVGuardPoint.Update);
|
|
||||||
if (remoteStore)
|
if (remoteStore)
|
||||||
{
|
{
|
||||||
var cbuff = dataBuffer.First;
|
var cbuff = dataBuffer.First;
|
||||||
@@ -213,7 +205,6 @@ public class PlayFabMgr : MonoBehaviour
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public string GetLocalData(string key)
|
public string GetLocalData(string key)
|
||||||
{
|
{
|
||||||
PlayerKVGameLoadGuard.RequireLegacyAllowed(PlayerKVGuardPoint.ReadLocal);
|
|
||||||
if (localData.ContainsKey(key))
|
if (localData.ContainsKey(key))
|
||||||
{
|
{
|
||||||
return localData[key];
|
return localData[key];
|
||||||
@@ -226,7 +217,6 @@ public class PlayFabMgr : MonoBehaviour
|
|||||||
|
|
||||||
private void SyncData()
|
private void SyncData()
|
||||||
{
|
{
|
||||||
if(PlayerKVGameLoadGuard.Reject(PlayerKVGuardPoint.Sync)) return;
|
|
||||||
// Logout clears credentials before RootCtx is destroyed. Keep pending data local.
|
// Logout clears credentials before RootCtx is destroyed. Keep pending data local.
|
||||||
if (!PlayFabClientAPI.IsClientLoggedIn()) return;
|
if (!PlayFabClientAPI.IsClientLoggedIn()) return;
|
||||||
var buf = dataBuffer.First;
|
var buf = dataBuffer.First;
|
||||||
@@ -282,7 +272,6 @@ public class PlayFabMgr : MonoBehaviour
|
|||||||
|
|
||||||
public async Task<bool> SyncDataImmidiate(string key, string data)
|
public async Task<bool> SyncDataImmidiate(string key, string data)
|
||||||
{
|
{
|
||||||
if(PlayerKVGameLoadGuard.Reject(PlayerKVGuardPoint.Immediate)) return false;
|
|
||||||
foreach (var buff in dataBuffer)
|
foreach (var buff in dataBuffer)
|
||||||
{
|
{
|
||||||
if (buff.ContainsKey(key))
|
if (buff.ContainsKey(key))
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ using System.Collections.Generic;
|
|||||||
|
|
||||||
public class BargainPackData
|
public class BargainPackData
|
||||||
{
|
{
|
||||||
[asap.core.Inject] public game.PlayerDataStorage.IPlayerSaveStore SaveStore { get; set; }
|
[asap.core.Inject] public Core.KV.IPlayerSaveStore SaveStore { get; set; }
|
||||||
|
|
||||||
private readonly Tables _tables = GContext.container.Resolve<Tables>();
|
private readonly Tables _tables = GContext.container.Resolve<Tables>();
|
||||||
private readonly TbEventPackManager _epm = GContext.container.Resolve<Tables>().TbEventPackManager;
|
private readonly TbEventPackManager _epm = GContext.container.Resolve<Tables>().TbEventPackManager;
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ public struct BpVipUnlock
|
|||||||
|
|
||||||
public class BattlePassData
|
public class BattlePassData
|
||||||
{
|
{
|
||||||
[asap.core.Inject] public game.PlayerDataStorage.IPlayerSaveStore playerSaveStore { get; set; }
|
[asap.core.Inject] public Core.KV.IPlayerSaveStore playerSaveStore { get; set; }
|
||||||
public BattlePassRecord BattlePassRecord;
|
public BattlePassRecord BattlePassRecord;
|
||||||
public BattleTask BattleTask;
|
public BattleTask BattleTask;
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ using System.Collections.Generic;
|
|||||||
|
|
||||||
public class BuffDataCenter
|
public class BuffDataCenter
|
||||||
{
|
{
|
||||||
[asap.core.Inject] public game.PlayerDataStorage.IPlayerSaveStore SaveStore { get; set; }
|
[asap.core.Inject] public Core.KV.IPlayerSaveStore SaveStore { get; set; }
|
||||||
|
|
||||||
private cfg.Tables _tables;
|
private cfg.Tables _tables;
|
||||||
public BuffDataCenter(cfg.Tables tables)
|
public BuffDataCenter(cfg.Tables tables)
|
||||||
|
|||||||
@@ -56,8 +56,8 @@ namespace GameCore
|
|||||||
{
|
{
|
||||||
public partial class FishingEventData : IDisposable
|
public partial class FishingEventData : IDisposable
|
||||||
{
|
{
|
||||||
game.PlayerDataStorage.IPlayerSaveStore boundSaveStore;
|
Core.KV.IPlayerSaveStore boundSaveStore;
|
||||||
[Inject] public game.PlayerDataStorage.IPlayerSaveStore playerSaveStore
|
[Inject] public Core.KV.IPlayerSaveStore playerSaveStore
|
||||||
{
|
{
|
||||||
get => boundSaveStore;
|
get => boundSaveStore;
|
||||||
set
|
set
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ namespace GameCore
|
|||||||
}
|
}
|
||||||
public class GuideDataCenter : IDisposable
|
public class GuideDataCenter : IDisposable
|
||||||
{
|
{
|
||||||
[asap.core.Inject] public game.PlayerDataStorage.IPlayerSaveStore playerSaveStore { get; set; }
|
[asap.core.Inject] public Core.KV.IPlayerSaveStore playerSaveStore { get; set; }
|
||||||
Tables _tables;
|
Tables _tables;
|
||||||
PlayerData playerData;
|
PlayerData playerData;
|
||||||
public GuideDataCenter(cfg.Tables tables, PlayerData playerData)
|
public GuideDataCenter(cfg.Tables tables, PlayerData playerData)
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
|
using Core.KV;
|
||||||
using asap.core;
|
using asap.core;
|
||||||
using asap.playfab.async;
|
using asap.playfab.async;
|
||||||
using cfg;
|
using cfg;
|
||||||
using game;
|
using game;
|
||||||
using game.PlayerDataStorage;
|
using game.KV;
|
||||||
using PlayFab.ClientModels;
|
using PlayFab.ClientModels;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
@@ -164,9 +165,9 @@ namespace GameCore
|
|||||||
{
|
{
|
||||||
Statistics.Add(new StatisticUpdate() { StatisticName = names[i], Value = value[i] });
|
Statistics.Add(new StatisticUpdate() { StatisticName = names[i], Value = value[i] });
|
||||||
}
|
}
|
||||||
if (userService is KVUserService kvUser)
|
if (userService is KvUserService kvUser)
|
||||||
{
|
{
|
||||||
var store = kvUser.SaveStore as KVPlayerSaveStore;
|
var store = kvUser.SaveStore as KvSaveStore;
|
||||||
var writtenNames = new HashSet<string>(StringComparer.Ordinal);
|
var writtenNames = new HashSet<string>(StringComparer.Ordinal);
|
||||||
var data = new Dictionary<string, string>(StringComparer.Ordinal);
|
var data = new Dictionary<string, string>(StringComparer.Ordinal);
|
||||||
foreach (var statistic in Statistics)
|
foreach (var statistic in Statistics)
|
||||||
@@ -175,7 +176,7 @@ namespace GameCore
|
|||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!KVSnapshot.ValidKey(statistic.StatisticName))
|
if (!KvSnapshot.ValidKey(statistic.StatisticName))
|
||||||
{
|
{
|
||||||
Debug.LogError("KV statistic write failed: " + statistic.StatisticName + ": Validation");
|
Debug.LogError("KV statistic write failed: " + statistic.StatisticName + ": Validation");
|
||||||
continue;
|
continue;
|
||||||
@@ -199,7 +200,7 @@ namespace GameCore
|
|||||||
}
|
}
|
||||||
public async Task<GetLeaderboardResult> GetLeadboardData(string leaderboardNaem, int maxCount = 50)
|
public async Task<GetLeaderboardResult> GetLeadboardData(string leaderboardNaem, int maxCount = 50)
|
||||||
{
|
{
|
||||||
if (userService is KVUserService)
|
if (userService is KvUserService)
|
||||||
{
|
{
|
||||||
var json = await customServerMgr.CustomServerPost("leaderboard/GetLeaderboard",
|
var json = await customServerMgr.CustomServerPost("leaderboard/GetLeaderboard",
|
||||||
Newtonsoft.Json.JsonConvert.SerializeObject(new
|
Newtonsoft.Json.JsonConvert.SerializeObject(new
|
||||||
@@ -214,7 +215,7 @@ namespace GameCore
|
|||||||
}
|
}
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var response = Newtonsoft.Json.JsonConvert.DeserializeObject<KVLeaderboardResponse>(json);
|
var response = Newtonsoft.Json.JsonConvert.DeserializeObject<KvLeaderboardResponse>(json);
|
||||||
if (response == null || response.Code != 1 || response.Leaderboard == null ||
|
if (response == null || response.Code != 1 || response.Leaderboard == null ||
|
||||||
response.Leaderboard.Any(entry => entry == null || entry.Profile == null))
|
response.Leaderboard.Any(entry => entry == null || entry.Profile == null))
|
||||||
{
|
{
|
||||||
@@ -244,7 +245,7 @@ namespace GameCore
|
|||||||
return await PlayFabClientAsyncAPI.GetLeaderboardAsync(reques);
|
return await PlayFabClientAsyncAPI.GetLeaderboardAsync(reques);
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class KVLeaderboardResponse
|
private sealed class KvLeaderboardResponse
|
||||||
{
|
{
|
||||||
public int Code
|
public int Code
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ namespace GameCore
|
|||||||
{
|
{
|
||||||
public class PlayerData
|
public class PlayerData
|
||||||
{
|
{
|
||||||
[asap.core.Inject] public game.PlayerDataStorage.IPlayerSaveStore playerSaveStore { get; set; }
|
[asap.core.Inject] public Core.KV.IPlayerSaveStore playerSaveStore { get; set; }
|
||||||
private cfg.Tables _tables;
|
private cfg.Tables _tables;
|
||||||
MMTService _MMService;
|
MMTService _MMService;
|
||||||
public PlayerData(cfg.Tables tables, MMTService mmService)
|
public PlayerData(cfg.Tables tables, MMTService mmService)
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ namespace GameCore
|
|||||||
}
|
}
|
||||||
public partial class PlayerFishData
|
public partial class PlayerFishData
|
||||||
{
|
{
|
||||||
[asap.core.Inject] public game.PlayerDataStorage.IPlayerSaveStore playerSaveStore { get; set; }
|
[asap.core.Inject] public Core.KV.IPlayerSaveStore playerSaveStore { get; set; }
|
||||||
private cfg.Tables _tables;
|
private cfg.Tables _tables;
|
||||||
private ICustomServerMgr customServerMgr;
|
private ICustomServerMgr customServerMgr;
|
||||||
public PlayerFishData(cfg.Tables tables, ICustomServerMgr customServerMgr)
|
public PlayerFishData(cfg.Tables tables, ICustomServerMgr customServerMgr)
|
||||||
|
|||||||
@@ -73,8 +73,8 @@ namespace GameCore
|
|||||||
}
|
}
|
||||||
public partial class PlayerShopData : IDisposable
|
public partial class PlayerShopData : IDisposable
|
||||||
{
|
{
|
||||||
game.PlayerDataStorage.IPlayerSaveStore boundSaveStore;
|
Core.KV.IPlayerSaveStore boundSaveStore;
|
||||||
[asap.core.Inject] public game.PlayerDataStorage.IPlayerSaveStore SaveStore
|
[asap.core.Inject] public Core.KV.IPlayerSaveStore SaveStore
|
||||||
{
|
{
|
||||||
get => boundSaveStore;
|
get => boundSaveStore;
|
||||||
set { CouponData.BindSaveStore(value); boundSaveStore = value; }
|
set { CouponData.BindSaveStore(value); boundSaveStore = value; }
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ namespace GameCore
|
|||||||
}
|
}
|
||||||
public class SigninData
|
public class SigninData
|
||||||
{
|
{
|
||||||
[asap.core.Inject] public game.PlayerDataStorage.IPlayerSaveStore SaveStore { get; set; }
|
[asap.core.Inject] public Core.KV.IPlayerSaveStore SaveStore { get; set; }
|
||||||
|
|
||||||
private cfg.Tables _tables;
|
private cfg.Tables _tables;
|
||||||
public SigninData(cfg.Tables tables)
|
public SigninData(cfg.Tables tables)
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ namespace GameCore
|
|||||||
}
|
}
|
||||||
public class SmallGameData
|
public class SmallGameData
|
||||||
{
|
{
|
||||||
[asap.core.Inject] public game.PlayerDataStorage.IPlayerSaveStore playerSaveStore { get; set; }
|
[asap.core.Inject] public Core.KV.IPlayerSaveStore playerSaveStore { get; set; }
|
||||||
private cfg.Tables _tables;
|
private cfg.Tables _tables;
|
||||||
IUserService userService;
|
IUserService userService;
|
||||||
ICustomServerMgr customServerMgr;
|
ICustomServerMgr customServerMgr;
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ namespace GameCore
|
|||||||
}
|
}
|
||||||
public class TriggerPackData
|
public class TriggerPackData
|
||||||
{
|
{
|
||||||
[asap.core.Inject] public game.PlayerDataStorage.IPlayerSaveStore SaveStore { get; set; }
|
[asap.core.Inject] public Core.KV.IPlayerSaveStore SaveStore { get; set; }
|
||||||
|
|
||||||
Tables _tables;
|
Tables _tables;
|
||||||
PlayerShopData playerShopData;
|
PlayerShopData playerShopData;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ using GameCore;
|
|||||||
using cfg;
|
using cfg;
|
||||||
public class EventPackData
|
public class EventPackData
|
||||||
{
|
{
|
||||||
[asap.core.Inject] public game.PlayerDataStorage.IPlayerSaveStore playerSaveStore { get; set; }
|
[asap.core.Inject] public Core.KV.IPlayerSaveStore playerSaveStore { get; set; }
|
||||||
private Tables _tables = GContext.container.Resolve<Tables>();
|
private Tables _tables = GContext.container.Resolve<Tables>();
|
||||||
private struct Data
|
private struct Data
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ namespace DataCenter
|
|||||||
// EventPartnerGather 管理器
|
// EventPartnerGather 管理器
|
||||||
public class EventPartnerGatherManager : IAutoMatchManagerContext
|
public class EventPartnerGatherManager : IAutoMatchManagerContext
|
||||||
{
|
{
|
||||||
[asap.core.Inject] public game.PlayerDataStorage.IPlayerSaveStore SaveStore { get; set; }
|
[asap.core.Inject] public Core.KV.IPlayerSaveStore SaveStore { get; set; }
|
||||||
|
|
||||||
private const bool EnableTraceLogs = false;
|
private const bool EnableTraceLogs = false;
|
||||||
public const string SaveKey = "EventPartnerGatherData";
|
public const string SaveKey = "EventPartnerGatherData";
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ namespace game
|
|||||||
{
|
{
|
||||||
public class EventPinballDataManager
|
public class EventPinballDataManager
|
||||||
{
|
{
|
||||||
[asap.core.Inject] public game.PlayerDataStorage.IPlayerSaveStore playerSaveStore { get; set; }
|
[asap.core.Inject] public Core.KV.IPlayerSaveStore playerSaveStore { get; set; }
|
||||||
|
|
||||||
#if UNITY_EDITOR
|
#if UNITY_EDITOR
|
||||||
private bool m_IsCheckCondition = true;
|
private bool m_IsCheckCondition = true;
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ namespace game
|
|||||||
{
|
{
|
||||||
public class EventPotionDataManager
|
public class EventPotionDataManager
|
||||||
{
|
{
|
||||||
[asap.core.Inject] public game.PlayerDataStorage.IPlayerSaveStore playerSaveStore { get; set; }
|
[asap.core.Inject] public Core.KV.IPlayerSaveStore playerSaveStore { get; set; }
|
||||||
#if UNITY_EDITOR
|
#if UNITY_EDITOR
|
||||||
private bool m_IsCheckCondition = true;
|
private bool m_IsCheckCondition = true;
|
||||||
private bool m_IsSyncData = true;
|
private bool m_IsSyncData = true;
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ namespace game
|
|||||||
{
|
{
|
||||||
public class EventScratchTicketDataManager
|
public class EventScratchTicketDataManager
|
||||||
{
|
{
|
||||||
[asap.core.Inject] public game.PlayerDataStorage.IPlayerSaveStore playerSaveStore { get; set; }
|
[asap.core.Inject] public Core.KV.IPlayerSaveStore playerSaveStore { get; set; }
|
||||||
#if UNITY_EDITOR
|
#if UNITY_EDITOR
|
||||||
private bool m_IsCheckCondition = true;
|
private bool m_IsCheckCondition = true;
|
||||||
private bool m_IsSyncData = true;
|
private bool m_IsSyncData = true;
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ public class FishingChallengeData
|
|||||||
|
|
||||||
public class FishingChallengeManager
|
public class FishingChallengeManager
|
||||||
{
|
{
|
||||||
[asap.core.Inject] public game.PlayerDataStorage.IPlayerSaveStore SaveStore { get; set; }
|
[asap.core.Inject] public Core.KV.IPlayerSaveStore SaveStore { get; set; }
|
||||||
|
|
||||||
//
|
//
|
||||||
private const string SaveKey = "FishingChallengeData";
|
private const string SaveKey = "FishingChallengeData";
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ namespace DataCenter
|
|||||||
|
|
||||||
public class OfferChainsChestManager
|
public class OfferChainsChestManager
|
||||||
{
|
{
|
||||||
[asap.core.Inject] public game.PlayerDataStorage.IPlayerSaveStore playerSaveStore { get; set; }
|
[asap.core.Inject] public Core.KV.IPlayerSaveStore playerSaveStore { get; set; }
|
||||||
|
|
||||||
public const string SaveKey = "OfferChainsChestData";
|
public const string SaveKey = "OfferChainsChestData";
|
||||||
[Inject] public Tables _tables { get; set; }
|
[Inject] public Tables _tables { get; set; }
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ namespace DataCenter
|
|||||||
// Disco Manager
|
// Disco Manager
|
||||||
public class OfferDiscoPartyManager
|
public class OfferDiscoPartyManager
|
||||||
{
|
{
|
||||||
[asap.core.Inject] public game.PlayerDataStorage.IPlayerSaveStore playerSaveStore { get; set; }
|
[asap.core.Inject] public Core.KV.IPlayerSaveStore playerSaveStore { get; set; }
|
||||||
public const string SaveKey = "OfferDiscoPartyManager";
|
public const string SaveKey = "OfferDiscoPartyManager";
|
||||||
|
|
||||||
// 免费抽奖次数
|
// 免费抽奖次数
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ using game;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
public class PiggyBankPackData
|
public class PiggyBankPackData
|
||||||
{
|
{
|
||||||
[asap.core.Inject] public game.PlayerDataStorage.IPlayerSaveStore playerSaveStore { get; set; }
|
[asap.core.Inject] public Core.KV.IPlayerSaveStore playerSaveStore { get; set; }
|
||||||
private readonly Tables _tables = GContext.container.Resolve<Tables>();
|
private readonly Tables _tables = GContext.container.Resolve<Tables>();
|
||||||
private readonly TbEventPackManager _epm
|
private readonly TbEventPackManager _epm
|
||||||
= GContext.container.Resolve<Tables>().TbEventPackManager;
|
= GContext.container.Resolve<Tables>().TbEventPackManager;
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ namespace GameCore
|
|||||||
}
|
}
|
||||||
public class TotalRechargeManager
|
public class TotalRechargeManager
|
||||||
{
|
{
|
||||||
readonly game.PlayerDataStorage.IPlayerSaveStore saveStore = GContext.container.Resolve<game.PlayerDataStorage.IPlayerSaveStore>();
|
readonly Core.KV.IPlayerSaveStore saveStore = GContext.container.Resolve<Core.KV.IPlayerSaveStore>();
|
||||||
|
|
||||||
public const string TotalRechargeDataKey = "EventTotalRecharge";
|
public const string TotalRechargeDataKey = "EventTotalRecharge";
|
||||||
public TotalRechargeData GetEventData()
|
public TotalRechargeData GetEventData()
|
||||||
|
|||||||
@@ -474,15 +474,15 @@ public class DebugFunc
|
|||||||
private static void LoginTime(int hours)
|
private static void LoginTime(int hours)
|
||||||
{
|
{
|
||||||
var userService = GContext.container.Resolve<game.IUserService>();
|
var userService = GContext.container.Resolve<game.IUserService>();
|
||||||
if (userService is not game.PlayFabUserService && userService is not game.KVUserService)
|
if (userService is not game.PlayFabUserService && userService is not game.KvUserService)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var loginTime = userService.LoginTime;
|
var loginTime = userService.LoginTime;
|
||||||
var newLoginTime = loginTime.AddHours(hours);
|
var newLoginTime = loginTime.AddHours(hours);
|
||||||
var prop = userService is game.KVUserService
|
var prop = userService is game.KvUserService
|
||||||
? typeof(game.KVUserService).GetProperty("LoginTime")
|
? typeof(game.KvUserService).GetProperty("LoginTime")
|
||||||
: userService.GetType().GetProperty("LoginTime");
|
: userService.GetType().GetProperty("LoginTime");
|
||||||
prop.SetValue(userService, newLoginTime);
|
prop.SetValue(userService, newLoginTime);
|
||||||
Debug.Log($"loginTime: {loginTime} newLoginTime: {newLoginTime} prop: {userService.LoginTime}");
|
Debug.Log($"loginTime: {loginTime} newLoginTime: {newLoginTime} prop: {userService.LoginTime}");
|
||||||
@@ -491,7 +491,7 @@ public class DebugFunc
|
|||||||
private static void AccoutInfo()
|
private static void AccoutInfo()
|
||||||
{
|
{
|
||||||
var userService = GContext.container.Resolve<game.IUserService>();
|
var userService = GContext.container.Resolve<game.IUserService>();
|
||||||
if (userService is not game.PlayFabUserService && userService is not game.KVUserService)
|
if (userService is not game.PlayFabUserService && userService is not game.KvUserService)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -728,7 +728,7 @@ public class DebugFunc
|
|||||||
}
|
}
|
||||||
static void ClearLastPaymentTime()
|
static void ClearLastPaymentTime()
|
||||||
{
|
{
|
||||||
GContext.container.Resolve<game.PlayerDataStorage.IPlayerSaveStore>().Write("LastPaymentTime", "").RequireAccepted();
|
GContext.container.Resolve<Core.KV.IPlayerSaveStore>().Write("LastPaymentTime", "").RequireAccepted();
|
||||||
}
|
}
|
||||||
static void HookWheelForecActive()
|
static void HookWheelForecActive()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -608,7 +608,7 @@ public class EventFishingDuelPanel : BasePanel
|
|||||||
void CreateMatchmakingTicket()
|
void CreateMatchmakingTicket()
|
||||||
{
|
{
|
||||||
if (GContext.container.Resolve<IUserService>() is not PlayFabUserService &&
|
if (GContext.container.Resolve<IUserService>() is not PlayFabUserService &&
|
||||||
GContext.container.Resolve<IUserService>() is not KVUserService)
|
GContext.container.Resolve<IUserService>() is not KvUserService)
|
||||||
{
|
{
|
||||||
Debug.LogWarning("KV matchmaking service is not connected.");
|
Debug.LogWarning("KV matchmaking service is not connected.");
|
||||||
FDM.MachingEnd(null);
|
FDM.MachingEnd(null);
|
||||||
@@ -654,7 +654,7 @@ public class EventFishingDuelPanel : BasePanel
|
|||||||
Debug.LogError("创建匹配请求失败: " + error);
|
Debug.LogError("创建匹配请求失败: " + error);
|
||||||
FDM.MachingEnd(null);
|
FDM.MachingEnd(null);
|
||||||
};
|
};
|
||||||
if (userService is KVUserService)
|
if (userService is KvUserService)
|
||||||
{
|
{
|
||||||
RequestKVMatch("CreateMatchmakingTicket", new
|
RequestKVMatch("CreateMatchmakingTicket", new
|
||||||
{
|
{
|
||||||
@@ -733,7 +733,7 @@ public class EventFishingDuelPanel : BasePanel
|
|||||||
Debug.LogError("检查匹配状态失败: " + error);
|
Debug.LogError("检查匹配状态失败: " + error);
|
||||||
MachingEnd();
|
MachingEnd();
|
||||||
};
|
};
|
||||||
if (GContext.container.Resolve<IUserService>() is KVUserService)
|
if (GContext.container.Resolve<IUserService>() is KvUserService)
|
||||||
{
|
{
|
||||||
RequestKVMatch("GetMatchmakingTicket", new
|
RequestKVMatch("GetMatchmakingTicket", new
|
||||||
{
|
{
|
||||||
@@ -750,7 +750,7 @@ public class EventFishingDuelPanel : BasePanel
|
|||||||
void GetMatchDetails(string matchId)
|
void GetMatchDetails(string matchId)
|
||||||
{
|
{
|
||||||
if (GContext.container.Resolve<IUserService>() is not PlayFabUserService &&
|
if (GContext.container.Resolve<IUserService>() is not PlayFabUserService &&
|
||||||
GContext.container.Resolve<IUserService>() is not KVUserService)
|
GContext.container.Resolve<IUserService>() is not KvUserService)
|
||||||
{
|
{
|
||||||
Debug.LogWarning("KV matchmaking service is not connected.");
|
Debug.LogWarning("KV matchmaking service is not connected.");
|
||||||
FDM.MachingEnd(null);
|
FDM.MachingEnd(null);
|
||||||
@@ -783,7 +783,7 @@ public class EventFishingDuelPanel : BasePanel
|
|||||||
for (int i = 0; i < result.Members.Count; i++)
|
for (int i = 0; i < result.Members.Count; i++)
|
||||||
{
|
{
|
||||||
MatchmakingPlayerWithTeamAssignment member = result.Members[i];
|
MatchmakingPlayerWithTeamAssignment member = result.Members[i];
|
||||||
if (member.Entity.Id != (userService is KVUserService ? userService.UserId : ((PlayFabUserService)userService).entityKey.Id))
|
if (member.Entity.Id != (userService is KvUserService ? userService.UserId : ((PlayFabUserService)userService).entityKey.Id))
|
||||||
{
|
{
|
||||||
JObject dataObject = JObject.FromObject(result.Members[i].Attributes.DataObject);
|
JObject dataObject = JObject.FromObject(result.Members[i].Attributes.DataObject);
|
||||||
string[] tierArray = dataObject["Tier"].ToObject<string[]>();
|
string[] tierArray = dataObject["Tier"].ToObject<string[]>();
|
||||||
@@ -809,7 +809,7 @@ public class EventFishingDuelPanel : BasePanel
|
|||||||
Debug.LogError("获取匹配详情失败: " + error);
|
Debug.LogError("获取匹配详情失败: " + error);
|
||||||
MachingEnd();
|
MachingEnd();
|
||||||
};
|
};
|
||||||
if (GContext.container.Resolve<IUserService>() is KVUserService)
|
if (GContext.container.Resolve<IUserService>() is KvUserService)
|
||||||
{
|
{
|
||||||
RequestKVMatch("GetMatch", new
|
RequestKVMatch("GetMatch", new
|
||||||
{
|
{
|
||||||
@@ -862,7 +862,7 @@ public class EventFishingDuelPanel : BasePanel
|
|||||||
{
|
{
|
||||||
Debug.LogError("取消匹配失败: " + error);
|
Debug.LogError("取消匹配失败: " + error);
|
||||||
};
|
};
|
||||||
if (GContext.container.Resolve<IUserService>() is KVUserService)
|
if (GContext.container.Resolve<IUserService>() is KvUserService)
|
||||||
{
|
{
|
||||||
RequestKVMatch("CancelMatchmakingTicket", new
|
RequestKVMatch("CancelMatchmakingTicket", new
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ namespace game
|
|||||||
GContext.OnEvent<TargetEvent>().Subscribe(OnEvent_TargetEvent).AddTo(Disposables);
|
GContext.OnEvent<TargetEvent>().Subscribe(OnEvent_TargetEvent).AddTo(Disposables);
|
||||||
}
|
}
|
||||||
private Tables _tables;
|
private Tables _tables;
|
||||||
[Inject] public PlayerDataStorage.IPlayerSaveStore SaveStore { get; set; }
|
[Inject] public Core.KV.IPlayerSaveStore SaveStore { get; set; }
|
||||||
bool disposed;
|
bool disposed;
|
||||||
|
|
||||||
[Inject]
|
[Inject]
|
||||||
|
|||||||
@@ -410,7 +410,7 @@ public class EventBingoStepState
|
|||||||
|
|
||||||
public class EventBingoPlayfabData
|
public class EventBingoPlayfabData
|
||||||
{
|
{
|
||||||
readonly game.PlayerDataStorage.IPlayerSaveStore saveStore = GContext.container.Resolve<game.PlayerDataStorage.IPlayerSaveStore>();
|
readonly Core.KV.IPlayerSaveStore saveStore = GContext.container.Resolve<Core.KV.IPlayerSaveStore>();
|
||||||
|
|
||||||
public int EventId, TicketCount, RoundCount, StepInRound, Seed, ChainProgress, ChainTaskTokenProgress;
|
public int EventId, TicketCount, RoundCount, StepInRound, Seed, ChainProgress, ChainTaskTokenProgress;
|
||||||
public float InflationRate;
|
public float InflationRate;
|
||||||
@@ -480,7 +480,7 @@ public class EventBingoPlayfabData
|
|||||||
|
|
||||||
public static EventBingoPlayfabData Load()
|
public static EventBingoPlayfabData Load()
|
||||||
{
|
{
|
||||||
var s = GContext.container.Resolve<game.PlayerDataStorage.IPlayerSaveStore>().Read(Key).ValueOrThrow();
|
var s = GContext.container.Resolve<Core.KV.IPlayerSaveStore>().Read(Key).ValueOrThrow();
|
||||||
return Deserialize(s);
|
return Deserialize(s);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ public class DrillModel : IHasChainPack
|
|||||||
|
|
||||||
public class EventBreakPfData
|
public class EventBreakPfData
|
||||||
{
|
{
|
||||||
readonly game.PlayerDataStorage.IPlayerSaveStore saveStore = GContext.container.Resolve<game.PlayerDataStorage.IPlayerSaveStore>();
|
readonly Core.KV.IPlayerSaveStore saveStore = GContext.container.Resolve<Core.KV.IPlayerSaveStore>();
|
||||||
|
|
||||||
public int EventId { get; set; }
|
public int EventId { get; set; }
|
||||||
public int TicketCount { get; set; }
|
public int TicketCount { get; set; }
|
||||||
|
|||||||
@@ -350,7 +350,7 @@ public class EventCanProgressTaskInfo
|
|||||||
|
|
||||||
public class EventCanPlayfabData
|
public class EventCanPlayfabData
|
||||||
{
|
{
|
||||||
readonly game.PlayerDataStorage.IPlayerSaveStore saveStore = GContext.container.Resolve<game.PlayerDataStorage.IPlayerSaveStore>();
|
readonly Core.KV.IPlayerSaveStore saveStore = GContext.container.Resolve<Core.KV.IPlayerSaveStore>();
|
||||||
|
|
||||||
public const string PlayFabKey = "EventCan";
|
public const string PlayFabKey = "EventCan";
|
||||||
private const char Splitter = '|', Comma = ',';
|
private const char Splitter = '|', Comma = ',';
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ using Random = UnityEngine.Random;
|
|||||||
|
|
||||||
public class EventCubeMeltManager
|
public class EventCubeMeltManager
|
||||||
{
|
{
|
||||||
[asap.core.Inject] public game.PlayerDataStorage.IPlayerSaveStore SaveStore { get; set; }
|
[asap.core.Inject] public Core.KV.IPlayerSaveStore SaveStore { get; set; }
|
||||||
|
|
||||||
//
|
//
|
||||||
public const string SaveKey = "EventCubeMeltData";
|
public const string SaveKey = "EventCubeMeltData";
|
||||||
|
|||||||
@@ -265,7 +265,7 @@ public class EventGatherCoreRewardInfo
|
|||||||
|
|
||||||
public class EventGatherCorePlayfabData
|
public class EventGatherCorePlayfabData
|
||||||
{
|
{
|
||||||
readonly game.PlayerDataStorage.IPlayerSaveStore saveStore = GContext.container.Resolve<game.PlayerDataStorage.IPlayerSaveStore>();
|
readonly Core.KV.IPlayerSaveStore saveStore = GContext.container.Resolve<Core.KV.IPlayerSaveStore>();
|
||||||
|
|
||||||
public int EventId { get; set; }
|
public int EventId { get; set; }
|
||||||
public int TicketCount { get; set; }
|
public int TicketCount { get; set; }
|
||||||
@@ -319,7 +319,7 @@ public class EventGatherCorePlayfabData
|
|||||||
|
|
||||||
public static EventGatherCorePlayfabData Load()
|
public static EventGatherCorePlayfabData Load()
|
||||||
{
|
{
|
||||||
var s = GContext.container.Resolve<game.PlayerDataStorage.IPlayerSaveStore>().Read(Key).ValueOrThrow();
|
var s = GContext.container.Resolve<Core.KV.IPlayerSaveStore>().Read(Key).ValueOrThrow();
|
||||||
var res = new EventGatherCorePlayfabData();
|
var res = new EventGatherCorePlayfabData();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ using UnityEngine;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class EventLuckyGameModel : ILuckyTaskGetData
|
public class EventLuckyGameModel : ILuckyTaskGetData
|
||||||
{
|
{
|
||||||
readonly game.PlayerDataStorage.IPlayerSaveStore saveStore = GContext.container.Resolve<game.PlayerDataStorage.IPlayerSaveStore>();
|
readonly Core.KV.IPlayerSaveStore saveStore = GContext.container.Resolve<Core.KV.IPlayerSaveStore>();
|
||||||
|
|
||||||
public static EventLuckyGameModel Get()
|
public static EventLuckyGameModel Get()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -261,7 +261,7 @@ public class EventOfferJourneyBlockData
|
|||||||
|
|
||||||
public class EventOfferJourneyPlayfabData
|
public class EventOfferJourneyPlayfabData
|
||||||
{
|
{
|
||||||
readonly game.PlayerDataStorage.IPlayerSaveStore saveStore = GContext.container.Resolve<game.PlayerDataStorage.IPlayerSaveStore>();
|
readonly Core.KV.IPlayerSaveStore saveStore = GContext.container.Resolve<Core.KV.IPlayerSaveStore>();
|
||||||
|
|
||||||
public int EventId, Progress, TicketCount, VipLvlOnTrigger, RoundCount;
|
public int EventId, Progress, TicketCount, VipLvlOnTrigger, RoundCount;
|
||||||
public float InflationRate;
|
public float InflationRate;
|
||||||
@@ -321,7 +321,7 @@ public class EventOfferJourneyPlayfabData
|
|||||||
|
|
||||||
public static EventOfferJourneyPlayfabData Load()
|
public static EventOfferJourneyPlayfabData Load()
|
||||||
{
|
{
|
||||||
var s = GContext.container.Resolve<game.PlayerDataStorage.IPlayerSaveStore>().Read(Key).ValueOrThrow();
|
var s = GContext.container.Resolve<Core.KV.IPlayerSaveStore>().Read(Key).ValueOrThrow();
|
||||||
return Deserialize(s);
|
return Deserialize(s);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ public class EventOfferJourneyLvlTriggerData : AEventOfferJourneyData
|
|||||||
|
|
||||||
public class EventOfferJourneyLvlTriggerPlayfabData
|
public class EventOfferJourneyLvlTriggerPlayfabData
|
||||||
{
|
{
|
||||||
readonly game.PlayerDataStorage.IPlayerSaveStore saveStore = GContext.container.Resolve<game.PlayerDataStorage.IPlayerSaveStore>();
|
readonly Core.KV.IPlayerSaveStore saveStore = GContext.container.Resolve<Core.KV.IPlayerSaveStore>();
|
||||||
|
|
||||||
public int Progress, TicketCount, RoundCount;
|
public int Progress, TicketCount, RoundCount;
|
||||||
private const string Key = "EventOfferJourneyLvlTriggerData";
|
private const string Key = "EventOfferJourneyLvlTriggerData";
|
||||||
@@ -197,7 +197,7 @@ public class EventOfferJourneyLvlTriggerPlayfabData
|
|||||||
|
|
||||||
public static EventOfferJourneyLvlTriggerPlayfabData Load()
|
public static EventOfferJourneyLvlTriggerPlayfabData Load()
|
||||||
{
|
{
|
||||||
var s = GContext.container.Resolve<game.PlayerDataStorage.IPlayerSaveStore>().Read(Key).ValueOrThrow();
|
var s = GContext.container.Resolve<Core.KV.IPlayerSaveStore>().Read(Key).ValueOrThrow();
|
||||||
return Deserialize(s);
|
return Deserialize(s);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,6 @@ namespace EventPartnerGather
|
|||||||
{
|
{
|
||||||
BuildPartners = new BuildPartner[_tables.TbEventPartnerConfig.InvitationCount[1]];
|
BuildPartners = new BuildPartner[_tables.TbEventPartnerConfig.InvitationCount[1]];
|
||||||
}
|
}
|
||||||
internal bool HasSubscriptionsForKVLoad => disposables != null;
|
|
||||||
//
|
//
|
||||||
public bool IsFullyMatched => BuildPartners.All(b => b != null && b.IsValid());
|
public bool IsFullyMatched => BuildPartners.All(b => b != null && b.IsValid());
|
||||||
|
|
||||||
|
|||||||
@@ -564,7 +564,7 @@ public class RewardItemNewFlyToStashRequest
|
|||||||
|
|
||||||
public class EventLuckMagicPlayfabData
|
public class EventLuckMagicPlayfabData
|
||||||
{
|
{
|
||||||
readonly game.PlayerDataStorage.IPlayerSaveStore saveStore = GContext.container.Resolve<game.PlayerDataStorage.IPlayerSaveStore>();
|
readonly Core.KV.IPlayerSaveStore saveStore = GContext.container.Resolve<Core.KV.IPlayerSaveStore>();
|
||||||
|
|
||||||
public int EventId;
|
public int EventId;
|
||||||
public int TicketCount;
|
public int TicketCount;
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ public class WelcomeBackData
|
|||||||
}
|
}
|
||||||
public class EventWelcomeBackManager
|
public class EventWelcomeBackManager
|
||||||
{
|
{
|
||||||
readonly game.PlayerDataStorage.IPlayerSaveStore saveStore = GContext.container.Resolve<game.PlayerDataStorage.IPlayerSaveStore>();
|
readonly Core.KV.IPlayerSaveStore saveStore = GContext.container.Resolve<Core.KV.IPlayerSaveStore>();
|
||||||
|
|
||||||
string DataKey_WelcomeBack = "WelcomeBack";
|
string DataKey_WelcomeBack = "WelcomeBack";
|
||||||
Tables tables;
|
Tables tables;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using game.PlayerDataStorage;
|
using Core.KV;
|
||||||
using asap.core;
|
using asap.core;
|
||||||
using cfg;
|
using cfg;
|
||||||
using DG.Tweening;
|
using DG.Tweening;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ using UnityEngine;
|
|||||||
|
|
||||||
public class InfiniteBuildingData
|
public class InfiniteBuildingData
|
||||||
{
|
{
|
||||||
readonly game.PlayerDataStorage.IPlayerSaveStore saveStore = GContext.container.Resolve<game.PlayerDataStorage.IPlayerSaveStore>();
|
readonly Core.KV.IPlayerSaveStore saveStore = GContext.container.Resolve<Core.KV.IPlayerSaveStore>();
|
||||||
|
|
||||||
public int Lvl, Step, StepLvl = 1, LastHangingBlock = -1;
|
public int Lvl, Step, StepLvl = 1, LastHangingBlock = -1;
|
||||||
public List<BlockRecord> Record = new List<BlockRecord>();
|
public List<BlockRecord> Record = new List<BlockRecord>();
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ using UnityEngine;
|
|||||||
|
|
||||||
public class LuckyADTaskPopupPanel : LuckyTaskPanelBase
|
public class LuckyADTaskPopupPanel : LuckyTaskPanelBase
|
||||||
{
|
{
|
||||||
game.PlayerDataStorage.IPlayerSaveStore saveStore;
|
Core.KV.IPlayerSaveStore saveStore;
|
||||||
|
|
||||||
List<LuckyTaskItemData> taskList;
|
List<LuckyTaskItemData> taskList;
|
||||||
List<LuckyADTaskItem> taskItems = new List<LuckyADTaskItem>();
|
List<LuckyADTaskItem> taskItems = new List<LuckyADTaskItem>();
|
||||||
@@ -20,7 +20,7 @@ public class LuckyADTaskPopupPanel : LuckyTaskPanelBase
|
|||||||
System.DateTime refreshTime;
|
System.DateTime refreshTime;
|
||||||
protected override void Awake()
|
protected override void Awake()
|
||||||
{
|
{
|
||||||
saveStore = GContext.container.Resolve<game.PlayerDataStorage.IPlayerSaveStore>();
|
saveStore = GContext.container.Resolve<Core.KV.IPlayerSaveStore>();
|
||||||
base.Awake();
|
base.Awake();
|
||||||
luckyTaskItem = transform.Find("root/hook_missions_object").GetComponent<LuckyADTask>();
|
luckyTaskItem = transform.Find("root/hook_missions_object").GetComponent<LuckyADTask>();
|
||||||
text_time = transform.Find("root/text_time").GetComponent<TMP_Text>();
|
text_time = transform.Find("root/text_time").GetComponent<TMP_Text>();
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ using UnityEngine.UI;
|
|||||||
|
|
||||||
public class LuckyTaskBtn : MonoBehaviour, IUIRedPoint
|
public class LuckyTaskBtn : MonoBehaviour, IUIRedPoint
|
||||||
{
|
{
|
||||||
game.PlayerDataStorage.IPlayerSaveStore saveStore;
|
Core.KV.IPlayerSaveStore saveStore;
|
||||||
|
|
||||||
Button btn;
|
Button btn;
|
||||||
ILuckyTaskGetData model;
|
ILuckyTaskGetData model;
|
||||||
@@ -18,7 +18,7 @@ public class LuckyTaskBtn : MonoBehaviour, IUIRedPoint
|
|||||||
public TMP_Text text_redpoint_big;
|
public TMP_Text text_redpoint_big;
|
||||||
void Awake()
|
void Awake()
|
||||||
{
|
{
|
||||||
saveStore = GContext.container.Resolve<game.PlayerDataStorage.IPlayerSaveStore>();
|
saveStore = GContext.container.Resolve<Core.KV.IPlayerSaveStore>();
|
||||||
btn = GetComponent<Button>();
|
btn = GetComponent<Button>();
|
||||||
btn.onClick.AddListener(OnClick);
|
btn.onClick.AddListener(OnClick);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,11 +9,11 @@ public class LuckyTaskPanel : LuckyTaskPanelBase
|
|||||||
{
|
{
|
||||||
protected override void Awake()
|
protected override void Awake()
|
||||||
{
|
{
|
||||||
saveStore = GContext.container.Resolve<game.PlayerDataStorage.IPlayerSaveStore>();
|
saveStore = GContext.container.Resolve<Core.KV.IPlayerSaveStore>();
|
||||||
base.Awake();
|
base.Awake();
|
||||||
}
|
}
|
||||||
|
|
||||||
game.PlayerDataStorage.IPlayerSaveStore saveStore;
|
Core.KV.IPlayerSaveStore saveStore;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// luckyModel 也作为是否自动全部领奖的标志
|
/// luckyModel 也作为是否自动全部领奖的标志
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ public enum FlipResult
|
|||||||
|
|
||||||
public class EventMatchGameManager
|
public class EventMatchGameManager
|
||||||
{
|
{
|
||||||
readonly game.PlayerDataStorage.IPlayerSaveStore saveStore = GContext.container.Resolve<game.PlayerDataStorage.IPlayerSaveStore>();
|
readonly Core.KV.IPlayerSaveStore saveStore = GContext.container.Resolve<Core.KV.IPlayerSaveStore>();
|
||||||
|
|
||||||
public string EventMatchGameDataKey => "EventMatchGameDataKey";
|
public string EventMatchGameDataKey => "EventMatchGameDataKey";
|
||||||
public int ItemSubType => 3;
|
public int ItemSubType => 3;
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ public interface IMiniBattlePassDataProvider
|
|||||||
|
|
||||||
public class MiniBattlePassDataProvider : IMiniBattlePassDataProvider
|
public class MiniBattlePassDataProvider : IMiniBattlePassDataProvider
|
||||||
{
|
{
|
||||||
readonly game.PlayerDataStorage.IPlayerSaveStore saveStore = GContext.container.Resolve<game.PlayerDataStorage.IPlayerSaveStore>();
|
readonly Core.KV.IPlayerSaveStore saveStore = GContext.container.Resolve<Core.KV.IPlayerSaveStore>();
|
||||||
|
|
||||||
private Tables _tables;
|
private Tables _tables;
|
||||||
PlayerItemData _playerItemData;
|
PlayerItemData _playerItemData;
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ using UnityEngine.ResourceManagement.AsyncOperations;
|
|||||||
|
|
||||||
public class DiggingGameModel
|
public class DiggingGameModel
|
||||||
{
|
{
|
||||||
readonly game.PlayerDataStorage.IPlayerSaveStore saveStore = GContext.container.Resolve<game.PlayerDataStorage.IPlayerSaveStore>();
|
readonly Core.KV.IPlayerSaveStore saveStore = GContext.container.Resolve<Core.KV.IPlayerSaveStore>();
|
||||||
|
|
||||||
#region 协议Const区域
|
#region 协议Const区域
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 748d56d3a1b2489d85e081bfed580314
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
@@ -8,7 +8,7 @@ using game.PlayerDataStorage;
|
|||||||
|
|
||||||
namespace game
|
namespace game
|
||||||
{
|
{
|
||||||
public class AggKVUserService : KVUserService
|
public class AggKvUserService : KvUserService
|
||||||
{
|
{
|
||||||
private bool channelLoginRunning;
|
private bool channelLoginRunning;
|
||||||
|
|
||||||
@@ -262,6 +262,10 @@ namespace game
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.TryGetValue("EAccoutUrl", out var data_value))
|
||||||
|
{
|
||||||
|
EAccoutUrl = Newtonsoft.Json.JsonConvert.DeserializeObject<Dictionary<string, string>>(data_value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -87,7 +87,7 @@ namespace game
|
|||||||
|
|
||||||
if (!loginInfo.isSuccess) return ELoginStep.Sdk;
|
if (!loginInfo.isSuccess) return ELoginStep.Sdk;
|
||||||
|
|
||||||
progress?.Invoke(1);
|
progress.Invoke(1);
|
||||||
|
|
||||||
CustomId = loginInfo.StrUserId;
|
CustomId = loginInfo.StrUserId;
|
||||||
|
|
||||||
|
|||||||
@@ -32,54 +32,52 @@ namespace game
|
|||||||
}
|
}
|
||||||
public class ClubService //: IClubService
|
public class ClubService //: IClubService
|
||||||
{
|
{
|
||||||
[Inject] public PlayerDataStorage.IPlayerSaveStore SaveStore { get; set; }
|
[Inject] public Core.KV.IPlayerSaveStore SaveStore { get; set; }
|
||||||
// BEGIN B01 fixed prompt owner
|
// BEGIN B01 fixed prompt owner
|
||||||
readonly bool requiresFormalKV;
|
private readonly bool requiresFormalKV;
|
||||||
PlayerDataStorage.PlayerKVGameSessionHandle promptOwner;
|
private Core.KV.KvSessionOwner promptOwner;
|
||||||
internal PlayerDataStorage.PlayerKVEditResult LastPromptEdit { get; private set; }
|
|
||||||
public ClubService() : this(RequireFrozenSelection()) { }
|
public ClubService() : this(RequireFrozenSelection()) { }
|
||||||
static bool RequireFrozenSelection()
|
private static bool RequireFrozenSelection()
|
||||||
{
|
{
|
||||||
var selection = GContext.container.Resolve<PlayerDataStorage.PlayerKVRuntimeSelection>();
|
var selection = GContext.container.Resolve<game.KV.KvRuntimeSelection>();
|
||||||
if (selection == null) throw DefaultSettingService.PromptFailure("IsOpenClubRed: startup selection missing.");
|
if (selection == null) throw DefaultSettingService.PromptFailure("IsOpenClubRed: startup selection missing.");
|
||||||
return selection.IsKV;
|
return selection.IsKV;
|
||||||
}
|
}
|
||||||
internal ClubService(bool formalKV) { requiresFormalKV = formalKV; }
|
internal ClubService(bool formalKV) { requiresFormalKV = formalKV; }
|
||||||
internal ClubService(PlayerDataStorage.PlayerKVGameSessionHandle owner) : this(true)
|
|
||||||
{
|
|
||||||
promptOwner = owner ?? throw new ArgumentNullException(nameof(owner));
|
|
||||||
RequirePromptOwner();
|
|
||||||
}
|
|
||||||
// The constructor captures the registered startup selection, never a later mutable config value.
|
// The constructor captures the registered startup selection, never a later mutable config value.
|
||||||
internal void PreflightFormalPromptOwner(PlayerDataStorage.PlayerKVGameSessionHandle owner)
|
internal void PreflightFormalPromptOwner(Core.KV.KvSessionOwner owner)
|
||||||
{
|
{
|
||||||
if (!requiresFormalKV || owner == null || (promptOwner != null && !ReferenceEquals(promptOwner, owner)))
|
if (!requiresFormalKV || owner == null || (promptOwner != null && !ReferenceEquals(promptOwner, owner)))
|
||||||
|
{
|
||||||
throw DefaultSettingService.PromptFailure("IsOpenClubRed: formal owner mismatch.");
|
throw DefaultSettingService.PromptFailure("IsOpenClubRed: formal owner mismatch.");
|
||||||
var read = KVMgr.ReadKV(owner, "IsOpenClubRed");
|
}
|
||||||
|
var read = game.KV.KvSaveStore.ReadKV(owner, "IsOpenClubRed");
|
||||||
if (!read.Success)
|
if (!read.Success)
|
||||||
|
{
|
||||||
throw DefaultSettingService.PromptFailure($"IsOpenClubRed owner={owner.OwnerInstanceId}: {read.Error}");
|
throw DefaultSettingService.PromptFailure($"IsOpenClubRed owner={owner.OwnerInstanceId}: {read.Error}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
internal void BindFormalPromptOwner(PlayerDataStorage.PlayerKVGameSessionHandle owner)
|
internal void BindFormalPromptOwner(Core.KV.KvSessionOwner owner)
|
||||||
{
|
{
|
||||||
PreflightFormalPromptOwner(owner);
|
PreflightFormalPromptOwner(owner);
|
||||||
if (promptOwner == null) promptOwner = owner;
|
if (promptOwner == null)
|
||||||
|
{
|
||||||
|
promptOwner = owner;
|
||||||
|
}
|
||||||
RequirePromptOwner();
|
RequirePromptOwner();
|
||||||
}
|
}
|
||||||
internal bool HasFormalPromptOwner(PlayerDataStorage.PlayerKVGameSessionHandle owner) =>
|
|
||||||
requiresFormalKV && ReferenceEquals(promptOwner, owner);
|
|
||||||
|
|
||||||
internal void ReleaseFailedPromptOwner(PlayerDataStorage.PlayerKVGameSessionHandle owner)
|
internal void ReleaseFailedPromptOwner(Core.KV.KvSessionOwner owner)
|
||||||
{
|
{
|
||||||
if (promptOwner == null)
|
if (promptOwner == null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!ReferenceEquals(promptOwner, owner) || owner.Status != PlayerDataStorage.PlayerKVSyncStatus.Closed)
|
if (!ReferenceEquals(promptOwner, owner) || owner.Status != Core.KV.KVSyncStatus.Closed)
|
||||||
{
|
{
|
||||||
throw DefaultSettingService.PromptFailure("IsOpenClubRed: failed owner has not closed.");
|
throw DefaultSettingService.PromptFailure("IsOpenClubRed: failed owner has not closed.");
|
||||||
}
|
}
|
||||||
promptOwner = null;
|
promptOwner = null;
|
||||||
LastPromptEdit = null;
|
|
||||||
}
|
}
|
||||||
internal void RequirePromptOwner()
|
internal void RequirePromptOwner()
|
||||||
{
|
{
|
||||||
@@ -87,9 +85,9 @@ namespace game
|
|||||||
else if (requiresFormalKV)
|
else if (requiresFormalKV)
|
||||||
throw DefaultSettingService.PromptFailure("IsOpenClubRed: formal owner not handed off.");
|
throw DefaultSettingService.PromptFailure("IsOpenClubRed: formal owner not handed off.");
|
||||||
}
|
}
|
||||||
private PlayerDataStorage.PlayerKVKeyReadResult ReadPrompt()
|
private Core.KV.PlayerSaveReadResult ReadPrompt()
|
||||||
{
|
{
|
||||||
var read = KVMgr.ReadKV(promptOwner, "IsOpenClubRed");
|
var read = game.KV.KvSaveStore.ReadKV(promptOwner, "IsOpenClubRed");
|
||||||
if (!read.Success)
|
if (!read.Success)
|
||||||
{
|
{
|
||||||
throw DefaultSettingService.PromptFailure($"IsOpenClubRed owner={promptOwner.OwnerInstanceId}: {read.Error}");
|
throw DefaultSettingService.PromptFailure($"IsOpenClubRed owner={promptOwner.OwnerInstanceId}: {read.Error}");
|
||||||
@@ -213,10 +211,10 @@ namespace game
|
|||||||
{
|
{
|
||||||
IsOpenClubRed = false;
|
IsOpenClubRed = false;
|
||||||
SetClubRed();
|
SetClubRed();
|
||||||
LastPromptEdit = KVMgr.WriteKV(promptOwner, "IsOpenClubRed", "1");
|
var promptEdit = game.KV.KvSaveStore.WriteKV(promptOwner, "IsOpenClubRed", "1");
|
||||||
if (!LastPromptEdit.Accepted)
|
if (!promptEdit.Success)
|
||||||
{
|
{
|
||||||
throw DefaultSettingService.PromptFailure($"IsOpenClubRed owner={promptOwner.OwnerInstanceId}: {LastPromptEdit.StateResult.Error}; uncertain={LastPromptEdit.StateResult.LocalCommitMayHaveSucceeded}");
|
throw DefaultSettingService.PromptFailure($"IsOpenClubRed owner={promptOwner.OwnerInstanceId}: {promptEdit.Error}; uncertain={promptEdit.LocalCommitMayHaveSucceeded}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
RequirePromptOwner();
|
RequirePromptOwner();
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ public interface IDeferredRewardStashService
|
|||||||
|
|
||||||
public class DeferredRewardStashService : IDeferredRewardStashService
|
public class DeferredRewardStashService : IDeferredRewardStashService
|
||||||
{
|
{
|
||||||
readonly game.PlayerDataStorage.IPlayerSaveStore saveStore = GContext.container.Resolve<game.PlayerDataStorage.IPlayerSaveStore>();
|
readonly Core.KV.IPlayerSaveStore saveStore = GContext.container.Resolve<Core.KV.IPlayerSaveStore>();
|
||||||
|
|
||||||
private readonly TbItem _itemTable = GContext.container.Resolve<Tables>().TbItem;
|
private readonly TbItem _itemTable = GContext.container.Resolve<Tables>().TbItem;
|
||||||
private readonly PlayerItemData _playerItemData = GContext.container.Resolve<PlayerItemData>();
|
private readonly PlayerItemData _playerItemData = GContext.container.Resolve<PlayerItemData>();
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
namespace game
|
|
||||||
{
|
|
||||||
public sealed class DevKVUserService : KVUserService
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
6
Client/Assets/Scripts/Services/DevKvUserService.cs
Normal file
6
Client/Assets/Scripts/Services/DevKvUserService.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace game
|
||||||
|
{
|
||||||
|
public sealed class DevKvUserService : KvUserService
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@ public interface IFootPrintService
|
|||||||
public class FootPrintService : IFootPrintService
|
public class FootPrintService : IFootPrintService
|
||||||
{
|
{
|
||||||
[asap.core.Inject]
|
[asap.core.Inject]
|
||||||
public game.PlayerDataStorage.IPlayerSaveStore SaveStore
|
public Core.KV.IPlayerSaveStore SaveStore
|
||||||
{
|
{
|
||||||
get;
|
get;
|
||||||
set;
|
set;
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ namespace game
|
|||||||
{
|
{
|
||||||
public class FriendService
|
public class FriendService
|
||||||
{
|
{
|
||||||
[Inject] public PlayerDataStorage.IPlayerSaveStore SaveStore { get; set; }
|
[Inject] public Core.KV.IPlayerSaveStore SaveStore { get; set; }
|
||||||
[Inject]
|
[Inject]
|
||||||
public IUserService userService { get; set; }
|
public IUserService userService { get; set; }
|
||||||
|
|
||||||
@@ -142,7 +142,7 @@ namespace game
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
var result = userService is KVUserService
|
var result = userService is KvUserService
|
||||||
? await GetKVFriendsListAsync()
|
? await GetKVFriendsListAsync()
|
||||||
: await PlayFabClientAsyncAPI.GetFriendsListAsync(getFriendsListRequest);
|
: await PlayFabClientAsyncAPI.GetFriendsListAsync(getFriendsListRequest);
|
||||||
var list = result?.Friends;
|
var list = result?.Friends;
|
||||||
|
|||||||
@@ -37,28 +37,14 @@ public class LocalNotificationService : ILocalNotificationService
|
|||||||
#endif
|
#endif
|
||||||
bool isInit = false;
|
bool isInit = false;
|
||||||
bool initing = false;
|
bool initing = false;
|
||||||
DefaultSettingService promptSettings;
|
private DefaultSettingService promptSettings;
|
||||||
Task initialization;
|
private Task initialization;
|
||||||
Task opening;
|
private Task opening;
|
||||||
public LocalNotificationService()
|
public LocalNotificationService()
|
||||||
{
|
{
|
||||||
_notificationList = GContext.container.Resolve<cfg.Tables>().TbNotification.DataList;
|
_notificationList = GContext.container.Resolve<cfg.Tables>().TbNotification.DataList;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal LocalNotificationService(cfg.Tables tables)
|
|
||||||
{
|
|
||||||
_notificationList = (tables ?? throw new ArgumentNullException(nameof(tables))).TbNotification.DataList;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal LocalNotificationService(DefaultSettingService settings, cfg.Tables tables)
|
|
||||||
{
|
|
||||||
if (settings == null || !settings.HasKVPromptOwner)
|
|
||||||
throw new ArgumentException("Fixed KV settings required.", nameof(settings));
|
|
||||||
promptSettings = settings;
|
|
||||||
_notificationList = (tables ?? throw new ArgumentNullException(nameof(tables))).TbNotification.DataList;
|
|
||||||
RequirePromptOwner();
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void RequirePromptOwner(ISettingService expected = null)
|
internal void RequirePromptOwner(ISettingService expected = null)
|
||||||
{
|
{
|
||||||
if (expected != null && (promptSettings != null || expected is DefaultSettingService setting && setting.HasKVPromptOwner)
|
if (expected != null && (promptSettings != null || expected is DefaultSettingService setting && setting.HasKVPromptOwner)
|
||||||
@@ -84,8 +70,6 @@ public class LocalNotificationService : ILocalNotificationService
|
|||||||
if (promptSettings == null) promptSettings = settings;
|
if (promptSettings == null) promptSettings = settings;
|
||||||
RequirePromptOwner(settings);
|
RequirePromptOwner(settings);
|
||||||
}
|
}
|
||||||
internal bool HasFormalPromptSettings(DefaultSettingService settings) =>
|
|
||||||
ReferenceEquals(promptSettings, settings);
|
|
||||||
|
|
||||||
public Task Init()
|
public Task Init()
|
||||||
{
|
{
|
||||||
@@ -95,7 +79,7 @@ public class LocalNotificationService : ILocalNotificationService
|
|||||||
return initialization = InitCoreAsync();
|
return initialization = InitCoreAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
async Task InitCoreAsync()
|
private async Task InitCoreAsync()
|
||||||
{
|
{
|
||||||
RequirePromptOwner();
|
RequirePromptOwner();
|
||||||
#if UNITY_EDITOR
|
#if UNITY_EDITOR
|
||||||
@@ -156,7 +140,7 @@ public class LocalNotificationService : ILocalNotificationService
|
|||||||
return opening = OpenSettingsCoreAsync();
|
return opening = OpenSettingsCoreAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
async Task OpenSettingsCoreAsync()
|
private async Task OpenSettingsCoreAsync()
|
||||||
{
|
{
|
||||||
ISettingService settings = promptSettings ?? GContext.container.Resolve<ISettingService>();
|
ISettingService settings = promptSettings ?? GContext.container.Resolve<ISettingService>();
|
||||||
RequirePromptOwner(settings);
|
RequirePromptOwner(settings);
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
|
using Core.KV;
|
||||||
|
using game.KV;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
|
||||||
using System.Net.Http;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using asap.core;
|
using asap.core;
|
||||||
using game.PlayerDataStorage;
|
|
||||||
using GameCore;
|
using GameCore;
|
||||||
using tysdk;
|
using tysdk;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
@@ -20,62 +19,101 @@ using TapjoyUnity.Internal;
|
|||||||
namespace game
|
namespace game
|
||||||
{
|
{
|
||||||
// Failed login attempts may retry; explicit logout ends this service lifetime.
|
// Failed login attempts may retry; explicit logout ends this service lifetime.
|
||||||
public class KVUserService : IUserService, IKVUserLifecycle
|
public class KvUserService : IUserService
|
||||||
{
|
{
|
||||||
[Inject]
|
[Inject]
|
||||||
public IConfig config { get; set; }
|
public IConfig config
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
[Inject]
|
[Inject]
|
||||||
public ICustomServerMgr customServerMgr { get; set; }
|
public ICustomServerMgr customServerMgr
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
[Inject]
|
[Inject]
|
||||||
public IPlayerSaveStore SaveStore { get; set; }
|
public IPlayerSaveStore SaveStore
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
private readonly PlayerKVRuntimeSelection selection;
|
private readonly KvRuntimeSelection selection;
|
||||||
//
|
|
||||||
private readonly Func<CookieContainer, HttpMessageHandler> handlerFactory;
|
|
||||||
|
|
||||||
private readonly CancellationTokenSource lifetime = new();
|
private readonly CancellationTokenSource lifetime = new();
|
||||||
private readonly Dictionary<string, PlayInfo> profiles = new();
|
private readonly Dictionary<string, PlayInfo> profiles = new();
|
||||||
private readonly Dictionary<string, int> levels = new();
|
private readonly Dictionary<string, int> levels = new();
|
||||||
|
|
||||||
private PlayerKVGameLoadCoordinator coordinator;
|
private KvGameLoadCoordinator coordinator;
|
||||||
private PlayerKVGameSessionHandle handle;
|
private KvSessionOwner handle;
|
||||||
private bool loading, closing, published, promptsBound;
|
private bool loading, closing, published;
|
||||||
private PlayerKVGameSessionHandle failedHandle;
|
private KvSessionOwner failedHandle;
|
||||||
private int mmTestValue;
|
private int mmTestValue;
|
||||||
private Task closeTask;
|
private Task closeTask;
|
||||||
private long epoch;
|
private long epoch;
|
||||||
private string playerId;
|
private string playerId;
|
||||||
private uint internalId;
|
private uint internalId;
|
||||||
public string CustomId { get; set; }
|
public string CustomId
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
public string UserId => KVHandoffReady ? playerId : null;
|
public string UserId => KVHandoffReady ? playerId : null;
|
||||||
public string ApiPlayerId => UserId;
|
public string ApiPlayerId => UserId;
|
||||||
public uint InternalId => KVHandoffReady ? internalId : 0;
|
public uint InternalId => KVHandoffReady ? internalId : 0;
|
||||||
public bool IsLogin => KVHandoffReady && playerId != null;
|
public bool IsLogin => KVHandoffReady && playerId != null;
|
||||||
public bool IsAuthorized => published && InternalId != 0 && KVHandoffReady;
|
public bool IsAuthorized => published && InternalId != 0 && KVHandoffReady;
|
||||||
public CancellationToken LoginLifetime => lifetime.Token;
|
public CancellationToken LoginLifetime => lifetime.Token;
|
||||||
public DateTime LastLoginTime { get; private set; }
|
public DateTime LastLoginTime
|
||||||
public DateTime LoginTime { get; private set; }
|
{
|
||||||
public DateTime CreateTime { get; private set; }
|
get;
|
||||||
public string DisplayName { get; private set; }
|
private set;
|
||||||
public string AvatarUrl { get; private set; }
|
}
|
||||||
public string Language { get; private set; }
|
public DateTime LoginTime
|
||||||
public string ContinentCode { get; private set; }
|
{
|
||||||
public Dictionary<string, string> EAccoutUrl { get; private set; } = new Dictionary<string, string>();
|
get;
|
||||||
internal PlayerKVGameLoadError KVLastError { get; private set; }
|
private set;
|
||||||
|
}
|
||||||
|
public DateTime CreateTime
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
private set;
|
||||||
|
}
|
||||||
|
public string DisplayName
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
private set;
|
||||||
|
}
|
||||||
|
public string AvatarUrl
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
private set;
|
||||||
|
}
|
||||||
|
public string Language
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
private set;
|
||||||
|
}
|
||||||
|
public string ContinentCode
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
private set;
|
||||||
|
}
|
||||||
|
public Dictionary<string, string> EAccoutUrl
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
protected set;
|
||||||
|
} = new Dictionary<string, string>();
|
||||||
internal bool KVHandoffReady => !closing && handle != null && coordinator != null &&
|
internal bool KVHandoffReady => !closing && handle != null && coordinator != null &&
|
||||||
coordinator.TryGetCurrentGameHandle(epoch, out var current) && ReferenceEquals(current, handle);
|
coordinator.TryGetCurrentGameHandle(epoch, out var current) && ReferenceEquals(current, handle);
|
||||||
bool IKVUserLifecycle.KVHandoffReady => KVHandoffReady;
|
|
||||||
PlayerKVGameLoadError IKVUserLifecycle.KVLastError => KVLastError;
|
|
||||||
|
|
||||||
public KVUserService() : this(SubBoostrap.RuntimeSelection, jar => new HttpClientHandler
|
public KvUserService()
|
||||||
{ CookieContainer = jar, UseCookies = true, AllowAutoRedirect = false }) { }
|
|
||||||
|
|
||||||
private KVUserService(PlayerKVRuntimeSelection selection, Func<CookieContainer, HttpMessageHandler> handlerFactory)
|
|
||||||
{
|
{
|
||||||
this.selection = selection;
|
selection = SubBoostrap.RuntimeSelection;
|
||||||
this.handlerFactory = handlerFactory ?? throw new ArgumentNullException(nameof(handlerFactory));
|
|
||||||
Application.quitting += OnApplicationQuitting;
|
Application.quitting += OnApplicationQuitting;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,7 +137,6 @@ namespace game
|
|||||||
}
|
}
|
||||||
if (selection == null || !selection.IsKV || !selection.TryAdmitPlayer(CustomId, out var canonicalId))
|
if (selection == null || !selection.IsKV || !selection.TryAdmitPlayer(CustomId, out var canonicalId))
|
||||||
{
|
{
|
||||||
KVLastError = PlayerKVGameLoadError.Input;
|
|
||||||
return ELoginStep.Api;
|
return ELoginStep.Api;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,9 +147,9 @@ namespace game
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
CustomServerMgr server;
|
CustomServerMgr server;
|
||||||
KVPlayerSaveStore store;
|
KvSaveStore store;
|
||||||
PlayerKVGameLoadResult result;
|
KvGameLoadResult result;
|
||||||
PlayerKVGameSessionHandle original;
|
KvSessionOwner original;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!await ReleaseFailedLoginAsync() || closing)
|
if (!await ReleaseFailedLoginAsync() || closing)
|
||||||
@@ -120,20 +157,29 @@ namespace game
|
|||||||
return ELoginStep.Api;
|
return ELoginStep.Api;
|
||||||
}
|
}
|
||||||
server = customServerMgr as CustomServerMgr;
|
server = customServerMgr as CustomServerMgr;
|
||||||
if (server == null || server.ApiBaseUri.AbsoluteUri != PlayerKVRuntimeSelection.FormalApiUrl)
|
if (server == null || server.ApiBaseUri.AbsoluteUri != selection.Api.AbsoluteUri)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("Formal KV transport required.");
|
throw new InvalidOperationException("Formal KV transport required.");
|
||||||
}
|
}
|
||||||
coordinator = new PlayerKVGameLoadCoordinator(
|
coordinator = new KvGameLoadCoordinator(
|
||||||
() => loading && !closing && !published && handle == null,
|
() => loading && !closing && !published && handle == null,
|
||||||
server.BindKVForGame, server.ClearKVForGame);
|
server.BindKVForGame, server.ClearKVForGame);
|
||||||
server.KVUnauthorized += OnUnauthorized;
|
server.KVUnauthorized += OnUnauthorized;
|
||||||
var capability = PlayerKVGameLoadCapability.CreateForGame(selection.Api,
|
var api = selection.Api;
|
||||||
Application.persistentDataPath, handlerFactory, handlerFactory, handlerFactory, prepareOriginalComparison: true);
|
var cacheRoot = Application.persistentDataPath;
|
||||||
result = await coordinator.LoadForGameAsync(canonicalId, capability, LoginLifetime,
|
if (api == null || !api.IsAbsoluteUri || (api.Scheme != "http" && api.Scheme != "https") ||
|
||||||
|
!string.IsNullOrEmpty(api.UserInfo) || !string.IsNullOrEmpty(api.Query) || !string.IsNullOrEmpty(api.Fragment))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Formal KV endpoint mismatch.", nameof(api));
|
||||||
|
}
|
||||||
|
if (string.IsNullOrEmpty(cacheRoot))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Controlled load configuration invalid.");
|
||||||
|
}
|
||||||
|
api = new Uri(api.AbsoluteUri.TrimEnd('/') + "/");
|
||||||
|
result = await coordinator.LoadForGameAsync(canonicalId, api, cacheRoot, LoginLifetime,
|
||||||
() => !closing && CustomId == requestedId);
|
() => !closing && CustomId == requestedId);
|
||||||
KVLastError = result.Error;
|
if (result.Error != KVGameLoadError.None ||
|
||||||
if (result.State != PlayerKVGameLoadState.PreparedForGame || result.Data == null ||
|
|
||||||
closing || CustomId != requestedId || result.Account == null ||
|
closing || CustomId != requestedId || result.Account == null ||
|
||||||
result.Account.PlayerId != canonicalId || result.Account.Epoch != result.Epoch ||
|
result.Account.PlayerId != canonicalId || result.Account.Epoch != result.Epoch ||
|
||||||
!coordinator.TryGetCurrentGameHandle(result.Epoch, out original))
|
!coordinator.TryGetCurrentGameHandle(result.Epoch, out original))
|
||||||
@@ -143,7 +189,7 @@ namespace game
|
|||||||
}
|
}
|
||||||
epoch = result.Epoch;
|
epoch = result.Epoch;
|
||||||
handle = original;
|
handle = original;
|
||||||
store = SaveStore as KVPlayerSaveStore;
|
store = SaveStore as KvSaveStore;
|
||||||
if (store == null)
|
if (store == null)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("KV save store required.");
|
throw new InvalidOperationException("KV save store required.");
|
||||||
@@ -152,7 +198,6 @@ namespace game
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
KVLastError = PlayerKVGameLoadError.Apply;
|
|
||||||
Debug.LogError("KV preparation failed: " + ex.GetType().Name);
|
Debug.LogError("KV preparation failed: " + ex.GetType().Name);
|
||||||
await FailLoginAsync();
|
await FailLoginAsync();
|
||||||
return ELoginStep.Api;
|
return ELoginStep.Api;
|
||||||
@@ -169,18 +214,16 @@ namespace game
|
|||||||
ZZTimeHelper.WathForcusChange();
|
ZZTimeHelper.WathForcusChange();
|
||||||
if (!original.CompareDatas(ZZTimeHelper.UtcNow().Ticks).Success)
|
if (!original.CompareDatas(ZZTimeHelper.UtcNow().Ticks).Success)
|
||||||
{
|
{
|
||||||
KVLastError = PlayerKVGameLoadError.Apply;
|
|
||||||
await FailLoginAsync();
|
await FailLoginAsync();
|
||||||
return ELoginStep.Api;
|
return ELoginStep.Api;
|
||||||
}
|
}
|
||||||
var merged = original.ReadLocalSnapshot();
|
var merged = original.ReadLocalSnapshot();
|
||||||
if (!merged.Success)
|
if (!merged.Success)
|
||||||
{
|
{
|
||||||
KVLastError = PlayerKVGameLoadError.Apply;
|
|
||||||
await FailLoginAsync();
|
await FailLoginAsync();
|
||||||
return ELoginStep.Api;
|
return ELoginStep.Api;
|
||||||
}
|
}
|
||||||
KVMgr.Bind(store);
|
KvMgr.Bind(store);
|
||||||
InitializePlayerData(merged.Data.ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.Ordinal));
|
InitializePlayerData(merged.Data.ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.Ordinal));
|
||||||
if (!KVHandoffReady || CustomId != requestedId)
|
if (!KVHandoffReady || CustomId != requestedId)
|
||||||
{
|
{
|
||||||
@@ -194,7 +237,6 @@ namespace game
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
KVLastError = PlayerKVGameLoadError.Apply;
|
|
||||||
Debug.LogError("KV prompt binding failed: " + ex.GetType().Name);
|
Debug.LogError("KV prompt binding failed: " + ex.GetType().Name);
|
||||||
await FailLoginAsync();
|
await FailLoginAsync();
|
||||||
return ELoginStep.Api;
|
return ELoginStep.Api;
|
||||||
@@ -236,7 +278,6 @@ namespace game
|
|||||||
{
|
{
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
KVLastError = PlayerKVGameLoadError.Apply;
|
|
||||||
Debug.LogError("KV login failed: " + ex.GetType().Name);
|
Debug.LogError("KV login failed: " + ex.GetType().Name);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -294,7 +335,7 @@ namespace game
|
|||||||
InitData(data);
|
InitData(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task PrepareProfileAsync(PlayerKVAccountDescriptor account)
|
private async Task PrepareProfileAsync(KvAccountDescriptor account)
|
||||||
{
|
{
|
||||||
if (account.InternalId == 0 || account.CreateTimeUtc == DateTime.MinValue || account.ServerTimeUtc == DateTime.MinValue)
|
if (account.InternalId == 0 || account.CreateTimeUtc == DateTime.MinValue || account.ServerTimeUtc == DateTime.MinValue)
|
||||||
{
|
{
|
||||||
@@ -361,21 +402,18 @@ namespace game
|
|||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
var status = await (coordinator?.ObserveQuitAsync() ?? Task.FromResult(PlayerKVShutdownStatus.Ready));
|
var status = await (coordinator?.ObserveQuitAsync() ?? Task.FromResult(KVShutdownStatus.Ready));
|
||||||
if (closing || status != PlayerKVShutdownStatus.Ready || closeTask.Status != TaskStatus.RanToCompletion)
|
if (closing || status != KVShutdownStatus.Ready || closeTask.Status != TaskStatus.RanToCompletion)
|
||||||
{
|
{
|
||||||
KVLastError = status == PlayerKVShutdownStatus.Failed || closeTask.IsFaulted ?
|
|
||||||
PlayerKVGameLoadError.PreviousCloseFailed : PlayerKVGameLoadError.PreviousClosePending;
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (failedHandle != null)
|
if (failedHandle != null)
|
||||||
{
|
{
|
||||||
KVMgr.Unbind(SaveStore as KVPlayerSaveStore);
|
KvMgr.Unbind(SaveStore as KvSaveStore);
|
||||||
(SaveStore as KVPlayerSaveStore)?.ReleaseFailedBinding(failedHandle);
|
(SaveStore as KvSaveStore)?.ReleaseFailedBinding(failedHandle);
|
||||||
(GContext.container.Resolve<ISettingService>() as DefaultSettingService)?.ReleaseFailedPromptOwner(failedHandle);
|
(GContext.container.Resolve<ISettingService>() as DefaultSettingService)?.ReleaseFailedPromptOwner(failedHandle);
|
||||||
GContext.container.Resolve<ClubService>().ReleaseFailedPromptOwner(failedHandle);
|
GContext.container.Resolve<ClubService>().ReleaseFailedPromptOwner(failedHandle);
|
||||||
}
|
}
|
||||||
promptsBound = false;
|
|
||||||
failedHandle = null;
|
failedHandle = null;
|
||||||
closeTask = null;
|
closeTask = null;
|
||||||
coordinator = null;
|
coordinator = null;
|
||||||
@@ -385,54 +423,67 @@ namespace game
|
|||||||
|
|
||||||
internal static DateTime ParseLastLoginTime(string value, bool found)
|
internal static DateTime ParseLastLoginTime(string value, bool found)
|
||||||
{
|
{
|
||||||
if (!found) return DateTime.MinValue;
|
if (!found)
|
||||||
|
{
|
||||||
|
return DateTime.MinValue;
|
||||||
|
}
|
||||||
if (!DateTime.TryParseExact(value, "O", CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var time) ||
|
if (!DateTime.TryParseExact(value, "O", CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var time) ||
|
||||||
time == DateTime.MinValue || time.Kind == DateTimeKind.Unspecified)
|
time == DateTime.MinValue || time.Kind == DateTimeKind.Unspecified)
|
||||||
|
{
|
||||||
throw new FormatException("Invalid LastLoginTimeUtc.");
|
throw new FormatException("Invalid LastLoginTimeUtc.");
|
||||||
|
}
|
||||||
return time.ToUniversalTime();
|
return time.ToUniversalTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
PlayerSaveReadResult Read(string key)
|
private PlayerSaveReadResult Read(string key)
|
||||||
{
|
{
|
||||||
var result = SaveStore.Read(key);
|
var result = SaveStore.Read(key);
|
||||||
if (!result.Success) throw new InvalidOperationException("KV profile read failed: " + result.Error);
|
if (!result.Success)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("KV profile read failed: " + result.Error);
|
||||||
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
void BindPromptServices(PlayerKVGameSessionHandle original)
|
private void BindPromptServices(KvSessionOwner original)
|
||||||
{
|
{
|
||||||
if (promptsBound || !KVHandoffReady) throw new InvalidOperationException("Prompt binding is stale.");
|
if (!KVHandoffReady)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Prompt binding is stale.");
|
||||||
|
}
|
||||||
var settings = GContext.container.Resolve<ISettingService>() as DefaultSettingService;
|
var settings = GContext.container.Resolve<ISettingService>() as DefaultSettingService;
|
||||||
var notifications = GContext.container.Resolve<ILocalNotificationService>() as LocalNotificationService;
|
var notifications = GContext.container.Resolve<ILocalNotificationService>() as LocalNotificationService;
|
||||||
var club = GContext.container.Resolve<ClubService>();
|
var club = GContext.container.Resolve<ClubService>();
|
||||||
if (settings == null || notifications == null || club == null ||
|
if (settings == null || notifications == null || club == null)
|
||||||
!ReferenceEquals(settings, GContext.container.Resolve<ISettingService>()) ||
|
{
|
||||||
!ReferenceEquals(notifications, GContext.container.Resolve<ILocalNotificationService>()) ||
|
|
||||||
!ReferenceEquals(club, GContext.container.Resolve<ClubService>()))
|
|
||||||
throw new InvalidOperationException("Formal prompt services required.");
|
throw new InvalidOperationException("Formal prompt services required.");
|
||||||
|
}
|
||||||
settings.PreflightFormalPromptOwner(original);
|
settings.PreflightFormalPromptOwner(original);
|
||||||
club.PreflightFormalPromptOwner(original);
|
club.PreflightFormalPromptOwner(original);
|
||||||
notifications.PreflightFormalPromptSettings(settings);
|
notifications.PreflightFormalPromptSettings(settings);
|
||||||
promptsBound = true;
|
|
||||||
settings.BindFormalPromptOwner(original);
|
settings.BindFormalPromptOwner(original);
|
||||||
club.BindFormalPromptOwner(original);
|
club.BindFormalPromptOwner(original);
|
||||||
notifications.BindFormalPromptSettings(settings);
|
notifications.BindFormalPromptSettings(settings);
|
||||||
if (!settings.HasFormalPromptOwner(original) || !club.HasFormalPromptOwner(original) ||
|
if (!KVHandoffReady)
|
||||||
!notifications.HasFormalPromptSettings(settings) || !KVHandoffReady)
|
{
|
||||||
throw new InvalidOperationException("Formal prompt binding changed.");
|
throw new InvalidOperationException("Formal prompt binding changed.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void OnUnauthorized(int version)
|
private void OnUnauthorized(int version)
|
||||||
{
|
{
|
||||||
if (version == customServerMgr.SessionVersion) Logout();
|
if (version == customServerMgr.SessionVersion)
|
||||||
|
{
|
||||||
|
Logout();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnApplicationQuitting()
|
private void OnApplicationQuitting()
|
||||||
{
|
{
|
||||||
var original = SaveStore as KVPlayerSaveStore;
|
var original = SaveStore as KvSaveStore;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
KVMgr.Stop(original);
|
KvMgr.Stop(original);
|
||||||
if (original != null && original.IsCurrent)
|
if (original != null && original.IsCurrent)
|
||||||
{
|
{
|
||||||
_ = original.SyncDataAsync();
|
_ = original.SyncDataAsync();
|
||||||
@@ -452,10 +503,16 @@ namespace game
|
|||||||
{
|
{
|
||||||
ObserveClose(CloseKVForValidationAsync());
|
ObserveClose(CloseKVForValidationAsync());
|
||||||
}
|
}
|
||||||
async void ObserveClose(Task task)
|
private async void ObserveClose(Task task)
|
||||||
{
|
{
|
||||||
try { await task; }
|
try
|
||||||
catch (Exception ex) { Debug.LogError("KV close failed: " + ex.GetType().Name); }
|
{
|
||||||
|
await task;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Debug.LogError("KV close failed: " + ex.GetType().Name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
internal Task CloseKVForValidationAsync()
|
internal Task CloseKVForValidationAsync()
|
||||||
{
|
{
|
||||||
@@ -465,8 +522,8 @@ namespace game
|
|||||||
}
|
}
|
||||||
closing = true;
|
closing = true;
|
||||||
published = false;
|
published = false;
|
||||||
KVMgr.Unbind(SaveStore as KVPlayerSaveStore);
|
KvMgr.Unbind(SaveStore as KvSaveStore);
|
||||||
(SaveStore as KVPlayerSaveStore)?.Dispose();
|
(SaveStore as KvSaveStore)?.Dispose();
|
||||||
playerId = null;
|
playerId = null;
|
||||||
internalId = 0;
|
internalId = 0;
|
||||||
handle = null;
|
handle = null;
|
||||||
@@ -484,10 +541,8 @@ namespace game
|
|||||||
lifetime.Cancel();
|
lifetime.Cancel();
|
||||||
return closeTask;
|
return closeTask;
|
||||||
}
|
}
|
||||||
Task IKVUserLifecycle.CloseKVForValidationAsync() => CloseKVForValidationAsync();
|
internal Task<KVShutdownStatus> ObserveKVQuitAsync() =>
|
||||||
internal Task<PlayerKVShutdownStatus> ObserveKVQuitAsync() =>
|
coordinator?.ObserveQuitAsync() ?? Task.FromResult(KVShutdownStatus.Ready);
|
||||||
coordinator?.ObserveQuitAsync() ?? Task.FromResult(PlayerKVShutdownStatus.Ready);
|
|
||||||
Task<PlayerKVShutdownStatus> IKVUserLifecycle.ObserveKVQuitAsync() => ObserveKVQuitAsync();
|
|
||||||
|
|
||||||
private async Task<bool> SaveProfile(string key, string value)
|
private async Task<bool> SaveProfile(string key, string value)
|
||||||
{
|
{
|
||||||
@@ -667,18 +722,105 @@ namespace game
|
|||||||
};
|
};
|
||||||
Debug.Log($"JsonUtility.ToJson(versionData) => {JsonConvert.SerializeObject(versionData)}");
|
Debug.Log($"JsonUtility.ToJson(versionData) => {JsonConvert.SerializeObject(versionData)}");
|
||||||
|
|
||||||
|
#if (UNITY_ANDROID || UNITY_IOS || UNITY_IPHONE) && !UNITY_EDITOR
|
||||||
var businessId = GConstant.Yidun_AndroidBusinessId;
|
var businessId = GConstant.Yidun_AndroidBusinessId;
|
||||||
#if UNITY_IOS
|
#if UNITY_IOS
|
||||||
businessId = GConstant.Yidun_IOSBusinessId;
|
businessId = GConstant.Yidun_IOSBusinessId;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if (UNITY_ANDROID || UNITY_IOS || UNITY_IPHONE) && !UNITY_EDITOR
|
|
||||||
yiDunManager.SetRoleInfo(businessId, roleId, roleName, roleAccount, null, -1, JsonConvert.SerializeObject(versionData));
|
yiDunManager.SetRoleInfo(businessId, roleId, roleName, roleAccount, null, -1, JsonConvert.SerializeObject(versionData));
|
||||||
yiDunManager.PostRiskCheck();
|
yiDunManager.PostRiskCheck();
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
public double CreateTotalSeconds() => (ZZTimeHelper.UtcNow() - CreateTime).TotalSeconds;
|
public double CreateTotalSeconds() => (ZZTimeHelper.UtcNow() - CreateTime).TotalSeconds;
|
||||||
|
|
||||||
|
private sealed class BindConflictPreviewResponse
|
||||||
|
{
|
||||||
|
public int Code
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
public bool Exists
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
public bool HasArchive
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
public string DisplayName
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
public string AvatarUrl
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
public int Level
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<PlayFabTools.PFPlayerInfo> GetBindConflictPreview(int sdkId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string json = null;
|
||||||
|
for (var attempt = 0; attempt < 4; attempt++)
|
||||||
|
{
|
||||||
|
json = await customServerMgr.AccountRequest("GetBindConflictPreview",
|
||||||
|
new { targetUserId = sdkId.ToString() });
|
||||||
|
if (json != null)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (attempt < 3)
|
||||||
|
{
|
||||||
|
await Task.Delay(TimeSpan.FromSeconds(1 << attempt));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (string.IsNullOrEmpty(json))
|
||||||
|
{
|
||||||
|
return PlayFabTools.PFPlayerInfo.Empty(sdkId);
|
||||||
|
}
|
||||||
|
|
||||||
|
var response = JsonConvert.DeserializeObject<BindConflictPreviewResponse>(json);
|
||||||
|
if (response == null || response.Code <= 0 || !response.Exists)
|
||||||
|
{
|
||||||
|
return PlayFabTools.PFPlayerInfo.Empty(sdkId);
|
||||||
|
}
|
||||||
|
if (!response.HasArchive)
|
||||||
|
{
|
||||||
|
return new PlayFabTools.PFPlayerInfo
|
||||||
|
{
|
||||||
|
SdkId = sdkId,
|
||||||
|
PlayFabId = sdkId.ToString()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return new PlayFabTools.PFPlayerInfo
|
||||||
|
{
|
||||||
|
SdkId = sdkId,
|
||||||
|
PlayFabId = sdkId.ToString(),
|
||||||
|
DisplayName = response.DisplayName,
|
||||||
|
Avatar = response.AvatarUrl,
|
||||||
|
Level = response.Level
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Debug.LogWarning(e.Message);
|
||||||
|
return PlayFabTools.PFPlayerInfo.Empty(sdkId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// The non-AGG channel has no account-link operation; never manufacture a successful binding.
|
// The non-AGG channel has no account-link operation; never manufacture a successful binding.
|
||||||
public virtual async Task<(int code, int userid)> LinkAccount(EAccoutType type)
|
public virtual async Task<(int code, int userid)> LinkAccount(EAccoutType type)
|
||||||
{
|
{
|
||||||
@@ -730,7 +872,10 @@ namespace game
|
|||||||
}
|
}
|
||||||
public PlayInfo GetPlayInfo(string id)
|
public PlayInfo GetPlayInfo(string id)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(id)) return null;
|
if (string.IsNullOrEmpty(id))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
if (!profiles.TryGetValue(id, out var profile))
|
if (!profiles.TryGetValue(id, out var profile))
|
||||||
{
|
{
|
||||||
profile = new PlayInfo { name = GetDefaultName(id) };
|
profile = new PlayInfo { name = GetDefaultName(id) };
|
||||||
@@ -781,7 +926,11 @@ namespace game
|
|||||||
avatarUrl = avatarUrl
|
avatarUrl = avatarUrl
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
public void ClearPlayInfos() { profiles.Clear(); levels.Clear(); }
|
public void ClearPlayInfos()
|
||||||
|
{
|
||||||
|
profiles.Clear();
|
||||||
|
levels.Clear();
|
||||||
|
}
|
||||||
private void UpdateProfileName()
|
private void UpdateProfileName()
|
||||||
{
|
{
|
||||||
if (!IsAuthorized)
|
if (!IsAuthorized)
|
||||||
@@ -808,13 +957,25 @@ namespace game
|
|||||||
}
|
}
|
||||||
public void UpdateProfile()
|
public void UpdateProfile()
|
||||||
{
|
{
|
||||||
if (IsAuthorized) customServerMgr.AccountRequest("UpdateProfile", new LoginRequest { playerId = ApiPlayerId,
|
if (IsAuthorized)
|
||||||
playerProfile = new PlayerProfile { Level = GContext.container.Resolve<PlayerData>().lv, Avatar = AvatarUrl, DisplayName = DisplayName } });
|
{
|
||||||
|
customServerMgr.AccountRequest("UpdateProfile", new LoginRequest
|
||||||
|
{
|
||||||
|
playerId = ApiPlayerId,
|
||||||
|
playerProfile = new PlayerProfile { Level = GContext.container.Resolve<PlayerData>().lv, Avatar = AvatarUrl, DisplayName = DisplayName }
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
public void UpdateProfileLv()
|
public void UpdateProfileLv()
|
||||||
{
|
{
|
||||||
if (IsAuthorized) customServerMgr.AccountRequest("UpdateProfile", new LoginRequest { playerId = ApiPlayerId,
|
if (IsAuthorized)
|
||||||
playerProfile = new PlayerProfile { Level = GContext.container.Resolve<PlayerData>().lv } });
|
{
|
||||||
|
customServerMgr.AccountRequest("UpdateProfile", new LoginRequest
|
||||||
|
{
|
||||||
|
playerId = ApiPlayerId,
|
||||||
|
playerProfile = new PlayerProfile { Level = GContext.container.Resolve<PlayerData>().lv }
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace game.PlayerDataStorage
|
|
||||||
{
|
|
||||||
internal interface IKVUserLifecycle
|
|
||||||
{
|
|
||||||
bool KVHandoffReady { get; }
|
|
||||||
PlayerKVGameLoadError KVLastError { get; }
|
|
||||||
Task CloseKVForValidationAsync();
|
|
||||||
Task<PlayerKVShutdownStatus> ObserveKVQuitAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 12a766df765ed0d4981d8da38eead5e3
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace game.PlayerDataStorage
|
|
||||||
{
|
|
||||||
// A store belongs to one login. Callers retain this instance across awaits.
|
|
||||||
public interface IPlayerSaveStore
|
|
||||||
{
|
|
||||||
bool IsCurrent { get; }
|
|
||||||
PlayerSaveReadResult Read(string key);
|
|
||||||
PlayerSaveWriteResult Write(string key, string value, bool remoteStore = true);
|
|
||||||
Task<bool> SyncDataImmidiate(string key, string value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class PlayerSaveReadResult
|
|
||||||
{
|
|
||||||
public bool Success => Error == null;
|
|
||||||
public bool? Found { get; }
|
|
||||||
public string Value { get; }
|
|
||||||
public string Error { get; }
|
|
||||||
internal PlayerSaveReadResult(bool found, string value) { Found = found; Value = value; }
|
|
||||||
internal PlayerSaveReadResult(string error) { Error = error ?? "ReadFailed"; }
|
|
||||||
public string ValueOrThrow()
|
|
||||||
{
|
|
||||||
if (!Success) throw new InvalidOperationException("Player save read failed: " + Error);
|
|
||||||
return Value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class PlayerSaveReceipt
|
|
||||||
{
|
|
||||||
internal readonly IPlayerSaveStore Store;
|
|
||||||
internal readonly object Ticket;
|
|
||||||
internal PlayerSaveReceipt(IPlayerSaveStore store, object ticket) { Store = store; Ticket = ticket; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class PlayerSaveWriteResult
|
|
||||||
{
|
|
||||||
public bool Accepted => Error == null;
|
|
||||||
public string Error { get; }
|
|
||||||
public PlayerSaveReceipt Receipt { get; }
|
|
||||||
internal PlayerSaveWriteResult(PlayerSaveReceipt receipt) { Receipt = receipt; }
|
|
||||||
internal PlayerSaveWriteResult(string error) { Error = error ?? "WriteRejected"; }
|
|
||||||
public void RequireAccepted()
|
|
||||||
{
|
|
||||||
if (!Accepted) throw new InvalidOperationException("Player save write rejected: " + Error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
285
Client/Assets/Scripts/Services/PlayerData/KvAccountLease.cs
Normal file
285
Client/Assets/Scripts/Services/PlayerData/KvAccountLease.cs
Normal file
@@ -0,0 +1,285 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.IO;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using game.Account;
|
||||||
|
|
||||||
|
namespace game.KV
|
||||||
|
{
|
||||||
|
public enum KVLoginPrepareError
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
Validation,
|
||||||
|
Cancelled,
|
||||||
|
Stale,
|
||||||
|
AuthenticationFailed,
|
||||||
|
OwnerRejected,
|
||||||
|
NotReady,
|
||||||
|
Unexpected,
|
||||||
|
CloseFailed
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class KvAccountDescriptor
|
||||||
|
{
|
||||||
|
internal string PlayerId
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
internal long Epoch
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
internal uint InternalId
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
internal bool NewlyCreated
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
internal DateTime CreateTimeUtc
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
internal DateTime ServerTimeUtc
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
internal KvAccountDescriptor(string playerId, long epoch, uint internalId,
|
||||||
|
bool newlyCreated, DateTime createTimeUtc, DateTime serverTimeUtc)
|
||||||
|
{
|
||||||
|
PlayerId = playerId;
|
||||||
|
Epoch = epoch;
|
||||||
|
InternalId = internalId;
|
||||||
|
NewlyCreated = newlyCreated;
|
||||||
|
CreateTimeUtc = createTimeUtc;
|
||||||
|
ServerTimeUtc = serverTimeUtc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
internal sealed class KvAccountResponse
|
||||||
|
{
|
||||||
|
internal bool IsCurrent
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
internal int? StatusCode
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
internal string Body
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
internal KvAccountResponse(bool isCurrent, int? statusCode = null, string body = null)
|
||||||
|
{
|
||||||
|
IsCurrent = isCurrent;
|
||||||
|
StatusCode = isCurrent ? statusCode : null;
|
||||||
|
Body = isCurrent ? body : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>A context-owned account transport. Close drains before disposing its client.</summary>
|
||||||
|
internal sealed class KvAccountLease
|
||||||
|
{
|
||||||
|
private readonly object gate = new object();
|
||||||
|
private readonly HttpClient client;
|
||||||
|
private readonly CookieContainer cookies;
|
||||||
|
private readonly Func<bool> current;
|
||||||
|
private readonly Func<bool> admission;
|
||||||
|
private readonly CancellationTokenSource lifetime = new CancellationTokenSource();
|
||||||
|
private TaskCompletionSource<bool> drained;
|
||||||
|
private TaskCompletionSource<bool> closing;
|
||||||
|
private int active;
|
||||||
|
internal Uri ApiBaseUri
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
internal long Epoch
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
internal CookieContainer FormalCookies => IsCurrent ? cookies : null;
|
||||||
|
internal bool IsCurrent
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (gate)
|
||||||
|
if (closing != null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return current();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal KvAccountLease(Uri api, long epoch, CookieContainer cookies,
|
||||||
|
Func<CookieContainer, HttpMessageHandler> capability, Func<bool> current, Func<bool> admission)
|
||||||
|
{
|
||||||
|
if (capability == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(capability));
|
||||||
|
}
|
||||||
|
ApiBaseUri = api;
|
||||||
|
Epoch = epoch;
|
||||||
|
this.cookies = cookies;
|
||||||
|
this.current = current;
|
||||||
|
this.admission = admission;
|
||||||
|
client = new HttpClient(capability(cookies)) { BaseAddress = api };
|
||||||
|
client.Timeout = TimeSpan.FromSeconds(15);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal async Task<KvAccountResponse> SendAsync(HttpMethod method, string route,
|
||||||
|
string body = null, string contentType = "application/json", CancellationToken token = default)
|
||||||
|
{
|
||||||
|
if (!IsCanonicalBusinessRoute(route))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Only canonical relative business routes are allowed.", nameof(route));
|
||||||
|
}
|
||||||
|
if (method != HttpMethod.Get && method != HttpMethod.Post)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Unsupported lease method.", nameof(method));
|
||||||
|
}
|
||||||
|
if (!IsCurrent)
|
||||||
|
{
|
||||||
|
return new KvAccountResponse(false);
|
||||||
|
}
|
||||||
|
lock (gate)
|
||||||
|
{
|
||||||
|
if (closing != null || !admission())
|
||||||
|
{
|
||||||
|
return new KvAccountResponse(false);
|
||||||
|
}
|
||||||
|
active++;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (var linked = CancellationTokenSource.CreateLinkedTokenSource(token, lifetime.Token))
|
||||||
|
using (var request = new HttpRequestMessage(method, route))
|
||||||
|
{
|
||||||
|
if (body != null)
|
||||||
|
{
|
||||||
|
request.Content = new StringContent(body, System.Text.Encoding.UTF8, contentType);
|
||||||
|
}
|
||||||
|
using (var response = await client.SendAsync(request, linked.Token).ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
if (!IsCurrent || linked.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
return new KvAccountResponse(false);
|
||||||
|
}
|
||||||
|
var text = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||||
|
// Do not follow redirects from the authenticated business endpoint.
|
||||||
|
if ((int)response.StatusCode >= 300 && (int)response.StatusCode < 400)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Redirects are not accepted.");
|
||||||
|
}
|
||||||
|
return IsCurrent && !linked.IsCancellationRequested
|
||||||
|
? new KvAccountResponse(true, (int)response.StatusCode, text)
|
||||||
|
: new KvAccountResponse(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) when (!IsCurrent || token.IsCancellationRequested || lifetime.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
return new KvAccountResponse(false);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
lock (gate)
|
||||||
|
{
|
||||||
|
active--;
|
||||||
|
if (active == 0)
|
||||||
|
{
|
||||||
|
drained?.TrySetResult(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsCanonicalBusinessRoute(string route)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(route) || route[0] == '/' || route[0] == '\\' ||
|
||||||
|
Uri.TryCreate(route, UriKind.Absolute, out _) ||
|
||||||
|
route.IndexOf("//", StringComparison.Ordinal) >= 0 ||
|
||||||
|
route.IndexOfAny(new[] { '?', '#', '%', '\\' }) >= 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var segments = route.Split('/');
|
||||||
|
foreach (var segment in segments)
|
||||||
|
{
|
||||||
|
if (segment.Length == 0 || segment == "." || segment == "..")
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
foreach (var character in segment)
|
||||||
|
if (char.IsControl(character))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return !route.Equals("account/Login", StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
!route.Equals("playerdata", StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
!route.StartsWith("playerdata/", StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal Task CloseAsync()
|
||||||
|
{
|
||||||
|
TaskCompletionSource<bool> completion;
|
||||||
|
lock (gate)
|
||||||
|
{
|
||||||
|
if (closing != null)
|
||||||
|
{
|
||||||
|
return closing.Task;
|
||||||
|
}
|
||||||
|
completion = closing = Signal();
|
||||||
|
drained = Signal();
|
||||||
|
if (active == 0)
|
||||||
|
{
|
||||||
|
drained.TrySetResult(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = completion.Task.ContinueWith(t =>
|
||||||
|
{
|
||||||
|
_ = t.Exception;
|
||||||
|
}, CancellationToken.None,
|
||||||
|
TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Exception cancellationFailure = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
lifetime.Cancel();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
cancellationFailure = ex;
|
||||||
|
}
|
||||||
|
await drained.Task.ConfigureAwait(false);
|
||||||
|
client.Dispose();
|
||||||
|
lifetime.Dispose();
|
||||||
|
if (cancellationFailure != null)
|
||||||
|
{
|
||||||
|
throw cancellationFailure;
|
||||||
|
}
|
||||||
|
completion.TrySetResult(true);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
completion.TrySetException(ex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return completion.Task;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TaskCompletionSource<bool> Signal() =>
|
||||||
|
new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
}
|
||||||
|
}
|
||||||
1054
Client/Assets/Scripts/Services/PlayerData/KvGameLoadCoordinator.cs
Normal file
1054
Client/Assets/Scripts/Services/PlayerData/KvGameLoadCoordinator.cs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,67 @@
|
|||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
|
||||||
|
namespace game.KV
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Immutable process-start selection with canonical positive numeric player identities.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class KvRuntimeSelection
|
||||||
|
{
|
||||||
|
internal bool IsKV
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
internal Uri Api
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
|
||||||
|
private KvRuntimeSelection(bool isKV, Uri api)
|
||||||
|
{
|
||||||
|
IsKV = isKV;
|
||||||
|
Api = api;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string CompiledBackend
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
#if USE_KV
|
||||||
|
return "kv";
|
||||||
|
#else
|
||||||
|
return "playfab";
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static KvRuntimeSelection CaptureForBuild(string api)
|
||||||
|
{
|
||||||
|
var backend = CompiledBackend;
|
||||||
|
if (backend == "playfab")
|
||||||
|
{
|
||||||
|
return new KvRuntimeSelection(false, null);
|
||||||
|
}
|
||||||
|
if (!Uri.TryCreate(api, UriKind.Absolute, out var address) ||
|
||||||
|
(address.Scheme != "http" && address.Scheme != "https") ||
|
||||||
|
!string.IsNullOrEmpty(address.UserInfo) || !string.IsNullOrEmpty(address.Query) ||
|
||||||
|
!string.IsNullOrEmpty(address.Fragment))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("KV API address is invalid.", nameof(api));
|
||||||
|
}
|
||||||
|
return new KvRuntimeSelection(true, new Uri(address.AbsoluteUri.TrimEnd('/') + "/"));
|
||||||
|
}
|
||||||
|
|
||||||
|
internal bool TryAdmitPlayer(string requestedId, out string canonicalId)
|
||||||
|
{
|
||||||
|
canonicalId = null;
|
||||||
|
if (!IsKV || !long.TryParse(requestedId, NumberStyles.None, CultureInfo.InvariantCulture, out var id) ||
|
||||||
|
id <= 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
canonicalId = id.ToString(CultureInfo.InvariantCulture);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user