feat(upserver): 接入认证与KV存档并完成限定钓鱼闭环

This commit is contained in:
Liubing\LB
2026-09-28 18:14:01 +08:00
parent f40a816cc4
commit 0eb43b2b40
129 changed files with 11612 additions and 573 deletions

View File

@@ -22,13 +22,13 @@ public class DebugConfig
private const string s101StaicResUrl = "http://192.168.9.101:8080/{0}";
private const string s101AddrUrl = "http://192.168.9.101:8080/{0}/{1}";
private const string s101EventAPIUrl = "http://192.168.9.101:5292/";
private const string s101StaicResUrl = "http://s101.local:8080/{0}";
private const string s101AddrUrl = "http://s101.local:8080/{0}/{1}";
private const string s101EventAPIUrl = "http://s101.local:18080/";
private const string s101RTMPid = "80000560";
private const string s101RTMServerEndpoint = "rtm-nx-front.ilivedata.com:13321";
private const string s101RTMHmacSecret = "7a1ba6bb85a4473d96fa4bf45b01f1dd";
private const string s101FuncUrl= "http://192.168.9.101:7071";
private const string s101FuncUrl= "http://s101.local:7071";
private const string s101FuncKey= "testKey";
private const string devStaicResUrl = "https://sls-cloudfunction-ap-beijing-code-1305151099.cos.ap-beijing.myqcloud.com/dev/{0}";

View File

@@ -0,0 +1,23 @@
#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") 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

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 4ce05f2f22c2492f8a3c51a82a208647

View File

@@ -0,0 +1,85 @@
#if UNITY_EDITOR
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Security.Cryptography;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
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)
{
if(caseId!="equipment-chain" && caseId!="fishing-consume" && caseId!="fishing-reward") 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(caseId=="equipment-chain"?"VerifyEquipmentForEditorAsync":caseId=="fishing-consume"?"VerifyFishingForEditorAsync":"VerifyFishingRewardForEditorAsync",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 ValidatePermit(string path)
{
var type=Find("PlayerKVLoopPermit");
try
{
var permit=type.GetMethod("Read",BindingFlags.Static|BindingFlags.NonPublic).Invoke(null,new object[]{Path.GetFullPath(path)});
return JsonConvert.SerializeObject(permit);
}
catch(TargetInvocationException e) { throw e.InnerException ?? e; }
}
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; }
}
public static string PreparePermit(string account,string backupPath,string[] allowedKeys)
=> PreparePermitCore(account,backupPath,allowedKeys,false);
public static string PrepareFishingPermit(string account,string backupPath,string[] allowedKeys)
=> PreparePermitCore(account,backupPath,allowedKeys,true);
public static string PrepareFishingRewardPermit(string account,string backupPath,string[] allowedKeys)
=> PreparePermitCore(account,backupPath,allowedKeys,true,true);
static string PreparePermitCore(string account,string backupPath,string[] allowedKeys,bool fishing,bool reward=false)
{
if(EditorApplication.isPlayingOrWillChangePlaymode || EditorApplication.isCompiling)
throw new InvalidOperationException("Stop play before preparing a new process run.");
backupPath=Path.GetFullPath(backupPath);
var bytes=File.ReadAllBytes(backupPath);
string hash;
using(var sha=SHA256.Create()) hash=BitConverter.ToString(sha.ComputeHash(bytes)).Replace("-","");
var candidate=Path.Combine(Root,"permit.candidate.json");
Directory.CreateDirectory(Root);
var json=JObject.FromObject(new{Version=reward?3:fishing?2:1,Enabled=true,PlayerId=account,
Api="http://s101.local:18080/",BackupPath=backupPath,BackupSha256=hash,AllowedKeys=allowedKeys});
if(fishing) json["Operation"]=reward?"fishing-reward-once":"fishing-consume-once";
File.WriteAllText(candidate,json.ToString(Formatting.Indented));
var validated=ValidatePermit(candidate); // Empty/mismatched backups cannot enable a run.
File.WriteAllText(Path.Combine(Root,"permit.json"),validated);
return State();
}
}
#endif

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: ff2728a480374b609aaed9ea0742215e

View File

@@ -0,0 +1,19 @@
#if UNITY_EDITOR
using System;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
public static class KVSaveRouteValidation
{
public static async Task<string> RunAsync(string caseId)
{
if (caseId != "fixed-route") throw new ArgumentException("Unknown fixed validation case.");
var assembly = AppDomain.CurrentDomain.GetAssemblies().Single(a => a.GetName().Name == "HotUpdate");
var type = assembly.GetType("game.PlayerDataStorage.PlayerKVSaveRouteEditorValidationHost", 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

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: dc9b5dbb615a4746a95df66b46987589

View File

@@ -0,0 +1,74 @@
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;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a8f49f7440c141e89d8519ff3aa1c034
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: