chore(m2): 保存功能验收及后续清理前检查点
This commit is contained in:
@@ -86,37 +86,17 @@ namespace game.PlayerDataStorage
|
||||
internal static PlayerKVLoopPermit Permit { get; private set; }
|
||||
internal static string Root => Path.GetFullPath(Path.Combine(Application.dataPath,"../../CodexOutput/upserver/KVGameplayLoopTests"));
|
||||
internal static string RunPath { get; private set; }
|
||||
internal static string CacheRoot => Path.Combine(RunPath,"cache");
|
||||
static PlayerKVGameSessionHandle handle;
|
||||
static Action<string> onFault;
|
||||
static Func<Task> onClose;
|
||||
static WriteBatch collecting;
|
||||
static int mainThread, failed;
|
||||
static SynchronizationContext mainContext;
|
||||
static readonly object auditGate=new object();
|
||||
|
||||
internal static void BeforeBootstrap()
|
||||
internal static void Attach(Action<string> fault)
|
||||
{
|
||||
var path=Path.Combine(Root,"permit.json");
|
||||
if(!File.Exists(path)) return;
|
||||
// A malformed or active permit must never silently fall back to PF.
|
||||
Enabled=true;
|
||||
if(JObject.Parse(File.ReadAllText(path))["Enabled"]?.Value<bool>()==false) { Enabled=false; return; }
|
||||
if(PlayFabMgr.HasLiveInstanceForKVLoad) throw new InvalidOperationException("Cannot switch a running storage backend.");
|
||||
Permit=PlayerKVLoopPermit.Read(path);
|
||||
using(var process=System.Diagnostics.Process.GetCurrentProcess())
|
||||
{
|
||||
var identity=process.Id+"_"+process.StartTime.ToUniversalTime().Ticks;
|
||||
PlayerKVLoopPermit.Claim(Root,identity);
|
||||
RunPath=Path.Combine(Root,"runs",identity); Directory.CreateDirectory(RunPath);
|
||||
}
|
||||
mainThread=Thread.CurrentThread.ManagedThreadId;
|
||||
mainContext=SynchronizationContext.Current ?? throw new InvalidOperationException("Unity main context required.");
|
||||
GContext.container.Resolve<IConfig>().Set("SaveBackend","kv-loop");
|
||||
GContext.container.Resolve<IConfig>().Set("CustomID",Permit.PlayerId);
|
||||
Audit("claim",new{Permit.PlayerId,Permit.BackupSha256});
|
||||
onFault = fault;
|
||||
}
|
||||
internal static void Attach(Action<string> fault,Func<Task> close) { onFault=fault; onClose=close; }
|
||||
internal static void Publish(PlayerKVGameSessionHandle value)
|
||||
{
|
||||
if(!Enabled || failed!=0 || handle!=null || value==null || value.PlayerId!=Permit.PlayerId)
|
||||
@@ -160,12 +140,6 @@ namespace game.PlayerDataStorage
|
||||
internal WriteBatch(PlayerKVGameSessionHandle owner) { Owner=owner; }
|
||||
public void Dispose() { if(ReferenceEquals(collecting,this)) collecting=null; }
|
||||
}
|
||||
internal static WriteBatch BeginEquip()
|
||||
{
|
||||
if(Permit?.AllowsFishing==true) throw Stop("Equipment is outside this fishing permit.");
|
||||
if(collecting!=null) throw Stop("Reentrant equipment action.");
|
||||
return collecting=new WriteBatch(Capture());
|
||||
}
|
||||
internal sealed class WriteRecord
|
||||
{
|
||||
internal readonly string Key,ValueHash;
|
||||
@@ -220,9 +194,6 @@ namespace game.PlayerDataStorage
|
||||
last.Values.Any(r=>!r.Remote || r.Edit.Ticket==null)) throw Stop("Fishing final writes require original remote tickets.");
|
||||
using(var timeout=new CancellationTokenSource(TimeSpan.FromSeconds(30)))
|
||||
{
|
||||
var flush=await KVMgr.FlushKVAsync(batch.Owner,timeout.Token);
|
||||
if(!FishingCurrent(action) || flush.Unknown>0 || flush.Blocked>0 || flush.TransportError!=PlayerKVError.None)
|
||||
throw Stop("Fishing flush: "+flush.TransportError);
|
||||
foreach(var record in batch.Records)
|
||||
{
|
||||
var ticket=record.Edit.Ticket;
|
||||
@@ -250,33 +221,6 @@ namespace game.PlayerDataStorage
|
||||
continuation();
|
||||
Audit("fishing-consume-ready",new{action.Owner.OwnerInstanceId,action.Owner.Epoch});
|
||||
}
|
||||
internal static async Task ConfirmEquipAsync(WriteBatch batch,PlayerKVEditResult primary)
|
||||
{
|
||||
if(primary?.Ticket==null || !batch.Edits.Contains(primary) || !ReferenceEquals(Capture(),batch.Owner))
|
||||
throw Stop("Missing original equipment ticket.");
|
||||
var flush=await KVMgr.FlushKVAsync(batch.Owner);
|
||||
if(flush.Unknown>0 || flush.Blocked>0 || flush.TransportError!=PlayerKVError.None) throw Stop("Equipment flush: "+flush.TransportError);
|
||||
foreach(var edit in batch.Edits)
|
||||
{
|
||||
if(edit.Ticket==null) continue; // localOnly is audited, never claimed as remotely confirmed.
|
||||
var result=await KVMgr.WaitForKVTicketAsync(batch.Owner,edit.Ticket);
|
||||
if(result.Status!=PlayerKVTicketStatus.Confirmed) throw Stop("Equipment ticket: "+result.Status);
|
||||
}
|
||||
if(!ReferenceEquals(Capture(),batch.Owner)) throw Stop("Equipment session changed.");
|
||||
Audit("equipment-confirmed",new{tickets=batch.Edits.Count(e=>e.Ticket!=null)});
|
||||
}
|
||||
internal static async Task FlushObservedAsync()
|
||||
{
|
||||
if(!Ready) return;
|
||||
try
|
||||
{
|
||||
var result=await KVMgr.FlushKVAsync(Capture());
|
||||
if(result.Unknown>0 || result.Blocked>0 || result.TransportError!=PlayerKVError.None) throw Stop("Periodic flush: "+result.TransportError);
|
||||
}
|
||||
catch(Exception e) { Fault(e.Message); }
|
||||
}
|
||||
internal static void Revoke() { Ready=false; }
|
||||
internal static Task CloseAsync() { Revoke(); return onClose?.Invoke() ?? Task.CompletedTask; }
|
||||
internal static InvalidOperationException Stop(string message) { Fault(message); return new InvalidOperationException(message); }
|
||||
internal static void Fault(string message)
|
||||
{
|
||||
@@ -299,127 +243,281 @@ namespace game.PlayerDataStorage
|
||||
static object validationCleanupFailure;
|
||||
internal static async Task<string> VerifyEquipmentForEditorAsync()
|
||||
{
|
||||
if(UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode || GContext.Ready ||
|
||||
UnityEngine.Resources.FindObjectsOfTypeAll<GContext>().Length!=0 || PlayFabMgr.HasLiveInstanceForKVLoad ||
|
||||
Enabled || handle!=null || File.Exists(Path.Combine(Root,"permit.json")) || validationCleanupFailure!=null)
|
||||
if (UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode || GContext.Ready || UnityEngine.Resources.FindObjectsOfTypeAll<GContext>().Length != 0 || PlayFabMgr.HasLiveInstanceForKVLoad || Enabled || handle != null || File.Exists(Path.Combine(Root, "permit.json")) || validationCleanupFailure != null)
|
||||
{
|
||||
throw new InvalidOperationException("An idle editor without a real loop permit is required.");
|
||||
if(Interlocked.CompareExchange(ref validationRunning,1,0)!=0) throw new InvalidOperationException("Validation already running.");
|
||||
var original=(Enabled,Ready,Error,Permit,RunPath,handle,onFault,onClose,collecting,mainThread,failed,mainContext);
|
||||
var folder=Path.Combine(Root,"fixed-equipment-runs",DateTime.UtcNow.ToString("yyyyMMddTHHmmss")+"_"+Guid.NewGuid().ToString("N"));
|
||||
EquipmentFixture fixture=null;
|
||||
Task pendingConfirmation=null;
|
||||
string result=null;
|
||||
int assertions=0, groups=0;
|
||||
void Check(bool ok,string reason) { assertions++; if(!ok) throw new InvalidOperationException("Equipment validation: "+reason); }
|
||||
}
|
||||
|
||||
if (Interlocked.CompareExchange(ref validationRunning, 1, 0) != 0)
|
||||
{
|
||||
throw new InvalidOperationException("Validation already running.");
|
||||
}
|
||||
|
||||
var original = (Enabled, Ready, Error, Permit, RunPath, handle, onFault, collecting, mainThread, failed, mainContext);
|
||||
var folder = Path.Combine(Root, "fixed-equipment-runs", DateTime.UtcNow.ToString("yyyyMMddTHHmmss") + "_" + Guid.NewGuid().ToString("N"));
|
||||
EquipmentFixture fixture = null;
|
||||
Task pendingConfirmation = null;
|
||||
string result = null;
|
||||
int assertions = 0, groups = 0;
|
||||
WriteBatch BeginEquip()
|
||||
{
|
||||
if (Permit?.AllowsFishing == true)
|
||||
{
|
||||
throw Stop("Equipment is outside this fishing permit.");
|
||||
}
|
||||
if (collecting != null)
|
||||
{
|
||||
throw Stop("Reentrant equipment action.");
|
||||
}
|
||||
return collecting = new WriteBatch(Capture());
|
||||
}
|
||||
|
||||
async Task ConfirmEquipAsync(WriteBatch batch, PlayerKVEditResult primary)
|
||||
{
|
||||
if (primary?.Ticket == null || !batch.Edits.Contains(primary) || !ReferenceEquals(Capture(), batch.Owner))
|
||||
{
|
||||
throw Stop("Missing original equipment ticket.");
|
||||
}
|
||||
foreach (var edit in batch.Edits)
|
||||
{
|
||||
// localOnly is audited, never claimed as remotely confirmed.
|
||||
if (edit.Ticket == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var result = await KVMgr.WaitForKVTicketAsync(batch.Owner, edit.Ticket);
|
||||
if (result.Status != PlayerKVTicketStatus.Confirmed)
|
||||
{
|
||||
throw Stop("Equipment ticket: " + result.Status);
|
||||
}
|
||||
}
|
||||
if (!ReferenceEquals(Capture(), batch.Owner))
|
||||
{
|
||||
throw Stop("Equipment session changed.");
|
||||
}
|
||||
Audit("equipment-confirmed", new { tickets = batch.Edits.Count(e => e.Ticket != null) });
|
||||
}
|
||||
|
||||
void Check(bool ok, string reason)
|
||||
{
|
||||
assertions++;
|
||||
if (!ok)
|
||||
{
|
||||
throw new InvalidOperationException("Equipment validation: " + reason);
|
||||
}
|
||||
}
|
||||
|
||||
async Task Start()
|
||||
{
|
||||
if(fixture!=null) await fixture.Close();
|
||||
fixture=new EquipmentFixture(Path.Combine(folder,groups.ToString()));
|
||||
var owner=await fixture.Open();
|
||||
Enabled=true; Ready=false; Error=null; failed=0; handle=null; collecting=null;
|
||||
Permit=new PlayerKVLoopPermit{PlayerId="101",AllowedKeys=new[]{"k","NewPlayerDataDic","late"}};
|
||||
RunPath=folder; mainThread=Thread.CurrentThread.ManagedThreadId; mainContext=SynchronizationContext.Current;
|
||||
Attach(_=>fixture.Faults++,fixture.Close); Publish(owner);
|
||||
if (fixture != null)
|
||||
{
|
||||
await fixture.Close();
|
||||
}
|
||||
|
||||
fixture = new EquipmentFixture(Path.Combine(folder, groups.ToString()));
|
||||
var owner = await fixture.Open();
|
||||
Enabled = true;
|
||||
Ready = false;
|
||||
Error = null;
|
||||
failed = 0;
|
||||
handle = null;
|
||||
collecting = null;
|
||||
Permit = new PlayerKVLoopPermit
|
||||
{
|
||||
PlayerId = "101",
|
||||
AllowedKeys = new[]
|
||||
{
|
||||
"k",
|
||||
"NewPlayerDataDic",
|
||||
"late"
|
||||
}
|
||||
};
|
||||
RunPath = folder;
|
||||
mainThread = Thread.CurrentThread.ManagedThreadId;
|
||||
mainContext = SynchronizationContext.Current;
|
||||
Attach(_ => fixture.Faults++);
|
||||
Publish(owner);
|
||||
groups++;
|
||||
}
|
||||
async Task Reject(Task task,string contains)
|
||||
|
||||
async Task Reject(Task task, string contains)
|
||||
{
|
||||
bool rejected=false;
|
||||
try { await task; } catch(InvalidOperationException e) { rejected=e.Message.Contains(contains); }
|
||||
Check(rejected,"expected rejection: "+contains);
|
||||
Check(!Ready && fixture.Faults==1,"one fault revokes facade");
|
||||
}
|
||||
try
|
||||
{
|
||||
if(!PlayerKVGameLoadGuard.TryEnter(out var probe)) throw new InvalidOperationException("Load guard occupied.");
|
||||
probe.Dispose(); Directory.CreateDirectory(folder);
|
||||
await Start();
|
||||
Check(Read("missing")==null,"valid missing key remains null");
|
||||
var batch=BeginEquip();
|
||||
// This nested callback represents the synchronous Publish subscriber in the equipment chain.
|
||||
Action nested=()=>Write("k","nested",true);
|
||||
nested(); var primary=Write("NewPlayerDataDic","whole-save",true); batch.Dispose();
|
||||
Check(batch.Edits.Count==2 && ReferenceEquals(primary,batch.Edits[1]),"all original edits collected");
|
||||
Check(fixture.Sets==0,"write acceptance is local only");
|
||||
await ConfirmEquipAsync(batch,primary);
|
||||
foreach(var edit in batch.Edits) Check((await KVMgr.WaitForKVTicketAsync(batch.Owner,edit.Ticket)).Status==PlayerKVTicketStatus.Confirmed,"each original ticket confirmed");
|
||||
Check(fixture.Remote["k"]=="nested" && fixture.Remote["NewPlayerDataDic"]=="whole-save" && Ready && fixture.Faults==0,"complete batch committed");
|
||||
|
||||
await Start(); batch=BeginEquip();
|
||||
var first=Write("k","first",true); Write("k","second",true);
|
||||
primary=Write("NewPlayerDataDic","whole-save",true); batch.Dispose();
|
||||
await Reject(ConfirmEquipAsync(batch,primary),"Superseded");
|
||||
Check((await KVMgr.WaitForKVTicketAsync(batch.Owner,first.Ticket)).Status==PlayerKVTicketStatus.Superseded,"original nested ticket never replaced");
|
||||
|
||||
await Start(); batch=BeginEquip();
|
||||
Write("k","nested",true); primary=Write("NewPlayerDataDic","watermark",true); batch.Dispose();
|
||||
fixture.HoldSet=true;
|
||||
var confirmation=ConfirmEquipAsync(batch,primary);
|
||||
pendingConfirmation=confirmation;
|
||||
var entered=await Task.WhenAny(fixture.SetEntered.Task,confirmation,Task.Delay(5000));
|
||||
Check(ReferenceEquals(entered,fixture.SetEntered.Task),"first set entered");
|
||||
var late=Write("late","later-write",true);
|
||||
Check(batch.Edits.Count==2,"later write excluded from synchronous batch");
|
||||
fixture.ReleaseSet.TrySetResult(true); await confirmation; pendingConfirmation=null;
|
||||
Check(!fixture.Remote.ContainsKey("late"),"flush watermark excludes later write");
|
||||
await KVMgr.FlushKVAsync(batch.Owner);
|
||||
Check((await KVMgr.WaitForKVTicketAsync(batch.Owner,late.Ticket)).Status==PlayerKVTicketStatus.Confirmed,"later write needs its own flush");
|
||||
|
||||
await Start(); batch=BeginEquip(); primary=Write("NewPlayerDataDic","unauthorized",true); batch.Dispose();
|
||||
fixture.Unauthorized=true;
|
||||
await Reject(ConfirmEquipAsync(batch,primary),"Equipment flush");
|
||||
Check(fixture.Auth==1 && fixture.Sets==1,"401 never reauthenticates or retries");
|
||||
|
||||
await Start(); batch=BeginEquip(); primary=Write("NewPlayerDataDic","lost-response",true); batch.Dispose();
|
||||
fixture.LoseResponse=true;
|
||||
await Reject(ConfirmEquipAsync(batch,primary),"Equipment flush");
|
||||
Check(fixture.Remote["NewPlayerDataDic"]=="lost-response" && fixture.Sets==1,"unknown remote commit is not replayed");
|
||||
Check((await KVMgr.WaitForKVTicketAsync(batch.Owner,primary.Ticket)).Status==PlayerKVTicketStatus.Unknown,"unknown original ticket preserved");
|
||||
|
||||
await Start(); batch=BeginEquip(); primary=Write("NewPlayerDataDic","closed",true); batch.Dispose();
|
||||
await fixture.Close();
|
||||
await Reject(ConfirmEquipAsync(batch,primary),"Equipment flush");
|
||||
Check(fixture.Sets==0,"close does not flush pending writes");
|
||||
|
||||
await Start(); bool denied=false;
|
||||
try { Write("unbacked-key","x",true); } catch(InvalidOperationException e) { denied=e.Message.Contains("permission"); }
|
||||
Check(denied && fixture.Sets==0 && !Ready,"key permission checked before writes");
|
||||
await Start(); await fixture.Close(); denied=false;
|
||||
try { Read("k"); } catch(InvalidOperationException) { denied=true; }
|
||||
Check(denied && !Ready,"stale read cannot become missing");
|
||||
|
||||
var originalContainer=GContext.container;
|
||||
var container=new Container();
|
||||
bool rejected = false;
|
||||
try
|
||||
{
|
||||
var configPath=Path.GetFullPath(Path.Combine(Application.dataPath,"../../cfgData"));
|
||||
var tables=new cfg.Tables(name=>SimpleJSON.JSON.Parse(File.ReadAllText(Path.Combine(configPath,name+".json"))));
|
||||
await task;
|
||||
}
|
||||
catch (InvalidOperationException e)
|
||||
{
|
||||
rejected = e.Message.Contains(contains);
|
||||
}
|
||||
|
||||
Check(rejected, "expected rejection: " + contains);
|
||||
Check(!Ready && fixture.Faults == 1, "one fault revokes facade");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (!PlayerKVGameLoadGuard.TryEnter(out var probe))
|
||||
{
|
||||
throw new InvalidOperationException("Load guard occupied.");
|
||||
}
|
||||
|
||||
probe.Dispose();
|
||||
Directory.CreateDirectory(folder);
|
||||
await Start();
|
||||
Check(Read("missing") == null, "valid missing key remains null");
|
||||
var batch = BeginEquip();
|
||||
// This nested callback represents the synchronous Publish subscriber in the equipment chain.
|
||||
Action nested = () => Write("k", "nested", true);
|
||||
nested();
|
||||
var primary = Write("NewPlayerDataDic", "whole-save", true);
|
||||
batch.Dispose();
|
||||
Check(batch.Edits.Count == 2 && ReferenceEquals(primary, batch.Edits[1]), "all original edits collected");
|
||||
Check(fixture.Sets == 0, "write acceptance is local only");
|
||||
_ = KVMgr.SyncKVAsync(batch.Owner); // One simulated ordinary timer tick, outside confirmation.
|
||||
await ConfirmEquipAsync(batch, primary);
|
||||
foreach (var edit in batch.Edits)
|
||||
Check((await KVMgr.WaitForKVTicketAsync(batch.Owner, edit.Ticket)).Status == PlayerKVTicketStatus.Confirmed, "each original ticket confirmed");
|
||||
Check(fixture.Remote["k"] == "nested" && fixture.Remote["NewPlayerDataDic"] == "whole-save" && Ready && fixture.Faults == 0, "complete batch committed");
|
||||
await Start();
|
||||
batch = BeginEquip();
|
||||
var first = Write("k", "first", true);
|
||||
Write("k", "second", true);
|
||||
primary = Write("NewPlayerDataDic", "whole-save", true);
|
||||
batch.Dispose();
|
||||
_ = KVMgr.SyncKVAsync(batch.Owner); // One simulated ordinary timer tick, outside confirmation.
|
||||
await Reject(ConfirmEquipAsync(batch, primary), "Superseded");
|
||||
Check((await KVMgr.WaitForKVTicketAsync(batch.Owner, first.Ticket)).Status == PlayerKVTicketStatus.Superseded, "original nested ticket never replaced");
|
||||
await Start();
|
||||
batch = BeginEquip();
|
||||
Write("k", "nested", true);
|
||||
primary = Write("NewPlayerDataDic", "watermark", true);
|
||||
batch.Dispose();
|
||||
fixture.HoldSet = true;
|
||||
_ = KVMgr.SyncKVAsync(batch.Owner); // One simulated ordinary timer tick, outside confirmation.
|
||||
var confirmation = ConfirmEquipAsync(batch, primary);
|
||||
pendingConfirmation = confirmation;
|
||||
var entered = await Task.WhenAny(fixture.SetEntered.Task, confirmation, Task.Delay(5000));
|
||||
Check(ReferenceEquals(entered, fixture.SetEntered.Task), "first set entered");
|
||||
var late = Write("late", "later-write", true);
|
||||
Check(batch.Edits.Count == 2, "later write excluded from synchronous batch");
|
||||
fixture.ReleaseSet.TrySetResult(true);
|
||||
await confirmation;
|
||||
pendingConfirmation = null;
|
||||
Check(!fixture.Remote.ContainsKey("late"), "flush watermark excludes later write");
|
||||
await KVMgr.SyncKVAsync(batch.Owner);
|
||||
Check((await KVMgr.WaitForKVTicketAsync(batch.Owner, late.Ticket)).Status == PlayerKVTicketStatus.Confirmed, "later write needs its own flush");
|
||||
await Start();
|
||||
batch = BeginEquip();
|
||||
primary = Write("NewPlayerDataDic", "unauthorized", true);
|
||||
batch.Dispose();
|
||||
fixture.Unauthorized = true;
|
||||
_ = KVMgr.SyncKVAsync(batch.Owner); // One simulated ordinary timer tick, outside confirmation.
|
||||
await Reject(ConfirmEquipAsync(batch, primary), "Equipment ticket");
|
||||
Check(fixture.Auth == 1 && fixture.Sets == 1, "401 never reauthenticates or retries");
|
||||
await Start();
|
||||
batch = BeginEquip();
|
||||
primary = Write("NewPlayerDataDic", "lost-response", true);
|
||||
batch.Dispose();
|
||||
fixture.LoseResponse = true;
|
||||
_ = KVMgr.SyncKVAsync(batch.Owner); // One simulated ordinary timer tick, outside confirmation.
|
||||
await Reject(ConfirmEquipAsync(batch, primary), "Equipment ticket");
|
||||
Check(fixture.Remote["NewPlayerDataDic"] == "lost-response" && fixture.Sets == 1, "unknown remote commit is not replayed");
|
||||
Check((await KVMgr.WaitForKVTicketAsync(batch.Owner, primary.Ticket)).Status == PlayerKVTicketStatus.Unknown, "unknown original ticket preserved");
|
||||
await Start();
|
||||
batch = BeginEquip();
|
||||
primary = Write("NewPlayerDataDic", "closed", true);
|
||||
batch.Dispose();
|
||||
await fixture.Close();
|
||||
await Reject(ConfirmEquipAsync(batch, primary), "Equipment ticket");
|
||||
Check(fixture.Sets == 0, "close does not flush pending writes");
|
||||
await Start();
|
||||
bool denied = false;
|
||||
try
|
||||
{
|
||||
Write("unbacked-key", "x", true);
|
||||
}
|
||||
catch (InvalidOperationException e)
|
||||
{
|
||||
denied = e.Message.Contains("permission");
|
||||
}
|
||||
|
||||
Check(denied && fixture.Sets == 0 && !Ready, "key permission checked before writes");
|
||||
await Start();
|
||||
await fixture.Close();
|
||||
denied = false;
|
||||
try
|
||||
{
|
||||
Read("k");
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
denied = true;
|
||||
}
|
||||
|
||||
Check(denied && !Ready, "stale read cannot become missing");
|
||||
var originalContainer = GContext.container;
|
||||
var container = new Container();
|
||||
try
|
||||
{
|
||||
var configPath = Path.GetFullPath(Path.Combine(Application.dataPath, "../../cfgData"));
|
||||
var tables = new cfg.Tables(name => SimpleJSON.JSON.Parse(File.ReadAllText(Path.Combine(configPath, name + ".json"))));
|
||||
container.RegisterInstance<cfg.Tables>(tables);
|
||||
EquipmentContextAccess.Use(container);
|
||||
using(container.InjectionScope()) container.Register<EventShootingRangeData>().AsSingleton();
|
||||
var shooting=container.Resolve<EventShootingRangeData>();
|
||||
Check(shooting!=null && ReferenceEquals(shooting,container.Resolve<EventShootingRangeData>()),"M24 container resolves one instance");
|
||||
shooting.ResetForKVLoad();
|
||||
Check(!shooting.KVLoadPresent,"M24 can enter the existing load transaction without activation");
|
||||
using (container.InjectionScope())
|
||||
container.Register<EventShootingRangeData>().AsSingleton();
|
||||
var shooting = container.Resolve<EventShootingRangeData>();
|
||||
Check(shooting != null && ReferenceEquals(shooting, container.Resolve<EventShootingRangeData>()), "M24 container resolves one instance");
|
||||
shooting.LoadData("{\"EventId\":11,\"AmmoCount\":7}");
|
||||
var shootingData = JObject.FromObject(Module02OriginalLoadTests.Field<object>(shooting, "_data"));
|
||||
Check((int)shootingData["EventId"] == 11 && (int)shootingData["AmmoCount"] == 7, "M24 registered instance uses original load method");
|
||||
}
|
||||
finally { EquipmentContextAccess.Use(originalContainer); container.Dispose(); }
|
||||
Check(ReferenceEquals(GContext.container,originalContainer),"M24 global container restored");
|
||||
finally
|
||||
{
|
||||
EquipmentContextAccess.Use(originalContainer);
|
||||
container.Dispose();
|
||||
}
|
||||
|
||||
Check(ReferenceEquals(GContext.container, originalContainer), "M24 global container restored");
|
||||
groups++;
|
||||
result=JsonConvert.SerializeObject(new{success=true,groups,assertions,formalRequests=0,synthetic=true,restored=true,folder});
|
||||
result = JsonConvert.SerializeObject(new { success = true, groups, assertions, formalRequests = 0, synthetic = true, restored = true, folder });
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
if(fixture!=null) await fixture.Close();
|
||||
if(pendingConfirmation!=null)
|
||||
try { await pendingConfirmation; } catch(Exception e) { Debug.LogWarning("Observed failed synthetic confirmation during cleanup: "+e.GetType().Name); }
|
||||
(Enabled,Ready,Error,Permit,RunPath,handle,onFault,onClose,collecting,mainThread,failed,mainContext)=original;
|
||||
if (fixture != null)
|
||||
{
|
||||
await fixture.Close();
|
||||
}
|
||||
|
||||
if (pendingConfirmation != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
await pendingConfirmation;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogWarning("Observed failed synthetic confirmation during cleanup: " + e.GetType().Name);
|
||||
}
|
||||
}
|
||||
|
||||
(Enabled, Ready, Error, Permit, RunPath, handle, onFault, collecting, mainThread, failed, mainContext) = original;
|
||||
}
|
||||
catch
|
||||
{
|
||||
validationCleanupFailure = fixture;
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
Interlocked.Exchange(ref validationRunning, 0);
|
||||
}
|
||||
catch { validationCleanupFailure=fixture; throw; }
|
||||
finally { Interlocked.Exchange(ref validationRunning,0); }
|
||||
}
|
||||
File.WriteAllText(Path.Combine(folder,"result.json"),result);
|
||||
|
||||
File.WriteAllText(Path.Combine(folder, "result.json"), result);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -430,37 +528,29 @@ namespace game.PlayerDataStorage
|
||||
}
|
||||
internal static string InspectPFBackupForEditor(string path)
|
||||
{
|
||||
if(Enabled || validationRunning!=0 || UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode || GContext.Ready ||
|
||||
UnityEngine.Resources.FindObjectsOfTypeAll<GContext>().Length!=0)
|
||||
throw new InvalidOperationException("Idle editor required for source inspection.");
|
||||
path=Path.GetFullPath(path);
|
||||
var allowed=Path.GetFullPath(Path.Combine(Root,"pf-source-backups"))+Path.DirectorySeparatorChar;
|
||||
if(!path.StartsWith(allowed,StringComparison.OrdinalIgnoreCase)) throw new ArgumentException("Expected local PF source backup.");
|
||||
var bytes=File.ReadAllBytes(path); var source=JObject.Parse(Encoding.UTF8.GetString(bytes));
|
||||
var id=(string)source["customId"];
|
||||
if((string)source["source"]!="PlayFab" || (string)source["titleId"]!="108EE8" || (id!="101" && id!="102"))
|
||||
throw new InvalidOperationException("Source identity mismatch.");
|
||||
var raw=source["data"].ToObject<Dictionary<string,string>>();
|
||||
var originalContainer=GContext.container; var isolated=new Container();
|
||||
try
|
||||
if (Enabled || validationRunning != 0 || UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode || GContext.Ready || UnityEngine.Resources.FindObjectsOfTypeAll<GContext>().Length != 0)
|
||||
{
|
||||
var configPath=Path.GetFullPath(Path.Combine(Application.dataPath,"../../cfgData"));
|
||||
var tables=new cfg.Tables(name=>SimpleJSON.JSON.Parse(File.ReadAllText(Path.Combine(configPath,name+".json"))));
|
||||
isolated.RegisterInstance<cfg.Tables>(tables); EquipmentContextAccess.Use(isolated);
|
||||
var now=DateTime.UtcNow;
|
||||
var snapshot=PlayerKVGameplayBindings.Snapshot(now);
|
||||
// Diagnostic-only context: this is NOT a real KV Prepare/Open and cannot publish a handle.
|
||||
var prepared=new PlayerKVLoginPrepareResult(PlayerKVLoginPrepareError.None,id,1,ownerId:"offline-PF-candidate",data:raw);
|
||||
var account=new PlayerKVAccountDescriptor(id,1,0,false,DateTime.SpecifyKind(DateTime.MinValue,DateTimeKind.Utc),now);
|
||||
var identity=new PlayerKVIdentitySnapshot((string)source["playFabId"],false,PlayerKVIdentityChannel.none,Array.Empty<PlayerKVIdentityChannel>());
|
||||
var decoded=PlayerKVSaveSchema.Decode(prepared,account,id,1,true,snapshot,snapshot.Version,identity);
|
||||
return JsonConvert.SerializeObject(new{sourceOnly=true,syntheticDescriptor=true,usesLocalUtc=true,customId=id,path,sha256=PlayerKVLoopPermit.Hash(bytes),keys=raw.Count,
|
||||
verdict=decoded.Verdict.ToString(),errors=decoded.Diagnostics.TotalErrors,
|
||||
details=decoded.Diagnostics.Details.Select(d=>new{module=d.Module,field=d.Field,code=d.Code.ToString()}),
|
||||
configuredRods=decoded.Data?.Module<PlayerKVFishState>(3).RodLevels.Keys.ToArray(),
|
||||
gameplayPublished=false,writes=0});
|
||||
throw new InvalidOperationException("Idle editor required for source inspection.");
|
||||
}
|
||||
finally { EquipmentContextAccess.Use(originalContainer); isolated.Dispose(); }
|
||||
|
||||
path = Path.GetFullPath(path);
|
||||
var allowed = Path.GetFullPath(Path.Combine(Root, "pf-source-backups")) + Path.DirectorySeparatorChar;
|
||||
if (!path.StartsWith(allowed, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new ArgumentException("Expected local PF source backup.");
|
||||
}
|
||||
|
||||
var bytes = File.ReadAllBytes(path);
|
||||
var source = JObject.Parse(Encoding.UTF8.GetString(bytes));
|
||||
var id = (string)source["customId"];
|
||||
if ((string)source["source"] != "PlayFab" || (string)source["titleId"] != "108EE8" || (id != "101" && id != "102"))
|
||||
{
|
||||
throw new InvalidOperationException("Source identity mismatch.");
|
||||
}
|
||||
|
||||
var raw = source["data"].ToObject<Dictionary<string, string>>();
|
||||
var actual = Module02OriginalLoadTests.InspectRaw(raw);
|
||||
return JsonConvert.SerializeObject(new { sourceOnly = true, usesLocalUtc = true, customId = id, path, sha256 = PlayerKVLoopPermit.Hash(bytes), keys = raw.Count, originalLoad = JObject.Parse(actual), gameplayPublished = false, formalRequests = 0 });
|
||||
}
|
||||
sealed class EquipmentFixture
|
||||
{
|
||||
@@ -514,108 +604,35 @@ namespace game.PlayerDataStorage
|
||||
internal async Task Close()
|
||||
{
|
||||
ReleaseSet.TrySetResult(true);
|
||||
await bridge.CloseAsync();
|
||||
session.Dispose();
|
||||
Exception closeError = null;
|
||||
try
|
||||
{
|
||||
await bridge.CloseAsync();
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
closeError = error;
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
session.Dispose();
|
||||
}
|
||||
catch (Exception disposeError)
|
||||
{
|
||||
if (closeError != null)
|
||||
{
|
||||
throw new AggregateException(closeError, disposeError);
|
||||
}
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#if !SERVICE_HOST
|
||||
namespace game
|
||||
{
|
||||
public partial class PlayFabUserService
|
||||
{
|
||||
PlayerKVGameSessionBridge loopBridge;
|
||||
PlayerKVGameLoadGuard loopGuard;
|
||||
Task loopClosing;
|
||||
TaskCompletionSource<bool> loopSettled;
|
||||
bool loopAttempted, loopStopping;
|
||||
internal bool KVLoopLoading { get; private set; }
|
||||
|
||||
async Task<ELoginStep> LoginKVGameplayLoopAsync(Action<int> progress,int generation)
|
||||
{
|
||||
if(loopAttempted || loopStopping || CustomId!=PlayerKVGameplayLoop.Permit.PlayerId) return ELoginStep.Api;
|
||||
loopAttempted=true; loopSettled=new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
loopBridge=new PlayerKVGameSessionBridge();
|
||||
PlayerKVGameplayLoop.Attach(message=>
|
||||
{
|
||||
_=StopKVGameplayLoopAsync();
|
||||
var main=UnityEngine.Application.isPlaying;
|
||||
if(main) _=ExitKVGameplayLoopAsync();
|
||||
},StopKVGameplayLoopAsync);
|
||||
try
|
||||
{
|
||||
if(customServerMgr.ApiBaseUri.AbsoluteUri!=PlayerKVGameplayLoop.Permit.Api)
|
||||
throw new InvalidOperationException("KV loop requires the fixed s101 API.");
|
||||
if(!await LoginPlayFab(generation) || generation!=logoutGeneration) throw new InvalidOperationException("PF identity login failed/stale.");
|
||||
if(!await LoginApiServer() || generation!=logoutGeneration) throw new InvalidOperationException("API login failed/stale.");
|
||||
if(InternalId!=0 || IsLogin) throw new InvalidOperationException("Gameplay identity published before load.");
|
||||
var opened=await loopBridge.OpenAsync(apiSession,PlayerKVGameplayLoop.CacheRoot,CustomId,customServerMgr.ApiBaseUri,
|
||||
generation,e=>!loopStopping && e==logoutGeneration,LoginLifetime);
|
||||
if(!opened.Success) throw new InvalidOperationException("KV open: "+opened.Error);
|
||||
var read=opened.Handle.ReadLocalSnapshot();
|
||||
if(!read.Success) throw new InvalidOperationException("KV snapshot: "+read.Error);
|
||||
var prepared=new PlayerKVLoginPrepareResult(PlayerKVLoginPrepareError.None,apiSession.PlayerId,generation,
|
||||
syncStatus:opened.Handle.Status,ownerId:opened.Handle.OwnerInstanceId,data:read.Data);
|
||||
var account=new PlayerKVAccountDescriptor(apiSession.PlayerId,generation,apiSession.InternalId,
|
||||
apiSession.NewlyCreated,apiSession.CreateTimeUtc,apiSession.ServerTimeUtc);
|
||||
var snapshot=PlayerKVGameplayBindings.Snapshot(apiSession.ServerTimeUtc);
|
||||
var identity=new PlayerKVIdentitySnapshot(UserId,config.Get<bool>("NewlyCreated",false),PlayerKVIdentityChannel.none,Array.Empty<PlayerKVIdentityChannel>());
|
||||
var decoded=PlayerKVSaveSchema.Decode(prepared,account,CustomId,generation,!loopStopping && generation==logoutGeneration,snapshot,snapshot.Version,identity);
|
||||
if(decoded.Data==null) throw new InvalidOperationException("KV schema: "+decoded.Verdict);
|
||||
if(!PlayerKVGameLoadGuard.TryEnter(out loopGuard)) throw new InvalidOperationException("Load guard occupied.");
|
||||
KVLoopLoading=true;
|
||||
var transaction=PlayerKVGameplayBindings.Bind(this);
|
||||
var applied=transaction.Apply(decoded.Data,()=>!loopStopping && generation==logoutGeneration,
|
||||
()=>loopGuard.IsCurrent && loopGuard.Violations==0,()=>{});
|
||||
if(applied!=PlayerKVApplyError.None) throw new InvalidOperationException("KV apply: "+applied);
|
||||
KVLoopLoading=false; loopGuard.Dispose(); loopGuard=null;
|
||||
PlayerKVGameplayLoop.Publish(opened.Handle);
|
||||
PlayFabMgr.Instance.StartKVGameplayLoopSync();
|
||||
InternalId=apiSession.InternalId;
|
||||
GContext.Publish(new EvtAPISvrLoginSuccess{playerId=apiSession.PlayerId,internalId=InternalId});
|
||||
OnInitData?.Invoke(decoded.Data.Raw.ToDictionary(p=>p.Key,p=>p.Value,StringComparer.Ordinal));
|
||||
SetMMTestValue();
|
||||
if(!PlayerKVGameplayLoop.Ready || loopStopping || generation!=logoutGeneration) throw new InvalidOperationException("Initialization revoked loop.");
|
||||
progress?.Invoke(2); return ELoginStep.Done;
|
||||
}
|
||||
catch(Exception e) { PlayerKVGameplayLoop.Fault(e.Message); return ELoginStep.Api; }
|
||||
finally { KVLoopLoading=false; loopSettled.TrySetResult(true); }
|
||||
}
|
||||
async Task ExitKVGameplayLoopAsync()
|
||||
{
|
||||
try { await GContext.container.Resolve<IStageService>().SwitchStageAsync("QuitStage"); }
|
||||
catch(Exception e) { Debug.LogException(e); }
|
||||
}
|
||||
internal Task StopKVGameplayLoopAsync()
|
||||
{
|
||||
if(loopClosing!=null) return loopClosing;
|
||||
loopStopping=true; PlayerKVGameplayLoop.Revoke(); logoutGeneration++; apiLoginGeneration++; InternalId=0;
|
||||
PlayFabMgr.Instance?.StopKVGameplayLoopSync();
|
||||
var completion=new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
loopClosing=completion.Task; PlayerKVShutdownRegistry.Process.Register(loopClosing);
|
||||
try { loginLifetime.Cancel(); } catch(Exception e) { Debug.LogException(e); }
|
||||
var drain=loopBridge?.CloseAsync() ?? Task.CompletedTask;
|
||||
_=FinishKVGameplayLoopAsync(drain,completion); return loopClosing;
|
||||
}
|
||||
async Task FinishKVGameplayLoopAsync(Task drain,TaskCompletionSource<bool> completion)
|
||||
{
|
||||
try
|
||||
{
|
||||
if(loopSettled!=null) await loopSettled.Task;
|
||||
await drain;
|
||||
if(loopBridge!=null) await loopBridge.CloseAsync();
|
||||
apiSession?.Dispose(); apiSession=null; customServerMgr.ClearAccountSession();
|
||||
loopGuard?.Dispose(); loopGuard=null;
|
||||
UserId=null; DisplayName=null; AvatarUrl=null; InternalId=0;
|
||||
PlayFab.PlayFabClientAPI.ForgetAllCredentials();
|
||||
PlayerKVGameplayLoop.Audit("closed",new{requiresProcessRestart=true}); completion.TrySetResult(true);
|
||||
}
|
||||
catch(Exception e) { completion.TrySetException(e); Debug.LogException(e); }
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
Reference in New Issue
Block a user