12 Commits

Author SHA1 Message Date
Liubing\LB
8e848b63d8 fix(build): 修复安卓构建依赖并整理服务器配置 2026-10-14 21:05:41 +08:00
Liubing\LB
73ac2bf6f8 docs: 明确KV整理范围与代码格式约束 2026-10-13 20:39:24 +08:00
Liubing\LB
1a81c331e3 build(kv): 完善构建宏处理和本地测试服切换 2026-10-13 20:39:03 +08:00
Liubing\LB
edb8a056c7 refactor(kv): 整理存档接线并清理迁移冗余实现 2026-10-13 20:38:41 +08:00
Liubing\LB
144e7f739f chore(m2): 保存功能验收及后续清理前检查点 2026-10-10 20:40:27 +08:00
Liubing\LB
d3c5394a55 添加忽略文件 2026-10-09 16:34:21 +08:00
Liubing\LB
6813da149a feat(upserver): 保存M2客户端KV迁移至本人统计写检查点
保存独立KV用户服务、固定会话存档与原业务接线,以及C3d资料缓存和C3e本人统计写。
保留原PF/AGG路线、业务公式和时序;榜单读取、好友资料、公告、匹配及首场景闸仍待完成。

验证:ServiceHost两条件各33组646断言;Unity CLI Loop编译0错误269警告。
包含相关方案、评审裁决、测试及状态文档。未操作服务器、正式账号或打包。
2026-09-30 19:19:05 +08:00
Liubing\LB
1c0cdf7a99 docs(upserver): 归档阶段裁决与批量迁移待办 2026-09-28 18:17:40 +08:00
Liubing\LB
a6f5e107f8 test(upserver): 归档存档迁移与玩法闭环回归工具 2026-09-28 18:16:14 +08:00
Liubing\LB
c4ae02def9 chore(unity): 固定Unity CLI Loop本地包并移除UnitySkills依赖 2026-09-28 18:14:59 +08:00
Liubing\LB
0eb43b2b40 feat(upserver): 接入认证与KV存档并完成限定钓鱼闭环 2026-09-28 18:14:01 +08:00
Liubing\LB
f40a816cc4 update:uplog 2026-09-15 16:25:44 +08:00
1124 changed files with 79756 additions and 1068 deletions

4
.gitignore vendored
View File

@@ -10,3 +10,7 @@ CLAUDE.md
QWEN.md QWEN.md
Client/.claude/ Client/.claude/
dev-ab-svr/debugconfig.json dev-ab-svr/debugconfig.json
/Client/.uloop/
/Client/.codex/
/CodexOutput/

70
AGENTS.md Normal file
View File

@@ -0,0 +1,70 @@
# 项目通用开发规则
- 2026-10-13最新授权(P98):P97登录链整理完成后,允许按完整评审进行KV专属namespace、类/文件名及KV引入警告的整理,取代此前命名后置优先级;只做等价名称/引用变化及已核无副作用的警告处理。保留Dev独立入口、公共PF/KV存档合同及原业务/异常语义,不为清零警告去掉async或新增伪await,不扩改无关代码。
- 2026-10-13后续优先级:先核迁移新增文件和类的实际必要性,确认无消费者则删除;职责可由现有实现等价承接时,先完整评审再合并,减少冗余文件和层次。不能只用排版整理代替结构核销,也不能把减少文件数冒称减少类。保留原业务、资源生命周期和用户明确要求的服务入口;namespace与命名统一仍后置。
- 2026-10-13当前优先级:先整理本次迁移代码,使实现简洁、准确、易读;`Core/KV` 移位后的 namespace 与类/文件命名统一列为后置事项,当前不穿插改名。先处理重复闲置实现和难读的紧凑写法,保持原业务、数据、同步/异步、异常和调用顺序;不以可读性为由增加层次或重设流程。修改前完整评审,保留用户编辑。
- `CodexOutput/` 仅用于本地开发文档、评审及验证产物,不得纳入 Git 提交/推送、服务器上传、部署或发布包;不得使用 `git add -f` 强制加入。已被 Git 跟踪的内容仅取消跟踪,保留本地文件及现有修改。
- 2026-10-10用户要求开发后读取并执行:[PlayFab到KV-存量核查纠偏与开发后检查执行要求](CodexOutput/PlayFab到KV-存量核查纠偏与开发后检查执行要求-2026-10-10.md)。开发会话完成当前批次后必须读取本文,将存量核查纳入已有唯一TODO;后续每批开发及独立AI复核完成后执行其中第六部分的受影响范围自检,本轮核查纠偏完成后再做最终汇总检查,并在既有实施日志记录范围、证据和未闭合项。不能仅记录“已读”或用AI报告代替开发端检查;已有匹配hash的评审复用,不每批重复全工程扫描。发现超出已审边界的问题先补完整方案评审,不扩大业务重构或删除许可。检查通过且仍有已授权工作时继续,不停等用户催促。
- 2026-10-10用户明确要求持续执行:阻塞解除后,只要下一步工作仍在当前客户端侧且已有授权,就立即继续,不能因阶段汇报、更新文档、完成一个检查点/模块或本批审核通过而结束执行、等待用户再说“继续”。按唯一TODO串行推进;必要方案评审和独立AI代码复核由当前侧主动完成,复核通过后继续下一项,已有匹配hash的报告复用。遇到局部阻塞先保留证据并继续不依赖它的已授权工作;只有确实需要用户裁决/外部交付且无其他可推进项、用户明确暂停,或授权范围内工作全部完成时才停止,并说明具体原因。此规则覆盖旧的逐模块停下等人工review节奏,不免除编码前完整方案评审,不扩大1:1迁移、服务器只读、保留工作树及暂不打包边界。
- 2026-10-09最新用户授权:因人工难以阅读整批差异,使用独立AI代码review承接本次模块review,参考裁决后继续后续。模块仍串行,编码前完整方案评审不省略;每模块代码与必要验证完成后由AI复核,发现问题先修本模块,通过后继续。已有匹配当前hash的独立AI报告优先复用,不重复送审;未定业务合同、行为差异或必须由用户裁决的问题仍集中说明。本授权不构成服务器修改、业务优化、整文件回滚或打包许可,也不冒充正式全链验收。
- 2026-10-09用户再次确认:PlayerKV精简仍受全流程1:1平移规则约束,不是架构升级或业务重构授权。只核减本次迁移新增且已被等价替换的重复实现;先列原版→当前→目标的方法、数据及顺序对照,再评审实施。已知行为偏差必须列入唯一模块TODO,未验证不得宣称无漂移;不因服务器暂未交付擅自改变业务、永久冻结功能或扩大改造范围。
- 当前执行边界(2026-10-08本轮用户指令):保留旧 PlayFabUserService、AggUserService 原实现,新增 KV 服务承接迁移功能;保留现有改动,不整文件回滚。下条此前记录的“最终删除旧类”不构成本轮删除许可,也不作为当前方案前提。先区分既有共用接口、原类行为差异和KV专用实现;冲突范围完成具体方案评审后再改,不继续向旧PF添加KV职责。核对记录见 CodexOutput/upserver/B3BatchMigration/M2/M2-保留旧PF决议与当前评审工作树核对-2026-10-08.md。
- PlayFabUserService 是迁移完成后要移除的旧实现,不是 KV 的宿主或最终双后端架构。迁移期间保留旧类仅用于原行为对照及尚未迁出的有效消费者;不得再为 KV 加接口、partial、验证构造器或加载分支。KVUserService / AggKVUserService / DevKVUserService 独立承接原能力,相关消费者与有效验证迁完后删除旧 PF 实现及其迁移残留。保持原业务行为不等于永久保留旧类;不整文件回滚,不覆盖用户编辑,不提前删除仍在使用的功能。
- KV模式的迁移目标是由KV管理器及其接线承接原PlayFab相关能力,不只迁个人存档;等级榜、公开资料、好友和匹配是原PlayFab调用的既有功能,不得当作新增产品需求、默认延期或直接转交服务端。先核客户端已有KV能力及等价调用映射;只有明确证实缺少必要能力时,才把具体接口证据集中写入沟通文档。不得反复要求用户确认“是否存KV”、新ID或原邮件流程。已审同合同替换连续实施,确有新合同或架构差异时一次合并评审,避免逐功能送审。
- 整个迁移流程、所有阶段与批次均只做平移,不限于 M2。全部迁移完成前,仅修改完成迁移所必需的存储、身份和接口接线,保留原业务规则、调用顺序、同步/异步方式、返回值、异常处理和界面流程。不得夹带额外业务修正、优化或代码重构;不得以“更安全”“更合理”或“顺手修复”为由改变原逻辑。
- 发现原有业务缺陷、代码结构问题或可优化点,统一记录到迁移完成后的待办,不在任何迁移阶段穿插处理。必须等整个迁移流程全部完成后,再由用户另行授权修正;单个阶段完成不构成提前修正的许可。此前方案或评审包含此类扩展时,以此规则及用户最新要求为准。
- 最终闲置实现清理仅限本次迁移造成且经核实已无消费者的实现,不得借清理重构代码或删除原有有效功能。确有无法保持原流程的迁移障碍,先说明具体差异,不自行改写业务规则或冻结功能。
- 在满足已确认的功能、正确性和必要安全边界的前提下,优先选择最简单、最直观、最容易维护的实现。抽象应服务于实际调用者;不要为假设中的扩展增加接口、适配器、管理器、状态机或重复保护层。
- 修改代码前完成相关方案的完整评审,记录裁决和开发边界。评审新增抽象时,说明它解决的具体问题、实际调用点,以及为何复用或小幅修改现有实现不足;无法说明必要性的层次不要引入。
- 同一事实保持单一来源,同一业务流程避免多重开关和重复状态。迁移中的临时路径须登记,最终通关前恢复或清理;最终代码删除闲置实现,但保留仍在使用的原有功能。
- 原设计是迁移基准,尽量做到 1:1 平移:优先复用原方法、业务调用链和初始化入口,仅替换必要的数据来源、身份值和存储接口。不得以迁移为由过度设计、升级架构,或另建与原流程并行的解码、模块装配、初始化和状态管理体系。只有经源码证实无法复用的具体差异才进入集中评审,说明最小改动及行为影响;不得先增加层次再要求原业务适配。已经引入的重复实现须列入方案修订,完整评审并验证等价后再替换和清理,不能直接删除仍在使用的代码。
- 2026-10-08历史节奏(已由2026-10-09独立AI模块review授权、2026-10-10持续执行规则替代):原要求每模块完成后停下等人工review。当前仍按模块串行、先完整评审再编码,提供原版对照/代码入口/差异/验证,模块问题先修本模块;独立AI复核通过后继续,不再机械等待人工回复。
## C# 代码格式
- 2026-10-13范围约束:显式访问限定符和代码格式规则只落实到本次KV迁移新增的代码及迁移确需修改的具体成员。与KV引入无关的原有类、字段、方法不因缺少private/public/internal/protected而修改;同一文件含KV接线也不构成整文件补齐或重排许可。已有合法且有效的访问范围保持,不为统一风格扩大差异。
- `[Inject]` 必须单独占一行,被标注的字段、属性或方法声明从下一行开始;不得写成 `[Inject] public ...`。新增或修改注入声明时遵守此规则,保留已有的换行格式;不因此批量重排无关代码。
- 字段必须显式声明访问修饰符,类内部使用的字段优先写 `private`,不要依赖省略修饰符时的默认可见性。例如写 `private readonly ...`、`private static ...`、`private ...`。此规则指访问级别,不要求给变量名增加前缀或改名。
- 2026-10-13补充:类实现中的方法、构造器及嵌套类型也必须显式声明访问修饰符,和字段一样按实际调用者选择 `private`、`internal`、`protected` 或 `public`;省略修饰符的既有私有成员补 `private` 时保持原访问范围。局部变量、参数、局部函数、静态构造器、析构器及语言不允许访问修饰符的显式接口实现不套用;不因此扩大无关代码重排。
- 按实际消费者选择最小必要访问级别:跨类调用、依赖注入、接口或继承确需 `internal`、`protected`、`public` 时保留对应权限,不得为了统一为 `private` 而破坏现有调用。构造器也须核对创建位置;局部变量、参数及不允许访问修饰符的显式接口实现不套用字段规则。不批量调整无关代码。
- `if`、`else if`、`else` 分支必须使用大括号,即使只有一条语句也不省略;条件、分支语句不能挤在同一行,不写 `if (...) return ...;` 或 `{ statement; }`。
- 使用大括号的代码块采用换行格式:左、右大括号各自独立成行,块内语句逐行书写并缩进。方法、构造器、属性访问器及控制流块均保持清晰展开,不用一行多个语句压缩代码;不改变条件、执行顺序或原业务行为。
```csharp
[Inject]
public ICustomServerMgr customServerMgr
{
get;
set;
}
[Inject]
public IPlayerSaveStore SaveStore
{
get;
set;
}
private readonly PlayerKVRuntimeSelection selection;
private PlayerKVGameSessionHandle handle;
```
条件分支示例:
```csharp
if (!IsAuthorized)
{
return ELoginStep.Api;
}
```

View File

@@ -4,7 +4,7 @@ ConfigPath=../config.json
[Unity] [Unity]
K_Ver=0.0.1 K_Ver=0.0.1
K_PlayFabe_Title=6DC6D K_PlayFab_Title=108EE8
K_Static_Res_URL=https://n3af96.blob.core.windows.net/client K_Static_Res_URL=https://n3af96.blob.core.windows.net/client
K_Event_API_URL=https://n3backend.azurewebsites.net/event K_Event_API_URL=https://n3backend.azurewebsites.net/event

View File

@@ -8,6 +8,7 @@
"RTMHmacSecret": "7a1ba6bb85a4473d96fa4bf45b01f1dd", "RTMHmacSecret": "7a1ba6bb85a4473d96fa4bf45b01f1dd",
"CustomID": "{{input}}", "CustomID": "{{input}}",
"GM": "1", "GM": "1",
"EventApiURL": "http://fishpro-game-test01-api.slg-test-bj.tuyougame.cn/",
"FuncUrl": "http://62.234.191.215:7071", "FuncUrl": "http://62.234.191.215:7071",
"FuncKey": "testKey", "FuncKey": "testKey",
"Ver": "0.47.d7748ea" "Ver": "0.47.d7748ea"

View File

@@ -8,6 +8,7 @@
"RTMHmacSecret": "7a1ba6bb85a4473d96fa4bf45b01f1dd", "RTMHmacSecret": "7a1ba6bb85a4473d96fa4bf45b01f1dd",
"CustomID": "{{input}}", "CustomID": "{{input}}",
"GM": "1", "GM": "1",
"EventApiURL": "http://fishpro-game-test01-api.slg-test-bj.tuyougame.cn/",
"FuncUrl": "http://62.234.191.215:7071", "FuncUrl": "http://62.234.191.215:7071",
"FuncKey": "testKey", "FuncKey": "testKey",
"Ver": "0.47.2f4aa9f" "Ver": "0.47.2f4aa9f"

File diff suppressed because one or more lines are too long

View File

@@ -91,7 +91,6 @@ MonoBehaviour:
- {fileID: 11400000, guid: 3d6eccb69e063c146a641bd5a7d173b2, type: 2} - {fileID: 11400000, guid: 3d6eccb69e063c146a641bd5a7d173b2, type: 2}
- {fileID: 11400000, guid: 38b112aa97ad6044781acbc6f439d4d7, type: 2} - {fileID: 11400000, guid: 38b112aa97ad6044781acbc6f439d4d7, type: 2}
- {fileID: 11400000, guid: fe5a82082740016478c19cd356393dcb, type: 2} - {fileID: 11400000, guid: fe5a82082740016478c19cd356393dcb, type: 2}
- {fileID: 11400000, guid: 55d262d4724350a47910f45e3df02d88, type: 2}
- {fileID: 11400000, guid: 6c36938b168c52b45ac08e17b4a81438, type: 2} - {fileID: 11400000, guid: 6c36938b168c52b45ac08e17b4a81438, type: 2}
- {fileID: 11400000, guid: 58d3e9279ba66aa498c73766fee507b7, type: 2} - {fileID: 11400000, guid: 58d3e9279ba66aa498c73766fee507b7, type: 2}
- {fileID: 11400000, guid: 321c4872f6ddb554ca9d64b65b640a24, type: 2} - {fileID: 11400000, guid: 321c4872f6ddb554ca9d64b65b640a24, type: 2}

View File

@@ -631,9 +631,24 @@ public class BuildTool
{ {
var buildTargetGroup = EditorUserBuildSettings.selectedBuildTargetGroup; var buildTargetGroup = EditorUserBuildSettings.selectedBuildTargetGroup;
PlayerSettings.GetScriptingDefineSymbolsForGroup(buildTargetGroup, out string[] symbols); PlayerSettings.GetScriptingDefineSymbolsForGroup(buildTargetGroup, out string[] symbols);
if (symbols.Contains(dfs)) return;
var newDefines = new List<string>(symbols); var newDefines = new List<string>(symbols);
newDefines.Add(dfs); var changed = false;
foreach (var symbol in dfs.Split(';'))
{
if (string.IsNullOrEmpty(symbol))
{
continue;
}
if (!newDefines.Contains(symbol))
{
newDefines.Add(symbol);
changed = true;
}
}
if (!changed)
{
return;
}
PlayerSettings.SetScriptingDefineSymbolsForGroup(buildTargetGroup, string.Join(";", newDefines.ToArray())); PlayerSettings.SetScriptingDefineSymbolsForGroup(buildTargetGroup, string.Join(";", newDefines.ToArray()));
} }
@@ -641,9 +656,24 @@ public class BuildTool
{ {
var buildTargetGroup = EditorUserBuildSettings.selectedBuildTargetGroup; var buildTargetGroup = EditorUserBuildSettings.selectedBuildTargetGroup;
PlayerSettings.GetScriptingDefineSymbolsForGroup(buildTargetGroup, out string[] symbols); PlayerSettings.GetScriptingDefineSymbolsForGroup(buildTargetGroup, out string[] symbols);
if (!symbols.Contains(dfs)) return;
var newDefines = new List<string>(symbols); var newDefines = new List<string>(symbols);
newDefines.Remove(dfs); var changed = false;
foreach (var symbol in dfs.Split(';'))
{
if (string.IsNullOrEmpty(symbol))
{
continue;
}
if (newDefines.Contains(symbol))
{
newDefines.Remove(symbol);
changed = true;
}
}
if (!changed)
{
return;
}
PlayerSettings.SetScriptingDefineSymbolsForGroup(buildTargetGroup, string.Join(";", newDefines.ToArray())); PlayerSettings.SetScriptingDefineSymbolsForGroup(buildTargetGroup, string.Join(";", newDefines.ToArray()));
} }

View File

@@ -22,13 +22,14 @@ public class DebugConfig
private const string s101StaicResUrl = "http://192.168.9.101:8080/{0}"; private const string s101StaicResUrl = "http://s101.local:8080/{0}";
private const string s101AddrUrl = "http://192.168.9.101:8080/{0}/{1}"; private const string s101AddrUrl = "http://s101.local:8080/{0}/{1}";
private const string s101EventAPIUrl = "http://192.168.9.101:5292/"; private const string s101EventAPIUrl = "http://s101.local:18080/";
private const string localTestEventAPIUrl = "http://172.16.69.228:8080/";
private const string s101RTMPid = "80000560"; private const string s101RTMPid = "80000560";
private const string s101RTMServerEndpoint = "rtm-nx-front.ilivedata.com:13321"; private const string s101RTMServerEndpoint = "rtm-nx-front.ilivedata.com:13321";
private const string s101RTMHmacSecret = "7a1ba6bb85a4473d96fa4bf45b01f1dd"; 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 s101FuncKey= "testKey";
private const string devStaicResUrl = "https://sls-cloudfunction-ap-beijing-code-1305151099.cos.ap-beijing.myqcloud.com/dev/{0}"; private const string devStaicResUrl = "https://sls-cloudfunction-ap-beijing-code-1305151099.cos.ap-beijing.myqcloud.com/dev/{0}";
@@ -97,10 +98,15 @@ public class DebugConfig
public static bool MustaphSvrValidation() public static bool MustaphSvrValidation()
{ {
var config = Config; var config = Config;
if (config != null && config.StaticResURL == string.Format(mustaphaStaicResUrl, VersionTool.PackVer)) if (config != null && config.StaticResURL == string.Format(mustaphaStaicResUrl, VersionTool.PackVer) &&
config.EventApiURL == mustaphaEventAPIUrl)
{
Menu.SetChecked("Tools/Debug/Mustaph Svr", true); Menu.SetChecked("Tools/Debug/Mustaph Svr", true);
}
else else
{
Menu.SetChecked("Tools/Debug/Mustaph Svr", false); Menu.SetChecked("Tools/Debug/Mustaph Svr", false);
}
return true; return true;
} }
@@ -125,10 +131,15 @@ public class DebugConfig
public static bool LiubingSvrValidation() public static bool LiubingSvrValidation()
{ {
var config = Config; var config = Config;
if (config != null && config.StaticResURL == string.Format(liubingStaicResUrl, VersionTool.PackVer)) if (config != null && config.StaticResURL == string.Format(liubingStaicResUrl, VersionTool.PackVer) &&
config.EventApiURL == liubingEventAPIUrl)
{
Menu.SetChecked("Tools/Debug/Liubing Svr", true); Menu.SetChecked("Tools/Debug/Liubing Svr", true);
}
else else
{
Menu.SetChecked("Tools/Debug/Liubing Svr", false); Menu.SetChecked("Tools/Debug/Liubing Svr", false);
}
return true; return true;
} }
@@ -156,10 +167,49 @@ public class DebugConfig
public static bool Svr101Validation() public static bool Svr101Validation()
{ {
var config = Config; var config = Config;
if (config != null && config.StaticResURL == string.Format(s101StaicResUrl, VersionTool.PackVer)) if (config != null && config.StaticResURL == string.Format(s101StaicResUrl, VersionTool.PackVer) &&
config.EventApiURL == s101EventAPIUrl)
{
Menu.SetChecked("Tools/Debug/Svr 101", true); Menu.SetChecked("Tools/Debug/Svr 101", true);
}
else else
{
Menu.SetChecked("Tools/Debug/Svr 101", false); Menu.SetChecked("Tools/Debug/Svr 101", false);
}
return true;
}
[MenuItem("Tools/Debug/Svr Local 172.16.69.228")]
public static void SvrLocal()
{
Newtonsoft.Json.Linq.JObject config;
if (File.Exists(DebugConfigPath))
{
config = Newtonsoft.Json.Linq.JObject.Parse(File.ReadAllText(DebugConfigPath));
}
else
{
config = Newtonsoft.Json.Linq.JObject.FromObject(new TmpClas
{
EventApiURL = s101EventAPIUrl,
StaticResURL = string.Format(s101StaicResUrl, VersionTool.PackVer),
Custom_Addressable_URL = string.Format(s101AddrUrl, VersionTool.PackVer, EditorUserBuildSettings.activeBuildTarget),
RTMPid = s101RTMPid,
RTMServerEndpoint = s101RTMServerEndpoint,
RTMHmacSecret = s101RTMHmacSecret,
FuncUrl = s101FuncUrl,
FuncKey = s101FuncKey
});
}
config["EventApiURL"] = localTestEventAPIUrl;
File.WriteAllText(DebugConfigPath, config.ToString(Formatting.Indented));
}
[MenuItem("Tools/Debug/Svr Local 172.16.69.228", true)]
public static bool SvrLocalValidation()
{
var config = Config;
Menu.SetChecked("Tools/Debug/Svr Local 172.16.69.228", config != null && config.EventApiURL == localTestEventAPIUrl);
return true; return true;
} }
@@ -208,10 +258,15 @@ public class DebugConfig
public static bool SvrDevValidation() public static bool SvrDevValidation()
{ {
var config = Config; var config = Config;
if (config != null && config.StaticResURL == string.Format(devStaicResUrl, VersionTool.PackVer)) if (config != null && config.StaticResURL == string.Format(devStaicResUrl, VersionTool.PackVer) &&
config.EventApiURL == devEventAPIUrl)
{
Menu.SetChecked("Tools/Debug/Svr Dev", true); Menu.SetChecked("Tools/Debug/Svr Dev", true);
}
else else
{
Menu.SetChecked("Tools/Debug/Svr Dev", false); Menu.SetChecked("Tools/Debug/Svr Dev", false);
}
return true; return true;
} }
@@ -243,10 +298,15 @@ public class DebugConfig
public static bool SvrTimeshiftValidation() public static bool SvrTimeshiftValidation()
{ {
var config = Config; var config = Config;
if (config != null && config.StaticResURL == string.Format(timeShiftStaicResUrl, VersionTool.PackVer)) if (config != null && config.StaticResURL == string.Format(timeShiftStaicResUrl, VersionTool.PackVer) &&
config.EventApiURL == timeShiftEventAPIUrl)
{
Menu.SetChecked("Tools/Debug/Svr timeshift", true); Menu.SetChecked("Tools/Debug/Svr timeshift", true);
}
else else
{
Menu.SetChecked("Tools/Debug/Svr timeshift", false); Menu.SetChecked("Tools/Debug/Svr timeshift", false);
}
return true; return true;
} }

View File

@@ -9,6 +9,17 @@ plugins {
**BUILD_SCRIPT_DEPS** **BUILD_SCRIPT_DEPS**
} }
// DataStore 1.1.7 在当前 AGP 下会选中 JVM 制品,显式使用包含 Android 类的同版本制品。
allprojects {
configurations.configureEach {
resolutionStrategy.dependencySubstitution {
['datastore', 'datastore-core', 'datastore-preferences', 'datastore-preferences-core'].each { artifact ->
substitute module("androidx.datastore:${artifact}") using module("androidx.datastore:${artifact}-android:1.1.7")
}
}
}
}
task clean(type: Delete) { task clean(type: Delete) {
delete rootProject.buildDir delete rootProject.buildDir
} }

View File

@@ -244,6 +244,8 @@ half WCE_calcShadowIntensity(WCE_Lit lit, half dotLN, WCE_CommonData dt) {
uint WCE_getMeshRendLayer() { uint WCE_getMeshRendLayer() {
#if defined(SHADERGRAPH_PREVIEW) || !defined(_LIGHT_LAYERS) #if defined(SHADERGRAPH_PREVIEW) || !defined(_LIGHT_LAYERS)
return 255; return 255;
#elif UNITY_VERSION >= 202220
return GetMeshRenderingLayer();
#else #else
return GetMeshRenderingLightLayer(); return GetMeshRenderingLightLayer();
#endif #endif

View File

@@ -4068,7 +4068,7 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0} m_AnchorMin: {x: 0.5, y: 0}
m_AnchorMax: {x: 0.5, y: 0} m_AnchorMax: {x: 0.5, y: 0}
m_AnchoredPosition: {x: 0, y: 109.05} m_AnchoredPosition: {x: 0, y: 109.049805}
m_SizeDelta: {x: 0, y: 74.69995} m_SizeDelta: {x: 0, y: 74.69995}
m_Pivot: {x: 0.5, y: 0.5} m_Pivot: {x: 0.5, y: 0.5}
--- !u!1 &2775237510373940736 --- !u!1 &2775237510373940736

View File

@@ -503,7 +503,7 @@ public class Boostrap : MonoBehaviour
private async Task AfterAssetUpdateV2() private async Task AfterAssetUpdateV2()
{ {
bsLoading.SetProgress(0.15f); bsLoading.SetProgress(0.15f);
await SubBoostrap.RegisterServices(ctx.gameObject); await SubBoostrap.RegisterServices(ctx.gameObject, config);
bsLoading.SetProgress(0.16f); bsLoading.SetProgress(0.16f);
await SubBoostrap.AfterAssetUpdate(config, bsLoading.SetProgress); await SubBoostrap.AfterAssetUpdate(config, bsLoading.SetProgress);
bsLoading.SetProgress(0.18f); bsLoading.SetProgress(0.18f);

View File

@@ -12,7 +12,16 @@ using UnityEngine.Networking;
public static class ConfigExtension public static class ConfigExtension
{ {
private const string defaultconfig = "9Nf/P8vm2+7Ciph2jH1QnEaHCK3cAl7lNnCJc/uxSKGVqcJgWilN3VgaC/74coHRiRpxEelC2+ia7qcjnHhA3OQN82rcVJOP24pGKrSbSgOIGbH9cCEUhdYRo5vnVgLBqlTS/c/AajowTkN59O0AYxDC7J3jq6ptLXdmuG27JYS9NXOQDkgxpzEpG7JTtczHKxhWAv+9uSzbJRCs16KeusbJX1YtNZM8wjCD9esTv4zXXMIpZPlXb86NjHiI02TakAdHo0plO+Di9n5UCC8/4+QGoKNyQgtvIkzMyoGziO4DOLy/VkfITenFv/+9DY0LmpZGa7Cog89znaYyVMTiC/3R2IefXXh04+8uqmyzeVKqh0grcU9qmz485wCoUl/TM9F8+s1Jhp/eaB/E7luOjc57Kz0Eyw2Qko7Fo+lCaRJE1uex9fB46v0LhddSHCEWw8QERmqG9KcmvG4LsyafupA8kliDzj7a7XIzGrgnNnsMxLgeYsSbNiBvXXESeeNCoqiOQTTBpk/br0QuTHTG2DRxUZZ4KM3SKZtHEZ2bZV2FUS4azFpfDXUGoc86O+xz6hAIMbdKCHyqyEeKMmHIzQI9OlIaa3KGwk+w1eFDUuq4zyTh+qGuClZynanUkfNe8mTmFtBax2f3MJHjGpXJT0VdtY/R+5ZQO0nCU30THfnryG0GURsrv6MQYiQX5hsLqG9Z72ybbbbqIR2rvE2w13DM/tqTb4i/hN0+pbeTKkUpdNIWEsR94Q=="; private const string defaultconfig = @"{
""EventApiURL"": ""http://fishpro-game-test01-api.slg-test-bj.tuyougame.cn/"",
""PlayFabTitle"": ""108EE8"",
""Google_Play_URL"": ""https://play.google.com/store/apps/details?id=com.arkgame.ft"",
""APP_Store_URL"": ""https://apps.apple.com/us/app/id6505145935"",
""HeartBeat"": 60,
""RTMPid"": ""80000586"",
""RTMServerEndpoint"": ""rtm-intl-frontgate.ilivedata.com:13321"",
""RTMHmacSecret"": ""57047697437f4f2c97a835e8d9a53358""
}";
private static string RemoteConfigUrl { get; set; } private static string RemoteConfigUrl { get; set; }
@@ -46,8 +55,7 @@ public static class ConfigExtension
if (remoteConfig == null) if (remoteConfig == null)
{ {
var remoteConfigStr = Decrypt(defaultconfig); remoteConfig = JSON.Parse(defaultconfig);
remoteConfig = JSON.Parse(remoteConfigStr);
remoteConfig["Ver"] = VersionTool.FromLocal().ToVerString(); remoteConfig["Ver"] = VersionTool.FromLocal().ToVerString();
isSuccessLoadRemoteConfigFile = false; isSuccessLoadRemoteConfigFile = false;
} }

View File

@@ -77,7 +77,7 @@ public class GConstant
public const string K_FuncUrl = "FuncUrl"; public const string K_FuncUrl = "FuncUrl";
public const string V_FuncUrl = "http://192.168.9.101:7071"; public const string V_FuncUrl = "http://s101.local:7071";
public const string K_FuncKey = "FuncKey"; public const string K_FuncKey = "FuncKey";
public const string V_FuncKey = "testKey"; public const string V_FuncKey = "testKey";

View File

@@ -9,6 +9,10 @@ namespace Activity.Data
/// </summary> /// </summary>
public class EventDataRepository : IEventDataRepository public class EventDataRepository : IEventDataRepository
{ {
private readonly Core.KV.IPlayerSaveStore store;
public EventDataRepository(Core.KV.IPlayerSaveStore 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
{ {
if (data == null) if (data == null)
@@ -23,7 +27,7 @@ namespace Activity.Data
#if UNITY_EDITOR #if UNITY_EDITOR
Debug.Log($"[EventDataRepository] Saving {typeof(T).Name}: {json}"); Debug.Log($"[EventDataRepository] Saving {typeof(T).Name}: {json}");
#endif #endif
PlayFabMgr.Instance?.UpdateUserDataValue(typeof(T).Name, json); store.Write(typeof(T).Name, json).RequireAccepted();
} }
catch (Exception e) catch (Exception e)
{ {
@@ -35,7 +39,7 @@ namespace Activity.Data
{ {
try try
{ {
var dataStr = PlayFabMgr.Instance?.GetLocalData(typeof(T).Name); var dataStr = store.Read(typeof(T).Name).ValueOrThrow();
if (string.IsNullOrEmpty(dataStr)) return new T(); if (string.IsNullOrEmpty(dataStr)) return new T();
// #if UNITY_EDITOR // #if UNITY_EDITOR
// Debug.Log($"[EventDataRepository] Load {typeof(T).Name}: {dataStr}"); // Debug.Log($"[EventDataRepository] Load {typeof(T).Name}: {dataStr}");
@@ -52,7 +56,7 @@ namespace Activity.Data
public bool Exists<T>() where T : IEventData public bool Exists<T>() where T : IEventData
{ {
var dataStr = PlayFabMgr.Instance?.GetLocalData(typeof(T).Name); var dataStr = store.Read(typeof(T).Name).ValueOrThrow();
return !string.IsNullOrEmpty(dataStr); return !string.IsNullOrEmpty(dataStr);
} }
} }

View File

@@ -10,6 +10,15 @@ namespace Activity
public class ActivitySentryManager public class ActivitySentryManager
{ {
private static Dictionary<(int, int), Type> _activitySentryTypes; private static Dictionary<(int, int), Type> _activitySentryTypes;
private static object _registrationContainer;
private static void EnsureCurrentContainer()
{
if (ReferenceEquals(_registrationContainer, GContext.container)) return;
_registrationContainer = GContext.container;
_activitySentryTypes = null;
TypeKeyCache.Clear();
}
/// <summary> /// <summary>
/// 类型键缓存:Sentry 类型 → (EventType, EventSubType) /// 类型键缓存:Sentry 类型 → (EventType, EventSubType)
@@ -21,6 +30,7 @@ namespace Activity
{ {
get get
{ {
EnsureCurrentContainer();
if (_activitySentryTypes != null) return _activitySentryTypes; if (_activitySentryTypes != null) return _activitySentryTypes;
InitActivitySentryTypes(); InitActivitySentryTypes();
@@ -42,6 +52,7 @@ namespace Activity
} }
public static void InitActivitySentryType(Type type) public static void InitActivitySentryType(Type type)
{ {
EnsureCurrentContainer();
_activitySentryTypes ??= new Dictionary<(int, int), Type>(); _activitySentryTypes ??= new Dictionary<(int, int), Type>();
try try
{ {

View File

@@ -28,6 +28,10 @@ namespace Activity.Services
/// </summary> /// </summary>
public class EventTokenService : IEventTokenService public class EventTokenService : IEventTokenService
{ {
private readonly Core.KV.IPlayerSaveStore store;
public EventTokenService(Core.KV.IPlayerSaveStore store)
{ this.store = store ?? throw new ArgumentNullException(nameof(store)); }
#region 常量与字段 #region 常量与字段
private const string TransitionDataKey = "TransitionData"; private const string TransitionDataKey = "TransitionData";
@@ -75,7 +79,7 @@ namespace Activity.Services
{ {
try try
{ {
var dataStr = PlayFabMgr.Instance?.GetLocalData(TransitionDataKey); var dataStr = store.Read(TransitionDataKey).ValueOrThrow();
if (!string.IsNullOrEmpty(dataStr)) if (!string.IsNullOrEmpty(dataStr))
{ {
_tokenData.Clear(); _tokenData.Clear();
@@ -106,7 +110,7 @@ namespace Activity.Services
try try
{ {
var json = Newtonsoft.Json.JsonConvert.SerializeObject(_tokenData); var json = Newtonsoft.Json.JsonConvert.SerializeObject(_tokenData);
PlayFabMgr.Instance?.UpdateUserDataValue(TransitionDataKey, json); store.Write(TransitionDataKey, json).RequireAccepted();
#if UNITY_EDITOR #if UNITY_EDITOR
Debug.Log($"[EventTokenService] Saved {_tokenData.Count} token entries to PlayFab"); Debug.Log($"[EventTokenService] Saved {_tokenData.Count} token entries to PlayFab");
#endif #endif

View File

@@ -1,4 +1,4 @@
using asap.core; using asap.core;
using cfg; using cfg;
using game; using game;
using GameCore; using GameCore;
@@ -12,6 +12,8 @@ using UnityEngine;
public class AquariumManager public class AquariumManager
{ {
public const string AquariumEventDataNewKey = "AquariumEventDataNewKey"; public const string AquariumEventDataNewKey = "AquariumEventDataNewKey";
readonly Core.KV.IPlayerSaveStore saveStore;
readonly IUserService loginUser;
private cfg.Tables _tables; private cfg.Tables _tables;
private ICustomServerMgr customServerMgr; private ICustomServerMgr customServerMgr;
public AquariumManagerData data { get; private set; } public AquariumManagerData data { get; private set; }
@@ -21,12 +23,15 @@ public class AquariumManager
public List<AquariumSlotData> sellList; public List<AquariumSlotData> sellList;
public AquariumManager() public AquariumManager()
{ {
saveStore = GContext.container.Resolve<Core.KV.IPlayerSaveStore>();
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() public static AquariumManagerData GetAquariumManagerData(Core.KV.IPlayerSaveStore store = null)
{ {
string dataStr = PlayFabMgr.Instance.GetLocalData(AquariumEventDataNewKey); var source = store ?? GContext.container.Resolve<Core.KV.IPlayerSaveStore>();
string dataStr = source.Read(AquariumEventDataNewKey).ValueOrThrow();
if (!string.IsNullOrEmpty(dataStr)) if (!string.IsNullOrEmpty(dataStr))
{ {
try try
@@ -43,7 +48,8 @@ public class AquariumManager
public async Task Synchrodata() public async Task Synchrodata()
{ {
//从服务器拿数据 //从服务器拿数据
var dataSever = GetAquariumManagerData(); if (!saveStore.IsCurrent) throw new OperationCanceledException();
var dataSever = GetAquariumManagerData(saveStore);
if (dataSever == null) if (dataSever == null)
{ {
//新数据 //新数据
@@ -63,8 +69,9 @@ public class AquariumManager
return; return;
} }
GetAquariumDataRequest request = new GetAquariumDataRequest(); GetAquariumDataRequest request = new GetAquariumDataRequest();
request.playFabId = GContext.container.Resolve<IUserService>().UserId; request.playFabId = loginUser.UserId;
string result = await customServerMgr.CustomServerPost("aquarium/GetAquariumData", Newtonsoft.Json.JsonConvert.SerializeObject(request)); string result = await customServerMgr.CustomServerPost("aquarium/GetAquariumData", Newtonsoft.Json.JsonConvert.SerializeObject(request));
if (!saveStore.IsCurrent) throw new OperationCanceledException();
AquariumServerData serverData = null; AquariumServerData serverData = null;
if (result != null) if (result != null)
{ {
@@ -611,7 +618,7 @@ public class AquariumManager
} }
void UpdateUserData() void UpdateUserData()
{ {
PlayFabMgr.Instance.UpdateUserDataValue(AquariumEventDataNewKey, data.Serialize()); saveStore.Write(AquariumEventDataNewKey, data.Serialize()).RequireAccepted();
} }
void SaveData(bool all = false) void SaveData(bool all = false)
{ {
@@ -637,7 +644,7 @@ public class AquariumManager
if (all) if (all)
{ {
UpdateAquariumAllDataRequest request = new UpdateAquariumAllDataRequest(); UpdateAquariumAllDataRequest request = new UpdateAquariumAllDataRequest();
request.playFabId = GContext.container.Resolve<IUserService>().UserId; request.playFabId = loginUser.UserId;
request.data = new AquariumServerData() request.data = new AquariumServerData()
{ {
startTime = data.startTime, startTime = data.startTime,
@@ -653,7 +660,7 @@ public class AquariumManager
else else
{ {
UpdateSlotDatasRequest request = new UpdateSlotDatasRequest(); UpdateSlotDatasRequest request = new UpdateSlotDatasRequest();
request.playFabId = GContext.container.Resolve<IUserService>().UserId; request.playFabId = loginUser.UserId;
request.slotDatas = data.slotDatas; request.slotDatas = data.slotDatas;
request.cardLv = string.Join(',', cardLv); request.cardLv = string.Join(',', cardLv);
customServerMgr.CustomServerPost("aquarium/UpdateSlotData", Newtonsoft.Json.JsonConvert.SerializeObject(request)); customServerMgr.CustomServerPost("aquarium/UpdateSlotData", Newtonsoft.Json.JsonConvert.SerializeObject(request));

View File

@@ -10,6 +10,8 @@ using UnityEngine;
public class CampDataMM public class CampDataMM
{ {
[asap.core.Inject] public Core.KV.IPlayerSaveStore SaveStore { get; set; }
private cfg.Tables _tables; private cfg.Tables _tables;
MMTService _MMService; MMTService _MMService;
public CampDataMM(cfg.Tables tables, MMTService mmService) public CampDataMM(cfg.Tables tables, MMTService mmService)
@@ -101,6 +103,11 @@ public class CampDataMM
//是否需要修复 //是否需要修复
public bool IsFix => _isFix && AreaIndex < AreaList.Count; public bool IsFix => _isFix && AreaIndex < AreaList.Count;
bool _isFix; bool _isFix;
public void InitData(Dictionary<string, string> userData) public void InitData(Dictionary<string, string> userData)
{ {
string value; string value;
@@ -124,7 +131,7 @@ public class CampDataMM
_isFix = isFix; _isFix = isFix;
if (isSave) if (isSave)
{ {
PlayFabMgr.Instance.UpdateUserDataValue("IsFix", _isFix.ToString()); SaveStore.Write("IsFix", _isFix.ToString()).RequireAccepted();
} }
} }
@@ -230,7 +237,7 @@ public class CampDataMM
UpdateCurrentData(); UpdateCurrentData();
} }
CampDataDic["CampId"] = Id; CampDataDic["CampId"] = Id;
PlayFabMgr.Instance.UpdateUserDataValue("CampDataDic", JsonMapper.ToJson(CampDataDic)); ; SaveStore.Write("CampDataDic", JsonMapper.ToJson(CampDataDic)).RequireAccepted(); ;
} }
else else
{ {
@@ -258,7 +265,7 @@ public class CampDataMM
if (NextMapID == -1 || !_tables.TbConstruction.DataMap.ContainsKey(NextMapID)) if (NextMapID == -1 || !_tables.TbConstruction.DataMap.ContainsKey(NextMapID))
{ {
_isOpenSkyscraper = "1"; _isOpenSkyscraper = "1";
PlayFabMgr.Instance.UpdateUserDataValue("IsOpenSkyscraper", _isOpenSkyscraper); SaveStore.Write("IsOpenSkyscraper", _isOpenSkyscraper).RequireAccepted();
return; return;
} }
Debug.LogError( Debug.LogError(
@@ -335,7 +342,7 @@ public class CampDataMM
if (NextMapID == -1 || !_tables.TbConstruction.DataMap.ContainsKey(NextMapID)) if (NextMapID == -1 || !_tables.TbConstruction.DataMap.ContainsKey(NextMapID))
{ {
_isOpenSkyscraper = "1"; _isOpenSkyscraper = "1";
PlayFabMgr.Instance.UpdateUserDataValue("IsOpenSkyscraper", _isOpenSkyscraper); SaveStore.Write("IsOpenSkyscraper", _isOpenSkyscraper).RequireAccepted();
} }
} }
} }

View File

@@ -4,8 +4,11 @@ using System;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Text; using System.Text;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using UnityEngine; using UnityEngine;
using game.Account;
using game.KV;
public interface ICustomServerMgr public interface ICustomServerMgr
{ {
@@ -21,96 +24,202 @@ public interface ICustomServerMgr
Task<T> EventPartnerRequest<T>(string url, object data = null); Task<T> EventPartnerRequest<T>(string url, object data = null);
Task<string> EventPartnerRequest(string url, object data); Task<string> EventPartnerRequest(string url, object data);
CookieContainer Cookie { get; } CookieContainer Cookie { get; }
Uri ApiBaseUri { get; }
int SessionVersion { get; }
event Action SessionChanged;
void UseAccountSession(GameServerAccountSession session);
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; }
//网络客户端 //网络客户端
HttpClient client; HttpClient client;
private CookieContainer _cookie; private CookieContainer _cookie;
private readonly Func<CookieContainer, HttpMessageHandler> handlerFactory;
private readonly SynchronizationContext ownerContext;
public int SessionVersion { get; private set; }
public event Action SessionChanged;
private KvAccountLease kvLease;
public CookieContainer Cookie => _cookie ?? throw new System.NullReferenceException(); private bool kvOnly;
private bool kvBusinessReady;
public CustomServerMgr(IConfig config) private bool kvBusinessPublished;
private int pfRequests;
internal event Action<int> KVUnauthorized;
internal bool IsFreshForKVLoad => pfRequests==0 && kvLease==null;
internal void BindKVForGame(KvAccountLease lease)
{ {
if (lease == null || !lease.IsCurrent ||
lease.ApiBaseUri.AbsoluteUri != ApiBaseUri.AbsoluteUri || !IsFreshForKVLoad)
{
throw new InvalidOperationException("Fresh formal KV lease required.");
}
kvOnly=true; kvBusinessReady=false; kvBusinessPublished=false; SessionVersion++; kvLease=lease; _cookie=null;
var previous=client; client=null; previous?.Dispose(); SessionChanged?.Invoke();
}
internal void EnableKVBusiness(long epoch)
{
if (!kvOnly || kvBusinessReady || kvBusinessPublished || kvLease == null || !kvLease.IsCurrent ||
kvLease.Epoch != epoch || kvLease.ApiBaseUri.AbsoluteUri != ApiBaseUri.AbsoluteUri)
{
throw new InvalidOperationException("Current formal KV lease required for business transport.");
}
kvBusinessPublished=true; kvBusinessReady=true;
}
internal void DisableKVBusiness()
{
if(!kvOnly || !kvBusinessReady) return;
kvBusinessReady=false; SessionVersion++;
try { SessionChanged?.Invoke(); }
catch(Exception ex) { Debug.LogError($"KV session change failed: {ex.GetType().Name}"); }
}
internal Task ClearKVForGame()
{
kvBusinessReady = false;
SessionVersion++;
var previous = kvLease;
kvLease = null;
_cookie = null;
var closing = previous?.CloseAsync() ?? Task.CompletedTask;
SessionChanged?.Invoke();
return closing;
}
public CookieContainer Cookie => kvOnly
? (kvBusinessReady && kvLease != null ? kvLease.FormalCookies : null) ?? throw new NullReferenceException()
: _cookie ?? throw new NullReferenceException();
public Uri ApiBaseUri { get; }
public CustomServerMgr(IConfig config) : this(config, null) { }
public CustomServerMgr(IConfig config, Func<CookieContainer, HttpMessageHandler> handlerFactory)
{
ownerContext=SynchronizationContext.Current;
this.handlerFactory = handlerFactory ?? (cookies => new HttpClientHandler
{
CookieContainer = cookies, UseCookies = true, AllowAutoRedirect = false
});
this.config = config; this.config = config;
var httpClientHandler = new HttpClientHandler(); var url = config.Get<string>(GConstant.K_Event_API_URL, DefaultApiUrl);
_cookie = new CookieContainer(); if (string.IsNullOrWhiteSpace(url))
httpClientHandler.CookieContainer = _cookie; {
client = new HttpClient(httpClientHandler); url = DefaultApiUrl;
}
client.BaseAddress = ApiBaseUri = new Uri(url.TrimEnd('/') + "/");
new System.Uri(config.Get<string>(GConstant.K_Event_API_URL, ClearAccountSession();
"http://192.168.9.101:5292/"));
client.Timeout = TimeSpan.FromSeconds(15);
} }
public async Task<string> CustomServerPost(string url, string bodyString, public void UseAccountSession(GameServerAccountSession session)
string contentType = "application/json")
{ {
if(kvOnly) throw new InvalidOperationException("Controlled container cannot switch to PlayFab.");
if (session == null || !session.IsAuthenticated || session.ApiBaseUri != ApiBaseUri)
throw new ArgumentException("An authenticated session for this API is required.", nameof(session));
ReplaceTransport(session.Cookies);
}
public void ClearAccountSession()
{
if (kvOnly)
{
KvShutdownRegistry.Process.Register(ClearKVForGame());
return;
}
ReplaceTransport(new CookieContainer());
}
private void ReplaceTransport(CookieContainer cookies)
{
SessionVersion++;
var previous = client;
_cookie = cookies;
client = new HttpClient(handlerFactory(cookies)) { BaseAddress = ApiBaseUri, Timeout = TimeSpan.FromSeconds(15) };
previous?.Dispose();
SessionChanged?.Invoke();
}
public Task<string> CustomServerPost(string url, string bodyString,
string contentType = "application/json") => SendAsync(HttpMethod.Post, url, bodyString, contentType);
public Task<string> GetStringAsync(string url) => SendAsync(HttpMethod.Get, url, null, null);
// Unity callers use the main thread. Capture a fixed transport before the first await.
private async Task<string> SendAsync(HttpMethod method, string url, string body, string contentType)
{
if(kvOnly)
{
if(ownerContext!=null && !ReferenceEquals(SynchronizationContext.Current,ownerContext))
{
var completion=new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
ownerContext.Post(async _=>
{
try { completion.TrySetResult(await SendAsync(method,url,body,contentType)); }
catch(Exception ex) { completion.TrySetException(ex); }
},null);
return await completion.Task.ConfigureAwait(false);
}
if(!kvBusinessReady) throw new InvalidOperationException("Formal KV business transport is not ready.");
var lease=kvLease; var kvVersion=SessionVersion;
if (lease == null || !lease.IsCurrent)
{
return null;
}
try try
{ {
//#if UNITY_EDITOR var response=await lease.SendAsync(method,url,body,contentType);
// Debug.Log("CustomServerPost url:" + url); if(!kvBusinessReady || kvVersion!=SessionVersion || !ReferenceEquals(lease,kvLease) || !response.IsCurrent)
// Debug.Log("CustomServerPost bodyString:" + bodyString); return null;
//#endif if(response.StatusCode==(int)HttpStatusCode.OK) return response.Body;
var content = new StringContent(bodyString, Encoding.UTF8, "application/json"); if(response.StatusCode==(int)HttpStatusCode.Unauthorized || response.StatusCode==(int)HttpStatusCode.Forbidden)
var response = await client.PostAsync(url, content);
string result = await response.Content.ReadAsStringAsync();
if (response.StatusCode == System.Net.HttpStatusCode.OK)
{ {
//GameDebug.Log("CustomServerPost result:" + result.ToString()); DisableKVBusiness();
return result; KVUnauthorized?.Invoke(SessionVersion);
} }
else if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized || response.StatusCode == System.Net.HttpStatusCode.Forbidden) else if(response.StatusCode.HasValue)
{ Debug.LogError($"API request failed: HTTP {response.StatusCode.Value}");
GContext.Publish(new game.EvtAPISvrUnauthorized());
return null; return null;
} }
catch(Exception e)
{
if(kvVersion==SessionVersion) Debug.LogError($"API request failed: {e.GetType().Name}");
return null;
}
}
pfRequests++;
var transport = client;
var version = SessionVersion;
try
{
using (var request = new HttpRequestMessage(method, url))
{
if (body != null) request.Content = new StringContent(body, Encoding.UTF8, contentType);
using (var response = await transport.SendAsync(request))
{
var result = await response.Content.ReadAsStringAsync();
if (version != SessionVersion) return null;
if (response.StatusCode == HttpStatusCode.OK) return result;
if (response.StatusCode == HttpStatusCode.Unauthorized || response.StatusCode == HttpStatusCode.Forbidden)
GContext.Publish(new game.EvtAPISvrUnauthorized { SessionVersion = version });
else else
{ Debug.LogError($"API request failed: HTTP {(int)response.StatusCode}");
Debug.LogError(response.StatusCode.ToString() + "+++CustomServerPost error:" +
result.ToString() + "+++CustomServerPost url:" + url + ":" +
bodyString);
return null; return null;
} }
} }
}
catch (Exception e) catch (Exception e)
{ {
Debug.LogError("CustomServerPost error:" + e.Message + "---CustomServerPost url:" + if (version == SessionVersion) Debug.LogError($"API request failed: {e.GetType().Name}");
url + ":" + bodyString + "\n" + e.StackTrace);
return null; return null;
} }
} }
public async Task<string> GetStringAsync(string url)
{
var response = await client.GetAsync(url);
if (response.StatusCode == HttpStatusCode.OK)
{
return await response.Content.ReadAsStringAsync();
}
else
{
string result = await response.Content.ReadAsStringAsync();
Debug.LogError(response.StatusCode.ToString() + "+++GetStringAsync error:" +
result.ToString() + "+++GetStringAsync url:" + url);
}
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
GContext.Publish(new game.EvtAPISvrUnauthorized());
}
return null;
}
public async Task<string> AccountRequest(string url, object data) public async Task<string> AccountRequest(string url, object data)
{ {
return await CustomServerPost($"account/{url}", return await CustomServerPost($"account/{url}",
@@ -177,3 +286,4 @@ public class CustomServerMgr : ICustomServerMgr
} }
} }
} }

View File

@@ -0,0 +1,278 @@
using System;
using System.Globalization;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace game.Account
{
public enum AccountLoginError
{
None,
Validation,
Http,
Rejected,
Protocol,
Network,
Timeout,
Cancelled
}
public sealed class AccountLoginResult
{
public bool Success => Error == AccountLoginError.None;
public AccountLoginError Error
{
get;
}
public int? StatusCode
{
get;
}
internal AccountLoginResult(AccountLoginError error, int? status = null)
{
Error = error;
StatusCode = status;
}
}
/// <summary>
/// Minimal Tuyou-ID authentication, independent of gameplay and PlayFab storage.
/// Each login owns a fresh cookie jar; dispose the session on logout.
/// </summary>
public sealed class GameServerAccountSession : IDisposable
{
private readonly object gate = new object();
private readonly Uri api;
private readonly Func<CookieContainer, HttpMessageHandler> handlerFactory;
private HttpClient http;
private int generation;
private bool disposed;
public bool IsAuthenticated
{
get;
private set;
}
public string PlayerId
{
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;
// KV supplies its owned transport; the PF path uses the default handler.
public GameServerAccountSession(Uri api, Func<CookieContainer, HttpMessageHandler> handlerFactory = null)
{
if (api == null || !api.IsAbsoluteUri ||
(api.Scheme != "http" && api.Scheme != "https") ||
!string.IsNullOrEmpty(api.Query) || !string.IsNullOrEmpty(api.Fragment) ||
!string.IsNullOrEmpty(api.UserInfo))
{
throw new ArgumentException("An absolute HTTP(S) API base URI is required.", nameof(api));
}
this.api = new Uri(api.AbsoluteUri.TrimEnd('/') + "/");
this.handlerFactory = handlerFactory ?? (jar => new HttpClientHandler
{
CookieContainer = jar,
UseCookies = true,
AllowAutoRedirect = false
});
}
public async Task<AccountLoginResult> LoginAsync(string tuyooId, CancellationToken token = default)
{
HttpClient client;
CookieContainer cookies;
int requestGeneration;
string id;
lock (gate)
{
if (disposed)
{
return Result(AccountLoginError.Cancelled);
}
generation++;
ClearIdentity();
http?.Dispose();
http = null;
Cookies = null;
if (string.IsNullOrEmpty(tuyooId) ||
!long.TryParse(tuyooId, NumberStyles.None, CultureInfo.InvariantCulture, out var numericId) || numericId <= 0)
{
return Result(AccountLoginError.Validation);
}
if (token.IsCancellationRequested)
{
return Result(AccountLoginError.Cancelled);
}
id = numericId.ToString(CultureInfo.InvariantCulture);
cookies = new CookieContainer();
client = new HttpClient(handlerFactory(cookies))
{
Timeout = TimeSpan.FromSeconds(15)
};
http = client;
Cookies = cookies;
requestGeneration = generation;
}
int? status = null;
try
{
using (var request = Request("account/Login", new JObject
{
["playerId"] = id
}))
using (var response = await client.SendAsync(request, token))
{
status = (int)response.StatusCode;
if (response.StatusCode != HttpStatusCode.OK)
{
return Result(AccountLoginError.Http, status);
}
var body = Parse(await response.Content.ReadAsStringAsync());
if (body["state"]?.Type != JTokenType.Integer)
{
return Result(AccountLoginError.Protocol, status);
}
if (body["state"].Value<long>() != 0)
{
return Result(AccountLoginError.Rejected, status);
}
if (body["internalId"]?.Type != JTokenType.Integer ||
!uint.TryParse(body["internalId"].ToString(), out var internalId) || internalId == 0 ||
body["newCreate"]?.Type != JTokenType.Boolean ||
!ReadUtc(body["createTime"], out var created) || !ReadUtc(body["svrTime"], out var serverTime) ||
cookies.GetCookies(api).Count == 0)
{
return Result(AccountLoginError.Protocol, status);
}
lock (gate)
{
if (disposed || generation != requestGeneration || token.IsCancellationRequested)
{
return Result(AccountLoginError.Cancelled, status);
}
PlayerId = id;
InternalId = internalId;
CreateTimeUtc = created;
ServerTimeUtc = serverTime;
NewlyCreated = body["newCreate"].Value<bool>();
IsAuthenticated = true;
}
return Result(AccountLoginError.None, status);
}
}
catch (OperationCanceledException)
{
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);
}
}
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")
};
private static JObject Parse(string text)
{
using (var reader = new JsonTextReader(new StringReader(text))
{
DateParseHandling = DateParseHandling.None
})
{
var value = JObject.Load(reader);
if (reader.Read())
{
throw new JsonReaderException("Trailing JSON content.");
}
return value;
}
}
private static bool ReadUtc(JToken value, out DateTime date)
{
date = default;
return value?.Type == JTokenType.String && DateTime.TryParse(value.Value<string>(), CultureInfo.InvariantCulture,
DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out date);
}
private static AccountLoginResult Result(AccountLoginError error, int? status = null) => new AccountLoginResult(error, status);
private void ClearIdentity()
{
IsAuthenticated = false;
PlayerId = null;
InternalId = 0;
CreateTimeUtc = default;
ServerTimeUtc = default;
NewlyCreated = false;
}
public void Dispose()
{
lock (gate)
{
if (disposed)
{
return;
}
disposed = true;
generation++;
ClearIdentity();
http?.Dispose();
http = null;
Cookies = null;
}
}
}
}

View File

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

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: f3f810de2c56488e979e3e585c94b35a
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View 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);
}
}
}
}

View File

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

View 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.
}
}
}

View File

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

View File

@@ -0,0 +1,247 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace Core.KV
{
/// <summary>
/// Private KV protocol for one fixed authenticated account/environment.
/// Caller owns the HttpClient, cookies and cancellation lifetime; never reuse
/// that transport for another account. This class neither retries nor logs payloads.
/// Production login/storage integration is deliberately outside this adapter.
/// </summary>
public sealed class KvClient
{
private readonly HttpClient http;
private readonly Uri api;
public KvClient(HttpClient http, Uri apiBaseUri)
{
this.http = http ?? throw new ArgumentNullException(nameof(http));
if (apiBaseUri == null || !apiBaseUri.IsAbsoluteUri ||
(apiBaseUri.Scheme != "http" && apiBaseUri.Scheme != "https") ||
!string.IsNullOrEmpty(apiBaseUri.UserInfo) ||
!string.IsNullOrEmpty(apiBaseUri.Query) || !string.IsNullOrEmpty(apiBaseUri.Fragment))
{
throw new ArgumentException("An absolute HTTP(S) API base URI is required.", nameof(apiBaseUri));
}
api = new Uri(apiBaseUri.AbsoluteUri.TrimEnd('/') + "/");
}
public Task<KvResult> GetAsync(CancellationToken token = default)
{
var body = new JObject();
return SendAsync("Get", body, false, token);
}
public Task<KvResult> SetAsync(IDictionary<string, string> data, CancellationToken token = default)
{
if (data == null)
{
return Invalid();
}
var snapshot = new JObject();
foreach (var pair in data)
{
if (!ValidKey(pair.Key) || pair.Value == null)
{
return Invalid();
}
snapshot.Add(pair.Key, pair.Value);
}
return SendAsync("Set", new JObject { ["data"] = snapshot }, true, token);
}
public Task<KvResult> DeleteAsync(IEnumerable<string> keys, CancellationToken token = default)
{
if (!CopyKeys(keys, out var snapshot))
{
return Invalid();
}
return SendAsync("Delete", new JObject { ["keys"] = new JArray(snapshot) }, true, token);
}
private static bool ValidKey(string key) => !string.IsNullOrEmpty(key) && key.Length <= 128;
private static bool CopyKeys(IEnumerable<string> keys, out List<string> snapshot)
{
snapshot = new List<string>();
if (keys == null)
{
return false;
}
foreach (var key in keys)
{
if (!ValidKey(key))
{
return false;
}
snapshot.Add(key);
}
return true;
}
private static Task<KvResult> Invalid() => Task.FromResult(new KvResult(KVError.Validation));
private async Task<KvResult> SendAsync(string action, JObject body, bool mutation, CancellationToken token)
{
if (token.IsCancellationRequested)
{
return new KvResult(KVError.Cancelled);
}
int? status = null;
bool enteredTransport = false;
try
{
using (var request = new HttpRequestMessage(HttpMethod.Post, new Uri(api, "playerdata/" + action)))
{
request.Content = new StringContent(body.ToString(Formatting.None), Encoding.UTF8, "application/json");
token.ThrowIfCancellationRequested();
enteredTransport = true;
using (var response = await http.SendAsync(request, token))
{
status = (int)response.StatusCode;
token.ThrowIfCancellationRequested();
if (response.StatusCode != HttpStatusCode.OK)
{
return new KvResult(status == 401 || status == 403
? KVError.Unauthorized : KVError.Http, mayHaveCommitted: mutation);
}
if (mutation)
{
return new KvResult(KVError.None);
}
var text = await response.Content.ReadAsStringAsync();
token.ThrowIfCancellationRequested();
var data = ParseData(text, out var updatedAtUtc);
return new KvResult(KVError.None, data, updatedAtUtc: updatedAtUtc);
}
}
}
catch (OperationCanceledException)
{
return new KvResult(token.IsCancellationRequested ? KVError.Cancelled : KVError.Timeout, mayHaveCommitted: mutation && enteredTransport);
}
catch (ObjectDisposedException)
{
return new KvResult(KVError.Cancelled, mayHaveCommitted: mutation && enteredTransport);
}
catch (HttpRequestException)
{
return new KvResult(KVError.Network, mayHaveCommitted: mutation && enteredTransport);
}
catch (IOException)
{
return new KvResult(KVError.Network, mayHaveCommitted: mutation && enteredTransport);
}
catch (JsonException)
{
return new KvResult(KVError.Protocol, mayHaveCommitted: mutation && enteredTransport);
}
}
private static Dictionary<string, string> ParseData(string text, out Dictionary<string, DateTime> updatedAtUtc)
{
updatedAtUtc = null;
using (var reader = new JsonTextReader(new StringReader(text)) { DateParseHandling = DateParseHandling.None })
{
var body = JObject.Load(reader, new JsonLoadSettings { DuplicatePropertyNameHandling = DuplicatePropertyNameHandling.Error });
if (reader.Read() || !(body["data"] is JObject data))
{
throw new JsonReaderException("Invalid KV response.");
}
var result = new Dictionary<string, string>(StringComparer.Ordinal);
foreach (var property in data.Properties())
{
if (property.Value.Type != JTokenType.String)
{
throw new JsonReaderException("KV value must be a string.");
}
result.Add(property.Name, property.Value.Value<string>());
}
var timestamps = body.Property("updatedAtUtc");
if (timestamps != null)
{
if (!(timestamps.Value is JObject times) || times.Count != result.Count)
{
throw new JsonReaderException("Invalid KV timestamp metadata.");
}
updatedAtUtc = new Dictionary<string, DateTime>(StringComparer.Ordinal);
var formats = new[] { "yyyy-MM-dd'T'HH:mm:ss'Z'", "yyyy-MM-dd'T'HH:mm:ss.FFFFFFF'Z'" };
foreach (var property in times.Properties())
{
if (!result.ContainsKey(property.Name) || property.Value.Type != JTokenType.String)
{
throw new JsonReaderException("Invalid KV timestamp entry.");
}
var value = property.Value.Value<string>();
if ((value.Length != 20 && (value.Length < 22 || value.Length > 28)) ||
!DateTime.TryParseExact(value, formats, CultureInfo.InvariantCulture,
DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var timestamp))
{
throw new JsonReaderException("KV timestamp must be UTC ISO 8601.");
}
updatedAtUtc.Add(property.Name, timestamp);
}
}
return result;
}
}
}
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;
}
}
}

View File

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

View 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);
}
}
}

View File

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

View File

@@ -0,0 +1,671 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Core.KV
{
public enum KVTicketStatus
{
Pending,
Confirmed,
Superseded,
Blocked,
Unknown,
Cancelled
}
public sealed class KvTicketResult
{
public KVTicketStatus Status
{
get;
}
public KVError TransportError
{
get;
}
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>
public sealed class KvSyncCoordinator : IDisposable
{
private sealed class TicketRecord
{
internal KvTicketResult Result;
internal bool Dispatched;
}
private readonly object ticketGate = new object();
private readonly Dictionary<long, TicketRecord> tickets = new Dictionary<long, TicketRecord>();
private readonly SemaphoreSlim dispatch = new SemaphoreSlim(1, 1);
private readonly KvSyncState state;
private readonly KvClient client;
private readonly System.Net.Http.HttpClient http;
private readonly Func<bool> isOwnerCurrent;
private readonly CancellationToken ownerToken;
private bool disposed;
private bool ordinaryDraining;
private TaskCompletionSource<(KVError Error, Dictionary<long, KvTicketResult> Results)> ordinaryDrainCompletion;
internal KvSyncCoordinator(KvSyncState state, KvClient client,
System.Net.Http.HttpClient http, Func<bool> isOwnerCurrent,
CancellationToken ownerToken)
{
this.state = state;
this.client = client;
this.http = http;
this.isOwnerCurrent = isOwnerCurrent;
this.ownerToken = ownerToken;
}
internal async Task<KvResult> InitializeAsync()
{
await dispatch.WaitAsync(ownerToken);
try
{
if (!Current())
{
return CancelledResult();
}
if (state.HasPendingComparison)
{
return new KvResult(KVError.Protocol);
}
var result = await client.GetAsync(ownerToken);
if (!Current())
{
return CancelledResult();
}
if (result.Success)
{
var reconciled = state.Reconcile(result.Data, result.UpdatedAtUtc);
return reconciled.Success ? result : new KvResult(KVError.Protocol);
}
state.RecordReadFailure(result.Error == KVError.Unauthorized);
return result;
}
finally
{
dispatch.Release();
}
}
internal KvStateResult CompareDatas(long nowTicks)
{
lock (ticketGate)
{
if (!Current())
{
return new KvStateResult(KVChangeError.Blocked);
}
var previous = Unsent(state.Inspect()).Select(item => item.Sequence).ToArray();
var result = state.CompareDatas(nowTicks);
if (result.Success)
{
CompleteRemovedSources(previous);
}
return result;
}
}
internal KvStateResult Update(string key, string value, bool remoteStore, long updateTicks)
{
lock (ticketGate)
{
var previous = Unsent(state.Inspect()).Select(item => item.Sequence).ToArray();
var result = state.Update(key, value, remoteStore, updateTicks);
if (!result.Success)
{
return result;
}
CompleteRemovedSources(previous);
if (!remoteStore || !Unsent(state.Inspect()).Any(item => item.Sequence == result.AttemptSequence))
{
return result;
}
tickets.Add(result.AttemptSequence, new TicketRecord());
return result;
}
}
private static IEnumerable<KvChange> Unsent(KvDiagnosticSnapshot value)
{
return value.OrdinaryBuffers.SelectMany(bucket => bucket.Values)
.Concat(value.LegacyPending.Values).Concat(value.ExplicitDeletes.Values);
}
private void CompleteRemovedSources(IEnumerable<long> previous)
{
var remaining = new HashSet<long>(Unsent(state.Inspect()).Select(item => item.Sequence));
foreach (var sequence in previous)
{
if (!remaining.Contains(sequence) && tickets.TryGetValue(sequence, out var record) &&
!record.Dispatched && record.Result == null)
{
Complete(record, KVTicketStatus.Superseded, KVError.None);
}
}
}
internal Task<bool> SyncDataImmidiate(string key, string value)
{
if (!KvSnapshot.ValidKey(key) || value == null)
{
return Task.FromResult(false);
}
return SyncDataImmidiate(new Dictionary<string, string> { [key] = value });
}
internal async Task<bool> SyncDataImmidiate(IReadOnlyDictionary<string, string> values)
{
if (values == null || values.Count == 0)
{
return false;
}
var data = values.ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.Ordinal);
IReadOnlyList<KvChange> operations;
lock (ticketGate)
{
var previous = Unsent(state.Inspect()).Select(item => item.Sequence).ToArray();
if (!Current() || !state.PrepareImmediate(data, out operations).Success)
{
return false;
}
CompleteRemovedSources(previous);
}
var acquired = false;
var dispatched = false;
try
{
await dispatch.WaitAsync(ownerToken);
acquired = true;
if (!Current() || state.Status != KVSyncStatus.Ready)
{
return false;
}
dispatched = true;
var result = await client.SetAsync(data, ownerToken);
if (!Current())
{
return false;
}
if (result.Success)
{
return state.CompleteImmediate(operations).Success;
}
if (result.Error == KVError.Unauthorized)
{
state.RecordReadFailure(true);
}
return false;
}
catch (Exception)
{
return false;
}
finally
{
if (!dispatched)
{
state.CancelUndispatchedImmediate(operations);
}
if (acquired)
{
dispatch.Release();
}
}
}
private static IEnumerable<KvChange> Outstanding(KvDiagnosticSnapshot snapshot)
{
return Unsent(snapshot).Concat(snapshot.PreparedOrdinaryBatches.SelectMany(batch => batch.Items))
.Concat(snapshot.OrdinaryUnknownBatches.SelectMany(batch => batch.Items))
.Concat(snapshot.ActiveBatch == null ? Enumerable.Empty<KvChange>() : snapshot.ActiveBatch.Items)
.Concat(snapshot.UnknownBatch == null ? Enumerable.Empty<KvChange>() : snapshot.UnknownBatch.Items);
}
// This channel sends one imported legacy batch, never ordinary buckets.
internal async Task<KvFlushResult> FlushThroughAsync(long watermark, long[] targetSequences)
{
var error = KVError.None;
var observed = new Dictionary<long, KVTicketStatus>();
var acquired = false;
try
{
await dispatch.WaitAsync(ownerToken);
acquired = true;
var sent = 0;
while (Current() && sent < 1)
{
KvStateResult begin;
KvBatch batch;
lock (ticketGate)
{
begin = state.BeginBatch(watermark, out batch);
if (begin.Success && batch != null)
{
MarkDispatched(batch);
}
}
if (!begin.Success)
{
MarkExplicitBlocked(watermark, KVError.None, observed);
break;
}
if (batch == null)
{
break;
}
sent++;
var result = await SendAsync(batch);
if (!Current() && result.Success)
{
result = new KvResult(KVError.Cancelled, mayHaveCommitted: true);
}
if (result.Success)
{
var confirmed = state.ConfirmBatch(batch.Id);
var outcome = confirmed.Success ? KVTicketStatus.Confirmed : KVTicketStatus.Unknown;
RecordBatch(batch, outcome, KVError.None, observed);
if (!confirmed.Success)
{
break;
}
continue;
}
error = result.Error;
state.FailBatch(batch.Id, result.MayHaveCommitted, result.Error == KVError.Unauthorized);
var failedStatus = result.MayHaveCommitted ? KVTicketStatus.Unknown :
ownerToken.IsCancellationRequested ? KVTicketStatus.Cancelled : KVTicketStatus.Blocked;
RecordBatch(batch, failedStatus, result.Error, observed);
break;
}
if (!Current())
{
error = KVError.Cancelled;
CloseTickets();
}
}
catch (OperationCanceledException)
{
error = KVError.Cancelled;
CloseTickets();
}
catch (Exception)
{
error = KVError.Protocol;
MarkExplicitBlocked(watermark, error, observed);
}
finally
{
if (acquired)
{
dispatch.Release();
}
}
return Summary(targetSequences ?? Array.Empty<long>(), error, observed);
}
internal Task<KvFlushResult> SyncDataAsync()
{
TaskCompletionSource<(KVError Error, Dictionary<long, KvTicketResult> Results)> completion;
bool start;
long[] targets;
lock (ticketGate)
{
var snapshot = state.Inspect();
if (!Current())
{
return Task.FromResult(new KvFlushResult(1, 0, KVError.Cancelled));
}
if (snapshot.LegacyPending.Count != 0)
{
var watermark = snapshot.NextSequence - 1;
return FlushThroughAsync(watermark, snapshot.LegacyPending.Values.Select(item => item.Sequence).ToArray());
}
var captured = state.CaptureOrdinaryBatch(out var batch);
if (!captured.Success)
{
foreach (var item in snapshot.OrdinaryBuffers[0].Values)
{
if (tickets.TryGetValue(item.Sequence, out var record))
{
Complete(record, KVTicketStatus.Blocked, KVError.None);
}
}
return Task.FromResult(new KvFlushResult(1, 0, KVError.None));
}
snapshot = state.Inspect();
if (batch == null && ordinaryDraining)
{
// The existing caller owns the queued work; an empty tick has no new result to observe.
return Task.FromResult(Summary(Array.Empty<long>(), KVError.None));
}
targets = batch != null ? batch.Items.Select(item => item.Sequence).ToArray() :
snapshot.PreparedOrdinaryBatches.SelectMany(item => item.Items).Select(item => item.Sequence).ToArray();
if (snapshot.PreparedOrdinaryBatches.Count == 0)
{
return Task.FromResult(Summary(targets, KVError.None));
}
start = !ordinaryDraining;
if (start)
{
ordinaryDraining = true;
ordinaryDrainCompletion = new TaskCompletionSource<(KVError Error, Dictionary<long, KvTicketResult> Results)>(TaskCreationOptions.RunContinuationsAsynchronously);
}
completion = ordinaryDrainCompletion;
}
if (start)
{
// The returned task waits for this actual worker, so the owner's lease covers its finally.
_ = DrainOrdinaryAsync(completion);
}
return ObserveOrdinaryAsync(completion.Task, targets);
}
private async Task<KvFlushResult> ObserveOrdinaryAsync(Task<(KVError Error, Dictionary<long, KvTicketResult> Results)> worker, long[] targets)
{
var completed = await worker;
var error = targets.Where(completed.Results.ContainsKey).Select(sequence => completed.Results[sequence].TransportError)
.FirstOrDefault(value => value != KVError.None);
if (error == KVError.None && targets.Any(sequence => !completed.Results.ContainsKey(sequence)))
{
error = completed.Error;
}
return Summary(targets, error, completed.Results.ToDictionary(pair => pair.Key, pair => pair.Value.Status));
}
private async Task DrainOrdinaryAsync(TaskCompletionSource<(KVError Error, Dictionary<long, KvTicketResult> Results)> completion)
{
var error = KVError.None;
var observed = new Dictionary<long, KvTicketResult>();
void RecordOrdinaryBatch(KvBatch batch, KVTicketStatus status, KVError transportError)
{
CompleteBatch(batch, status, transportError);
foreach (var item in batch.Items)
{
observed[item.Sequence] = new KvTicketResult(status, transportError);
}
}
KvBatch active = null;
var acquired = false;
var dispatched = false;
try
{
while (Current())
{
await dispatch.WaitAsync(ownerToken);
acquired = true;
KvStateResult begin;
lock (ticketGate)
{
begin = state.BeginOrdinaryBatch(out active);
if (!begin.Success || active == null)
{
// Enqueue and the end-of-drain decision share this lock; no Task.IsCompleted race.
ordinaryDraining = false;
if (!begin.Success)
{
foreach (var item in state.Inspect().PreparedOrdinaryBatches.SelectMany(batch => batch.Items))
{
if (tickets.TryGetValue(item.Sequence, out var record))
{
Complete(record, KVTicketStatus.Blocked, KVError.None);
}
}
}
break;
}
MarkDispatched(active);
}
dispatched = true;
var result = await SendAsync(active);
if (!Current() && result.Success)
{
result = new KvResult(KVError.Cancelled, mayHaveCommitted: true);
}
if (result.Success)
{
var confirmed = state.ConfirmBatch(active.Id);
RecordOrdinaryBatch(active, confirmed.Success ? KVTicketStatus.Confirmed : KVTicketStatus.Unknown, KVError.None);
if (!confirmed.Success)
{
break;
}
}
else
{
error = result.Error;
var failed = state.FailBatch(active.Id, result.MayHaveCommitted, result.Error == KVError.Unauthorized);
RecordOrdinaryBatch(active, result.MayHaveCommitted ? KVTicketStatus.Unknown :
ownerToken.IsCancellationRequested ? KVTicketStatus.Cancelled : KVTicketStatus.Blocked, result.Error);
if (!failed.Success || state.Status != KVSyncStatus.Ready)
{
break;
}
}
active = null;
dispatched = false;
dispatch.Release();
acquired = false;
}
}
catch (OperationCanceledException)
{
error = KVError.Cancelled;
if (active != null)
{
state.FailBatch(active.Id, dispatched);
RecordOrdinaryBatch(active, dispatched ? KVTicketStatus.Unknown : KVTicketStatus.Cancelled, error);
}
}
catch (Exception)
{
error = KVError.Protocol;
if (active != null)
{
state.FailBatch(active.Id, dispatched);
RecordOrdinaryBatch(active, dispatched ? KVTicketStatus.Unknown : KVTicketStatus.Blocked, error);
}
}
finally
{
if (acquired)
{
dispatch.Release();
}
lock (ticketGate)
{
if (ReferenceEquals(ordinaryDrainCompletion, completion))
{
ordinaryDraining = false;
ordinaryDrainCompletion = null;
}
try
{
if (!Current())
{
CloseTickets();
}
}
catch (Exception)
{
error = KVError.Protocol;
}
}
completion.TrySetResult((error, observed));
}
}
private Task<KvResult> SendAsync(KvBatch batch)
{
if (batch.IsDelete)
{
return client.DeleteAsync(batch.Items.Select(item => item.Key), ownerToken);
}
return client.SetAsync(batch.Items.ToDictionary(item => item.Key, item => item.Target.Value, StringComparer.Ordinal), ownerToken);
}
private void RecordBatch(KvBatch batch, KVTicketStatus status, KVError error,
Dictionary<long, KVTicketStatus> observed)
{
CompleteBatch(batch, status, error);
foreach (var item in batch.Items)
{
observed[item.Sequence] = status;
}
}
private void MarkExplicitBlocked(long watermark, KVError error,
Dictionary<long, KVTicketStatus> observed)
{
lock (ticketGate)
{
var snapshot = state.Inspect();
var source = snapshot.LegacyPending.Values;
foreach (var item in source.Where(item => item.Sequence <= watermark))
{
var outcome = KVTicketStatus.Blocked;
if (tickets.TryGetValue(item.Sequence, out var record))
{
outcome = record.Dispatched ? KVTicketStatus.Unknown : KVTicketStatus.Blocked;
Complete(record, outcome, error);
}
observed[item.Sequence] = outcome;
}
}
}
internal void CloseTickets()
{
lock (ticketGate)
{
foreach (var record in tickets.Values)
{
if (record.Result == null)
{
Complete(record, record.Dispatched ? KVTicketStatus.Unknown : KVTicketStatus.Cancelled,
KVError.Cancelled);
}
}
}
}
private void MarkDispatched(KvBatch batch)
{
lock (ticketGate)
foreach (var item in batch.Items)
if (tickets.TryGetValue(item.Sequence, out var record))
{
record.Dispatched = true;
}
}
private void CompleteBatch(KvBatch batch, KVTicketStatus status, KVError error)
{
lock (ticketGate)
foreach (var item in batch.Items)
if (tickets.TryGetValue(item.Sequence, out var record))
{
Complete(record, status, error);
}
}
private void Complete(TicketRecord record, KVTicketStatus status, KVError error)
{
if (record.Result == null)
{
record.Result = new KvTicketResult(status, error);
}
}
private KvFlushResult Summary(IEnumerable<long> targetSequences, KVError error,
Dictionary<long, KVTicketStatus> observed = null)
{
lock (ticketGate)
{
var counts = new Dictionary<KVTicketStatus, int>();
var snapshot = state.Inspect();
var unknown = new HashSet<long>(snapshot.OrdinaryUnknownBatches.SelectMany(batch => batch.Items).Select(item => item.Sequence));
if (snapshot.UnknownBatch != null)
{
unknown.UnionWith(snapshot.UnknownBatch.Items.Select(item => item.Sequence));
}
var outstanding = new HashSet<long>(Outstanding(snapshot).Select(item => item.Sequence));
foreach (var sequence in targetSequences.Distinct())
{
KVTicketStatus outcome;
if (tickets.TryGetValue(sequence, out var record) && record.Result != null)
{
outcome = record.Result.Status;
}
else if (observed != null && observed.TryGetValue(sequence, out var actual))
{
outcome = actual;
}
else if (unknown.Contains(sequence))
{
outcome = KVTicketStatus.Unknown;
}
else if (outstanding.Contains(sequence) || tickets.ContainsKey(sequence))
{
outcome = KVTicketStatus.Pending;
}
else
{
continue;
}
counts[outcome] = counts.TryGetValue(outcome, out var count) ? count + 1 : 1;
}
int Count(KVTicketStatus status)
{
return counts.TryGetValue(status, out var count) ? count : 0;
}
return new KvFlushResult(Count(KVTicketStatus.Blocked) + Count(KVTicketStatus.Cancelled), Count(KVTicketStatus.Unknown), error);
}
}
private bool Current() => !ownerToken.IsCancellationRequested && isOwnerCurrent();
private static KvResult CancelledResult() => new KvResult(KVError.Cancelled);
public void Dispose()
{
if (disposed)
{
return;
}
disposed = true;
http.Dispose();
dispatch.Dispose();
}
}
}

View File

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

View File

@@ -0,0 +1,665 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace Core.KV
{
public enum KVSyncStatus
{
NotLoaded,
Ready,
ReadFailed,
Conflict,
UnknownOutcome,
Unauthorized,
StorageError,
Closed
}
public enum KVOpenError
{
None,
Validation,
Busy,
StorageError,
RecoveryRequired
}
public enum KVChangeError
{
None,
Validation,
Blocked,
StorageError
}
public sealed class KvStateResult
{
public bool Success => Error == KVChangeError.None;
public KVChangeError Error
{
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.
public sealed class KvValue : IEquatable<KvValue>
{
public static readonly KvValue Missing = new KvValue(false, null);
public bool Exists
{
get;
}
public string Value
{
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;
internal static KvValue At(IReadOnlyDictionary<string, string> data, string key) =>
data.TryGetValue(key, out var value) ? Present(value) : Missing;
}
public sealed class KvChange
{
public string Key
{
get;
}
public long Sequence
{
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 KvBatch
{
public string Id
{
get;
}
public IReadOnlyList<KvChange> Items
{
get;
}
public bool IsDelete => !Items[0].Target.Exists;
public bool IsOrdinary
{
get;
}
internal KvBatch(string id, IEnumerable<KvChange> items, bool ordinary = false)
{
Id = id;
Items = Array.AsReadOnly(items.ToArray());
IsOrdinary = ordinary;
}
}
public sealed class KvDiagnosticSnapshot
{
public long NextSequence
{
get;
}
public KvBatch ActiveBatch
{
get;
}
public KvBatch UnknownBatch
{
get;
}
public IReadOnlyList<IReadOnlyDictionary<string, KvChange>> OrdinaryBuffers
{
get;
}
public IReadOnlyList<KvBatch> PreparedOrdinaryBatches
{
get;
}
public IReadOnlyList<KvBatch> OrdinaryUnknownBatches
{
get;
}
public IReadOnlyDictionary<string, KvChange> LegacyPending
{
get;
}
public IReadOnlyDictionary<string, KvChange> ExplicitDeletes
{
get;
}
internal KvDiagnosticSnapshot(KvSnapshot source)
{
NextSequence = source.NextSequence;
ActiveBatch = source.Active;
UnknownBatch = source.Unknown;
OrdinaryBuffers = Array.AsReadOnly(source.OrdinaryBuffers.Select(bucket => ReadOnly(bucket)).ToArray());
PreparedOrdinaryBatches = Array.AsReadOnly(source.PreparedOrdinaryBatches.ToArray());
OrdinaryUnknownBatches = Array.AsReadOnly(source.OrdinaryUnknownBatches.ToArray());
LegacyPending = ReadOnly(source.LegacyPending);
ExplicitDeletes = ReadOnly(source.ExplicitDeletes);
}
private static IReadOnlyDictionary<string, T> ReadOnly<T>(Dictionary<string, T> source) =>
new ReadOnlyDictionary<string, T>(new Dictionary<string, T>(source, StringComparer.Ordinal));
}
internal sealed class KvIdentity
{
internal readonly string Api, PlayerId, DirectoryPath;
internal KvIdentity(string root, Uri api, string playerId)
{
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 ||
!long.TryParse(playerId, NumberStyles.None, CultureInfo.InvariantCulture, out var id) || id <= 0)
{
throw new ArgumentException("Invalid KV cache identity.");
}
Api = api.AbsoluteUri.TrimEnd('/') + "/";
PlayerId = id.ToString(CultureInfo.InvariantCulture);
using (var hash = SHA256.Create())
{
var bytes = hash.ComputeHash(Encoding.UTF8.GetBytes(Api));
var fingerprint = string.Concat(bytes.Select(b => b.ToString("x2", CultureInfo.InvariantCulture)));
DirectoryPath = Path.Combine(Path.GetFullPath(root), "player-kv", "v1", fingerprint, PlayerId);
}
}
}
internal sealed class KvSnapshot
{
internal long Revision, NextSequence = 1;
internal bool HasBaseline, AuthenticationInvalid;
internal Dictionary<string, string> Baseline = new Dictionary<string, string>(StringComparer.Ordinal);
internal Dictionary<string, string> LocalData = new Dictionary<string, string>(StringComparer.Ordinal);
internal Dictionary<string, long> LocalUpdateTicks = new Dictionary<string, long>(StringComparer.Ordinal);
// A local origin has no second copy of its value. Ordinary origins can outlive a sent batch.
internal Dictionary<string, (bool LocalOnly, long Sequence)> LocalOrigins =
new Dictionary<string, (bool LocalOnly, long Sequence)>(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.
internal Dictionary<string, string> LegacyLocalOnlyUnderImmediate = new Dictionary<string, string>(StringComparer.Ordinal);
internal List<Dictionary<string, KvChange>> OrdinaryBuffers = Enumerable.Range(0, 5)
.Select(_ => new Dictionary<string, KvChange>(StringComparer.Ordinal)).ToList();
internal List<KvBatch> PreparedOrdinaryBatches = new List<KvBatch>();
internal List<KvBatch> OrdinaryUnknownBatches = new List<KvBatch>();
internal Dictionary<string, KvChange> LegacyPending = new Dictionary<string, KvChange>(StringComparer.Ordinal);
internal Dictionary<string, KvChange> ExplicitDeletes = new Dictionary<string, KvChange>(StringComparer.Ordinal);
internal Dictionary<string, KvValue> Conflicts = new Dictionary<string, KvValue>(StringComparer.Ordinal);
internal Dictionary<long, KvChange> RemoteOnlyWrites = new Dictionary<long, KvChange>();
internal KvBatch Active, Unknown;
internal IEnumerable<KvChange> Unsent()
{
return OrdinaryBuffers.SelectMany(bucket => bucket.Values).Concat(LegacyPending.Values).Concat(ExplicitDeletes.Values);
}
internal KvSnapshot Clone()
{
return new KvSnapshot
{
Revision = Revision, NextSequence = NextSequence, HasBaseline = HasBaseline, AuthenticationInvalid = AuthenticationInvalid,
Baseline = new Dictionary<string, string>(Baseline, StringComparer.Ordinal),
LocalData = new Dictionary<string, string>(LocalData, StringComparer.Ordinal),
LocalUpdateTicks = new Dictionary<string, long>(LocalUpdateTicks, StringComparer.Ordinal),
LocalOrigins = new Dictionary<string, (bool LocalOnly, long Sequence)>(LocalOrigins, StringComparer.Ordinal),
ImmediatePreservedKeys = new HashSet<string>(ImmediatePreservedKeys, StringComparer.Ordinal),
LegacyLocalOnlyUnderImmediate = new Dictionary<string, string>(LegacyLocalOnlyUnderImmediate, StringComparer.Ordinal),
OrdinaryBuffers = OrdinaryBuffers.Select(bucket => new Dictionary<string, KvChange>(bucket, StringComparer.Ordinal)).ToList(),
PreparedOrdinaryBatches = new List<KvBatch>(PreparedOrdinaryBatches),
OrdinaryUnknownBatches = new List<KvBatch>(OrdinaryUnknownBatches),
LegacyPending = new Dictionary<string, KvChange>(LegacyPending, StringComparer.Ordinal),
ExplicitDeletes = new Dictionary<string, KvChange>(ExplicitDeletes, StringComparer.Ordinal),
Conflicts = new Dictionary<string, KvValue>(Conflicts, StringComparer.Ordinal),
Active = Active, Unknown = Unknown,
RemoteOnlyWrites = new Dictionary<long, KvChange>(RemoteOnlyWrites)
};
}
internal static bool ValidKey(string key) => !string.IsNullOrEmpty(key) && key.Length <= 128;
internal string Encode(KvIdentity identity)
{
var conflicts = new JObject();
foreach (var pair in Conflicts)
{
conflicts.Add(pair.Key, ValueJson(pair.Value));
}
var origins = new JObject();
foreach (var pair in LocalOrigins)
{
origins.Add(pair.Key, new JObject { ["localOnly"] = pair.Value.LocalOnly, ["sequence"] = pair.Value.Sequence });
}
return new JObject
{
["formatVersion"] = 3, ["apiBaseUri"] = identity.Api, ["playerId"] = identity.PlayerId,
["revision"] = Revision, ["nextSequence"] = NextSequence, ["hasBaseline"] = HasBaseline,
["authenticationInvalid"] = AuthenticationInvalid, ["baseline"] = JObject.FromObject(Baseline),
["localData"] = JObject.FromObject(LocalData), ["localUpdateTicks"] = JObject.FromObject(LocalUpdateTicks),
["localOrigins"] = origins, ["immediatePreservedKeys"] = new JArray(ImmediatePreservedKeys),
["legacyLocalOnlyUnderImmediate"] = JObject.FromObject(LegacyLocalOnlyUnderImmediate),
["ordinaryBuffers"] = new JArray(OrdinaryBuffers.Select(bucket => (JToken)ChangesJson(bucket.Values))),
["preparedOrdinaryBatches"] = new JArray(PreparedOrdinaryBatches.Select(BatchJson)),
["ordinaryUnknownBatches"] = new JArray(OrdinaryUnknownBatches.Select(BatchJson)),
["legacyPending"] = ChangesJson(LegacyPending.Values), ["explicitDeletes"] = ChangesJson(ExplicitDeletes.Values),
["conflictObservations"] = conflicts, ["activeBatch"] = BatchJson(Active), ["unknownBatch"] = BatchJson(Unknown),
["remoteOnlyWrites"] = ChangesJson(RemoteOnlyWrites.Values)
}.ToString(Formatting.None);
}
private static JArray ChangesJson(IEnumerable<KvChange> values)
{
return new JArray(values.OrderBy(item => item.Sequence).Select(ChangeJson));
}
private static JObject ValueJson(KvValue value)
{
return new JObject { ["exists"] = value.Exists, ["value"] = value.Value };
}
private static JObject ChangeJson(KvChange item)
{
return new JObject { ["key"] = item.Key, ["sequence"] = item.Sequence,
["base"] = ValueJson(item.BaseValue), ["target"] = ValueJson(item.Target) };
}
private static JToken BatchJson(KvBatch batch)
{
if (batch == null)
{
return JValue.CreateNull();
}
return new JObject { ["id"] = batch.Id, ["ordinary"] = batch.IsOrdinary, ["items"] = ChangesJson(batch.Items) };
}
internal static KvSnapshot Decode(string text, KvIdentity identity)
{
JObject root;
using (var reader = new JsonTextReader(new StringReader(text)) { DateParseHandling = DateParseHandling.None })
{
root = JObject.Load(reader, new JsonLoadSettings { DuplicatePropertyNameHandling = DuplicatePropertyNameHandling.Error });
if (reader.Read())
{
throw Invalid();
}
}
var version = Integer(root, "formatVersion");
if (version < 1 || version > 3 || String(root, "apiBaseUri") != identity.Api || String(root, "playerId") != identity.PlayerId)
{
throw Invalid();
}
var result = new KvSnapshot
{
Revision = Integer(root, "revision"), NextSequence = Integer(root, "nextSequence"),
HasBaseline = Boolean(root, "hasBaseline"), AuthenticationInvalid = Boolean(root, "authenticationInvalid"),
Baseline = Strings(Field(root, "baseline", JTokenType.Object)),
Active = Batch(root, "activeBatch", version == 3), Unknown = Batch(root, "unknownBatch", version == 3)
};
foreach (var property in ((JObject)Field(root, "conflictObservations", JTokenType.Object)).Properties())
{
if (!ValidKey(property.Name))
{
throw Invalid();
}
result.Conflicts.Add(property.Name, Value(property.Value));
}
if (version >= 2)
{
foreach (var item in Field(root, "remoteOnlyWrites", JTokenType.Array))
{
var change = Change(item);
if (!change.Target.Exists || result.RemoteOnlyWrites.ContainsKey(change.Sequence))
{
throw Invalid();
}
result.RemoteOnlyWrites.Add(change.Sequence, change);
}
}
if (version < 3)
{
DecodeLegacy(root, version, result);
}
else
{
result.LocalData = Strings(Field(root, "localData", JTokenType.Object));
result.LegacyLocalOnlyUnderImmediate = Strings(Field(root, "legacyLocalOnlyUnderImmediate", JTokenType.Object));
foreach (var property in ((JObject)Field(root, "localUpdateTicks", JTokenType.Object)).Properties())
{
if (!ValidKey(property.Name) || property.Value.Type != JTokenType.Integer)
{
throw Invalid();
}
result.LocalUpdateTicks.Add(property.Name, (long)property.Value);
}
foreach (var property in ((JObject)Field(root, "localOrigins", JTokenType.Object)).Properties())
{
if (!ValidKey(property.Name))
{
throw Invalid();
}
result.LocalOrigins.Add(property.Name, (Boolean(property.Value, "localOnly"), Integer(property.Value, "sequence")));
}
foreach (var key in Field(root, "immediatePreservedKeys", JTokenType.Array))
{
if (key.Type != JTokenType.String || !ValidKey((string)key) || !result.ImmediatePreservedKeys.Add((string)key))
{
throw Invalid();
}
}
result.OrdinaryBuffers = Field(root, "ordinaryBuffers", JTokenType.Array).Select(Changes).ToList();
result.LegacyPending = Changes(Field(root, "legacyPending", JTokenType.Array));
result.ExplicitDeletes = Changes(Field(root, "explicitDeletes", JTokenType.Array));
result.PreparedOrdinaryBatches = ReadBatches(root, "preparedOrdinaryBatches");
result.OrdinaryUnknownBatches = ReadBatches(root, "ordinaryUnknownBatches");
}
result.Validate();
return result;
}
private static Dictionary<string, KvChange> Changes(JToken array)
{
if (array.Type != JTokenType.Array)
{
throw Invalid();
}
var values = new Dictionary<string, KvChange>(StringComparer.Ordinal);
foreach (var token in array)
{
var item = Change(token);
if (values.ContainsKey(item.Key))
{
throw Invalid();
}
values.Add(item.Key, item);
}
return values;
}
private static List<KvBatch> ReadBatches(JToken root, string name)
{
var result = new List<KvBatch>();
foreach (var item in Field(root, name, JTokenType.Array))
{
var wrapper = new JObject { ["batch"] = item.DeepClone() };
var batch = Batch(wrapper, "batch", true);
if (batch == null || !batch.IsOrdinary || batch.IsDelete)
{
throw Invalid();
}
result.Add(batch);
}
return result;
}
private static void DecodeLegacy(JObject root, long version, KvSnapshot result)
{
var pending = Changes(Field(root, "pending", JTokenType.Array));
var localOnly = Strings(Field(root, "localOnly", JTokenType.Object));
var overrides = new Dictionary<string, KvValue>(StringComparer.Ordinal);
if (version == 2)
{
foreach (var property in ((JObject)Field(root, "remoteLocalOverrides", JTokenType.Object)).Properties())
{
if (!ValidKey(property.Name))
{
throw Invalid();
}
overrides.Add(property.Name, Value(property.Value));
}
}
// Validate the old shape before converting; new format rules must not accept a corrupt old file.
if (result.Revision < 1 || result.NextSequence < 1 || (result.Active != null && result.Unknown != null) ||
(!result.HasBaseline && (result.Baseline.Count != 0 || localOnly.Count != 0 || pending.Count != 0 ||
result.Active != null || result.Unknown != null || overrides.Count != 0 || result.RemoteOnlyWrites.Count != 0)))
{
throw Invalid();
}
var sequences = new HashSet<long>();
foreach (var item in pending.Values.Concat(result.RemoteOnlyWrites.Values))
{
if (item.Sequence < 1 || item.Sequence >= result.NextSequence || !sequences.Add(item.Sequence))
{
throw Invalid();
}
}
if (result.Conflicts.Keys.Any(key => !pending.ContainsKey(key)))
{
throw Invalid();
}
var oldBatch = result.Active ?? result.Unknown;
if (oldBatch != null)
{
foreach (var item in oldBatch.Items)
{
var hasPending = pending.TryGetValue(item.Key, out var latest);
var hasSource = result.RemoteOnlyWrites.Values.Any(source => source.Key == item.Key && source.Sequence > item.Sequence);
var same = hasPending && latest.Sequence == item.Sequence && latest.BaseValue.Equals(item.BaseValue) && latest.Target.Equals(item.Target);
if (item.Sequence < 1 || item.Sequence >= result.NextSequence || (sequences.Contains(item.Sequence) && !same) ||
(!hasPending && !hasSource) || (hasPending && (latest.Sequence < item.Sequence || !latest.BaseValue.Equals(item.BaseValue) ||
(latest.Sequence == item.Sequence && !same))))
{
throw Invalid();
}
sequences.Add(item.Sequence);
}
}
result.LegacyPending = pending;
result.LocalData = new Dictionary<string, string>(result.Baseline, StringComparer.Ordinal);
foreach (var item in pending.Values)
{
ApplyValue(result.LocalData, item.Key, item.Target);
result.LocalOrigins[item.Key] = (false, item.Sequence);
}
foreach (var pair in localOnly)
{
result.LocalData[pair.Key] = pair.Value;
// Old local-only writes had no stored sequence; zero is permitted only for this converted origin.
result.LocalOrigins[pair.Key] = (true, 0);
}
foreach (var pair in overrides)
{
ApplyValue(result.LocalData, pair.Key, pair.Value);
result.ImmediatePreservedKeys.Add(pair.Key);
if (localOnly.TryGetValue(pair.Key, out var lower))
{
result.LegacyLocalOnlyUnderImmediate[pair.Key] = lower;
}
}
}
private static void ApplyValue(Dictionary<string, string> values, string key, KvValue value)
{
if (value.Exists)
{
values[key] = value.Value;
}
else
{
values.Remove(key);
}
}
internal void Validate()
{
if (Revision < 1 || NextSequence < 1 || OrdinaryBuffers.Count != 5 ||
(Active != null && Unknown != null) || (Unknown != null && Unknown.IsOrdinary) ||
OrdinaryBuffers.Any(bucket => bucket.Count > 10 || bucket.Values.Any(item => !item.Target.Exists)) ||
ExplicitDeletes.Values.Any(item => item.Target.Exists) ||
(!HasBaseline && (Baseline.Count != 0 || LocalData.Count != 0 || Unsent().Any() || Active != null || Unknown != null ||
PreparedOrdinaryBatches.Count != 0 || OrdinaryUnknownBatches.Count != 0 || RemoteOnlyWrites.Count != 0 ||
LocalOrigins.Count != 0 || LocalUpdateTicks.Count != 0 || ImmediatePreservedKeys.Count != 0 || LegacyLocalOnlyUnderImmediate.Count != 0)))
{
throw Invalid();
}
var sequences = new Dictionary<long, KvChange>();
void Add(KvChange item, bool sourceReference = false)
{
if (!ValidKey(item.Key) || item.Sequence < 1 || item.Sequence >= NextSequence)
{
throw Invalid();
}
if (sequences.TryGetValue(item.Sequence, out var prior))
{
if (!sourceReference || prior.Key != item.Key || !prior.BaseValue.Equals(item.BaseValue) || !prior.Target.Equals(item.Target))
{
throw Invalid();
}
}
else
{
sequences.Add(item.Sequence, item);
}
}
foreach (var item in Unsent().Concat(RemoteOnlyWrites.Values))
{
Add(item);
}
var ids = new HashSet<string>(StringComparer.Ordinal);
foreach (var batch in PreparedOrdinaryBatches.Concat(OrdinaryUnknownBatches).Concat(new[] { Active, Unknown }).Where(batch => batch != null))
{
if (!Guid.TryParseExact(batch.Id, "N", out _) || !ids.Add(batch.Id) || batch.Items.Count < 1 || batch.Items.Count > 10 ||
batch.Items.Select(item => item.Key).Distinct(StringComparer.Ordinal).Count() != batch.Items.Count ||
batch.Items.Any(item => item.Target.Exists != batch.Items[0].Target.Exists) || (batch.IsOrdinary && batch.IsDelete) ||
!batch.Items.Select(item => item.Sequence).SequenceEqual(batch.Items.Select(item => item.Sequence).OrderBy(value => value)))
{
throw Invalid();
}
foreach (var item in batch.Items)
{
var reference = !batch.IsOrdinary && (LegacyPending.Values.Concat(ExplicitDeletes.Values)
.Any(source => source.Sequence == item.Sequence));
Add(item, reference);
}
}
if (RemoteOnlyWrites.Any(pair => pair.Key != pair.Value.Sequence || !pair.Value.Target.Exists) ||
PreparedOrdinaryBatches.Concat(OrdinaryUnknownBatches).Any(batch => !batch.IsOrdinary) ||
LocalOrigins.Any(pair => !ValidKey(pair.Key) || pair.Value.Sequence < 0 || pair.Value.Sequence >= NextSequence ||
(!pair.Value.LocalOnly && pair.Value.Sequence == 0)) ||
LocalUpdateTicks.Any(pair => !ValidKey(pair.Key) || pair.Value < 0 || pair.Value > DateTime.MaxValue.Ticks) ||
ImmediatePreservedKeys.Any(key => !ValidKey(key)) ||
LegacyLocalOnlyUnderImmediate.Any(pair => !ImmediatePreservedKeys.Contains(pair.Key) ||
!LocalOrigins.TryGetValue(pair.Key, out var origin) || !origin.LocalOnly) ||
Conflicts.Keys.Any(key => !Unsent().Any(item => item.Key == key) && !PreparedOrdinaryBatches.Any(batch => batch.Items.Any(item => item.Key == key)) &&
Unknown == null && OrdinaryUnknownBatches.Count == 0))
{
throw Invalid();
}
}
private static Dictionary<string, string> Strings(JToken value)
{
var result = new Dictionary<string, string>(StringComparer.Ordinal);
foreach (var property in ((JObject)value).Properties())
{
if (!ValidKey(property.Name) || property.Value.Type != JTokenType.String)
{
throw Invalid();
}
result.Add(property.Name, (string)property.Value);
}
return result;
}
private static KvChange Change(JToken item)
{
var key = String(item, "key");
if (!ValidKey(key))
{
throw Invalid();
}
return new KvChange(key, Integer(item, "sequence"), Value(Field(item, "base", JTokenType.Object)), Value(Field(item, "target", JTokenType.Object)));
}
private static KvValue Value(JToken item)
{
var exists = Boolean(item, "exists");
var value = Field(item, "value", exists ? JTokenType.String : JTokenType.Null);
return exists ? KvValue.Present((string)value) : KvValue.Missing;
}
private static KvBatch Batch(JToken parent, string name, bool withKind = false)
{
var node = parent[name];
if (node == null)
{
throw Invalid();
}
if (node.Type == JTokenType.Null)
{
return null;
}
var id = String(node, "id");
if (!Guid.TryParseExact(id, "N", out _))
{
throw Invalid();
}
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 ||
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();
}
return new KvBatch(id, items, withKind && Boolean(node, "ordinary"));
}
private static JToken Field(JToken parent, string name, JTokenType type)
{
if (!(parent is JObject obj) || obj[name] == null || obj[name].Type != type)
{
throw Invalid();
}
return obj[name];
}
private static string String(JToken parent, string name) => (string)Field(parent, name, JTokenType.String);
private static long Integer(JToken parent, string name) => (long)Field(parent, name, JTokenType.Integer);
private static bool Boolean(JToken parent, string name) => (bool)Field(parent, name, JTokenType.Boolean);
internal static JsonException Invalid() => new JsonException("Invalid KV cache snapshot.");
}
}

View File

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

View File

@@ -0,0 +1,739 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
namespace Core.KV
{
/// <summary>
/// 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
/// admitted network operations before disposing this state and its filesystem lease.
/// </summary>
public sealed class KvSyncState : IDisposable
{
private readonly object gate = new object();
private readonly KvCacheStore store;
private KvSnapshot snapshot;
private KVSyncStatus status;
private readonly HashSet<long> recoveredImmediate;
private bool recoverImmediateView = true;
private bool recoveredOrdinaryUnknown;
private KvSnapshot comparisonLocal;
private Dictionary<string, string> comparisonRemote;
private Dictionary<string, DateTime> comparisonTimes;
private Dictionary<string, string> comparisonView;
private HashSet<string> comparisonImmediateKeys;
internal bool HasPendingComparison
{
get
{
lock (gate)
{
return comparisonLocal != null;
}
}
}
internal bool AuthenticationInvalid
{
get
{
lock (gate)
{
return snapshot.AuthenticationInvalid;
}
}
}
public KVSyncStatus Status
{
get
{
lock (gate)
return status;
}
}
private KvSyncState(KvCacheStore store, KvSnapshot snapshot)
{
this.store = store;
this.snapshot = snapshot;
recoveredImmediate = new HashSet<long>(snapshot.RemoteOnlyWrites.Keys);
// Authentication belongs to the old owner. A new owner must obtain a fresh
// successful Get before Ready; this never clears a persisted unknown batch.
snapshot.AuthenticationInvalid = false;
// A recovered active batch might have reached the server. Never acknowledge it by guessing.
if (snapshot.Active != null)
{
if (snapshot.Active.IsOrdinary)
{
snapshot.OrdinaryUnknownBatches.Add(snapshot.Active);
}
else
{
snapshot.Unknown = snapshot.Active;
}
snapshot.Active = null;
}
recoveredOrdinaryUnknown = snapshot.OrdinaryUnknownBatches.Count != 0;
status = snapshot.Unknown != null || recoveredOrdinaryUnknown
? KVSyncStatus.UnknownOutcome : KVSyncStatus.NotLoaded;
}
public static KvOpenResult Open(string root, Uri api, string playerId)
{
KvIdentity identity;
try
{
identity = new KvIdentity(root, api, playerId);
}
catch (Exception ex) when (ex is ArgumentException || ex is NotSupportedException || ex is PathTooLongException)
{
return new KvOpenResult(KVOpenError.Validation);
}
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 KvDiagnosticSnapshot Inspect()
{
lock (gate)
return new KvDiagnosticSnapshot(snapshot);
}
public IReadOnlyDictionary<string, string> ReadLocal()
{
lock (gate)
{
if (status != KVSyncStatus.Ready)
{
throw new InvalidOperationException("KV data is not ready.");
}
return new ReadOnlyDictionary<string, string>(ReadView());
}
}
// The caller must supply a successful complete Get. Failed reads must use RecordReadFailure.
internal KvStateResult Reconcile(IReadOnlyDictionary<string, string> remote,
IReadOnlyDictionary<string, DateTime> updatedAtUtc)
{
lock (gate)
{
if (Terminal || snapshot.Active != null || snapshot.AuthenticationInvalid || comparisonLocal != null)
{
return Blocked();
}
if (remote == null || remote.Any(pair => !KvSnapshot.ValidKey(pair.Key) || pair.Value == null))
{
return Invalid();
}
if (!recoverImmediateView || updatedAtUtc == null ||
updatedAtUtc.Count != remote.Count || remote.Keys.Any(key => !updatedAtUtc.ContainsKey(key)) ||
updatedAtUtc.Values.Any(time => time.Kind != DateTimeKind.Utc))
{
return Invalid();
}
var original = snapshot;
var copy = snapshot.Clone();
copy.Conflicts.Clear();
if (copy.Unknown != null)
{
foreach (var item in copy.Unsent())
{
copy.Conflicts[item.Key] = KvValue.At(remote, item.Key);
}
return Commit(copy, KVSyncStatus.UnknownOutcome);
}
var recoveringKeys = new HashSet<string>(copy.ImmediatePreservedKeys, StringComparer.Ordinal);
foreach (var sequence in recoveredImmediate)
{
var source = copy.RemoteOnlyWrites[sequence];
Rebase(copy, source.Key, KvValue.At(remote, source.Key));
}
foreach (var source in Sources(copy))
{
if (copy.OrdinaryBuffers.Contains(source))
{
continue;
}
foreach (var item in source.Values.ToArray())
{
var observed = KvValue.At(remote, item.Key);
if (observed.Equals(item.Target))
{
source.Remove(item.Key);
}
else if (!observed.Equals(item.BaseValue))
{
copy.Conflicts[item.Key] = observed;
}
}
}
foreach (var item in copy.PreparedOrdinaryBatches.SelectMany(batch => batch.Items))
{
var observed = KvValue.At(remote, item.Key);
if (!observed.Equals(item.Target) && !observed.Equals(item.BaseValue))
{
copy.Conflicts[item.Key] = observed;
}
}
var visible = new Dictionary<string, string>(remote, StringComparer.Ordinal);
foreach (var pair in copy.LocalOrigins.ToArray())
{
if (pair.Value.LocalOnly)
{
if (copy.LegacyLocalOnlyUnderImmediate.TryGetValue(pair.Key, out var lower))
{
visible[pair.Key] = lower;
}
else
{
Apply(visible, pair.Key, KvValue.At(copy.LocalData, pair.Key));
}
}
else
{
var source = copy.Unsent().Concat(copy.PreparedOrdinaryBatches.SelectMany(batch => batch.Items))
.Concat(copy.OrdinaryUnknownBatches.SelectMany(batch => batch.Items))
.FirstOrDefault(item => item.Sequence == pair.Value.Sequence);
if (source != null)
{
Apply(visible, pair.Key, recoveringKeys.Contains(pair.Key)
? source.Target : KvValue.At(copy.LocalData, pair.Key));
}
else
{
copy.LocalOrigins.Remove(pair.Key);
}
}
}
copy.Baseline = remote.ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.Ordinal);
copy.HasBaseline = true;
var reconciled = Commit(copy, copy.Conflicts.Count == 0 ? KVSyncStatus.Ready : KVSyncStatus.Conflict);
if (reconciled.Success)
{
recoverImmediateView = false;
comparisonLocal = original;
comparisonRemote = remote.ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.Ordinal);
comparisonTimes = updatedAtUtc.ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.Ordinal);
comparisonView = visible;
comparisonImmediateKeys = new HashSet<string>(StringComparer.Ordinal);
}
return reconciled;
}
}
private Dictionary<string, string> ReadView()
{
if (comparisonLocal == null)
{
return LocalView(snapshot);
}
var view = new Dictionary<string, string>(comparisonView, StringComparer.Ordinal);
foreach (var pair in snapshot.LocalOrigins)
{
if (pair.Value.Sequence >= comparisonLocal.NextSequence)
{
Apply(view, pair.Key, KvValue.At(snapshot.LocalData, pair.Key));
}
}
return view;
}
internal KvStateResult CompareDatas(long nowTicks)
{
lock (gate)
{
if (status != KVSyncStatus.Ready || snapshot.AuthenticationInvalid || snapshot.Active != null ||
snapshot.Unknown != null || comparisonLocal == null)
{
return Blocked();
}
if ((comparisonLocal.OrdinaryUnknownBatches.Count != 0 && !recoveredOrdinaryUnknown) ||
snapshot.OrdinaryUnknownBatches.Any(batch => !comparisonLocal.OrdinaryUnknownBatches.Any(initial => initial.Id == batch.Id)))
{
return Blocked();
}
if (nowTicks < 0 || nowTicks > DateTime.MaxValue.Ticks)
{
return Invalid();
}
var copy = snapshot.Clone();
// A new login uses the original time comparison, not the old request's outcome.
// Retire only its initial ordinary evidence in the same commit as that comparison.
foreach (var batch in comparisonLocal.OrdinaryUnknownBatches)
{
copy.OrdinaryUnknownBatches.RemoveAll(current => current.Id == batch.Id);
foreach (var item in batch.Items)
{
if (copy.LocalOrigins.TryGetValue(item.Key, out var origin) && origin.Sequence == item.Sequence)
{
copy.LocalOrigins.Remove(item.Key);
}
}
}
var protectedKeys = new HashSet<string>(StringComparer.Ordinal);
foreach (var source in new[] { comparisonLocal, snapshot })
{
protectedKeys.UnionWith(source.PreparedOrdinaryBatches.SelectMany(batch => batch.Items).Select(item => item.Key));
protectedKeys.UnionWith(source.LegacyPending.Keys);
protectedKeys.UnionWith(source.ExplicitDeletes.Keys);
protectedKeys.UnionWith(source.ImmediatePreservedKeys);
protectedKeys.UnionWith(source.RemoteOnlyWrites.Values.Select(item => item.Key));
}
protectedKeys.UnionWith(snapshot.LocalOrigins.Where(pair => pair.Value.Sequence >= comparisonLocal.NextSequence).Select(pair => pair.Key));
var visible = ReadView();
foreach (var key in protectedKeys)
{
Apply(copy.LocalData, key, KvValue.At(visible, key));
}
foreach (var key in comparisonLocal.LocalData.Keys.Concat(comparisonRemote.Keys).Distinct(StringComparer.Ordinal))
{
if (protectedKeys.Contains(key))
{
continue;
}
var localExists = comparisonLocal.HasBaseline && comparisonLocal.LocalData.TryGetValue(key, out _);
comparisonLocal.LocalData.TryGetValue(key, out var localValue);
var remoteExists = comparisonRemote.TryGetValue(key, out var remoteValue);
var localTime = comparisonLocal.LocalUpdateTicks.TryGetValue(key, out var ticks) ? ticks : 0;
var remoteTime = remoteExists ? comparisonTimes[key].Ticks : 0;
if (!localExists || (remoteExists && remoteTime - localTime > TimeSpan.TicksPerSecond * 60))
{
copy.LocalData[key] = remoteValue;
copy.LocalUpdateTicks[key] = remoteTime;
foreach (var bucket in copy.OrdinaryBuffers)
{
if (bucket.TryGetValue(key, out var current) && comparisonLocal.OrdinaryBuffers.Any(old =>
old.TryGetValue(key, out var item) && item.Sequence == current.Sequence))
{
bucket.Remove(key);
if (copy.LocalOrigins.TryGetValue(key, out var origin) && origin.Sequence == current.Sequence)
{
copy.LocalOrigins.Remove(key);
}
}
}
}
else if (!remoteExists || localTime - remoteTime > TimeSpan.TicksPerSecond * 60)
{
if (copy.NextSequence == long.MaxValue)
{
return Invalid();
}
ApplyUpdate(copy, key, localValue, true, nowTicks);
}
else
{
copy.LocalData[key] = localValue;
copy.LocalUpdateTicks[key] = localTime;
}
}
foreach (var sequence in comparisonLocal.RemoteOnlyWrites.Keys)
{
copy.RemoteOnlyWrites.Remove(sequence);
}
foreach (var key in comparisonLocal.LegacyLocalOnlyUnderImmediate.Keys)
{
copy.LegacyLocalOnlyUnderImmediate.Remove(key);
}
foreach (var key in comparisonLocal.ImmediatePreservedKeys)
{
if (!comparisonImmediateKeys.Contains(key))
{
copy.ImmediatePreservedKeys.Remove(key);
}
}
var result = Commit(copy, KVSyncStatus.Ready);
if (result.Success)
{
recoveredOrdinaryUnknown = false;
ClearComparison();
recoveredImmediate.Clear();
}
return result;
}
}
private void ClearComparison()
{
comparisonLocal = null;
comparisonRemote = null;
comparisonTimes = null;
comparisonView = null;
comparisonImmediateKeys = null;
}
public KvStateResult RecordReadFailure(bool unauthorized = false)
{
lock (gate)
{
if (Terminal || snapshot.Active != null)
{
return Blocked();
}
if (unauthorized)
{
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();
}
var copy = snapshot.Clone();
var sequence = ApplyUpdate(copy, key, value, remoteStore, updateTicks);
return Commit(copy, KVSyncStatus.Ready, sequence);
}
}
private static long ApplyUpdate(KvSnapshot copy, string key, string value, bool remoteStore, long updateTicks)
{
var sequence = copy.NextSequence++;
copy.LocalData[key] = value;
copy.ImmediatePreservedKeys.Remove(key);
copy.LegacyLocalOnlyUnderImmediate.Remove(key);
copy.LocalUpdateTicks[key] = updateTicks;
var acceptedRemote = false;
if (remoteStore)
{
foreach (var bucket in copy.OrdinaryBuffers)
{
if (!bucket.ContainsKey(key) && bucket.Count >= 10)
{
continue;
}
var baseline = bucket.TryGetValue(key, out var previous)
? previous.BaseValue : KvValue.At(copy.Baseline, key);
bucket[key] = new KvChange(key, sequence, baseline, KvValue.Present(value));
copy.LegacyPending.Remove(key);
copy.ExplicitDeletes.Remove(key);
acceptedRemote = true;
break;
}
}
copy.LocalOrigins[key] = (!acceptedRemote, sequence);
return sequence;
}
internal KvStateResult CaptureOrdinaryBatch(out KvBatch batch)
{
lock (gate)
{
batch = null;
if (status != KVSyncStatus.Ready || snapshot.AuthenticationInvalid)
{
return Blocked();
}
if (snapshot.OrdinaryBuffers[0].Count == 0)
{
return new KvStateResult(KVChangeError.None);
}
var copy = snapshot.Clone();
var first = copy.OrdinaryBuffers[0];
var captured = new KvBatch(Guid.NewGuid().ToString("N"), first.Values.OrderBy(item => item.Sequence), true);
copy.PreparedOrdinaryBatches.Add(captured);
copy.OrdinaryBuffers.RemoveAt(0);
copy.OrdinaryBuffers.Add(new Dictionary<string, KvChange>(StringComparer.Ordinal));
var result = Commit(copy, KVSyncStatus.Ready);
if (result.Success)
{
batch = captured;
}
return result;
}
}
internal KvStateResult BeginOrdinaryBatch(out KvBatch batch)
{
lock (gate)
{
batch = null;
if (status != KVSyncStatus.Ready || snapshot.Active != null || snapshot.Unknown != null || snapshot.AuthenticationInvalid)
{
return Blocked();
}
if (snapshot.PreparedOrdinaryBatches.Count == 0)
{
return new KvStateResult(KVChangeError.None);
}
var copy = snapshot.Clone();
copy.Active = copy.PreparedOrdinaryBatches[0];
copy.PreparedOrdinaryBatches.RemoveAt(0);
var result = Commit(copy, KVSyncStatus.Ready);
if (result.Success)
{
batch = snapshot.Active;
}
return result;
}
}
// Explicit requests retain their original source until the outcome is known. Ordinary buckets are never selected here.
internal KvStateResult BeginBatch(long throughSequence, out KvBatch batch)
{
lock (gate)
{
batch = null;
if (status != KVSyncStatus.Ready || snapshot.Active != null || snapshot.Unknown != null || snapshot.AuthenticationInvalid)
{
return Blocked();
}
if (throughSequence < 0)
{
return Invalid();
}
var source = snapshot.LegacyPending.Values;
var ordered = source.Where(item => item.Sequence <= throughSequence).OrderBy(item => item.Sequence).ToArray();
if (ordered.Length == 0)
{
return new KvStateResult(KVChangeError.None);
}
var copy = snapshot.Clone();
copy.Active = new KvBatch(Guid.NewGuid().ToString("N"), ordered.TakeWhile(item => item.Target.Exists == ordered[0].Target.Exists).Take(10));
var result = Commit(copy, KVSyncStatus.Ready);
if (result.Success)
{
batch = snapshot.Active;
}
return result;
}
}
public KvStateResult ConfirmBatch(string batchId)
{
lock (gate)
{
if (status != KVSyncStatus.Ready || snapshot.Active == null || snapshot.Active.Id != batchId)
{
return Blocked();
}
var copy = snapshot.Clone();
foreach (var item in copy.Active.Items)
{
Apply(copy.Baseline, item.Key, item.Target);
if (!copy.Active.IsOrdinary)
{
foreach (var source in new[] { copy.LegacyPending, copy.ExplicitDeletes })
{
if (source.TryGetValue(item.Key, out var current) && current.Sequence == item.Sequence)
{
source.Remove(item.Key);
}
}
}
Rebase(copy, item.Key, item.Target);
}
copy.Active = null;
return Commit(copy, KVSyncStatus.Ready);
}
}
// A definitely undispatched ordinary batch retires, as the original SDK call did; explicit source references stay.
public KvStateResult FailBatch(string batchId, bool mayHaveCommitted, bool unauthorized = false)
{
lock (gate)
{
if (Terminal || snapshot.Active == null || snapshot.Active.Id != batchId)
{
return Blocked();
}
var copy = snapshot.Clone();
var ordinary = copy.Active.IsOrdinary;
copy.AuthenticationInvalid |= unauthorized;
if (mayHaveCommitted)
{
if (ordinary)
{
copy.OrdinaryUnknownBatches.Add(copy.Active);
}
else
{
copy.Unknown = copy.Active;
}
}
copy.Active = null;
var next = copy.AuthenticationInvalid ? KVSyncStatus.Unauthorized : ordinary ? KVSyncStatus.Ready : KVSyncStatus.ReadFailed;
if (copy.Unknown != null)
{
next = KVSyncStatus.UnknownOutcome;
}
return Commit(copy, next);
}
}
private static IEnumerable<Dictionary<string, KvChange>> Sources(KvSnapshot value)
{
return value.OrdinaryBuffers.Concat(new[] { value.LegacyPending, value.ExplicitDeletes });
}
private static void RemoveSources(KvSnapshot value, string key)
{
foreach (var source in Sources(value))
{
source.Remove(key);
}
}
private static void Rebase(KvSnapshot value, string key, KvValue baseline)
{
foreach (var source in Sources(value))
{
if (source.TryGetValue(key, out var pending))
{
source[key] = pending.WithBase(baseline);
}
}
}
internal KvStateResult PrepareImmediate(IReadOnlyDictionary<string, string> data,
out IReadOnlyList<KvChange> operations)
{
lock (gate)
{
operations = null;
if (status != KVSyncStatus.Ready || snapshot.AuthenticationInvalid)
{
return Blocked();
}
if (data == null || data.Count == 0 || data.Count > long.MaxValue - snapshot.NextSequence ||
data.Any(pair => !KvSnapshot.ValidKey(pair.Key) || pair.Value == null))
{
return Invalid();
}
var copy = snapshot.Clone();
var prepared = new List<KvChange>();
foreach (var pair in data)
{
copy.ImmediatePreservedKeys.Add(pair.Key);
var operation = new KvChange(pair.Key, copy.NextSequence++,
KvValue.At(copy.Baseline, pair.Key), KvValue.Present(pair.Value));
copy.RemoteOnlyWrites.Add(operation.Sequence, operation);
RemoveSources(copy, pair.Key);
prepared.Add(operation);
}
var result = Commit(copy, KVSyncStatus.Ready, prepared[0].Sequence);
if (result.Success)
{
operations = prepared.AsReadOnly();
if (comparisonLocal != null)
{
comparisonImmediateKeys.UnionWith(data.Keys);
}
}
return result;
}
}
internal KvStateResult CompleteImmediate(IReadOnlyList<KvChange> operations)
{
lock (gate)
{
if (status != KVSyncStatus.Ready || snapshot.AuthenticationInvalid || snapshot.Active != null ||
snapshot.Unknown != null || operations.Any(operation => !snapshot.RemoteOnlyWrites.ContainsKey(operation.Sequence)))
{
return Blocked();
}
var copy = snapshot.Clone();
foreach (var operation in operations)
{
Apply(copy.Baseline, operation.Key, operation.Target);
Rebase(copy, operation.Key, operation.Target);
copy.RemoteOnlyWrites.Remove(operation.Sequence);
copy.ImmediatePreservedKeys.Add(operation.Key);
}
return Commit(copy, KVSyncStatus.Ready);
}
}
internal void CancelUndispatchedImmediate(IReadOnlyList<KvChange> operations)
{
lock (gate)
{
if (Terminal)
{
return;
}
var protectedBatches = snapshot.OrdinaryUnknownBatches.Concat(new[] { snapshot.Active, snapshot.Unknown }).Where(batch => batch != null);
var removable = operations.Where(operation => snapshot.RemoteOnlyWrites.ContainsKey(operation.Sequence) &&
!protectedBatches.Any(batch => batch.Items.Any(item => item.Key == operation.Key && item.Sequence < operation.Sequence))).ToArray();
if (removable.Length == 0)
{
return;
}
var copy = snapshot.Clone();
foreach (var operation in removable)
{
copy.RemoteOnlyWrites.Remove(operation.Sequence);
}
Commit(copy, status);
}
}
private static Dictionary<string, string> LocalView(KvSnapshot source)
{
return new Dictionary<string, string>(source.LocalData, StringComparer.Ordinal);
}
private bool Terminal => status == KVSyncStatus.Closed || status == KVSyncStatus.StorageError;
private KvStateResult Commit(KvSnapshot copy, KVSyncStatus nextStatus, long sequence = 0)
{
if (snapshot.Revision == long.MaxValue)
{
status = KVSyncStatus.StorageError;
return new KvStateResult(KVChangeError.StorageError, sequence);
}
copy.Revision = snapshot.Revision + 1;
if (!store.Save(copy, out var uncertain))
{
status = KVSyncStatus.StorageError;
return new KvStateResult(KVChangeError.StorageError, sequence, uncertain);
}
snapshot = copy;
status = nextStatus;
return new KvStateResult(KVChangeError.None, sequence);
}
private static void Apply(Dictionary<string, string> data, string key, KvValue value)
{
if (value.Exists)
{
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()
{
lock (gate)
{
if (status == KVSyncStatus.Closed)
{
return;
}
status = KVSyncStatus.Closed;
ClearComparison();
store.Dispose();
}
}
}
}

View File

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

View 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;
}
}
}

View File

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

View File

@@ -25,6 +25,7 @@ public class PlayFabMgr : MonoBehaviour
private CancellationTokenSource tokenSource; private CancellationTokenSource tokenSource;
static PlayFabMgr _instance = null; static PlayFabMgr _instance = null;
internal static bool HasLiveInstanceForKVLoad => _instance!=null;
public static PlayFabMgr Instance public static PlayFabMgr Instance
{ {
@@ -156,6 +157,7 @@ public class PlayFabMgr : MonoBehaviour
{ {
tokenSource?.Cancel(); tokenSource?.Cancel();
tokenSource = null; tokenSource = null;
PlayerPrefs.Save();
SyncData(); SyncData();
} }
@@ -215,6 +217,8 @@ public class PlayFabMgr : MonoBehaviour
private void SyncData() private void SyncData()
{ {
// Logout clears credentials before RootCtx is destroyed. Keep pending data local.
if (!PlayFabClientAPI.IsClientLoggedIn()) return;
var buf = dataBuffer.First; var buf = dataBuffer.First;
if (buf.Value.Count == 0) if (buf.Value.Count == 0)
return; return;
@@ -265,6 +269,7 @@ public class PlayFabMgr : MonoBehaviour
GameDebug.LogError("[PlayFabMgr]UpdateUserData Error: {0}", error.ToString()); GameDebug.LogError("[PlayFabMgr]UpdateUserData Error: {0}", error.ToString());
} }
public async Task<bool> SyncDataImmidiate(string key, string data) public async Task<bool> SyncDataImmidiate(string key, string data)
{ {
foreach (var buff in dataBuffer) foreach (var buff in dataBuffer)
@@ -293,4 +298,5 @@ public class PlayFabMgr : MonoBehaviour
); );
return await taskSource.Task; return await taskSource.Task;
} }
} }

View File

@@ -198,23 +198,26 @@ namespace Game
} }
protected CompositeDisposable disposables; protected CompositeDisposable disposables;
private int initializationVersion;
public async Task Init(System.Action<float> progress) public async Task Init(System.Action<float> progress)
{ {
//playerData = GContext.container.Resolve<PlayerData>(); if (residentClip != null || disposables != null) Release();
_tables = GContext.container.Resolve<cfg.Tables>(); var version = ++initializationVersion;
//var service = (SoundService)soundService; var container = GContext.container;
await soundService.InitAsync(); _tables = container.Resolve<cfg.Tables>();
var opMap = new Dictionary<string, AsyncOperationHandle<AudioClip>>(); var opMap = new Dictionary<string, AsyncOperationHandle<AudioClip>>();
CompositeDisposable pendingSubscriptions = null;
bool transferred = false;
try
{
await soundService.InitAsync();
if (version != initializationVersion || !ReferenceEquals(container, GContext.container)) return;
foreach (var addr in residentFish) foreach (var addr in residentFish)
{ {
opMap[addr] = Addressables.LoadAssetAsync<AudioClip>(addr); if (!opMap.ContainsKey(addr)) opMap.Add(addr, Addressables.LoadAssetAsync<AudioClip>(addr));
} }
var tasks = opMap.Values.Select(_ => _.Task).ToArray();
var tasks = opMap.Values.Select(_ => _.Task); var total = (float)tasks.Length;
var total = (float)tasks.Count();
while (true) while (true)
{ {
var completed = tasks.Count(_ => _.IsCompleted); var completed = tasks.Count(_ => _.IsCompleted);
@@ -223,29 +226,41 @@ namespace Game
progress.Invoke(1f); progress.Invoke(1f);
break; break;
} }
else
{
progress.Invoke(completed / total); progress.Invoke(completed / total);
}
await Awaiters.NextFrame; await Awaiters.NextFrame;
if (version != initializationVersion || !ReferenceEquals(container, GContext.container)) return;
} }
if (version != initializationVersion || !ReferenceEquals(container, GContext.container)) return;
var loadedClips = opMap.ToDictionary(_ => _.Key, _ => _.Value.Result);
pendingSubscriptions = new CompositeDisposable();
GContext.OnEvent<EventUISound>().Subscribe(PlaySound).AddTo(pendingSubscriptions);
GContext.OnEvent<EventRewardSound>().Subscribe(PlayRewardSound).AddTo(pendingSubscriptions);
GContext.OnEvent<EventFishingSound>().Subscribe(PlayFishingSound).AddTo(pendingSubscriptions);
GContext.OnEvent<EventBGMSound>().Subscribe(PlayMusic).AddTo(pendingSubscriptions);
GContext.OnEvent<FadeOutEvent>().Subscribe(FadeOutEvent).AddTo(pendingSubscriptions);
GContext.OnEvent<AssetReferenceAudioClip>().Subscribe(PlaySoundByReference).AddTo(pendingSubscriptions);
GContext.OnEvent<AssetReferenceAudioVoiceClip>().Subscribe(PlayVoiceSoundByReference).AddTo(pendingSubscriptions);
residentClip = opMap.ToDictionary(_ => _.Key, _ => _.Value.Result); GContext.OnEvent<ChangeRodDataEvent>().Subscribe(OnChangeRodData).AddTo(pendingSubscriptions);
disposables = new CompositeDisposable(); GContext.OnEvent<SoundMuteEvent>().Subscribe(OnSoundMute).AddTo(pendingSubscriptions);
GContext.OnEvent<EventUISound>().Subscribe(PlaySound).AddTo(disposables); GContext.OnEvent<OnSoundStateEvent>().Subscribe(OnSoundStateEvent).AddTo(pendingSubscriptions);
GContext.OnEvent<EventRewardSound>().Subscribe(PlayRewardSound).AddTo(disposables); soundService.BGMVol = container.Resolve<ISettingService>().Music ? 80 : 0;
GContext.OnEvent<EventFishingSound>().Subscribe(PlayFishingSound).AddTo(disposables); soundService.SFXVol = container.Resolve<ISettingService>().Sound ? 80 : 0;
GContext.OnEvent<EventBGMSound>().Subscribe(PlayMusic).AddTo(disposables); soundService.VoiceVol = container.Resolve<ISettingService>().Sound ? 80 : 0;
GContext.OnEvent<FadeOutEvent>().Subscribe(FadeOutEvent).AddTo(disposables); residentClip = loadedClips;
GContext.OnEvent<AssetReferenceAudioClip>().Subscribe(PlaySoundByReference).AddTo(disposables); disposables = pendingSubscriptions;
GContext.OnEvent<AssetReferenceAudioVoiceClip>().Subscribe(PlayVoiceSoundByReference).AddTo(disposables); transferred = true;
}
GContext.OnEvent<ChangeRodDataEvent>().Subscribe(OnChangeRodData).AddTo(disposables); finally
GContext.OnEvent<SoundMuteEvent>().Subscribe(OnSoundMute).AddTo(disposables); {
GContext.OnEvent<OnSoundStateEvent>().Subscribe(OnSoundStateEvent).AddTo(disposables); if (!transferred)
soundService.BGMVol = GContext.container.Resolve<ISettingService>().Music ? 80 : 0; {
soundService.SFXVol = GContext.container.Resolve<ISettingService>().Sound ? 80 : 0; pendingSubscriptions?.Dispose();
soundService.VoiceVol = GContext.container.Resolve<ISettingService>().Sound ? 80 : 0; if (version == initializationVersion) Release();
foreach (var handle in opMap.Values)
if (handle.IsValid()) Addressables.Release(handle);
}
}
} }
void OnSoundStateEvent(OnSoundStateEvent onSoundStateEvent) void OnSoundStateEvent(OnSoundStateEvent onSoundStateEvent)
{ {
@@ -757,6 +772,7 @@ namespace Game
} }
public void Release() public void Release()
{ {
initializationVersion++;
if (disposables != null) if (disposables != null)
{ {
disposables.Dispose(); disposables.Dispose();

View File

@@ -32,6 +32,15 @@ public class AchievementDataManager : IDisposable
{ {
_tables = tables; _tables = tables;
} }
public void LoadAchievementData(string value) public void LoadAchievementData(string value)
{ {
_achievementDatas = Newtonsoft.Json.JsonConvert.DeserializeObject<Dictionary<ConditionType, AchievementData>>(value); _achievementDatas = Newtonsoft.Json.JsonConvert.DeserializeObject<Dictionary<ConditionType, AchievementData>>(value);

View File

@@ -8,11 +8,21 @@ using System.Collections.Generic;
public class BargainPackData public class BargainPackData
{ {
[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;
private readonly TbSpecialPack _spp = GContext.container.Resolve<Tables>().TbSpecialPack; private readonly TbSpecialPack _spp = GContext.container.Resolve<Tables>().TbSpecialPack;
private GeneralEventPackData _data; private GeneralEventPackData _data;
public int CurrentEventID => _data.currentEventID; public int CurrentEventID => _data.currentEventID;
public FishingEvent CurrentEvent => _tables.TbFishingEvent[_data.currentEventID]; public FishingEvent CurrentEvent => _tables.TbFishingEvent[_data.currentEventID];
@@ -25,10 +35,11 @@ public class BargainPackData
{ {
get get
{ {
if (CurrentEventID == 0) return false; var fishingEvent = _tables.TbFishingEvent.GetOrDefault(CurrentEventID);
if (!(fishingEvent?.TimeDefinition is LimitedTime time)) return false;
var et = DateTime.Parse((_tables.TbFishingEvent[_data.currentEventID].TimeDefinition as LimitedTime).EndTime); var et = DateTime.Parse(time.EndTime);
var st = DateTime.Parse((_tables.TbFishingEvent[_data.currentEventID].TimeDefinition as LimitedTime).StartTime); var st = DateTime.Parse(time.StartTime);
return ZZTimeHelper.UtcNow() >= st && ZZTimeHelper.UtcNow() < et; return ZZTimeHelper.UtcNow() >= st && ZZTimeHelper.UtcNow() < et;
} }
} }
@@ -121,8 +132,8 @@ public class BargainPackData
public void SaveData() public void SaveData()
{ {
//Debug.LogError("Testing, data not saved for debug purpose."); //Debug.LogError("Testing, data not saved for debug purpose.");
PlayFabMgr.Instance.UpdateUserDataValue("GeneralEventPackData", SaveStore.Write("GeneralEventPackData",
Newtonsoft.Json.JsonConvert.SerializeObject(_data)); Newtonsoft.Json.JsonConvert.SerializeObject(_data)).RequireAccepted();
} }
public void LoadData(GeneralEventPackData data) public void LoadData(GeneralEventPackData data)

View File

@@ -33,9 +33,14 @@ public struct BpVipUnlock
public class BattlePassData public class BattlePassData
{ {
[asap.core.Inject] public Core.KV.IPlayerSaveStore playerSaveStore { get; set; }
public BattlePassRecord BattlePassRecord; public BattlePassRecord BattlePassRecord;
public BattleTask BattleTask; public BattleTask BattleTask;
private Tables _tables; private Tables _tables;
@@ -91,7 +96,7 @@ public class BattlePassData
{ {
string battlePassRecord = BattlePassRecord.SerializeObject(); string battlePassRecord = BattlePassRecord.SerializeObject();
string battleTask = BattleTask.SerializeObject(); string battleTask = BattleTask.SerializeObject();
PlayFabMgr.Instance.UpdateUserDataValue("BattlePassRecordTask", battlePassRecord + "#" + battleTask); playerSaveStore.Write("BattlePassRecordTask", battlePassRecord + "#" + battleTask).RequireAccepted();
} }
public void SaveBattlePassRecord() public void SaveBattlePassRecord()
{ {
@@ -136,9 +141,10 @@ public class BattlePassData
public void RefreshDaiyTask() public void RefreshDaiyTask()
{ {
var bpMain = _tables.TbBattlePassMain.GetOrDefault(_tables.TbFishingEvent.GetOrDefault(BattlePassRecord.ID).RedirectID); var fishingEvent = _tables.TbFishingEvent.GetOrDefault(BattlePassRecord.ID);
if (bpMain != null) if (fishingEvent == null) return;
{ var bpMain = _tables.TbBattlePassMain.GetOrDefault(fishingEvent.RedirectID);
if (bpMain == null) return;
BattleTask.DailyTaskEndTime = ConvertTools.GetDateTimeYMD(ZZTimeHelper.UtcNow().UtcNowOffset()).AddDays(1); BattleTask.DailyTaskEndTime = ConvertTools.GetDateTimeYMD(ZZTimeHelper.UtcNow().UtcNowOffset()).AddDays(1);
var dailyTasks = bpMain.DailyTaskList; var dailyTasks = bpMain.DailyTaskList;
BattleTask.dic_dailyTasks.Clear(); BattleTask.dic_dailyTasks.Clear();
@@ -146,21 +152,16 @@ public class BattlePassData
{ {
BattleTask.dic_dailyTasks.Add(task, 0); BattleTask.dic_dailyTasks.Add(task, 0);
} }
}
else
{
BattleTask = new BattleTask();
}
GContext.Publish(new ConditionTypeEvent { type = ConditionType.LoginDays, count = 1 }); GContext.Publish(new ConditionTypeEvent { type = ConditionType.LoginDays, count = 1 });
SaveBattleTask(); SaveBattleTask();
} }
public void RefreshWeeklyTask() public void RefreshWeeklyTask()
{ {
var fishingEvent = _tables.TbFishingEvent.GetOrDefault(BattlePassRecord.ID);
var bpMain = _tables.TbBattlePassMain.GetOrDefault(_tables.TbFishingEvent.GetOrDefault(BattlePassRecord.ID).RedirectID); if (fishingEvent == null) return;
if (bpMain != null) var bpMain = _tables.TbBattlePassMain.GetOrDefault(fishingEvent.RedirectID);
{ if (bpMain == null) return;
var weeklyTasks = bpMain.WeeklyTaskList; var weeklyTasks = bpMain.WeeklyTaskList;
BattleTask.dic_weeklyTasks.Clear(); BattleTask.dic_weeklyTasks.Clear();
foreach (var task in weeklyTasks) foreach (var task in weeklyTasks)
@@ -168,12 +169,6 @@ public class BattlePassData
BattleTask.dic_weeklyTasks.Add(task, 0); BattleTask.dic_weeklyTasks.Add(task, 0);
} }
BattleTask.WeeklyTaskEndTime = ConvertTools.GetDateTimeSunDay(ZZTimeHelper.UtcNow().UtcNowOffset()).AddDays(1); BattleTask.WeeklyTaskEndTime = ConvertTools.GetDateTimeSunDay(ZZTimeHelper.UtcNow().UtcNowOffset()).AddDays(1);
}
//当前没有战令了
else
{
BattleTask = new BattleTask();
}
BattlePassRecord.InflationRate = GContext.container.Resolve<PlayerData>().InflationRate; BattlePassRecord.InflationRate = GContext.container.Resolve<PlayerData>().InflationRate;
GContext.Publish(new ConditionTypeEvent { type = ConditionType.LoginDays, count = 1 }); GContext.Publish(new ConditionTypeEvent { type = ConditionType.LoginDays, count = 1 });
@@ -207,7 +202,7 @@ public class BattlePassDataProvider : IDisposable
private Tables _tables; private Tables _tables;
private Dictionary<int, BattlePassLevel> dic_BPLevel; private Dictionary<int, BattlePassLevel> dic_BPLevel;
public bool IsActivating => Data.BattlePassRecord.ID != 0; public bool IsActivating => Data.BattlePassRecord.ID != 0 && curPassMain != null;
public bool UILockVip = false; public bool UILockVip = false;
public bool IsUnlockVip => Data.BattlePassRecord.IsUnlockVip == 1; public bool IsUnlockVip => Data.BattlePassRecord.IsUnlockVip == 1;
@@ -249,6 +244,7 @@ public class BattlePassDataProvider : IDisposable
public void CheckIfRefreshBattblePass() public void CheckIfRefreshBattblePass()
{ {
if (!IsActivating) return;
DateTime endTime = GContext.container.Resolve<FishingEventData>().GetEventEndTime(Data.BattlePassRecord.ID); DateTime endTime = GContext.container.Resolve<FishingEventData>().GetEventEndTime(Data.BattlePassRecord.ID);
if (ZZTimeHelper.UtcNow().UtcNowOffset() >= endTime && IsActivating) if (ZZTimeHelper.UtcNow().UtcNowOffset() >= endTime && IsActivating)

View File

@@ -6,6 +6,8 @@ using System.Collections.Generic;
public class BuffDataCenter public class BuffDataCenter
{ {
[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)
{ {
@@ -16,6 +18,11 @@ public class BuffDataCenter
Dictionary<int, FishBuffTimeData> globalBuffDataList = new Dictionary<int, FishBuffTimeData>(); Dictionary<int, FishBuffTimeData> globalBuffDataList = new Dictionary<int, FishBuffTimeData>();
FishBuffTimeData fishDailyBuff = null; FishBuffTimeData fishDailyBuff = null;
public List<FishBuffTimeData> showFishBuffIds = new List<FishBuffTimeData>(); public List<FishBuffTimeData> showFishBuffIds = new List<FishBuffTimeData>();
public void InitData(Dictionary<string, string> userDatas) public void InitData(Dictionary<string, string> userDatas)
{ {
if (userDatas.TryGetValue("BuffDataList", out string buffDataStr)) if (userDatas.TryGetValue("BuffDataList", out string buffDataStr))
@@ -169,7 +176,7 @@ public class BuffDataCenter
DateTime now = ZZTimeHelper.UtcNow().UtcNowOffset(); DateTime now = ZZTimeHelper.UtcNow().UtcNowOffset();
mapBuffData.buffEndTime = now.AddSeconds(buffData.CountDown); mapBuffData.buffEndTime = now.AddSeconds(buffData.CountDown);
mapBuffDataList.Add(mapID, mapBuffData); mapBuffDataList.Add(mapID, mapBuffData);
PlayFabMgr.Instance.UpdateUserDataValue("BuffDataList", Newtonsoft.Json.JsonConvert.SerializeObject(mapBuffDataList)); SaveStore.Write("BuffDataList", Newtonsoft.Json.JsonConvert.SerializeObject(mapBuffDataList)).RequireAccepted();
return true; return true;
} }
} }
@@ -258,12 +265,12 @@ public class BuffDataCenter
if (targetBuffDataList[i].buffID == fishBuffTimeData.buffID && targetBuffDataList[i].sourceID == fishBuffTimeData.sourceID) if (targetBuffDataList[i].buffID == fishBuffTimeData.buffID && targetBuffDataList[i].sourceID == fishBuffTimeData.sourceID)
{ {
targetBuffDataList[i] = fishBuffTimeData; targetBuffDataList[i] = fishBuffTimeData;
PlayFabMgr.Instance.UpdateUserDataValue("TargetBuffDataList", Newtonsoft.Json.JsonConvert.SerializeObject(targetBuffDataList)); SaveStore.Write("TargetBuffDataList", Newtonsoft.Json.JsonConvert.SerializeObject(targetBuffDataList)).RequireAccepted();
return; return;
} }
} }
targetBuffDataList.Add(fishBuffTimeData); targetBuffDataList.Add(fishBuffTimeData);
PlayFabMgr.Instance.UpdateUserDataValue("TargetBuffDataList", Newtonsoft.Json.JsonConvert.SerializeObject(targetBuffDataList)); SaveStore.Write("TargetBuffDataList", Newtonsoft.Json.JsonConvert.SerializeObject(targetBuffDataList)).RequireAccepted();
} }
} }
else else
@@ -274,7 +281,7 @@ public class BuffDataCenter
public void ClearTargetBuff() public void ClearTargetBuff()
{ {
targetBuffDataList.Clear(); targetBuffDataList.Clear();
PlayFabMgr.Instance.UpdateUserDataValue("TargetBuffDataList", Newtonsoft.Json.JsonConvert.SerializeObject(targetBuffDataList)); SaveStore.Write("TargetBuffDataList", Newtonsoft.Json.JsonConvert.SerializeObject(targetBuffDataList)).RequireAccepted();
} }
/// <summary> /// <summary>
/// CollectionTargetWeight的特殊处理 /// CollectionTargetWeight的特殊处理
@@ -312,7 +319,7 @@ public class BuffDataCenter
else if (fishItemId == eTKeepsake.FishIDList[collectionTargetWeight.TargetFishIndex - 1]) else if (fishItemId == eTKeepsake.FishIDList[collectionTargetWeight.TargetFishIndex - 1])
{ {
item.isEnd = true; item.isEnd = true;
PlayFabMgr.Instance.UpdateUserDataValue("TargetBuffDataList", Newtonsoft.Json.JsonConvert.SerializeObject(targetBuffDataList)); SaveStore.Write("TargetBuffDataList", Newtonsoft.Json.JsonConvert.SerializeObject(targetBuffDataList)).RequireAccepted();
break; break;
} }
} }
@@ -422,7 +429,7 @@ public class BuffDataCenter
fishBuffTimeData.addDown = buff.CountDown; fishBuffTimeData.addDown = buff.CountDown;
showFishBuffIds.Add(fishBuffTimeData); showFishBuffIds.Add(fishBuffTimeData);
} }
PlayFabMgr.Instance.UpdateUserDataValue("GlobalBuffDataList", Newtonsoft.Json.JsonConvert.SerializeObject(globalBuffDataList)); SaveStore.Write("GlobalBuffDataList", Newtonsoft.Json.JsonConvert.SerializeObject(globalBuffDataList)).RequireAccepted();
GContext.Publish(new AddGlobalBuffEvent(buffID)); GContext.Publish(new AddGlobalBuffEvent(buffID));
} }
} }
@@ -526,7 +533,7 @@ public class BuffDataCenter
} }
AddGlobalBuff(buff.BuffID); AddGlobalBuff(buff.BuffID);
fishDailyBuff.isEnd = false; fishDailyBuff.isEnd = false;
PlayFabMgr.Instance.UpdateUserDataValue("FishDailyBuff", Newtonsoft.Json.JsonConvert.SerializeObject(fishDailyBuff)); SaveStore.Write("FishDailyBuff", Newtonsoft.Json.JsonConvert.SerializeObject(fishDailyBuff)).RequireAccepted();
return fishDailyBuff; return fishDailyBuff;
} }
} }

View File

@@ -56,6 +56,17 @@ namespace GameCore
{ {
public partial class FishingEventData : IDisposable public partial class FishingEventData : IDisposable
{ {
Core.KV.IPlayerSaveStore boundSaveStore;
[Inject] public Core.KV.IPlayerSaveStore playerSaveStore
{
get => boundSaveStore;
set
{
ThanksGivingPackData.BindSaveStore(value);
ChainPackWithProgressMigrationData.BindSaveStore(value);
boundSaveStore = value;
}
}
private cfg.Tables _tables; private cfg.Tables _tables;
private MMTService _mmtService; private MMTService _mmtService;
private IEventTokenService _eventTokenService; private IEventTokenService _eventTokenService;
@@ -130,6 +141,10 @@ namespace GameCore
//待释放 //待释放
IDisposable disposable; IDisposable disposable;
public void InitData(Dictionary<string, string> userDatas) public void InitData(Dictionary<string, string> userDatas)
{ {
InitTurntableRoll(_tables.TbGlobalConfig.InitWheelTicket); InitTurntableRoll(_tables.TbGlobalConfig.InitWheelTicket);
@@ -538,7 +553,7 @@ namespace GameCore
id = t.ID, id = t.ID,
endTime = ZZTimeHelper.UtcNow().UtcNowOffset().AddSeconds(fixedTime.StartTime).ToString(), endTime = ZZTimeHelper.UtcNow().UtcNowOffset().AddSeconds(fixedTime.StartTime).ToString(),
}; };
PlayFabMgr.Instance.UpdateUserDataValue("FixedTimeEventDic", JsonMapper.ToJson(fixedTimeEventDic)); playerSaveStore.Write("FixedTimeEventDic", JsonMapper.ToJson(fixedTimeEventDic)).RequireAccepted();
} }
} }
switch (t.Type) switch (t.Type)
@@ -806,7 +821,7 @@ namespace GameCore
private bool Condition_NoRechargeSeconds(int timeSeconds) private bool Condition_NoRechargeSeconds(int timeSeconds)
{ {
var lastPaymentStr = PlayFabMgr.Instance.GetLocalData("LastPaymentTime"); var lastPaymentStr = playerSaveStore.Read("LastPaymentTime").ValueOrThrow();
DateTime curTime = ZZTimeHelper.UtcNow().UtcNowOffset(); DateTime curTime = ZZTimeHelper.UtcNow().UtcNowOffset();
DateTime endTime = curTime; DateTime endTime = curTime;
if (string.IsNullOrEmpty(lastPaymentStr) || !lastPaymentStr.Contains("__")) if (string.IsNullOrEmpty(lastPaymentStr) || !lastPaymentStr.Contains("__"))
@@ -826,7 +841,8 @@ namespace GameCore
} }
public static bool Condition_NoRechargeDay(int timeDay = 7) public static bool Condition_NoRechargeDay(int timeDay = 7)
{ {
var lastPaymentStr = PlayFabMgr.Instance.GetLocalData("LastPaymentTime"); var store = GContext.container.Resolve<FishingEventData>().playerSaveStore;
var lastPaymentStr = store.Read("LastPaymentTime").ValueOrThrow();
if (!string.IsNullOrEmpty(lastPaymentStr) && lastPaymentStr.Contains("__")) if (!string.IsNullOrEmpty(lastPaymentStr) && lastPaymentStr.Contains("__"))
return false; return false;
DateTime curTime = ZZTimeHelper.UtcNow(); DateTime curTime = ZZTimeHelper.UtcNow();
@@ -1012,7 +1028,7 @@ namespace GameCore
case 2: case 2:
// Debug.Log($"[EventBreak] Id {e.ID} activated."); // Debug.Log($"[EventBreak] Id {e.ID} activated.");
EventBreakAct.Ctx = EventBreakTableContext.ReadTables(e.ID, GContext.container.Resolve<Tables>()); EventBreakAct.Ctx = EventBreakTableContext.ReadTables(e.ID, GContext.container.Resolve<Tables>());
var playfabData = EventBreakPfData.Deserialize(PlayFabMgr.Instance.GetLocalData(EventBreakPfData.PfKey)); var playfabData = EventBreakPfData.Deserialize(playerSaveStore.Read(EventBreakPfData.PfKey).ValueOrThrow());
var eventBreakModel = GContext.container.Resolve<DrillModel>(); var eventBreakModel = GContext.container.Resolve<DrillModel>();
if (eventBreakModel == null) if (eventBreakModel == null)
{ {
@@ -1034,7 +1050,7 @@ namespace GameCore
break; break;
case 4: case 4:
var canModel = GContext.container.Resolve<EventCanModel>(); var canModel = GContext.container.Resolve<EventCanModel>();
var ecPlayfabString = PlayFabMgr.Instance.GetLocalData(EventCanPlayfabData.PlayFabKey); var ecPlayfabString = playerSaveStore.Read(EventCanPlayfabData.PlayFabKey).ValueOrThrow();
var ecPpString = PlayerPrefs.GetString(EventCanPlayerPreferenceData.PlayerPrefsKey); var ecPpString = PlayerPrefs.GetString(EventCanPlayerPreferenceData.PlayerPrefsKey);
var ecPlayfabData = EventCanPlayfabData.Deserialize(ecPlayfabString); var ecPlayfabData = EventCanPlayfabData.Deserialize(ecPlayfabString);
var ecPlayerPrefsData = EventCanPlayerPreferenceData.Deserialize(ecPpString); var ecPlayerPrefsData = EventCanPlayerPreferenceData.Deserialize(ecPpString);
@@ -1110,7 +1126,7 @@ namespace GameCore
SetTurntable(t); SetTurntable(t);
break; break;
case 3: case 3:
var pfString = PlayFabMgr.Instance.GetLocalData(EventLuckMagicPlayfabData.Key); var pfString = playerSaveStore.Read(EventLuckMagicPlayfabData.Key).ValueOrThrow();
var pfData = EventLuckMagicPlayfabData.Deserialize(pfString); var pfData = EventLuckMagicPlayfabData.Deserialize(pfString);
var ppString = PlayerPrefs.GetString(EventLuckMagicPlayerPreferenceData.Key); var ppString = PlayerPrefs.GetString(EventLuckMagicPlayerPreferenceData.Key);
var ppData = EventLuckMagicPlayerPreferenceData.Deserialize(ppString); var ppData = EventLuckMagicPlayerPreferenceData.Deserialize(ppString);
@@ -1299,7 +1315,7 @@ namespace GameCore
SaveTransitionData(turntableEvent.ID, TurntableCoupon); SaveTransitionData(turntableEvent.ID, TurntableCoupon);
} }
TurntableRed(); TurntableRed();
PlayFabMgr.Instance.UpdateUserDataValue("TurntableRoll", TurntableCoupon.ToString()); playerSaveStore.Write("TurntableRoll", TurntableCoupon.ToString()).RequireAccepted();
} }
public void RewardTurntableRoll(int value, bool remote = true) public void RewardTurntableRoll(int value, bool remote = true)
{ {
@@ -1318,7 +1334,7 @@ namespace GameCore
.AddContent("change_num", value); .AddContent("change_num", value);
} }
#endif #endif
PlayFabMgr.Instance.UpdateUserDataValue("TurntableRoll", TurntableCoupon.ToString(), remote); playerSaveStore.Write("TurntableRoll", TurntableCoupon.ToString(), remote).RequireAccepted();
} }
public void TurntableRed() public void TurntableRed()
@@ -1403,7 +1419,7 @@ namespace GameCore
void SetLimitedTimeEventDic() void SetLimitedTimeEventDic()
{ {
PlayFabMgr.Instance.UpdateUserDataValue("LimitedTimeEventDic", Newtonsoft.Json.JsonConvert.SerializeObject(limitedTimeEventDic)); playerSaveStore.Write("LimitedTimeEventDic", Newtonsoft.Json.JsonConvert.SerializeObject(limitedTimeEventDic)).RequireAccepted();
} }
#region 目标奖励活动 #region 目标奖励活动
@@ -1575,10 +1591,14 @@ namespace GameCore
public async Task<bool> CheckResourceLoadQueue(string key, List<string> label) public async Task<bool> CheckResourceLoadQueue(string key, List<string> label)
{ {
var store = playerSaveStore;
if (!store.IsCurrent) return false;
if (!collectingTargetResoure) if (!collectingTargetResoure)
{ {
ILoadResourceService loadResourceService = GContext.container.Resolve<ILoadResourceService>(); ILoadResourceService loadResourceService = GContext.container.Resolve<ILoadResourceService>();
collectingTargetResoure = await loadResourceService.CheckResourceLoadQueue(key, label); var loaded = await loadResourceService.CheckResourceLoadQueue(key, label);
if (!store.IsCurrent) return false;
collectingTargetResoure = loaded;
} }
return collectingTargetResoure; return collectingTargetResoure;
} }
@@ -2201,8 +2221,8 @@ namespace GameCore
public void UpdateTurnTableData() public void UpdateTurnTableData()
{ {
PlayFabMgr.Instance.UpdateUserDataValue("WheelLevelCount", JsonMapper.ToJson(WheelLevelCount)); playerSaveStore.Write("WheelLevelCount", JsonMapper.ToJson(WheelLevelCount)).RequireAccepted();
PlayFabMgr.Instance.UpdateUserDataValue("WheelLevelFreeCount", freecount.ToString()); playerSaveStore.Write("WheelLevelFreeCount", freecount.ToString()).RequireAccepted();
} }
/// <summary> /// <summary>
/// Add item by dropId and multiplier, or just return that item without giving to player, if doAdd is false. /// Add item by dropId and multiplier, or just return that item without giving to player, if doAdd is false.
@@ -2223,10 +2243,10 @@ namespace GameCore
{ {
IncreaseScore(score * multiplier); IncreaseScore(score * multiplier);
WheelOperateCount++; WheelOperateCount++;
PlayFabMgr.Instance.UpdateUserDataValue("WheelOperateCount", WheelOperateCount.ToString()); playerSaveStore.Write("WheelOperateCount", WheelOperateCount.ToString()).RequireAccepted();
AllWheelOperateCount += multiplier; AllWheelOperateCount += multiplier;
PlayFabMgr.Instance.UpdateUserDataValue("AllWheelOperateCount", AllWheelOperateCount.ToString()); playerSaveStore.Write("AllWheelOperateCount", AllWheelOperateCount.ToString()).RequireAccepted();
} }
public void IncreaseScore(int score) public void IncreaseScore(int score)
{ {
@@ -2243,7 +2263,7 @@ namespace GameCore
} }
else else
{ {
PlayFabMgr.Instance.UpdateUserDataValue("WheelLevelCount", JsonMapper.ToJson(WheelLevelCount)); playerSaveStore.Write("WheelLevelCount", JsonMapper.ToJson(WheelLevelCount)).RequireAccepted();
} }
} }
@@ -2265,7 +2285,7 @@ namespace GameCore
{ {
freecount = 0; freecount = 0;
} }
PlayFabMgr.Instance.UpdateUserDataValue("WheelLevelFreeCount", freecount.ToString()); playerSaveStore.Write("WheelLevelFreeCount", freecount.ToString()).RequireAccepted();
} }
//转盘等级增加函数 //转盘等级增加函数
public void IncreaseWheelLevel() public void IncreaseWheelLevel()
@@ -2285,7 +2305,7 @@ namespace GameCore
// 更新积分进度 // 更新积分进度
WheelLevelCount[wheelLevel.Level] = progress; WheelLevelCount[wheelLevel.Level] = progress;
TurntableRed(); TurntableRed();
PlayFabMgr.Instance.UpdateUserDataValue("WheelLevelCount", JsonMapper.ToJson(WheelLevelCount)); playerSaveStore.Write("WheelLevelCount", JsonMapper.ToJson(WheelLevelCount)).RequireAccepted();
} }
} }

View File

@@ -41,6 +41,7 @@ namespace GameCore
} }
public class GuideDataCenter : IDisposable public class GuideDataCenter : IDisposable
{ {
[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)
@@ -56,6 +57,10 @@ namespace GameCore
public string popupPanelNode; public string popupPanelNode;
public GuidanceGroupDefine curGroupDefine; public GuidanceGroupDefine curGroupDefine;
public GuidanceDefineSetting guidanceDefineSetting; public GuidanceDefineSetting guidanceDefineSetting;
//待释放 //待释放
IDisposable disposable; IDisposable disposable;
IDisposable disposable2; IDisposable disposable2;
@@ -80,7 +85,10 @@ namespace GameCore
} }
public async void Init() public async void Init()
{ {
guidanceDefineSetting = (GuidanceDefineSetting)await Addressables.LoadAssetAsync<object>("GuidanceConfig"); var store = playerSaveStore;
var setting = (GuidanceDefineSetting)await Addressables.LoadAssetAsync<object>("GuidanceConfig");
if (disposed || !store.IsCurrent) return;
guidanceDefineSetting = setting;
if (guidanceDefineSetting != null) if (guidanceDefineSetting != null)
{ {
bool isUpdate = false; bool isUpdate = false;
@@ -101,6 +109,7 @@ namespace GameCore
if (GuideSaveDataDic.TryGetValue(item.groupName.ToString(), out _saveData)) if (GuideSaveDataDic.TryGetValue(item.groupName.ToString(), out _saveData))
{ {
if (!_saveData.isFinish && _saveData.guideSubIndex > 0) if (!_saveData.isFinish && _saveData.guideSubIndex > 0)
{ {
for (int i = _saveData.guideSubIndex; i < item.guidanceDefines.Length; i++) for (int i = _saveData.guideSubIndex; i < item.guidanceDefines.Length; i++)
@@ -153,6 +162,7 @@ namespace GameCore
} }
} }
} }
public bool GetIsFinish(string groupName) public bool GetIsFinish(string groupName)
{ {
return GuideSaveDataDic.ContainsKey(groupName) && GuideSaveDataDic[groupName].isFinish; return GuideSaveDataDic.ContainsKey(groupName) && GuideSaveDataDic[groupName].isFinish;
@@ -451,7 +461,9 @@ namespace GameCore
} }
async void TriggerGuide(string groupName, string curPanelName, GuidanceGroupDefine guidanceGroupDefine) async void TriggerGuide(string groupName, string curPanelName, GuidanceGroupDefine guidanceGroupDefine)
{ {
var store = playerSaveStore;
await System.Threading.Tasks.Task.Delay(200); await System.Threading.Tasks.Task.Delay(200);
if (disposed || !store.IsCurrent) return;
curSaveData = new GuideSaveData(); curSaveData = new GuideSaveData();
curSaveData.groupName = groupName; curSaveData.groupName = groupName;
GuideSaveDataDic[groupName] = curSaveData; GuideSaveDataDic[groupName] = curSaveData;
@@ -598,6 +610,7 @@ namespace GameCore
} }
async void ShowPanel(string PanelName) async void ShowPanel(string PanelName)
{ {
if (disposed || !playerSaveStore.IsCurrent) return;
if (curDefine.fingerType != FingerType.None) if (curDefine.fingerType != FingerType.None)
{ {
disposable3 = GContext.OnEvent<GuideNextEvent>().Subscribe(GuideNextEvent); disposable3 = GContext.OnEvent<GuideNextEvent>().Subscribe(GuideNextEvent);
@@ -608,10 +621,13 @@ namespace GameCore
public async void ShowGuidancePopupPanel(string nodeName) public async void ShowGuidancePopupPanel(string nodeName)
{ {
var store = playerSaveStore;
if (disposed || !store.IsCurrent) return;
popupPanelNode = nodeName; popupPanelNode = nodeName;
if (!string.IsNullOrEmpty(popupPanelNode)) if (!string.IsNullOrEmpty(popupPanelNode))
{ {
await UIManager.Instance.ShowUI(UITypes.GuidancePopupPanel); await UIManager.Instance.ShowUI(UITypes.GuidancePopupPanel);
if (disposed || !store.IsCurrent) return;
UIManager.Instance.HideUI(UITypes.GuidancePanel); UIManager.Instance.HideUI(UITypes.GuidancePanel);
} }
} }
@@ -668,11 +684,13 @@ namespace GameCore
{ {
List<string> strings = GuideSaveDataDic.Select(kv => kv.Key + "|" + kv.Value.guideSubIndex + "|" + (kv.Value.isFinish ? "1" : "0")).ToList(); List<string> strings = GuideSaveDataDic.Select(kv => kv.Key + "|" + kv.Value.guideSubIndex + "|" + (kv.Value.isFinish ? "1" : "0")).ToList();
string value = string.Join(";", strings); string value = string.Join(";", strings);
PlayFabMgr.Instance.UpdateUserDataValue("GuideSaveDataNew", value); playerSaveStore.Write("GuideSaveDataNew", value).RequireAccepted();
} }
bool disposed;
public void Dispose() public void Dispose()
{ {
disposed = true;
disposable?.Dispose(); disposable?.Dispose();
disposable = null; disposable = null;
disposable2?.Dispose(); disposable2?.Dispose();

View File

@@ -1,10 +1,13 @@
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.KV;
using PlayFab.ClientModels; using PlayFab.ClientModels;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using tysdk; using tysdk;
@@ -162,6 +165,33 @@ 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)
{
var store = kvUser.SaveStore as KvSaveStore;
var writtenNames = new HashSet<string>(StringComparer.Ordinal);
var data = new Dictionary<string, string>(StringComparer.Ordinal);
foreach (var statistic in Statistics)
{
if (!writtenNames.Add(statistic.StatisticName))
{
continue;
}
if (!KvSnapshot.ValidKey(statistic.StatisticName))
{
Debug.LogError("KV statistic write failed: " + statistic.StatisticName + ": Validation");
continue;
}
data.Add(statistic.StatisticName, statistic.Value.ToString(CultureInfo.InvariantCulture));
}
if (data.Count > 0)
{
if (store == null || !await store.SyncDataImmidiate(data))
{
Debug.LogError("KV statistic save failed: " + string.Join(",", data.Keys));
}
}
return;
}
UpdatePlayerStatisticsRequest request = new UpdatePlayerStatisticsRequest() UpdatePlayerStatisticsRequest request = new UpdatePlayerStatisticsRequest()
{ {
Statistics = Statistics, Statistics = Statistics,
@@ -170,6 +200,35 @@ 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)
{
var json = await customServerMgr.CustomServerPost("leaderboard/GetLeaderboard",
Newtonsoft.Json.JsonConvert.SerializeObject(new
{
statisticName = leaderboardNaem,
startPosition = 0,
maxResultsCount = maxCount
}));
if (string.IsNullOrEmpty(json))
{
return null;
}
try
{
var response = Newtonsoft.Json.JsonConvert.DeserializeObject<KvLeaderboardResponse>(json);
if (response == null || response.Code != 1 || response.Leaderboard == null ||
response.Leaderboard.Any(entry => entry == null || entry.Profile == null))
{
return null;
}
return new GetLeaderboardResult { Leaderboard = response.Leaderboard };
}
catch (Newtonsoft.Json.JsonException ex)
{
Debug.LogError("KV leaderboard response failed: " + ex.GetType().Name);
return null;
}
}
GetLeaderboardRequest reques = new GetLeaderboardRequest() GetLeaderboardRequest reques = new GetLeaderboardRequest()
{ {
StartPosition = 0, StartPosition = 0,
@@ -186,6 +245,21 @@ namespace GameCore
return await PlayFabClientAsyncAPI.GetLeaderboardAsync(reques); return await PlayFabClientAsyncAPI.GetLeaderboardAsync(reques);
} }
private sealed class KvLeaderboardResponse
{
public int Code
{
get;
set;
}
public List<PlayerLeaderboardEntry> Leaderboard
{
get;
set;
}
}
public async void SetLvData() public async void SetLvData()
{ {
isRefreshing = true; isRefreshing = true;

View File

@@ -1,4 +1,4 @@
using asap.core; using asap.core;
using Assets.Scripts.UI.OfferSweep; using Assets.Scripts.UI.OfferSweep;
using cfg; using cfg;
using CodeStage.AntiCheat.ObscuredTypes; using CodeStage.AntiCheat.ObscuredTypes;
@@ -17,6 +17,7 @@ namespace GameCore
{ {
public class PlayerData public class PlayerData
{ {
[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)
@@ -34,6 +35,10 @@ namespace GameCore
public const string Diamond = "DiamondTrue"; public const string Diamond = "DiamondTrue";
Dictionary<string, string> NewPlayerDataDic = new Dictionary<string, string>(); Dictionary<string, string> NewPlayerDataDic = new Dictionary<string, string>();
public IReadOnlyDictionary<string, string> PlayerDataDic => NewPlayerDataDic; public IReadOnlyDictionary<string, string> PlayerDataDic => NewPlayerDataDic;
public ObscuredInt lv { get; set; } public ObscuredInt lv { get; set; }
public ObscuredInt vip { get; private set; } public ObscuredInt vip { get; private set; }
public ObscuredInt vipAllScore { get; private set; } public ObscuredInt vipAllScore { get; private set; }
@@ -300,7 +305,7 @@ namespace GameCore
if (!newRodList.Contains(id)) if (!newRodList.Contains(id))
{ {
newRodList.Add(id); newRodList.Add(id);
PlayFabMgr.Instance.UpdateUserDataValue("NewRodList", JsonMapper.ToJson(newRodList)); playerSaveStore.Write("NewRodList", JsonMapper.ToJson(newRodList)).RequireAccepted();
if (addShowNewRod) if (addShowNewRod)
{ {
showNewRodList.Add(id); showNewRodList.Add(id);
@@ -331,7 +336,7 @@ namespace GameCore
public void RewardNewRod(int id) public void RewardNewRod(int id)
{ {
newRodList.Remove(id); newRodList.Remove(id);
PlayFabMgr.Instance.UpdateUserDataValue("NewRodList", JsonMapper.ToJson(newRodList)); playerSaveStore.Write("NewRodList", JsonMapper.ToJson(newRodList)).RequireAccepted();
GContext.container.Resolve<PlayerFishData>().SetRedPoint(); GContext.container.Resolve<PlayerFishData>().SetRedPoint();
} }
@@ -1143,7 +1148,7 @@ namespace GameCore
NewPlayerDataDic["ct"] = GContext.container.Resolve<FishingEventData>().CTSerialize(); NewPlayerDataDic["ct"] = GContext.container.Resolve<FishingEventData>().CTSerialize();
NewPlayerDataDic["rt"] = GContext.container.Resolve<FishingEventData>().RTSerialize(); ; NewPlayerDataDic["rt"] = GContext.container.Resolve<FishingEventData>().RTSerialize(); ;
PlayFabMgr.Instance.UpdateUserDataValue("NewPlayerDataDic", Newtonsoft.Json.JsonConvert.SerializeObject(NewPlayerDataDic), remote); playerSaveStore.Write("NewPlayerDataDic", Newtonsoft.Json.JsonConvert.SerializeObject(NewPlayerDataDic), remote).RequireAccepted();
} }
public void Init() public void Init()
{ {
@@ -1301,13 +1306,14 @@ namespace GameCore
} }
public async void CoRecoveryEnergy(int energyRecoveryTime) public async void CoRecoveryEnergy(int energyRecoveryTime)
{ {
var store = playerSaveStore;
tokenSource?.Cancel(); tokenSource?.Cancel();
tokenSource = new System.Threading.CancellationTokenSource(); tokenSource = new System.Threading.CancellationTokenSource();
var token = tokenSource.Token; var token = tokenSource.Token;
_isRecoveryEnergy = true; _isRecoveryEnergy = true;
int initEnergyRestorationSpeed = _tables.TbGlobalConfig.InitEnergyRestorationSpeed * 60; int initEnergyRestorationSpeed = _tables.TbGlobalConfig.InitEnergyRestorationSpeed * 60;
while (Energy < MaxEnergy && !token.IsCancellationRequested) while (Energy < MaxEnergy && !token.IsCancellationRequested && store.IsCurrent)
{ {
if (energyRecoveryTime >= initEnergyRestorationSpeed) if (energyRecoveryTime >= initEnergyRestorationSpeed)
{ {
@@ -1340,7 +1346,7 @@ namespace GameCore
SavePlayerData(); SavePlayerData();
} }
await Awaiters.Seconds(1); await Awaiters.Seconds(1);
if (token.IsCancellationRequested) if (token.IsCancellationRequested || !store.IsCurrent)
{ {
return; return;
} }
@@ -1411,14 +1417,17 @@ namespace GameCore
public async void OpenAppraisePopupPanel(int fishId) public async void OpenAppraisePopupPanel(int fishId)
{ {
var store = playerSaveStore;
if (!store.IsCurrent) return;
if (lastMapId != _tables.TbMapData.DataList[0].ID && !IsOpenAppraise) if (lastMapId != _tables.TbMapData.DataList[0].ID && !IsOpenAppraise)
{ {
var fishData = _tables.TbFishData.GetOrDefault(fishId); var fishData = _tables.TbFishData.GetOrDefault(fishId);
if (fishData != null && fishData.Quality >= 5) if (fishData != null && fishData.Quality >= 5)
{ {
await UIManager.Instance.ShowUI(UITypes.AppraisePopupPanel); await UIManager.Instance.ShowUI(UITypes.AppraisePopupPanel);
if (!store.IsCurrent) return;
IsOpenAppraise = true; IsOpenAppraise = true;
PlayFabMgr.Instance.UpdateUserDataValue("IsOpenAppraise", "true"); playerSaveStore.Write("IsOpenAppraise", "true").RequireAccepted();
} }
} }
} }

View File

@@ -1,4 +1,4 @@
using System; using System;
using System.Linq; using System.Linq;
using UnityEngine; using UnityEngine;
using cfg; using cfg;
@@ -52,6 +52,7 @@ namespace GameCore
} }
public partial class PlayerFishData public partial class PlayerFishData
{ {
[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)
@@ -85,6 +86,10 @@ namespace GameCore
public Dictionary<string, float> LuckDataDic = new Dictionary<string, float>(); public Dictionary<string, float> LuckDataDic = new Dictionary<string, float>();
public bool IsOpenPiercing = false; public bool IsOpenPiercing = false;
public int RodSelectedInPack = 0; public int RodSelectedInPack = 0;
public void InitData(Dictionary<string, string> userDatas) public void InitData(Dictionary<string, string> userDatas)
{ {
string data_value; string data_value;
@@ -386,7 +391,7 @@ namespace GameCore
{ {
GContext.container.Resolve<PlayerData>().AddNewRod(id); GContext.container.Resolve<PlayerData>().AddNewRod(id);
rodData.Add(id, 0); rodData.Add(id, 0);
PlayFabMgr.Instance.UpdateUserDataValue("RodData", JsonMapper.ToJson(rodData)); playerSaveStore.Write("RodData", JsonMapper.ToJson(rodData)).RequireAccepted();
//鱼竿红点数据修改 //鱼竿红点数据修改
RedPointManager.Instance.SetRedPointState(RedPointName.Home_Rod, true); RedPointManager.Instance.SetRedPointState(RedPointName.Home_Rod, true);
} }
@@ -487,7 +492,7 @@ namespace GameCore
.AddContent("rod_chips_consume", 0); .AddContent("rod_chips_consume", 0);
} }
#endif #endif
PlayFabMgr.Instance.UpdateUserDataValue("RodLevelData", JsonMapper.ToJson(rodLevelData)); playerSaveStore.Write("RodLevelData", JsonMapper.ToJson(rodLevelData)).RequireAccepted();
GContext.container.Resolve<PlayerData>().SetPearl(GContext.container.Resolve<PlayerData>().pearl - count); GContext.container.Resolve<PlayerData>().SetPearl(GContext.container.Resolve<PlayerData>().pearl - count);
} }
@@ -546,7 +551,7 @@ namespace GameCore
} }
//鱼竿红点数据修改 //鱼竿红点数据修改
CheckRodRedPointUp(); CheckRodRedPointUp();
PlayFabMgr.Instance.UpdateUserDataValue("RodData", JsonMapper.ToJson(rodData)); playerSaveStore.Write("RodData", JsonMapper.ToJson(rodData)).RequireAccepted();
} }
public int GetRodPiece(int id) public int GetRodPiece(int id)
@@ -576,7 +581,7 @@ namespace GameCore
} }
} }
PlayFabMgr.Instance.UpdateUserDataValue("RodData", JsonMapper.ToJson(rodData)); playerSaveStore.Write("RodData", JsonMapper.ToJson(rodData)).RequireAccepted();
} }
public void GMSetStarAll(int lv) public void GMSetStarAll(int lv)
@@ -602,7 +607,7 @@ namespace GameCore
} }
} }
PlayFabMgr.Instance.UpdateUserDataValue("RodData", JsonMapper.ToJson(rodData)); playerSaveStore.Write("RodData", JsonMapper.ToJson(rodData)).RequireAccepted();
} }
public void GMAddAllFishMastery(bool all = false) public void GMAddAllFishMastery(bool all = false)
@@ -1173,7 +1178,7 @@ namespace GameCore
void MapNewUpdateUserDataValue(int mapID, Dictionary<int, FishWeightData> keyValuePairs) void MapNewUpdateUserDataValue(int mapID, Dictionary<int, FishWeightData> keyValuePairs)
{ {
Dictionary<int, string> mapData = keyValuePairs.ToDictionary(_ => _.Key, _ => _.Value.Serialize()); Dictionary<int, string> mapData = keyValuePairs.ToDictionary(_ => _.Key, _ => _.Value.Serialize());
PlayFabMgr.Instance.UpdateUserDataValue($"Map_5_{mapID}", JsonMapper.ToJson(mapData)); playerSaveStore.Write($"Map_5_{mapID}", JsonMapper.ToJson(mapData)).RequireAccepted();
} }
public int GetFishLevelByMap(int mapId) public int GetFishLevelByMap(int mapId)
{ {
@@ -1306,10 +1311,10 @@ namespace GameCore
if (fishId != 0) if (fishId != 0)
{ {
fishSwimToMapCount[id] = 0; fishSwimToMapCount[id] = 0;
PlayFabMgr.Instance.UpdateUserDataValue("FishSwimToMapCount", JsonMapper.ToJson(fishSwimToMapCount)); playerSaveStore.Write("FishSwimToMapCount", JsonMapper.ToJson(fishSwimToMapCount)).RequireAccepted();
} }
fishSwimToMap[id] = fishId; fishSwimToMap[id] = fishId;
PlayFabMgr.Instance.UpdateUserDataValue("FishSwimToMap", JsonMapper.ToJson(fishSwimToMap)); playerSaveStore.Write("FishSwimToMap", JsonMapper.ToJson(fishSwimToMap)).RequireAccepted();
} }
public void SetFishSwimToMapCount(int id, int count) public void SetFishSwimToMapCount(int id, int count)
{ {
@@ -1320,7 +1325,7 @@ namespace GameCore
{ {
SetFishSwimToMap(id, 0); SetFishSwimToMap(id, 0);
} }
PlayFabMgr.Instance.UpdateUserDataValue("FishSwimToMapCount", JsonMapper.ToJson(fishSwimToMapCount)); playerSaveStore.Write("FishSwimToMapCount", JsonMapper.ToJson(fishSwimToMapCount)).RequireAccepted();
} }
} }
//===========================鱼图鉴=========================== //===========================鱼图鉴===========================
@@ -1390,7 +1395,7 @@ namespace GameCore
} }
GContext.container.Resolve<PlayerItemData>().AddItemByDropList(dorpList, null); GContext.container.Resolve<PlayerItemData>().AddItemByDropList(dorpList, null);
PlayFabMgr.Instance.UpdateUserDataValue("MapProgress", Newtonsoft.Json.JsonConvert.SerializeObject(mapProgress)); playerSaveStore.Write("MapProgress", Newtonsoft.Json.JsonConvert.SerializeObject(mapProgress)).RequireAccepted();
} }
public int GetAllPTSByMap(int mapID) public int GetAllPTSByMap(int mapID)

View File

@@ -1,4 +1,4 @@
using cfg; using cfg;
using asap.core; using asap.core;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;

View File

@@ -73,6 +73,13 @@ namespace GameCore
} }
public partial class PlayerShopData : IDisposable public partial class PlayerShopData : IDisposable
{ {
Core.KV.IPlayerSaveStore boundSaveStore;
[asap.core.Inject] public Core.KV.IPlayerSaveStore SaveStore
{
get => boundSaveStore;
set { CouponData.BindSaveStore(value); boundSaveStore = value; }
}
[Inject] [Inject]
public cfg.Tables _tables { get; set; } public cfg.Tables _tables { get; set; }
[Inject] [Inject]
@@ -126,6 +133,11 @@ namespace GameCore
Dictionary<int, int> advertCount = new Dictionary<int, int>(); Dictionary<int, int> advertCount = new Dictionary<int, int>();
#endregion #endregion
public void InitData(Dictionary<string, string> userDatas) public void InitData(Dictionary<string, string> userDatas)
{ {
if (userDatas == null || userDatas.Count < 1) if (userDatas == null || userDatas.Count < 1)
@@ -212,7 +224,7 @@ namespace GameCore
public void AddVoucher(int value) public void AddVoucher(int value)
{ {
Voucher = Voucher + value; Voucher = Voucher + value;
PlayFabMgr.Instance.UpdateUserDataValue("Voucher", Voucher.ToString()); SaveStore.Write("Voucher", Voucher.ToString()).RequireAccepted();
} }
public void Init() public void Init()
{ {
@@ -331,10 +343,10 @@ namespace GameCore
if (Voucher >= iAPItemList.CostToken) if (Voucher >= iAPItemList.CostToken)
{ {
Voucher = Voucher - iAPItemList.CostToken; Voucher = Voucher - iAPItemList.CostToken;
PlayFabMgr.Instance.UpdateUserDataValue("Voucher", Voucher.ToString()); SaveStore.Write("Voucher", Voucher.ToString()).RequireAccepted();
//二次弹窗 //二次弹窗
//回调 //回调
PlayFabMgr.Instance.UpdateUserDataValue("LastPaymentTime", $"{ZZTimeHelper.UtcNow()}__{0}"); SaveStore.Write("LastPaymentTime", $"{ZZTimeHelper.UtcNow()}__{0}").RequireAccepted();
BuySuccess(buyType, iAPItemList, itemDataGift, rewardShowType, true); BuySuccess(buyType, iAPItemList, itemDataGift, rewardShowType, true);
OrderManager.OrderManager.OnOrderSuccess(buyType, iAPItemList, itemDataGift, rewardShowType, true); OrderManager.OrderManager.OnOrderSuccess(buyType, iAPItemList, itemDataGift, rewardShowType, true);
if (!buyType.NoShow) if (!buyType.NoShow)
@@ -391,7 +403,7 @@ namespace GameCore
if (result.isSuccess) if (result.isSuccess)
{ {
PlayFabMgr.Instance.UpdateUserDataValue("LastPaymentTime", $"{ZZTimeHelper.UtcNow()}__{sku.price}"); SaveStore.Write("LastPaymentTime", $"{ZZTimeHelper.UtcNow()}__{sku.price}").RequireAccepted();
BuySuccess(buyType, iAPItemList, itemDataGift, rewardShowType); BuySuccess(buyType, iAPItemList, itemDataGift, rewardShowType);
OrderManager.OrderManager.OnOrderSuccess(buyType, iAPItemList, itemDataGift, rewardShowType); OrderManager.OrderManager.OnOrderSuccess(buyType, iAPItemList, itemDataGift, rewardShowType);
if (!buyType.NoShow) if (!buyType.NoShow)
@@ -566,7 +578,7 @@ namespace GameCore
} }
} }
} }
PlayFabMgr.Instance.UpdateUserDataValue("LastPaymentTime", $"{ZZTimeHelper.UtcNow()}__{data.ProdPrice}"); SaveStore.Write("LastPaymentTime", $"{ZZTimeHelper.UtcNow()}__{data.ProdPrice}").RequireAccepted();
BuySuccess(data.BuyType, iAPItemList, itemDataGift, data.ShowType); BuySuccess(data.BuyType, iAPItemList, itemDataGift, data.ShowType);
if (data.BuyType.extraPurcheData != null) if (data.BuyType.extraPurcheData != null)
{ {
@@ -590,7 +602,7 @@ namespace GameCore
buyData[0] += iAPItemList.PaymentAmount; buyData[0] += iAPItemList.PaymentAmount;
} }
buyData.Add(iAPItemList.ID); buyData.Add(iAPItemList.ID);
PlayFabMgr.Instance.UpdateUserDataValue("BuyData", JsonMapper.ToJson(buyData)); SaveStore.Write("BuyData", JsonMapper.ToJson(buyData)).RequireAccepted();
#if AGG #if AGG
GEvent.SetUserProp("Total_consumption", buyData[0]); GEvent.SetUserProp("Total_consumption", buyData[0]);
#endif #endif
@@ -661,7 +673,7 @@ namespace GameCore
} }
TurntableAdData = null; TurntableAdData = null;
shopPackDailyBuyCount["0"] = ZZTimeHelper.UtcNow().UtcNowOffset().DayOfYear; shopPackDailyBuyCount["0"] = ZZTimeHelper.UtcNow().UtcNowOffset().DayOfYear;
PlayFabMgr.Instance.UpdateUserDataValue("ShopPackBuyCount", JsonMapper.ToJson(shopPackDailyBuyCount)); SaveStore.Write("ShopPackBuyCount", JsonMapper.ToJson(shopPackDailyBuyCount)).RequireAccepted();
} }
//} //}
} }
@@ -691,7 +703,7 @@ namespace GameCore
{ {
shopPackDailyBuyCount["0"] = ZZTimeHelper.UtcNow().UtcNowOffset().DayOfYear; shopPackDailyBuyCount["0"] = ZZTimeHelper.UtcNow().UtcNowOffset().DayOfYear;
} }
PlayFabMgr.Instance.UpdateUserDataValue("ShopPackBuyCount", JsonMapper.ToJson(shopPackDailyBuyCount)); SaveStore.Write("ShopPackBuyCount", JsonMapper.ToJson(shopPackDailyBuyCount)).RequireAccepted();
} }
public int GetShopPackBuyCount(int id) public int GetShopPackBuyCount(int id)
{ {
@@ -748,7 +760,7 @@ namespace GameCore
{ {
shopGiftBuyCount.Add(id, count); shopGiftBuyCount.Add(id, count);
} }
PlayFabMgr.Instance.UpdateUserDataValue("ShopGiftBuyCount", JsonMapper.ToJson(shopGiftBuyCount)); SaveStore.Write("ShopGiftBuyCount", JsonMapper.ToJson(shopGiftBuyCount)).RequireAccepted();
} }
/// <summary> /// <summary>
@@ -817,7 +829,7 @@ namespace GameCore
index = 0, index = 0,
time = time.ToString() time = time.ToString()
}; };
PlayFabMgr.Instance.UpdateUserDataValue("ChainPackDataNew", JsonMapper.ToJson(ChainPackData)); SaveStore.Write("ChainPackDataNew", JsonMapper.ToJson(ChainPackData)).RequireAccepted();
chainPackM = null; chainPackM = null;
} }
if (chainPackM == null) if (chainPackM == null)
@@ -859,7 +871,7 @@ namespace GameCore
{ {
AddAllPackCount(chainPackM.ID, 1); AddAllPackCount(chainPackM.ID, 1);
} }
PlayFabMgr.Instance.UpdateUserDataValue("ChainPackDataNew", JsonMapper.ToJson(ChainPackData)); SaveStore.Write("ChainPackDataNew", JsonMapper.ToJson(ChainPackData)).RequireAccepted();
//链式礼包 特殊处理 //链式礼包 特殊处理
GContext.Publish(new TargetEvent(chainPackM.ID, 3, 10)); GContext.Publish(new TargetEvent(chainPackM.ID, 3, 10));
} }
@@ -889,7 +901,7 @@ namespace GameCore
shopAllPackBuyCount.Add(id, count); shopAllPackBuyCount.Add(id, count);
} }
GContext.Publish(new ConditionTypeEvent(ConditionType.BuyPack, count)); GContext.Publish(new ConditionTypeEvent(ConditionType.BuyPack, count));
PlayFabMgr.Instance.UpdateUserDataValue("ShopAllPackBuyCount", JsonMapper.ToJson(shopAllPackBuyCount)); SaveStore.Write("ShopAllPackBuyCount", JsonMapper.ToJson(shopAllPackBuyCount)).RequireAccepted();
} }
#endregion #endregion
//========================================礼包======================================== //========================================礼包========================================
@@ -927,7 +939,7 @@ namespace GameCore
} }
} }
#endif #endif
PlayFabMgr.Instance.UpdateUserDataValue("AdvertAllCount", JsonMapper.ToJson(advertCount)); SaveStore.Write("AdvertAllCount", JsonMapper.ToJson(advertCount)).RequireAccepted();
GContext.Publish(new ResAddEvent(1006)); GContext.Publish(new ResAddEvent(1006));
} }
public int GetAdticketCount() public int GetAdticketCount()
@@ -953,7 +965,7 @@ namespace GameCore
{ {
advertCount.Add(id, 1); advertCount.Add(id, 1);
} }
PlayFabMgr.Instance.UpdateUserDataValue("AdvertAllCount", JsonMapper.ToJson(advertCount)); SaveStore.Write("AdvertAllCount", JsonMapper.ToJson(advertCount)).RequireAccepted();
return advertCount[id]; return advertCount[id];
} }
public void ClearAdvertCount() public void ClearAdvertCount()
@@ -968,7 +980,7 @@ namespace GameCore
advertCount[-1] = ZZTimeHelper.UtcNow().UtcNowOffset().DayOfYear; advertCount[-1] = ZZTimeHelper.UtcNow().UtcNowOffset().DayOfYear;
advertCount[0] = allCount; advertCount[0] = allCount;
advertCount[-2] = ticket; advertCount[-2] = ticket;
PlayFabMgr.Instance.UpdateUserDataValue("AdvertAllCount", JsonMapper.ToJson(advertCount)); SaveStore.Write("AdvertAllCount", JsonMapper.ToJson(advertCount)).RequireAccepted();
} }
} }
else else
@@ -1012,7 +1024,7 @@ namespace GameCore
AdConfig adConfig1 = adConfigs[VIPAdPackData.index - 1]; AdConfig adConfig1 = adConfigs[VIPAdPackData.index - 1];
VIPAdPackData.cdTime = ZZTimeHelper.UtcNow().UtcNowOffset().AddSeconds(adConfig1.Interval).ToString(); VIPAdPackData.cdTime = ZZTimeHelper.UtcNow().UtcNowOffset().AddSeconds(adConfig1.Interval).ToString();
} }
PlayFabMgr.Instance.UpdateUserDataValue("VIPAdPackData", JsonMapper.ToJson(VIPAdPackData)); SaveStore.Write("VIPAdPackData", JsonMapper.ToJson(VIPAdPackData)).RequireAccepted();
//GContext.Publish(new VIPAdPackDataEvent()); //GContext.Publish(new VIPAdPackDataEvent());
} }
public DateTime GetVIPAdPackTime() public DateTime GetVIPAdPackTime()
@@ -1044,7 +1056,7 @@ namespace GameCore
VIPAdPackData = new AdvertData(); VIPAdPackData = new AdvertData();
VIPAdPackData.day = ZZTimeHelper.UtcNow().UtcNowOffset().DayOfYear; VIPAdPackData.day = ZZTimeHelper.UtcNow().UtcNowOffset().DayOfYear;
VIPAdPackData.isOpen = true; VIPAdPackData.isOpen = true;
PlayFabMgr.Instance.UpdateUserDataValue("VIPAdPackData", JsonMapper.ToJson(VIPAdPackData)); SaveStore.Write("VIPAdPackData", JsonMapper.ToJson(VIPAdPackData)).RequireAccepted();
} }
public bool IsOpenAdPack() public bool IsOpenAdPack()
{ {
@@ -1101,7 +1113,7 @@ namespace GameCore
// { // {
// isOverForToday = true; // isOverForToday = true;
} }
PlayFabMgr.Instance.UpdateUserDataValue("TurntableAdData", Newtonsoft.Json.JsonConvert.SerializeObject(TurntableAdData)); SaveStore.Write("TurntableAdData", Newtonsoft.Json.JsonConvert.SerializeObject(TurntableAdData)).RequireAccepted();
// GContext.Publish(new VIPAdPackDataEvent()); // GContext.Publish(new VIPAdPackDataEvent());
} }
public bool IsTurntableAdMaxOut() public bool IsTurntableAdMaxOut()
@@ -1195,7 +1207,7 @@ namespace GameCore
{ {
_diamondShopItemRecords[itemShopId].hasBought = 1; _diamondShopItemRecords[itemShopId].hasBought = 1;
SetItemShopRedPoint(); SetItemShopRedPoint();
PlayFabMgr.Instance.UpdateUserDataValue("DiamondShopItemRecords", JsonConvert.SerializeObject(_diamondShopItemRecords)); SaveStore.Write("DiamondShopItemRecords", JsonConvert.SerializeObject(_diamondShopItemRecords)).RequireAccepted();
} }
@@ -1210,7 +1222,7 @@ namespace GameCore
{ {
RefreshItemShop(); RefreshItemShop();
_diamondShopRefreshCount++; _diamondShopRefreshCount++;
PlayFabMgr.Instance.UpdateUserDataValue("DiamondShopRefreshCount", _diamondShopRefreshCount.ToString()); SaveStore.Write("DiamondShopRefreshCount", _diamondShopRefreshCount.ToString()).RequireAccepted();
} }
} }
@@ -1250,8 +1262,8 @@ namespace GameCore
_diamondShopRefreshTime = new DateTime(now.Year, now.Month, now.Day).AddDays(1) + timeSpan; _diamondShopRefreshTime = new DateTime(now.Year, now.Month, now.Day).AddDays(1) + timeSpan;
_diamondShopRefreshCount = 0; _diamondShopRefreshCount = 0;
PlayFabMgr.Instance.UpdateUserDataValue("DiamondShopRefreshCount", _diamondShopRefreshCount.ToString()); SaveStore.Write("DiamondShopRefreshCount", _diamondShopRefreshCount.ToString()).RequireAccepted();
PlayFabMgr.Instance.UpdateUserDataValue("DiamondShopRefreshTime", _diamondShopRefreshTime.ToString()); SaveStore.Write("DiamondShopRefreshTime", _diamondShopRefreshTime.ToString()).RequireAccepted();
RefreshItemShop(); RefreshItemShop();
} }
private void RefreshItemShop() private void RefreshItemShop()
@@ -1326,7 +1338,7 @@ namespace GameCore
} }
} }
PlayFabMgr.Instance.UpdateUserDataValue("DiamondShopItemRecords", JsonConvert.SerializeObject(_diamondShopItemRecords)); SaveStore.Write("DiamondShopItemRecords", JsonConvert.SerializeObject(_diamondShopItemRecords)).RequireAccepted();
SetItemShopRedPoint(); SetItemShopRedPoint();
} }
#endregion #endregion

View File

@@ -29,6 +29,8 @@ namespace GameCore
} }
public class SigninData public class SigninData
{ {
[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)
{ {
@@ -37,6 +39,10 @@ namespace GameCore
MonthlySignEvent _monthlySignEvent = null; MonthlySignEvent _monthlySignEvent = null;
public DailySignEvent dailySignEvent public DailySignEvent dailySignEvent
{ {
get => _monthlySignEvent == null ? null : _monthlySignEvent.dailySignEvent; get => _monthlySignEvent == null ? null : _monthlySignEvent.dailySignEvent;
@@ -89,7 +95,7 @@ namespace GameCore
} }
if (isSave) if (isSave)
{ {
PlayFabMgr.Instance.UpdateUserDataValue("MonthlySignEvent", JsonMapper.ToJson(_monthlySignEvent)); SaveStore.Write("MonthlySignEvent", JsonMapper.ToJson(_monthlySignEvent)).RequireAccepted();
} }
bool isReward = CanDailySign(); bool isReward = CanDailySign();
if (isReward) if (isReward)
@@ -141,7 +147,7 @@ namespace GameCore
} }
} }
//GContext.Publish(new ShowData()); //GContext.Publish(new ShowData());
PlayFabMgr.Instance.UpdateUserDataValue("MonthlySignEvent", JsonMapper.ToJson(_monthlySignEvent)); SaveStore.Write("MonthlySignEvent", JsonMapper.ToJson(_monthlySignEvent)).RequireAccepted();
GContext.Publish(new OnSignEvent() { itemDataList = monthlySignReward }); GContext.Publish(new OnSignEvent() { itemDataList = monthlySignReward });
} }
return isSign; return isSign;

View File

@@ -20,6 +20,7 @@ namespace GameCore
} }
public class SmallGameData public class SmallGameData
{ {
[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;
@@ -47,6 +48,10 @@ namespace GameCore
public int FishHuntGameCount => _fishHuntGameCount; public int FishHuntGameCount => _fishHuntGameCount;
public int BombGameCount => _bombGameCount; public int BombGameCount => _bombGameCount;
//public UnityEngine.Vector3 image_extra_pos { set; get; } //public UnityEngine.Vector3 image_extra_pos { set; get; }
public void SetHeistGameCount(int value) public void SetHeistGameCount(int value)
@@ -56,7 +61,7 @@ namespace GameCore
public void SaveHeistGameCount() public void SaveHeistGameCount()
{ {
_heistGameCount++; _heistGameCount++;
PlayFabMgr.Instance.UpdateUserDataValue("HeistGameCount", _heistGameCount.ToString()); playerSaveStore.Write("HeistGameCount", _heistGameCount.ToString()).RequireAccepted();
} }
public void SetBombGameCount(int value) public void SetBombGameCount(int value)
{ {
@@ -65,7 +70,7 @@ namespace GameCore
public void SaveBombGameCount() public void SaveBombGameCount()
{ {
_bombGameCount++; _bombGameCount++;
PlayFabMgr.Instance.UpdateUserDataValue("BombGameCount", _bombGameCount.ToString()); playerSaveStore.Write("BombGameCount", _bombGameCount.ToString()).RequireAccepted();
} }
public void SetFishHuntGameCount(int value) public void SetFishHuntGameCount(int value)
{ {
@@ -74,7 +79,7 @@ namespace GameCore
public void SaveFishHuntGameCount() public void SaveFishHuntGameCount()
{ {
_fishHuntGameCount++; _fishHuntGameCount++;
PlayFabMgr.Instance.UpdateUserDataValue("FishHuntGameCount", _fishHuntGameCount.ToString()); playerSaveStore.Write("FishHuntGameCount", _fishHuntGameCount.ToString()).RequireAccepted();
} }
#region 小游戏仇人 #region 小游戏仇人
public List<OpponentItemData> opponentItemDatas = new List<OpponentItemData>(); public List<OpponentItemData> opponentItemDatas = new List<OpponentItemData>();

View File

@@ -38,6 +38,8 @@ namespace GameCore
} }
public class TriggerPackData public class TriggerPackData
{ {
[asap.core.Inject] public Core.KV.IPlayerSaveStore SaveStore { get; set; }
Tables _tables; Tables _tables;
PlayerShopData playerShopData; PlayerShopData playerShopData;
public TriggerPackData(Tables tables) public TriggerPackData(Tables tables)
@@ -52,6 +54,10 @@ namespace GameCore
//VIP 等级叠加 //VIP 等级叠加
public Dictionary<string, GiftVIPDData> giftVIPDData = new Dictionary<string, GiftVIPDData>(); public Dictionary<string, GiftVIPDData> giftVIPDData = new Dictionary<string, GiftVIPDData>();
public void Init() public void Init()
{ {
playerShopData = GContext.container.Resolve<PlayerShopData>(); playerShopData = GContext.container.Resolve<PlayerShopData>();
@@ -197,7 +203,7 @@ namespace GameCore
id = t.ID, id = t.ID,
endTime = dateTime.AddSeconds(fixedTime.EndTime).ToString(), endTime = dateTime.AddSeconds(fixedTime.EndTime).ToString(),
}; };
PlayFabMgr.Instance.UpdateUserDataValue("FixedTimeShopDic", JsonMapper.ToJson(fixedTimeShopDic)); SaveStore.Write("FixedTimeShopDic", JsonMapper.ToJson(fixedTimeShopDic)).RequireAccepted();
TriggerPackBuyData _newSignEvent = null; TriggerPackBuyData _newSignEvent = null;
for (int i = 0; i < triggerPackList.Count; i++) for (int i = 0; i < triggerPackList.Count; i++)
@@ -228,7 +234,7 @@ namespace GameCore
_newSignEvent.vipLevel = GetVIPLevel(t.PackType); _newSignEvent.vipLevel = GetVIPLevel(t.PackType);
} }
GContext.container.Resolve<IFaceUIService>().AddGiftFaceUIOnLogin(_newSignEvent.ID, UITypes.AdsPackPanel, _newSignEvent.lastID); GContext.container.Resolve<IFaceUIService>().AddGiftFaceUIOnLogin(_newSignEvent.ID, UITypes.AdsPackPanel, _newSignEvent.lastID);
PlayFabMgr.Instance.UpdateUserDataValue("TriggerPackDataList", JsonMapper.ToJson(triggerPackList)); SaveStore.Write("TriggerPackDataList", JsonMapper.ToJson(triggerPackList)).RequireAccepted();
} }
} }
@@ -264,7 +270,7 @@ namespace GameCore
endTime = time, endTime = time,
}; };
fixedTimeShopDic[t.ID] = fixedTimeEvent; fixedTimeShopDic[t.ID] = fixedTimeEvent;
PlayFabMgr.Instance.UpdateUserDataValue("FixedTimeShopDic", JsonMapper.ToJson(fixedTimeShopDic)); SaveStore.Write("FixedTimeShopDic", JsonMapper.ToJson(fixedTimeShopDic)).RequireAccepted();
} }
else if (t.TimeDefinition is LimitedTime) else if (t.TimeDefinition is LimitedTime)
{ {
@@ -283,7 +289,7 @@ namespace GameCore
}; };
triggerPackList.Insert(0, _newSignEvent); triggerPackList.Insert(0, _newSignEvent);
playerShopData.AddShopPackDailyBuyCount(t.PackType, 1, false); playerShopData.AddShopPackDailyBuyCount(t.PackType, 1, false);
PlayFabMgr.Instance.UpdateUserDataValue("TriggerPackDataList", JsonMapper.ToJson(triggerPackList)); SaveStore.Write("TriggerPackDataList", JsonMapper.ToJson(triggerPackList)).RequireAccepted();
} }
} }
/// <summary> /// <summary>
@@ -314,7 +320,7 @@ namespace GameCore
endTime = ZZTimeHelper.UtcNow().UtcNowOffset().AddSeconds(fixedTime.StartTime).ToString(), endTime = ZZTimeHelper.UtcNow().UtcNowOffset().AddSeconds(fixedTime.StartTime).ToString(),
}; };
PlayFabMgr.Instance.UpdateUserDataValue("FixedTimeShopDic", JsonMapper.ToJson(fixedTimeShopDic)); SaveStore.Write("FixedTimeShopDic", JsonMapper.ToJson(fixedTimeShopDic)).RequireAccepted();
} }
if (FixedTimeEventExpired(t.ID)) if (FixedTimeEventExpired(t.ID))
{ {
@@ -347,7 +353,7 @@ namespace GameCore
endTime = ZZTimeHelper.UtcNow().UtcNowOffset().AddSeconds(fixedTime.StartTime).ToString(), endTime = ZZTimeHelper.UtcNow().UtcNowOffset().AddSeconds(fixedTime.StartTime).ToString(),
}; };
PlayFabMgr.Instance.UpdateUserDataValue("FixedTimeShopDic", JsonMapper.ToJson(fixedTimeShopDic)); SaveStore.Write("FixedTimeShopDic", JsonMapper.ToJson(fixedTimeShopDic)).RequireAccepted();
} }
if (FixedTimeEventExpired(t.ID)) if (FixedTimeEventExpired(t.ID))
{ {
@@ -449,7 +455,7 @@ namespace GameCore
} }
if (isUpdate) if (isUpdate)
{ {
PlayFabMgr.Instance.UpdateUserDataValue("GiftVIPDData", JsonMapper.ToJson(giftVIPDData)); SaveStore.Write("GiftVIPDData", JsonMapper.ToJson(giftVIPDData)).RequireAccepted();
} }
} }
} }
@@ -493,7 +499,7 @@ namespace GameCore
}; };
} }
giftVIPDData[PackType] = giftVIPD; giftVIPDData[PackType] = giftVIPD;
PlayFabMgr.Instance.UpdateUserDataValue("GiftVIPDData", JsonMapper.ToJson(giftVIPDData)); SaveStore.Write("GiftVIPDData", JsonMapper.ToJson(giftVIPDData)).RequireAccepted();
} }
} }
#region 触发礼包 #region 触发礼包
@@ -550,7 +556,7 @@ namespace GameCore
}; };
triggerPackList.Insert(0, _newSignEvent); triggerPackList.Insert(0, _newSignEvent);
playerShopData.AddShopPackDailyBuyCount(t.PackType, 1, false); playerShopData.AddShopPackDailyBuyCount(t.PackType, 1, false);
PlayFabMgr.Instance.UpdateUserDataValue("TriggerPackDataList", JsonMapper.ToJson(triggerPackList)); SaveStore.Write("TriggerPackDataList", JsonMapper.ToJson(triggerPackList)).RequireAccepted();
} }
} }
public void AddTriggerPackCount(int id) public void AddTriggerPackCount(int id)
@@ -563,7 +569,7 @@ namespace GameCore
{ {
triggerPackList.Remove(triggerPackList[i]); triggerPackList.Remove(triggerPackList[i]);
} }
PlayFabMgr.Instance.UpdateUserDataValue("TriggerPackDataList", JsonMapper.ToJson(triggerPackList)); SaveStore.Write("TriggerPackDataList", JsonMapper.ToJson(triggerPackList)).RequireAccepted();
break; break;
} }
} }
@@ -618,7 +624,7 @@ namespace GameCore
{ {
triggerPackList.Remove(dailySignEvent); triggerPackList.Remove(dailySignEvent);
GContext.Publish(new TriggerPackEvent()); GContext.Publish(new TriggerPackEvent());
PlayFabMgr.Instance.UpdateUserDataValue("TriggerPackDataList", JsonMapper.ToJson(triggerPackList)); SaveStore.Write("TriggerPackDataList", JsonMapper.ToJson(triggerPackList)).RequireAccepted();
} }
return endTime - curTime; return endTime - curTime;
} }

View File

@@ -3,6 +3,7 @@ using GameCore;
using cfg; using cfg;
public class EventPackData public class EventPackData
{ {
[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
{ {
@@ -19,6 +20,11 @@ public class EventPackData
} }
} }
private Data _data; private Data _data;
public int CurrentEventID { get { return _data.currentEventID; } } public int CurrentEventID { get { return _data.currentEventID; } }
public int PurchaseCount { get { return _data.purchaseCount; } } public int PurchaseCount { get { return _data.purchaseCount; } }
public int VIPLvlWhenEventActivated { get { return _data.VIPLvlWhenEventActivate; } } public int VIPLvlWhenEventActivated { get { return _data.VIPLvlWhenEventActivate; } }
@@ -87,8 +93,8 @@ public class EventPackData
} }
public void SaveEventPackData() public void SaveEventPackData()
{ {
PlayFabMgr.Instance.UpdateUserDataValue("EventPackData", playerSaveStore.Write("EventPackData",
Newtonsoft.Json.JsonConvert.SerializeObject(_data)); Newtonsoft.Json.JsonConvert.SerializeObject(_data)).RequireAccepted();
} }
private void TriggerEventPack() private void TriggerEventPack()
{ {

View File

@@ -100,8 +100,14 @@ namespace DataCenter
// EventPartnerGather 管理器 // EventPartnerGather 管理器
public class EventPartnerGatherManager : IAutoMatchManagerContext public class EventPartnerGatherManager : IAutoMatchManagerContext
{ {
[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";
[Inject] public Tables _tables { get; set; } [Inject] public Tables _tables { get; set; }
[Inject] public EventPartnerService EventPartnerService { get; set; } [Inject] public EventPartnerService EventPartnerService { get; set; }
@@ -819,7 +825,7 @@ namespace DataCenter
Trace($"SaveData eventId={EventId}"); Trace($"SaveData eventId={EventId}");
#endif #endif
{ {
PlayFabMgr.Instance.UpdateUserDataValue(SaveKey, JsonConvert.SerializeObject(GatherData)); SaveStore.Write(SaveKey, JsonConvert.SerializeObject(GatherData)).RequireAccepted();
} }
} }
public void LoadData(string dataValue) public void LoadData(string dataValue)

View File

@@ -11,6 +11,7 @@ namespace game
{ {
public class EventPinballDataManager public class EventPinballDataManager
{ {
[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;
@@ -375,7 +376,7 @@ namespace game
public void SyncData() public void SyncData()
{ {
if (!m_IsSyncData) return; if (!m_IsSyncData) return;
PlayFabMgr.Instance.UpdateUserDataValue(EventPinballConfig.SAVE_DATA_KEY, Newtonsoft.Json.JsonConvert.SerializeObject(m_PinballData)); playerSaveStore.Write(EventPinballConfig.SAVE_DATA_KEY, Newtonsoft.Json.JsonConvert.SerializeObject(m_PinballData)).RequireAccepted();
} }
#endregion #endregion
@@ -449,6 +450,11 @@ namespace game
#region 数据相关 #region 数据相关
private EventPinballData m_PinballData; private EventPinballData m_PinballData;
public EventPinballData PinballData { get { return m_PinballData; } } public EventPinballData PinballData { get { return m_PinballData; } }
public int RewardMaxIndex public int RewardMaxIndex
{ {

View File

@@ -13,6 +13,7 @@ namespace game
{ {
public class EventPotionDataManager public class EventPotionDataManager
{ {
[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;
@@ -125,6 +126,12 @@ namespace game
#region 数据相关 #region 数据相关
private EventPotionData m_PotionData; private EventPotionData m_PotionData;
public EventPotionData PotionData { get { return m_PotionData; } } public EventPotionData PotionData { get { return m_PotionData; } }
public void InitLevel() public void InitLevel()
@@ -523,7 +530,7 @@ namespace game
public void SyncData() public void SyncData()
{ {
if (!m_IsSyncData) return; if (!m_IsSyncData) return;
PlayFabMgr.Instance.UpdateUserDataValue(EventPotionConfig.SAVE_DATA_KEY, Newtonsoft.Json.JsonConvert.SerializeObject(m_PotionData)); playerSaveStore.Write(EventPotionConfig.SAVE_DATA_KEY, Newtonsoft.Json.JsonConvert.SerializeObject(m_PotionData)).RequireAccepted();
} }
public PotionItemData GetOperationItemData() public PotionItemData GetOperationItemData()
{ {

View File

@@ -14,6 +14,7 @@ namespace game
{ {
public class EventScratchTicketDataManager public class EventScratchTicketDataManager
{ {
[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;
@@ -94,6 +95,11 @@ namespace game
#region 数据相关 #region 数据相关
private EventScratchTicketData m_ScratchTicketData; private EventScratchTicketData m_ScratchTicketData;
public EventScratchTicketData ScratchTicketData { get { return m_ScratchTicketData; } } public EventScratchTicketData ScratchTicketData { get { return m_ScratchTicketData; } }
public EventScratchMain GetFreshScratchTicketData() public EventScratchMain GetFreshScratchTicketData()
{ {
@@ -555,7 +561,7 @@ namespace game
public void SyncData() public void SyncData()
{ {
if (!m_IsSyncData) return; if (!m_IsSyncData) return;
PlayFabMgr.Instance.UpdateUserDataValue(EventScratchTicketConfig.SAVE_DATA_KEY, Newtonsoft.Json.JsonConvert.SerializeObject(m_ScratchTicketData)); playerSaveStore.Write(EventScratchTicketConfig.SAVE_DATA_KEY, Newtonsoft.Json.JsonConvert.SerializeObject(m_ScratchTicketData)).RequireAccepted();
} }
List<int> magList = new List<int>() { 1, 5, 10, 20 }; List<int> magList = new List<int>() { 1, 5, 10, 20 };

View File

@@ -13,6 +13,10 @@ public class FishingBoxData
public Dictionary<int, int> FishingNewBoxs = new(); public Dictionary<int, int> FishingNewBoxs = new();
public Dictionary<int, int> FishingBoxOpenNum = new(); public Dictionary<int, int> FishingBoxOpenNum = new();
public void InitData(string data) public void InitData(string data)
{ {
if (string.IsNullOrEmpty(data) == false) if (string.IsNullOrEmpty(data) == false)

View File

@@ -40,6 +40,8 @@ public class FishingChallengeData
public class FishingChallengeManager public class FishingChallengeManager
{ {
[asap.core.Inject] public Core.KV.IPlayerSaveStore SaveStore { get; set; }
// //
private const string SaveKey = "FishingChallengeData"; private const string SaveKey = "FishingChallengeData";
@@ -61,6 +63,11 @@ public class FishingChallengeManager
private DateTime EndTime; private DateTime EndTime;
// //
public FishingChallengeData fishingChallengeData { get; private set; } public FishingChallengeData fishingChallengeData { get; private set; }
public EventChallengeMain eventChallengeMain { get; private set; } public EventChallengeMain eventChallengeMain { get; private set; }
public EventChallengeConfig eventChallengeConfig { get; private set; } public EventChallengeConfig eventChallengeConfig { get; private set; }
public MapData challengeMapData { get; private set; } public MapData challengeMapData { get; private set; }
@@ -1047,7 +1054,7 @@ public class FishingChallengeManager
private void DoSave() private void DoSave()
{ {
PlayFabMgr.Instance.UpdateUserDataValue(SaveKey, JsonConvert.SerializeObject(fishingChallengeData)); SaveStore.Write(SaveKey, JsonConvert.SerializeObject(fishingChallengeData)).RequireAccepted();
} }
public string GetOpenEventKey() public string GetOpenEventKey()

View File

@@ -19,7 +19,7 @@ namespace GameCore
{ {
if (Pack1A1Data == null) if (Pack1A1Data == null)
{ {
string json_duelData = PlayFabMgr.Instance.GetLocalData(Pack1A1DataKey); string json_duelData = playerSaveStore.Read(Pack1A1DataKey).ValueOrThrow();
if (json_duelData != null) if (json_duelData != null)
{ {
Pack1A1Data = Newtonsoft.Json.JsonConvert.DeserializeObject<TriggerPackBuyData>(json_duelData); Pack1A1Data = Newtonsoft.Json.JsonConvert.DeserializeObject<TriggerPackBuyData>(json_duelData);
@@ -42,7 +42,7 @@ namespace GameCore
}; };
RedPointManager.Instance.SetRedPointState(HomeBtnOneOnePack.redKey, false); RedPointManager.Instance.SetRedPointState(HomeBtnOneOnePack.redKey, false);
PlayFabMgr.Instance.UpdateUserDataValue(Pack1A1DataKey, JsonMapper.ToJson(Pack1A1Data)); playerSaveStore.Write(Pack1A1DataKey, JsonMapper.ToJson(Pack1A1Data)).RequireAccepted();
} }
public void SetPack1A1Index(int index) public void SetPack1A1Index(int index)
{ {
@@ -58,7 +58,7 @@ namespace GameCore
GContext.Publish(new TargetEvent(Pack1A1Data.lastID, 3, 11)); GContext.Publish(new TargetEvent(Pack1A1Data.lastID, 3, 11));
} }
RedPointManager.Instance.SetRedPointState(HomeBtnOneOnePack.redKey, Pack1A1Data.index == 1); RedPointManager.Instance.SetRedPointState(HomeBtnOneOnePack.redKey, Pack1A1Data.index == 1);
PlayFabMgr.Instance.UpdateUserDataValue(Pack1A1DataKey, JsonMapper.ToJson(Pack1A1Data)); playerSaveStore.Write(Pack1A1DataKey, JsonMapper.ToJson(Pack1A1Data)).RequireAccepted();
} }
public int GetPack1A1Index() public int GetPack1A1Index()
{ {

View File

@@ -50,7 +50,7 @@ namespace GameCore
{ {
if (Pack1A2Data == null) if (Pack1A2Data == null)
{ {
string json_duelData = PlayFabMgr.Instance.GetLocalData(Pack1A2DataKey); string json_duelData = playerSaveStore.Read(Pack1A2DataKey).ValueOrThrow();
if (json_duelData != null) if (json_duelData != null)
{ {
Pack1A2Data = Newtonsoft.Json.JsonConvert.DeserializeObject<Pack1A2Data>(json_duelData); Pack1A2Data = Newtonsoft.Json.JsonConvert.DeserializeObject<Pack1A2Data>(json_duelData);
@@ -74,7 +74,7 @@ namespace GameCore
SetType(); SetType();
SetRodDstId(); SetRodDstId();
SetChainPackRed(); SetChainPackRed();
PlayFabMgr.Instance.UpdateUserDataValue(Pack1A2DataKey, JsonMapper.ToJson(Pack1A2Data)); playerSaveStore.Write(Pack1A2DataKey, JsonMapper.ToJson(Pack1A2Data)).RequireAccepted();
} }
void SetType() void SetType()
{ {
@@ -109,7 +109,7 @@ namespace GameCore
return; return;
} }
Pack1A2Data.index++; Pack1A2Data.index++;
PlayFabMgr.Instance.UpdateUserDataValue(Pack1A2DataKey, JsonMapper.ToJson(Pack1A2Data)); playerSaveStore.Write(Pack1A2DataKey, JsonMapper.ToJson(Pack1A2Data)).RequireAccepted();
SetChainPackRed(); SetChainPackRed();
} }
void SetChainPackRed() void SetChainPackRed()

View File

@@ -87,9 +87,12 @@ namespace GameCore
} }
public async void ShowRankTierChange() public async void ShowRankTierChange()
{ {
var store = playerSaveStore;
if (!store.IsCurrent) return;
if (rankTierChangeState > 0 && curSolomain != null) if (rankTierChangeState > 0 && curSolomain != null)
{ {
GameObject go = await UIManager.Instance.ShowUI(UITypes.EventFishingDuelRankChangePopupPanel); GameObject go = await UIManager.Instance.ShowUI(UITypes.EventFishingDuelRankChangePopupPanel);
if (!store.IsCurrent) return;
EventFishingDuelRankChangePopupPanel changePopupPanel = go.GetComponent<EventFishingDuelRankChangePopupPanel>(); EventFishingDuelRankChangePopupPanel changePopupPanel = go.GetComponent<EventFishingDuelRankChangePopupPanel>();
changePopupPanel.SetRank(rankTierChangeState == 1, curSolomain); changePopupPanel.SetRank(rankTierChangeState == 1, curSolomain);
rankTierChangeState = 0; rankTierChangeState = 0;
@@ -98,7 +101,7 @@ namespace GameCore
} }
public void SoloCheckClaimReward() public void SoloCheckClaimReward()
{ {
string json_duelData = PlayFabMgr.Instance.GetLocalData(FishingDuelManager.SoloDataKey); string json_duelData = playerSaveStore.Read(FishingDuelManager.SoloDataKey).ValueOrThrow();
if (json_duelData != null) if (json_duelData != null)
{ {
duelData = Newtonsoft.Json.JsonConvert.DeserializeObject<DuelData>(json_duelData); duelData = Newtonsoft.Json.JsonConvert.DeserializeObject<DuelData>(json_duelData);
@@ -120,7 +123,7 @@ namespace GameCore
duelData.rewardEventID = 0; duelData.rewardEventID = 0;
duelData.preProgress = 0; duelData.preProgress = 0;
string json_duelData = Newtonsoft.Json.JsonConvert.SerializeObject(duelData); string json_duelData = Newtonsoft.Json.JsonConvert.SerializeObject(duelData);
PlayFabMgr.Instance.UpdateUserDataValue(FishingDuelManager.SoloDataKey, json_duelData); playerSaveStore.Write(FishingDuelManager.SoloDataKey, json_duelData).RequireAccepted();
} }
} }
public void GMAddSoloProgress(int count) public void GMAddSoloProgress(int count)
@@ -133,7 +136,7 @@ namespace GameCore
} }
public void InitSoloData(FishingEvent t) public void InitSoloData(FishingEvent t)
{ {
string json_duelData = PlayFabMgr.Instance.GetLocalData(FishingDuelManager.SoloDataKey); string json_duelData = playerSaveStore.Read(FishingDuelManager.SoloDataKey).ValueOrThrow();
if (json_duelData != null) if (json_duelData != null)
{ {
duelData = Newtonsoft.Json.JsonConvert.DeserializeObject<DuelData>(json_duelData); duelData = Newtonsoft.Json.JsonConvert.DeserializeObject<DuelData>(json_duelData);
@@ -262,7 +265,7 @@ namespace GameCore
public void AddPVPToken(int value) public void AddPVPToken(int value)
{ {
PVPToken += value; PVPToken += value;
PlayFabMgr.Instance.UpdateUserDataValue("PVPToken", PVPToken.ToString()); playerSaveStore.Write("PVPToken", PVPToken.ToString()).RequireAccepted();
if (value < 0 && duelData != null) if (value < 0 && duelData != null)
{ {
duelData.rePVPToken -= value; duelData.rePVPToken -= value;
@@ -272,7 +275,7 @@ namespace GameCore
public void SaveDuelData() public void SaveDuelData()
{ {
string json_duelData = Newtonsoft.Json.JsonConvert.SerializeObject(duelData); string json_duelData = Newtonsoft.Json.JsonConvert.SerializeObject(duelData);
PlayFabMgr.Instance.UpdateUserDataValue(FishingDuelManager.SoloDataKey, json_duelData); playerSaveStore.Write(FishingDuelManager.SoloDataKey, json_duelData).RequireAccepted();
} }
#endregion SoloData #endregion SoloData
} }

View File

@@ -41,6 +41,7 @@ namespace DataCenter
public class OfferChainsChestManager public class OfferChainsChestManager
{ {
[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; }
@@ -51,6 +52,11 @@ namespace DataCenter
public List<EventOfferChainsChestRewards> OfferChainsChestRewards { get; set; } public List<EventOfferChainsChestRewards> OfferChainsChestRewards { get; set; }
// 当前可保存数据 // 当前可保存数据
public TOfferChainsData OfferChainsData { get; private set; } public TOfferChainsData OfferChainsData { get; private set; }
public List<int> ValidIds; public List<int> ValidIds;
public bool IsFirstOpen public bool IsFirstOpen
@@ -255,7 +261,7 @@ namespace DataCenter
private void SaveData() private void SaveData()
{ {
ELog($"SaveData-> {JsonConvert.SerializeObject(OfferChainsData)}"); ELog($"SaveData-> {JsonConvert.SerializeObject(OfferChainsData)}");
PlayFabMgr.Instance.UpdateUserDataValue(SaveKey, JsonConvert.SerializeObject(OfferChainsData)); playerSaveStore.Write(SaveKey, JsonConvert.SerializeObject(OfferChainsData)).RequireAccepted();
} }
public void LoadData(string dataValue) public void LoadData(string dataValue)
@@ -267,6 +273,7 @@ namespace DataCenter
// 进入游戏舞台 // 进入游戏舞台
public async void OnEnterGameAct() public async void OnEnterGameAct()
{ {
if (!playerSaveStore.IsCurrent) return;
Log("OnEnterGameAct()"); Log("OnEnterGameAct()");
IsFirstOpen = false; IsFirstOpen = false;
await UIManager.Instance.ShowUI(new UIType(OfferChainsChestMain.UIPanel)); await UIManager.Instance.ShowUI(new UIType(OfferChainsChestMain.UIPanel));

View File

@@ -44,6 +44,7 @@ namespace DataCenter
// Disco Manager // Disco Manager
public class OfferDiscoPartyManager public class OfferDiscoPartyManager
{ {
[asap.core.Inject] public Core.KV.IPlayerSaveStore playerSaveStore { get; set; }
public const string SaveKey = "OfferDiscoPartyManager"; public const string SaveKey = "OfferDiscoPartyManager";
// 免费抽奖次数 // 免费抽奖次数
@@ -62,6 +63,11 @@ namespace DataCenter
public TOfferDiscoPartyData OfferDiscoPartyData { get; private set; } public TOfferDiscoPartyData OfferDiscoPartyData { get; private set; }
public List<int> ValidIds; // public List<int> ValidIds; //
#endregion #endregion
@@ -269,7 +275,7 @@ namespace DataCenter
private void SaveData() private void SaveData()
{ {
PlayFabMgr.Instance.UpdateUserDataValue(SaveKey, JsonConvert.SerializeObject(OfferDiscoPartyData)); playerSaveStore.Write(SaveKey, JsonConvert.SerializeObject(OfferDiscoPartyData)).RequireAccepted();
} }
public void LoadData(string dataValue) public void LoadData(string dataValue)
@@ -287,6 +293,7 @@ namespace DataCenter
public async void OnEnterGameAct() public async void OnEnterGameAct()
{ {
if (!playerSaveStore.IsCurrent) return;
ELog("OnEnterGameAct()"); ELog("OnEnterGameAct()");
IsFirstOpen = false; IsFirstOpen = false;
await UIManager.Instance.ShowUI(new UIType(OfferDiscoParty.UIPanel)); await UIManager.Instance.ShowUI(new UIType(OfferDiscoParty.UIPanel));

View File

@@ -8,6 +8,7 @@ using game;
using System.Linq; using System.Linq;
public class PiggyBankPackData public class PiggyBankPackData
{ {
[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;
@@ -34,14 +35,22 @@ public class PiggyBankPackData
} }
} }
private Data _data; private Data _data;
public bool IsWithinEventTime public bool IsWithinEventTime
{ {
get get
{ {
if (!_tables.TbFishingEvent.DataMap.Keys.Contains(_data.currentEventID)) var fishingEvent = _tables.TbFishingEvent.GetOrDefault(_data.currentEventID);
return false; if (!(fishingEvent?.TimeDefinition is LimitedTime time)) return false;
var et = DateTime.Parse((_tables.TbFishingEvent[_data.currentEventID].TimeDefinition as LimitedTime).EndTime); var et = DateTime.Parse(time.EndTime);
var st = DateTime.Parse((_tables.TbFishingEvent[_data.currentEventID].TimeDefinition as LimitedTime).StartTime); var st = DateTime.Parse(time.StartTime);
return ZZTimeHelper.UtcNow() >= st && ZZTimeHelper.UtcNow() < et; return ZZTimeHelper.UtcNow() >= st && ZZTimeHelper.UtcNow() < et;
} }
} }
@@ -160,8 +169,8 @@ public class PiggyBankPackData
} }
public void SaveData() public void SaveData()
{ {
PlayFabMgr.Instance.UpdateUserDataValue("PiggyBankPackData", playerSaveStore.Write("PiggyBankPackData",
Newtonsoft.Json.JsonConvert.SerializeObject(_data)); Newtonsoft.Json.JsonConvert.SerializeObject(_data)).RequireAccepted();
} }
public bool AddProgress(int fishTier, int magnification) public bool AddProgress(int fishTier, int magnification)
{ {

View File

@@ -74,7 +74,7 @@ namespace GameCore
public void SaveInitRodAccessoriesData() public void SaveInitRodAccessoriesData()
{ {
string value = Newtonsoft.Json.JsonConvert.SerializeObject(rodAccessoriesData); string value = Newtonsoft.Json.JsonConvert.SerializeObject(rodAccessoriesData);
PlayFabMgr.Instance.UpdateUserDataValue(InitRodAccessoriesDataKey, value); playerSaveStore.Write(InitRodAccessoriesDataKey, value).RequireAccepted();
} }
/// <summary> /// <summary>
/// 手是否解锁 /// 手是否解锁
@@ -272,7 +272,7 @@ namespace GameCore
.AddContent("rod_chips_consume", rquiredFragments); .AddContent("rod_chips_consume", rquiredFragments);
} }
#endif #endif
PlayFabMgr.Instance.UpdateUserDataValue("RodData", JsonMapper.ToJson(rodData)); playerSaveStore.Write("RodData", JsonMapper.ToJson(rodData)).RequireAccepted();
//鱼竿红点数据修改 //鱼竿红点数据修改
CheckRodRedPointUp(); CheckRodRedPointUp();
//合成鱼竿 //合成鱼竿
@@ -354,7 +354,7 @@ namespace GameCore
public void SaveHonorLevelData() public void SaveHonorLevelData()
{ {
string value = Newtonsoft.Json.JsonConvert.SerializeObject(_honorlevel); string value = Newtonsoft.Json.JsonConvert.SerializeObject(_honorlevel);
PlayFabMgr.Instance.UpdateUserDataValue(HonorLevelDataKey, value); playerSaveStore.Write(HonorLevelDataKey, value).RequireAccepted();
} }
#endregion 荣耀等级 #endregion 荣耀等级

View File

@@ -1,4 +1,4 @@
using asap.core; using asap.core;
using cfg; using cfg;
using game; using game;
using System; using System;

View File

@@ -1,4 +1,4 @@
using asap.core; using asap.core;
using cfg; using cfg;
using game; using game;
using LitJson; using LitJson;
@@ -17,12 +17,14 @@ namespace GameCore
} }
public class TotalRechargeManager public class TotalRechargeManager
{ {
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()
{ {
var totalRechargeData = new TotalRechargeData(); var totalRechargeData = new TotalRechargeData();
string data = PlayFabMgr.Instance.GetLocalData(TotalRechargeDataKey); string data = saveStore.Read(TotalRechargeDataKey).ValueOrThrow();
if (!string.IsNullOrEmpty(data)) if (!string.IsNullOrEmpty(data))
{ {
try try
@@ -175,7 +177,7 @@ namespace GameCore
void SaveTotalRechargeData(TotalRechargeData totalRechargeData) void SaveTotalRechargeData(TotalRechargeData totalRechargeData)
{ {
PlayFabMgr.Instance.UpdateUserDataValue(TotalRechargeDataKey, JsonMapper.ToJson(totalRechargeData)); saveStore.Write(TotalRechargeDataKey, JsonMapper.ToJson(totalRechargeData)).RequireAccepted();
} }
} }
} }

View File

@@ -1,4 +1,4 @@
using asap.core; using asap.core;
using game; using game;
using GameCore; using GameCore;
using IngameDebugConsole; using IngameDebugConsole;
@@ -473,20 +473,28 @@ public class DebugFunc
private static void LoginTime(int hours) private static void LoginTime(int hours)
{ {
var userService = GContext.container.Resolve<game.IUserService>() as game.PlayFabUserService; var userService = GContext.container.Resolve<game.IUserService>();
if (userService == null) return; if (userService is not game.PlayFabUserService && userService is not game.KvUserService)
{
return;
}
var loginTime = userService.LoginTime; var loginTime = userService.LoginTime;
var newLoginTime = loginTime.AddHours(hours); var newLoginTime = loginTime.AddHours(hours);
var prop = userService.GetType().GetProperty("LoginTime"); var prop = userService is game.KvUserService
? typeof(game.KvUserService).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}");
} }
private static void AccoutInfo() private static void AccoutInfo()
{ {
var userService = GContext.container.Resolve<game.IUserService>() as game.PlayFabUserService; var userService = GContext.container.Resolve<game.IUserService>();
if (userService == null) return; if (userService is not game.PlayFabUserService && userService is not game.KvUserService)
{
return;
}
Debug.Log($"pfid: {userService.UserId} tyid: {userService.CustomId} lastLogin: {userService.LoginTime}"); Debug.Log($"pfid: {userService.UserId} tyid: {userService.CustomId} lastLogin: {userService.LoginTime}");
if (PlayerPrefs.HasKey("KEY_ACCOUNT_INFO")) if (PlayerPrefs.HasKey("KEY_ACCOUNT_INFO"))
@@ -720,7 +728,7 @@ public class DebugFunc
} }
static void ClearLastPaymentTime() static void ClearLastPaymentTime()
{ {
PlayFabMgr.Instance.UpdateUserDataValue("LastPaymentTime", ""); GContext.container.Resolve<Core.KV.IPlayerSaveStore>().Write("LastPaymentTime", "").RequireAccepted();
} }
static void HookWheelForecActive() static void HookWheelForecActive()
{ {

View File

@@ -607,6 +607,13 @@ public class EventFishingDuelPanel : BasePanel
} }
void CreateMatchmakingTicket() void CreateMatchmakingTicket()
{ {
if (GContext.container.Resolve<IUserService>() is not PlayFabUserService &&
GContext.container.Resolve<IUserService>() is not KvUserService)
{
Debug.LogWarning("KV matchmaking service is not connected.");
FDM.MachingEnd(null);
return;
}
int MaxMatchTime = FDM.eventSoloConfig.MaxMatchTime; int MaxMatchTime = FDM.eventSoloConfig.MaxMatchTime;
//可匹配段位列表,杯数,选择倍率,地图列表,随机种子seed,鱼卡等级 //可匹配段位列表,杯数,选择倍率,地图列表,随机种子seed,鱼卡等级
List<int> matchTierList = FDM.eventSolomain.MatchTierList; List<int> matchTierList = FDM.eventSolomain.MatchTierList;
@@ -625,32 +632,14 @@ public class EventFishingDuelPanel : BasePanel
IUserService userService = GContext.container.Resolve<IUserService>(); IUserService userService = GContext.container.Resolve<IUserService>();
List<string> Group = FDM.GetGroups(); List<string> Group = FDM.GetGroups();
string details = GetDetails(); string details = GetDetails();
var request = new CreateMatchmakingTicketRequest var attributes = new
{
Creator = new MatchmakingPlayer
{
Entity = new EntityKey
{
Id = userService.entityKey.Id,
Type = userService.entityKey.Type
},
Attributes = new MatchmakingPlayerAttributes
{
DataObject = new
{ {
Tier = matchTierListStr.ToArray(), Tier = matchTierListStr.ToArray(),
Map = mapIDsStr.ToArray(), Map = mapIDsStr.ToArray(),
Group = Group.ToArray(), Group = Group.ToArray(),
Details = details, Details = details,
}
}
},
GiveUpAfterSeconds = MaxMatchTime,
QueueName = FishingDuelManager.QueueName // 需要根据实际的匹配队列名称修改
}; };
Action<CreateMatchmakingTicketResult> success = (result) =>
PlayFabMultiplayerAPI.CreateMatchmakingTicket(request,
(result) =>
{ {
currentTicketId = result.TicketId; currentTicketId = result.TicketId;
Debug.Log("匹配请求已创建,等待匹配..."); Debug.Log("匹配请求已创建,等待匹配...");
@@ -659,12 +648,42 @@ public class EventFishingDuelPanel : BasePanel
StartCoroutine(CheckMatchmakingStatus()); StartCoroutine(CheckMatchmakingStatus());
//显示取消按钮 //显示取消按钮
but_stop.gameObject.SetActive(true); but_stop.gameObject.SetActive(true);
}, };
(error) => Action<string> failure = (error) =>
{ {
Debug.LogError("创建匹配请求失败: " + error.GenerateErrorReport()); Debug.LogError("创建匹配请求失败: " + error);
FDM.MachingEnd(null); FDM.MachingEnd(null);
}); };
if (userService is KvUserService)
{
RequestKVMatch("CreateMatchmakingTicket", new
{
queueName = FishingDuelManager.QueueName,
giveUpAfterSeconds = MaxMatchTime,
attributes
}, response => response.ToObject<CreateMatchmakingTicketResult>(), success, failure);
}
else
{
var request = new CreateMatchmakingTicketRequest
{
Creator = new MatchmakingPlayer
{
Entity = new EntityKey
{
Id = ((PlayFabUserService)userService).entityKey.Id,
Type = ((PlayFabUserService)userService).entityKey.Type
},
Attributes = new MatchmakingPlayerAttributes
{
DataObject = attributes
}
},
GiveUpAfterSeconds = MaxMatchTime,
QueueName = FishingDuelManager.QueueName
};
PlayFabMultiplayerAPI.CreateMatchmakingTicket(request, success, error => failure(error.GenerateErrorReport()));
}
} }
IEnumerator CheckMatchmakingStatus() IEnumerator CheckMatchmakingStatus()
@@ -674,7 +693,9 @@ public class EventFishingDuelPanel : BasePanel
yield return new WaitForSeconds(3f); yield return new WaitForSeconds(3f);
if (string.IsNullOrEmpty(currentTicketId)) if (string.IsNullOrEmpty(currentTicketId))
{
yield break; yield break;
}
var request = new GetMatchmakingTicketRequest var request = new GetMatchmakingTicketRequest
{ {
@@ -683,8 +704,7 @@ public class EventFishingDuelPanel : BasePanel
EscapeObject = false EscapeObject = false
}; };
PlayFabMultiplayerAPI.GetMatchmakingTicket(request, Action<GetMatchmakingTicketResult> success = (result) =>
(result) =>
{ {
string statusInfo = "匹配状态: " + result.Status; string statusInfo = "匹配状态: " + result.Status;
if (result.MatchId != null) if (result.MatchId != null)
@@ -707,16 +727,35 @@ public class EventFishingDuelPanel : BasePanel
MachingEnd(); MachingEnd();
//} //}
} }
}, };
(error) => Action<string> failure = (error) =>
{ {
Debug.LogError("检查匹配状态失败: " + error.GenerateErrorReport()); Debug.LogError("检查匹配状态失败: " + error);
MachingEnd(); MachingEnd();
}); };
if (GContext.container.Resolve<IUserService>() is KvUserService)
{
RequestKVMatch("GetMatchmakingTicket", new
{
queueName = request.QueueName,
ticketId = request.TicketId
}, response => response.ToObject<GetMatchmakingTicketResult>(), success, failure);
}
else
{
PlayFabMultiplayerAPI.GetMatchmakingTicket(request, success, error => failure(error.GenerateErrorReport()));
}
} }
} }
void GetMatchDetails(string matchId) void GetMatchDetails(string matchId)
{ {
if (GContext.container.Resolve<IUserService>() is not PlayFabUserService &&
GContext.container.Resolve<IUserService>() is not KvUserService)
{
Debug.LogWarning("KV matchmaking service is not connected.");
FDM.MachingEnd(null);
return;
}
var request = new PlayFab.MultiplayerModels.GetMatchRequest var request = new PlayFab.MultiplayerModels.GetMatchRequest
{ {
MatchId = matchId, MatchId = matchId,
@@ -725,8 +764,7 @@ public class EventFishingDuelPanel : BasePanel
ReturnMemberAttributes = true ReturnMemberAttributes = true
}; };
PlayFabMultiplayerAPI.GetMatch(request, Action<GetMatchResult> success = (result) =>
(result) =>
{ {
Debug.Log("匹配成功: " + result.MatchId); Debug.Log("匹配成功: " + result.MatchId);
if (string.IsNullOrEmpty(currentTicketId)) if (string.IsNullOrEmpty(currentTicketId))
@@ -745,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.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[]>();
@@ -765,12 +803,24 @@ public class EventFishingDuelPanel : BasePanel
FDM.MachingEnd(result.MatchId, matchDetails, mapArray); FDM.MachingEnd(result.MatchId, matchDetails, mapArray);
StartCoroutine(CheckReady()); StartCoroutine(CheckReady());
} }
}, };
(error) => Action<string> failure = (error) =>
{ {
Debug.LogError("获取匹配详情失败: " + error.GenerateErrorReport()); Debug.LogError("获取匹配详情失败: " + error);
MachingEnd(); MachingEnd();
}); };
if (GContext.container.Resolve<IUserService>() is KvUserService)
{
RequestKVMatch("GetMatch", new
{
queueName = request.QueueName,
matchId = request.MatchId
}, ReadKVMatch, success, failure);
}
else
{
PlayFabMultiplayerAPI.GetMatch(request, success, error => failure(error.GenerateErrorReport()));
}
} }
IEnumerator CheckReady() IEnumerator CheckReady()
{ {
@@ -794,7 +844,9 @@ public class EventFishingDuelPanel : BasePanel
void CancelMatchmaking() void CancelMatchmaking()
{ {
if (string.IsNullOrEmpty(currentTicketId)) if (string.IsNullOrEmpty(currentTicketId))
{
return; return;
}
but_stop.gameObject.SetActive(false); but_stop.gameObject.SetActive(false);
var request = new CancelMatchmakingTicketRequest var request = new CancelMatchmakingTicketRequest
{ {
@@ -802,16 +854,99 @@ public class EventFishingDuelPanel : BasePanel
QueueName = FishingDuelManager.QueueName // 需要根据实际的匹配队列名称修改 QueueName = FishingDuelManager.QueueName // 需要根据实际的匹配队列名称修改
}; };
currentTicketId = null; currentTicketId = null;
PlayFabMultiplayerAPI.CancelMatchmakingTicket(request, Action<CancelMatchmakingTicketResult> success = (result) =>
(result) =>
{ {
Debug.Log("匹配已取消: "); Debug.Log("匹配已取消: ");
}, };
(error) => Action<string> failure = (error) =>
{ {
Debug.LogError("取消匹配失败: " + error.GenerateErrorReport()); Debug.LogError("取消匹配失败: " + error);
};
if (GContext.container.Resolve<IUserService>() is KvUserService)
{
RequestKVMatch("CancelMatchmakingTicket", new
{
queueName = request.QueueName,
ticketId = request.TicketId
}, response => response.ToObject<CancelMatchmakingTicketResult>(), success, failure);
}
else
{
PlayFabMultiplayerAPI.CancelMatchmakingTicket(request, success, error => failure(error.GenerateErrorReport()));
}
}
private static async void RequestKVMatch<T>(string action, object request, Func<JObject, T> parse,
Action<T> success, Action<string> failure)
{
string body = await GContext.container.Resolve<ICustomServerMgr>().CustomServerPost("matchmaking/" + action,
Newtonsoft.Json.JsonConvert.SerializeObject(request));
T result = default;
string error = "KV matchmaking request failed";
bool accepted = false;
try
{
if (!string.IsNullOrEmpty(body))
{
var response = Newtonsoft.Json.JsonConvert.DeserializeObject<JObject>(body);
if (response != null && response["code"]?.ToObject<int>(Newtonsoft.Json.JsonSerializer.CreateDefault()) == 1)
{
result = parse(response);
accepted = true;
}
}
}
catch (Newtonsoft.Json.JsonException ex)
{
error = "KV matchmaking response: " + ex.GetType().Name;
}
if (accepted)
{
success(result);
}
else
{
failure(error);
}
}
private static GetMatchResult ReadKVMatch(JObject response)
{
var result = new GetMatchResult
{
MatchId = response["matchId"]?.ToObject<string>()
};
var members = response["members"];
if (members == null || members.Type == JTokenType.Null)
{
return result;
}
if (members is not JArray array)
{
throw new Newtonsoft.Json.JsonSerializationException("Invalid match members.");
}
result.Members = new List<MatchmakingPlayerWithTeamAssignment>();
foreach (var token in array)
{
if (token is not JObject member || member["playerId"]?.Type != JTokenType.String ||
member["attributes"] is not JObject attributes)
{
throw new Newtonsoft.Json.JsonSerializationException("Invalid match member.");
}
result.Members.Add(new MatchmakingPlayerWithTeamAssignment
{
Entity = new EntityKey
{
Id = member["playerId"].ToObject<string>()
},
Attributes = new MatchmakingPlayerAttributes
{
DataObject = attributes
}
}); });
} }
return result;
}
#endregion Matchmaking #endregion Matchmaking
} }

View File

@@ -16,6 +16,8 @@ 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 Core.KV.IPlayerSaveStore SaveStore { get; set; }
bool disposed;
[Inject] [Inject]
public Tables Tables public Tables Tables
@@ -28,6 +30,7 @@ namespace game
public virtual void Dispose() public virtual void Dispose()
{ {
disposed = true;
Disposables?.Dispose(); Disposables?.Dispose();
} }
@@ -38,6 +41,7 @@ namespace game
protected virtual void OnEvent_TargetEvent(TargetEvent targetEvent) protected virtual void OnEvent_TargetEvent(TargetEvent targetEvent)
{ {
if (disposed || SaveStore == null || !SaveStore.IsCurrent) return;
if (targetEvent.type == EventType && targetEvent.subType == EventSubType) RefreshData(targetEvent.id); if (targetEvent.type == EventType && targetEvent.subType == EventSubType) RefreshData(targetEvent.id);
} }
@@ -54,7 +58,8 @@ namespace game
#if UNITY_EDITOR #if UNITY_EDITOR
Debug.Log($"jsd {typeof(T).Name} SaveData {JsonConvert.SerializeObject(Data)}"); Debug.Log($"jsd {typeof(T).Name} SaveData {JsonConvert.SerializeObject(Data)}");
#endif #endif
PlayFabMgr.Instance.UpdateUserDataValue(typeof(T).Name, JsonConvert.SerializeObject(Data)); if (disposed) throw new ObjectDisposedException(GetType().Name);
SaveStore.Write(typeof(T).Name, JsonConvert.SerializeObject(Data)).RequireAccepted();
} }
protected virtual void SaveData(T data) protected virtual void SaveData(T data)
@@ -62,7 +67,8 @@ namespace game
#if UNITY_EDITOR #if UNITY_EDITOR
Debug.Log($"jsd {typeof(T).Name} SaveData {JsonConvert.SerializeObject(data)}"); Debug.Log($"jsd {typeof(T).Name} SaveData {JsonConvert.SerializeObject(data)}");
#endif #endif
PlayFabMgr.Instance.UpdateUserDataValue(typeof(T).Name, JsonConvert.SerializeObject(data)); if (disposed) throw new ObjectDisposedException(GetType().Name);
SaveStore.Write(typeof(T).Name, JsonConvert.SerializeObject(data)).RequireAccepted();
} }
protected virtual void RefreshData(int eventId) protected virtual void RefreshData(int eventId)
@@ -82,7 +88,8 @@ namespace game
protected T GetCacheData() protected T GetCacheData()
{ {
var dataStr = PlayFabMgr.Instance.GetLocalData(typeof(T).Name); if (disposed) throw new ObjectDisposedException(GetType().Name);
var dataStr = SaveStore.Read(typeof(T).Name).ValueOrThrow();
#if UNITY_EDITOR #if UNITY_EDITOR
Debug.Log($"jsd {typeof(T).Name} LoadData sData {dataStr}"); Debug.Log($"jsd {typeof(T).Name} LoadData sData {dataStr}");
#endif #endif

View File

@@ -1,4 +1,4 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using GameCore; using GameCore;
using System; using System;
@@ -410,6 +410,8 @@ public class EventBingoStepState
public class EventBingoPlayfabData public class EventBingoPlayfabData
{ {
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;
private const int TokenCount = 8; private const int TokenCount = 8;
@@ -473,12 +475,12 @@ public class EventBingoPlayfabData
public void Save() public void Save()
{ {
PlayFabMgr.Instance.UpdateUserDataValue(Key, Serialize()); saveStore.Write(Key, Serialize()).RequireAccepted();
} }
public static EventBingoPlayfabData Load() public static EventBingoPlayfabData Load()
{ {
var s = PlayFabMgr.Instance.GetLocalData(Key); var s = GContext.container.Resolve<Core.KV.IPlayerSaveStore>().Read(Key).ValueOrThrow();
return Deserialize(s); return Deserialize(s);
} }
} }

View File

@@ -1,4 +1,4 @@
using System.Linq; using System.Linq;
using UnityEngine; using UnityEngine;
using SystemRandom = System.Random; using SystemRandom = System.Random;
using System.Text; using System.Text;
@@ -152,6 +152,8 @@ public class DrillModel : IHasChainPack
public class EventBreakPfData public class EventBreakPfData
{ {
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; }
public int ChainPackProgress { get; set; } public int ChainPackProgress { get; set; }
@@ -238,7 +240,7 @@ public class EventBreakPfData
public void Save() public void Save()
{ {
var s = Serialize(); var s = Serialize();
PlayFabMgr.Instance.UpdateUserDataValue(PfKey, s); saveStore.Write(PfKey, s).RequireAccepted();
} }
} }

View File

@@ -1,4 +1,4 @@
using asap.core; using asap.core;
using GameCore; using GameCore;
using UnityEngine; using UnityEngine;
using System.Collections.Generic; using System.Collections.Generic;
@@ -350,6 +350,8 @@ public class EventCanProgressTaskInfo
public class EventCanPlayfabData public class EventCanPlayfabData
{ {
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 = ',';
private const int SerializedTokenCount = 11; private const int SerializedTokenCount = 11;
@@ -433,7 +435,7 @@ public class EventCanPlayfabData
public void Save() public void Save()
{ {
var s = Serialize(); var s = Serialize();
PlayFabMgr.Instance.UpdateUserDataValue(PlayFabKey, s); saveStore.Write(PlayFabKey, s).RequireAccepted();
} }
public void Log() public void Log()

View File

@@ -22,6 +22,8 @@ using Random = UnityEngine.Random;
public class EventCubeMeltManager public class EventCubeMeltManager
{ {
[asap.core.Inject] public Core.KV.IPlayerSaveStore SaveStore { get; set; }
// //
public const string SaveKey = "EventCubeMeltData"; public const string SaveKey = "EventCubeMeltData";
@@ -137,7 +139,7 @@ public class EventCubeMeltManager
{ {
ELog($"SaveData()-> {eventDataJson}"); ELog($"SaveData()-> {eventDataJson}");
} }
PlayFabMgr.Instance.UpdateUserDataValue(SaveKey, eventDataJson); SaveStore.Write(SaveKey, eventDataJson).RequireAccepted();
} }
public void AddTicket(int costValue) public void AddTicket(int costValue)
@@ -169,6 +171,11 @@ public class EventCubeMeltManager
public EventCubeMeltData EventData { get; private set; } public EventCubeMeltData EventData { get; private set; }
public FishingEvent FishingEvent {get => _fishingEvent;set => _fishingEvent = value;} public FishingEvent FishingEvent {get => _fishingEvent;set => _fishingEvent = value;}

View File

@@ -1,4 +1,4 @@
using System.Collections.Generic; using System.Collections.Generic;
using UnityEngine; using UnityEngine;
using GameCore; using GameCore;
using asap.core; using asap.core;
@@ -265,6 +265,8 @@ public class EventGatherCoreRewardInfo
public class EventGatherCorePlayfabData public class EventGatherCorePlayfabData
{ {
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; }
public int ChainPackID { get; set; } public int ChainPackID { get; set; }
@@ -312,12 +314,12 @@ public class EventGatherCorePlayfabData
public void Save() public void Save()
{ {
PlayFabMgr.Instance.UpdateUserDataValue(Key, Serialize()); saveStore.Write(Key, Serialize()).RequireAccepted();
} }
public static EventGatherCorePlayfabData Load() public static EventGatherCorePlayfabData Load()
{ {
var s = PlayFabMgr.Instance.GetLocalData(Key); var s = GContext.container.Resolve<Core.KV.IPlayerSaveStore>().Read(Key).ValueOrThrow();
var res = new EventGatherCorePlayfabData(); var res = new EventGatherCorePlayfabData();
try try
{ {

View File

@@ -1,4 +1,4 @@
using asap.core; using asap.core;
using cfg; using cfg;
using GameCore; using GameCore;
using System; using System;
@@ -11,6 +11,8 @@ using UnityEngine;
/// </summary> /// </summary>
public class EventLuckyGameModel : ILuckyTaskGetData public class EventLuckyGameModel : ILuckyTaskGetData
{ {
readonly Core.KV.IPlayerSaveStore saveStore = GContext.container.Resolve<Core.KV.IPlayerSaveStore>();
public static EventLuckyGameModel Get() public static EventLuckyGameModel Get()
{ {
var model = GContext.container.Resolve<EventLuckyGameModel>(); var model = GContext.container.Resolve<EventLuckyGameModel>();
@@ -57,7 +59,7 @@ public class EventLuckyGameModel : ILuckyTaskGetData
string s = Newtonsoft.Json.JsonConvert.SerializeObject(eventLuckGameRecord); string s = Newtonsoft.Json.JsonConvert.SerializeObject(eventLuckGameRecord);
//Debug.Log($"[EventLuckGameModel] Save EventLuckGameRecord: {s}"); //Debug.Log($"[EventLuckGameModel] Save EventLuckGameRecord: {s}");
PlayFabMgr.Instance.UpdateUserDataValue(Key, s); saveStore.Write(Key, s).RequireAccepted();
} }
//=========================以下是活动数据========================= //=========================以下是活动数据=========================
@@ -163,7 +165,7 @@ public class EventLuckyGameModel : ILuckyTaskGetData
{ {
return; return;
} }
string s = PlayFabMgr.Instance.GetLocalData(Key); string s = saveStore.Read(Key).ValueOrThrow();
if (string.IsNullOrEmpty(s)) if (string.IsNullOrEmpty(s))
{ {
eventLuckGameRecord = new EventLuckGameRecord(); eventLuckGameRecord = new EventLuckGameRecord();

View File

@@ -1,4 +1,4 @@
using System.Text; using System.Text;
using asap.core; using asap.core;
using GameCore; using GameCore;
using UnityEngine; using UnityEngine;
@@ -261,6 +261,8 @@ public class EventOfferJourneyBlockData
public class EventOfferJourneyPlayfabData public class EventOfferJourneyPlayfabData
{ {
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;
private const string Key = "EventOfferJourneyData"; private const string Key = "EventOfferJourneyData";
@@ -319,14 +321,14 @@ public class EventOfferJourneyPlayfabData
public static EventOfferJourneyPlayfabData Load() public static EventOfferJourneyPlayfabData Load()
{ {
var s = PlayFabMgr.Instance.GetLocalData(Key); var s = GContext.container.Resolve<Core.KV.IPlayerSaveStore>().Read(Key).ValueOrThrow();
return Deserialize(s); return Deserialize(s);
} }
public void Save() public void Save()
{ {
var s = Serialize(); var s = Serialize();
PlayFabMgr.Instance.UpdateUserDataValue(Key, s); saveStore.Write(Key, s).RequireAccepted();
} }
} }

View File

@@ -1,4 +1,4 @@
using System; using System;
using System.Text; using System.Text;
using GameCore; using GameCore;
using UnityEngine; using UnityEngine;
@@ -156,6 +156,8 @@ public class EventOfferJourneyLvlTriggerData : AEventOfferJourneyData
public class EventOfferJourneyLvlTriggerPlayfabData public class EventOfferJourneyLvlTriggerPlayfabData
{ {
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";
private const int TokenCount = 3; private const int TokenCount = 3;
@@ -195,13 +197,13 @@ public class EventOfferJourneyLvlTriggerPlayfabData
public static EventOfferJourneyLvlTriggerPlayfabData Load() public static EventOfferJourneyLvlTriggerPlayfabData Load()
{ {
var s = PlayFabMgr.Instance.GetLocalData(Key); var s = GContext.container.Resolve<Core.KV.IPlayerSaveStore>().Read(Key).ValueOrThrow();
return Deserialize(s); return Deserialize(s);
} }
public void Save() public void Save()
{ {
var s = Serialize(); var s = Serialize();
PlayFabMgr.Instance.UpdateUserDataValue(Key, s); saveStore.Write(Key, s).RequireAccepted();
} }
} }

View File

@@ -24,7 +24,7 @@ namespace EventPartnerGather
public class PartnerInfoManager : IDisposable public class PartnerInfoManager : IDisposable
{ {
// [Inject] // [Inject]
private static Tables _tables { get; set; } = GContext.container.Resolve<Tables>(); private readonly Tables _tables = GContext.container.Resolve<Tables>();
private FriendService _friendService { get; set; } = GContext.container.Resolve<FriendService>(); private FriendService _friendService { get; set; } = GContext.container.Resolve<FriendService>();
private IUserService _userService { get; } = GContext.container.Resolve<IUserService>(); private IUserService _userService { get; } = GContext.container.Resolve<IUserService>();
@@ -41,9 +41,14 @@ namespace EventPartnerGather
public int EventId { get; set; } public int EventId { get; set; }
// //
public static readonly int BuilderCapacity = _tables.TbEventPartnerConfig.InvitationCount[1]; // D4: current-context read. Unlike the old field, this is not frozen across configuration changes.
public static int BuilderCapacity => GContext.container.Resolve<Tables>().TbEventPartnerConfig.InvitationCount[1];
public readonly BuildPartner[] BuildPartners = new BuildPartner[BuilderCapacity]; public readonly BuildPartner[] BuildPartners;
public PartnerInfoManager()
{
BuildPartners = new BuildPartner[_tables.TbEventPartnerConfig.InvitationCount[1]];
}
// //
public bool IsFullyMatched => BuildPartners.All(b => b != null && b.IsValid()); public bool IsFullyMatched => BuildPartners.All(b => b != null && b.IsValid());
@@ -387,9 +392,9 @@ namespace EventPartnerGather
public bool AddBuildPartner(BuildPartner buildPartner) public bool AddBuildPartner(BuildPartner buildPartner)
{ {
var index = buildPartner.SlotId; var index = buildPartner.SlotId;
if (index >= BuilderCapacity) if (index >= BuildPartners.Length)
{ {
LogWarning($"AddBuildPartnerFailed eventId={EventId} reason=SlotOutOfRange slotId={index} capacity={BuilderCapacity}"); LogWarning($"AddBuildPartnerFailed eventId={EventId} reason=SlotOutOfRange slotId={index} capacity={BuildPartners.Length}");
return false; return false;
} }

View File

@@ -1,4 +1,4 @@
using asap.core; using asap.core;
using cfg; using cfg;
using game; using game;
using GameCore; using GameCore;
@@ -564,6 +564,8 @@ public class RewardItemNewFlyToStashRequest
public class EventLuckMagicPlayfabData public class EventLuckMagicPlayfabData
{ {
readonly Core.KV.IPlayerSaveStore saveStore = GContext.container.Resolve<Core.KV.IPlayerSaveStore>();
public int EventId; public int EventId;
public int TicketCount; public int TicketCount;
public int ChainProgress; public int ChainProgress;
@@ -668,7 +670,7 @@ public class EventLuckMagicPlayfabData
{ {
var s = Serialize(); var s = Serialize();
// Debug.Log($"[EventLuckMagic] Save playfab data : \n" + s); // Debug.Log($"[EventLuckMagic] Save playfab data : \n" + s);
PlayFabMgr.Instance.UpdateUserDataValue(Key, s); saveStore.Write(Key, s).RequireAccepted();
} }
} }

View File

@@ -1,4 +1,4 @@
using asap.core; using asap.core;
using cfg; using cfg;
using GameCore; using GameCore;
using System; using System;
@@ -15,6 +15,8 @@ public class WelcomeBackData
} }
public class EventWelcomeBackManager public class EventWelcomeBackManager
{ {
readonly Core.KV.IPlayerSaveStore saveStore = GContext.container.Resolve<Core.KV.IPlayerSaveStore>();
string DataKey_WelcomeBack = "WelcomeBack"; string DataKey_WelcomeBack = "WelcomeBack";
Tables tables; Tables tables;
public TbEventWelcomeBackConfig eventWelcomeBackConfig; public TbEventWelcomeBackConfig eventWelcomeBackConfig;
@@ -28,7 +30,7 @@ public class EventWelcomeBackManager
{ {
try try
{ {
string data = PlayFabMgr.Instance.GetLocalData(DataKey_WelcomeBack); string data = saveStore.Read(DataKey_WelcomeBack).ValueOrThrow();
if (!string.IsNullOrEmpty(data)) if (!string.IsNullOrEmpty(data))
{ {
welcomeBackData = Newtonsoft.Json.JsonConvert.DeserializeObject<WelcomeBackData>(data) ?? new WelcomeBackData(); welcomeBackData = Newtonsoft.Json.JsonConvert.DeserializeObject<WelcomeBackData>(data) ?? new WelcomeBackData();
@@ -228,7 +230,7 @@ public class EventWelcomeBackManager
void SaveData() void SaveData()
{ {
PlayFabMgr.Instance.UpdateUserDataValue(DataKey_WelcomeBack, Newtonsoft.Json.JsonConvert.SerializeObject(welcomeBackData)); saveStore.Write(DataKey_WelcomeBack, Newtonsoft.Json.JsonConvert.SerializeObject(welcomeBackData)).RequireAccepted();
} }
} }
public struct ClaimComeBack public struct ClaimComeBack

View File

@@ -1,3 +1,4 @@
using Core.KV;
using asap.core; using asap.core;
using cfg; using cfg;
using DG.Tweening; using DG.Tweening;
@@ -23,6 +24,7 @@ public class ShowHandEvent
[DefaultExecutionOrder(10000)] [DefaultExecutionOrder(10000)]
public partial class FishingBehaviorB : MonoBehaviour public partial class FishingBehaviorB : MonoBehaviour
{ {
IPlayerSaveStore saveStore;
Tables _tables; Tables _tables;
[Header("RELATED OBJECT SETTINGS")] [Header("RELATED OBJECT SETTINGS")]
[SerializeField] [SerializeField]
@@ -166,6 +168,7 @@ public partial class FishingBehaviorB : MonoBehaviour
} }
public void Init() public void Init()
{ {
saveStore = GContext.container.Resolve<IPlayerSaveStore>();
fishingData = GContext.container.Resolve<FishingData>(); fishingData = GContext.container.Resolve<FishingData>();
fishingBehaviorConf = fishingData.fishingBehaviorConf; fishingBehaviorConf = fishingData.fishingBehaviorConf;
collectingData = GContext.container.Resolve<CollectingData>(); collectingData = GContext.container.Resolve<CollectingData>();
@@ -213,7 +216,13 @@ public partial class FishingBehaviorB : MonoBehaviour
{ {
Addressables.ReleaseInstance(handAB); Addressables.ReleaseInstance(handAB);
} }
handAB = await Addressables.InstantiateAsync(gloveData.Fbx, transform).Task; var loadedHand = await Addressables.InstantiateAsync(gloveData.Fbx, transform).Task;
if (this == null)
{
if (loadedHand != null) Addressables.ReleaseInstance(loadedHand);
return;
}
handAB = loadedHand;
if (handAB != null) if (handAB != null)
{ {
HandController = handAB.GetComponent<HandController>(); HandController = handAB.GetComponent<HandController>();
@@ -225,7 +234,13 @@ public partial class FishingBehaviorB : MonoBehaviour
} }
async void LoadBait() async void LoadBait()
{ {
baitAB = await Addressables.InstantiateAsync("BaitRoot", _bait.transform).Task; var loadedBait = await Addressables.InstantiateAsync("BaitRoot", _bait.transform).Task;
if (this == null)
{
if (loadedBait != null) Addressables.ReleaseInstance(loadedBait);
return;
}
baitAB = loadedBait;
baitAB.transform.localPosition = Vector3.zero; baitAB.transform.localPosition = Vector3.zero;
baitAB.transform.localRotation = Quaternion.identity; baitAB.transform.localRotation = Quaternion.identity;
baitAB.transform.localScale = Vector3.one; baitAB.transform.localScale = Vector3.one;
@@ -421,10 +436,20 @@ public partial class FishingBehaviorB : MonoBehaviour
float FishingScale = fishRodSkinData.FishingScale; float FishingScale = fishRodSkinData.FishingScale;
var goPrefab = await Addressables.LoadAssetAsync<GameObject>(path).Task; var goPrefab = await Addressables.LoadAssetAsync<GameObject>(path).Task;
if (this == null)
{
if (goPrefab != null) Addressables.Release(goPrefab);
return;
}
if (goPrefab == null) if (goPrefab == null)
{ {
Debug.LogError("Load Rod Elemental as Default Rod"); Debug.LogError("Load Rod Elemental as Default Rod");
goPrefab = await Addressables.LoadAssetAsync<GameObject>("Rod_elemental").Task; goPrefab = await Addressables.LoadAssetAsync<GameObject>("Rod_elemental").Task;
if (this == null)
{
if (goPrefab != null) Addressables.Release(goPrefab);
return;
}
} }
_bait.transform.SetParent(transform); _bait.transform.SetParent(transform);
if (goPrefab != null) if (goPrefab != null)
@@ -761,7 +786,7 @@ public partial class FishingBehaviorB : MonoBehaviour
{ {
GContext.Publish(new ConditionTypeEvent(ConditionType.ConsumeEnergy, energy)); GContext.Publish(new ConditionTypeEvent(ConditionType.ConsumeEnergy, energy));
} }
var success = await PlayFabMgr.Instance.SyncDataImmidiate("NewPlayerDataDic", var success = await saveStore.SyncDataImmidiate("NewPlayerDataDic",
Newtonsoft.Json.JsonConvert.SerializeObject(commonDatas)); Newtonsoft.Json.JsonConvert.SerializeObject(commonDatas));
Debug.Log($"[FishBehaviourB::ConsumeBaitOnPlayfab] {success}"); Debug.Log($"[FishBehaviourB::ConsumeBaitOnPlayfab] {success}");
} }
@@ -1259,7 +1284,7 @@ public partial class FishingBehaviorB : MonoBehaviour
} }
void SetIKTarget(Transform target) void SetIKTarget(Transform target)
{ {
if (RodBehaviour.cCDIK) if (RodBehaviour != null && RodBehaviour.cCDIK)
{ {
RodBehaviour.cCDIK.solver.target = target; RodBehaviour.cCDIK.solver.target = target;
} }

View File

@@ -1,4 +1,4 @@
using asap.core; using asap.core;
using cfg; using cfg;
using GameCore; using GameCore;
using System.Collections.Generic; using System.Collections.Generic;
@@ -6,6 +6,8 @@ using UnityEngine;
public class InfiniteBuildingData public class InfiniteBuildingData
{ {
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>();
// private readonly TbConstructionUnits BlocksTable = GContext.container.Resolve<Tables>().TbConstructionUnits; // private readonly TbConstructionUnits BlocksTable = GContext.container.Resolve<Tables>().TbConstructionUnits;
@@ -64,7 +66,7 @@ public class InfiniteBuildingData
//Debug.Log("Upload!!!!!!!!!!!!!!"); //Debug.Log("Upload!!!!!!!!!!!!!!");
//Debug.Log(s); //Debug.Log(s);
GContext.container.Resolve<PlayerData>().InfiniteBuildingData = s; GContext.container.Resolve<PlayerData>().InfiniteBuildingData = s;
PlayFabMgr.Instance.UpdateUserDataValue("InfiniteBuildingData", s); saveStore.Write("InfiniteBuildingData", s).RequireAccepted();
} }
public void DownloadData(string s) public void DownloadData(string s)

View File

@@ -1,4 +1,4 @@
using asap.core; using asap.core;
using cfg; using cfg;
using game; using game;
using GameCore; using GameCore;
@@ -9,6 +9,8 @@ using UnityEngine;
public class LuckyADTaskPopupPanel : LuckyTaskPanelBase public class LuckyADTaskPopupPanel : LuckyTaskPanelBase
{ {
Core.KV.IPlayerSaveStore saveStore;
List<LuckyTaskItemData> taskList; List<LuckyTaskItemData> taskList;
List<LuckyADTaskItem> taskItems = new List<LuckyADTaskItem>(); List<LuckyADTaskItem> taskItems = new List<LuckyADTaskItem>();
List<LuckyADTaskItemData> taskADList = new List<LuckyADTaskItemData>(); List<LuckyADTaskItemData> taskADList = new List<LuckyADTaskItemData>();
@@ -18,6 +20,7 @@ public class LuckyADTaskPopupPanel : LuckyTaskPanelBase
System.DateTime refreshTime; System.DateTime refreshTime;
protected override void Awake() protected override void Awake()
{ {
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>();
@@ -120,7 +123,7 @@ public class LuckyADTaskPopupPanel : LuckyTaskPanelBase
int round = taskData.GetValueOrDefault(-1, 0); int round = taskData.GetValueOrDefault(-1, 0);
float inflationRate = taskData.GetValueOrDefault(-3, 0) / 100f; float inflationRate = taskData.GetValueOrDefault(-3, 0) / 100f;
int group = taskData.GetValueOrDefault(-4, 0); int group = taskData.GetValueOrDefault(-4, 0);
string adData = PlayFabMgr.Instance.GetLocalData(LuckyTaskData.ADTaskADKey); string adData = saveStore.Read(LuckyTaskData.ADTaskADKey).ValueOrThrow();
int index = 0; int index = 0;
int curDay = ZZTimeHelper.UtcNow().UtcNowOffset().DayOfYear; int curDay = ZZTimeHelper.UtcNow().UtcNowOffset().DayOfYear;
if (!string.IsNullOrEmpty(adData)) if (!string.IsNullOrEmpty(adData))
@@ -134,13 +137,13 @@ public class LuckyADTaskPopupPanel : LuckyTaskPanelBase
else else
{ {
adData = $"{index}|{curDay}"; adData = $"{index}|{curDay}";
PlayFabMgr.Instance.UpdateUserDataValue(LuckyTaskData.ADTaskADKey, adData); saveStore.Write(LuckyTaskData.ADTaskADKey, adData).RequireAccepted();
} }
} }
else else
{ {
adData = $"{index}|{curDay}"; adData = $"{index}|{curDay}";
PlayFabMgr.Instance.UpdateUserDataValue(LuckyTaskData.ADTaskADKey, adData); saveStore.Write(LuckyTaskData.ADTaskADKey, adData).RequireAccepted();
} }
List<LuckyADTaskItemData> taskADList = new List<LuckyADTaskItemData>(); List<LuckyADTaskItemData> taskADList = new List<LuckyADTaskItemData>();
TbEventLuckyTask eventLuckyTask = LuckyTaskData.GetTbEventLuckyTask(luckyModel.EventId, round, group); TbEventLuckyTask eventLuckyTask = LuckyTaskData.GetTbEventLuckyTask(luckyModel.EventId, round, group);
@@ -180,7 +183,7 @@ public class LuckyADTaskPopupPanel : LuckyTaskPanelBase
luckyADTaskItem.Play(); luckyADTaskItem.Play();
string adData = PlayFabMgr.Instance.GetLocalData(LuckyTaskData.ADTaskADKey); string adData = saveStore.Read(LuckyTaskData.ADTaskADKey).ValueOrThrow();
int index = 0; int index = 0;
int day = ZZTimeHelper.UtcNow().UtcNowOffset().DayOfYear; int day = ZZTimeHelper.UtcNow().UtcNowOffset().DayOfYear;
if (!string.IsNullOrEmpty(adData)) if (!string.IsNullOrEmpty(adData))
@@ -195,7 +198,7 @@ public class LuckyADTaskPopupPanel : LuckyTaskPanelBase
//保存进度 //保存进度
index++; index++;
adData = $"{index}|{day}"; adData = $"{index}|{day}";
PlayFabMgr.Instance.UpdateUserDataValue(LuckyTaskData.ADTaskADKey, adData); saveStore.Write(LuckyTaskData.ADTaskADKey, adData).RequireAccepted();
RefreshADItemState(index); RefreshADItemState(index);
canvasGroup.blocksRaycasts = true; canvasGroup.blocksRaycasts = true;
GContext.Publish(new ShowData()); GContext.Publish(new ShowData());

View File

@@ -1,3 +1,4 @@
using asap.core;
using cfg; using cfg;
using GameCore; using GameCore;
using TMPro; using TMPro;
@@ -6,6 +7,8 @@ using UnityEngine.UI;
public class LuckyTaskBtn : MonoBehaviour, IUIRedPoint public class LuckyTaskBtn : MonoBehaviour, IUIRedPoint
{ {
Core.KV.IPlayerSaveStore saveStore;
Button btn; Button btn;
ILuckyTaskGetData model; ILuckyTaskGetData model;
@@ -15,6 +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<Core.KV.IPlayerSaveStore>();
btn = GetComponent<Button>(); btn = GetComponent<Button>();
btn.onClick.AddListener(OnClick); btn.onClick.AddListener(OnClick);
} }
@@ -29,6 +33,7 @@ public class LuckyTaskBtn : MonoBehaviour, IUIRedPoint
if (model != null) if (model != null)
{ {
GameObject taskPanelName = await UIManager.Instance.ShowUI(new UIType(model.TaskPanelName)); GameObject taskPanelName = await UIManager.Instance.ShowUI(new UIType(model.TaskPanelName));
if (this == null || !saveStore.IsCurrent) return;
if (taskPanelName != null) if (taskPanelName != null)
{ {
var panel = taskPanelName.GetComponent<LuckyTaskPanelBase>(); var panel = taskPanelName.GetComponent<LuckyTaskPanelBase>();
@@ -61,7 +66,7 @@ public class LuckyTaskBtn : MonoBehaviour, IUIRedPoint
int count = RedPointManager.Instance.GetRedPointCount(key); int count = RedPointManager.Instance.GetRedPointCount(key);
if (!state) if (!state)
{ {
string adData = PlayFabMgr.Instance.GetLocalData(LuckyTaskData.ADTaskADKey); string adData = saveStore.Read(LuckyTaskData.ADTaskADKey).ValueOrThrow();
if (!string.IsNullOrEmpty(adData)) if (!string.IsNullOrEmpty(adData))
{ {
string[] strings = adData.Split("|"); string[] strings = adData.Split("|");

View File

@@ -1,4 +1,4 @@
using asap.core; using asap.core;
using cfg; using cfg;
using game; using game;
using GameCore; using GameCore;
@@ -7,13 +7,21 @@ using UnityEngine;
public class LuckyTaskPanel : LuckyTaskPanelBase public class LuckyTaskPanel : LuckyTaskPanelBase
{ {
protected override void Awake()
{
saveStore = GContext.container.Resolve<Core.KV.IPlayerSaveStore>();
base.Awake();
}
Core.KV.IPlayerSaveStore saveStore;
/// <summary> /// <summary>
/// luckyModel 也作为是否自动全部领奖的标志 /// luckyModel 也作为是否自动全部领奖的标志
/// </summary> /// </summary>
List<LuckyTaskItem> taskItems = new List<LuckyTaskItem>(); List<LuckyTaskItem> taskItems = new List<LuckyTaskItem>();
protected override void SetItem() protected override void SetItem()
{ {
string adData = PlayFabMgr.Instance.GetLocalData(LuckyTaskData.ADTaskADKey); string adData = saveStore.Read(LuckyTaskData.ADTaskADKey).ValueOrThrow();
int curDay = ZZTimeHelper.UtcNow().UtcNowOffset().DayOfYear; int curDay = ZZTimeHelper.UtcNow().UtcNowOffset().DayOfYear;
if (!string.IsNullOrEmpty(adData)) if (!string.IsNullOrEmpty(adData))
{ {
@@ -22,13 +30,13 @@ public class LuckyTaskPanel : LuckyTaskPanelBase
if (strings.Length != 2 || !int.TryParse(strings[1], out day) || day != curDay) if (strings.Length != 2 || !int.TryParse(strings[1], out day) || day != curDay)
{ {
adData = $"{0}|{curDay}"; adData = $"{0}|{curDay}";
PlayFabMgr.Instance.UpdateUserDataValue(LuckyTaskData.ADTaskADKey, adData); saveStore.Write(LuckyTaskData.ADTaskADKey, adData).RequireAccepted();
} }
} }
else else
{ {
adData = $"{0}|{curDay}"; adData = $"{0}|{curDay}";
PlayFabMgr.Instance.UpdateUserDataValue(LuckyTaskData.ADTaskADKey, adData); saveStore.Write(LuckyTaskData.ADTaskADKey, adData).RequireAccepted();
} }
List<LuckyTaskItemData> taskList = new List<LuckyTaskItemData>(); List<LuckyTaskItemData> taskList = new List<LuckyTaskItemData>();
int level = taskData.GetValueOrDefault(-2, 0); int level = taskData.GetValueOrDefault(-2, 0);

View File

@@ -1,4 +1,4 @@
using asap.core; using asap.core;
using cfg; using cfg;
using GameCore; using GameCore;
using LitJson; using LitJson;
@@ -36,6 +36,8 @@ public enum FlipResult
public class EventMatchGameManager public class EventMatchGameManager
{ {
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;
public int ItemType => 6; public int ItemType => 6;
@@ -59,7 +61,7 @@ public class EventMatchGameManager
{ {
var matchGameData = new MatchGameData(); var matchGameData = new MatchGameData();
string data = PlayFabMgr.Instance.GetLocalData(EventMatchGameDataKey); string data = saveStore.Read(EventMatchGameDataKey).ValueOrThrow();
if (!string.IsNullOrEmpty(data)) if (!string.IsNullOrEmpty(data))
{ {
try try
@@ -491,7 +493,7 @@ public class EventMatchGameManager
public void SaveData(MatchGameData matchGameData) public void SaveData(MatchGameData matchGameData)
{ {
PlayFabMgr.Instance.UpdateUserDataValue(EventMatchGameDataKey, JsonMapper.ToJson(matchGameData)); saveStore.Write(EventMatchGameDataKey, JsonMapper.ToJson(matchGameData)).RequireAccepted();
} }
} }

View File

@@ -1,4 +1,4 @@
using asap.core; using asap.core;
using cfg; using cfg;
using game; using game;
using GameCore; using GameCore;
@@ -53,6 +53,8 @@ public interface IMiniBattlePassDataProvider
public class MiniBattlePassDataProvider : IMiniBattlePassDataProvider public class MiniBattlePassDataProvider : IMiniBattlePassDataProvider
{ {
readonly Core.KV.IPlayerSaveStore saveStore = GContext.container.Resolve<Core.KV.IPlayerSaveStore>();
private Tables _tables; private Tables _tables;
PlayerItemData _playerItemData; PlayerItemData _playerItemData;
string newDataKey => "MiniBPKeyNew"; string newDataKey => "MiniBPKeyNew";
@@ -138,10 +140,11 @@ public class MiniBattlePassDataProvider : IMiniBattlePassDataProvider
public MiniBattlePassRecord GetMiniBattlePassRecord(string key) public MiniBattlePassRecord GetMiniBattlePassRecord(string key)
{ {
if (!saveStore.IsCurrent) throw new System.InvalidOperationException("Expired battle pass session.");
MiniBattlePassRecord Data; MiniBattlePassRecord Data;
if (datas == null) if (datas == null)
{ {
string dataStr = PlayFabMgr.Instance.GetLocalData(newDataKey); string dataStr = saveStore.Read(newDataKey).ValueOrThrow();
//Debug.Log($"GetMiniBattlePassRecord: dataStr = {dataStr}"); //Debug.Log($"GetMiniBattlePassRecord: dataStr = {dataStr}");
if (string.IsNullOrEmpty(dataStr)) if (string.IsNullOrEmpty(dataStr))
{ {
@@ -168,7 +171,7 @@ public class MiniBattlePassDataProvider : IMiniBattlePassDataProvider
public void SetMiniBattlePassRecord(string key, MiniBattlePassRecord record) public void SetMiniBattlePassRecord(string key, MiniBattlePassRecord record)
{ {
datas[key] = record; datas[key] = record;
PlayFabMgr.Instance.UpdateUserDataValue(newDataKey, JsonConvert.SerializeObject(datas)); saveStore.Write(newDataKey, JsonConvert.SerializeObject(datas)).RequireAccepted();
} }
} }
public class MiniBattlePassProgressInfo public class MiniBattlePassProgressInfo

View File

@@ -101,7 +101,7 @@ public class ZZTimeHelper
HttpClient client = new HttpClient(); HttpClient client = new HttpClient();
client.Timeout = TimeSpan.FromSeconds(5); client.Timeout = TimeSpan.FromSeconds(5);
IConfig config = GContext.container.Resolve<IConfig>(); IConfig config = GContext.container.Resolve<IConfig>();
string apiUrl = config.Get<string>(GConstant.K_Event_API_URL, "http://192.168.9.101:5292/"); string apiUrl = config.Get<string>(GConstant.K_Event_API_URL, CustomServerMgr.DefaultApiUrl);
string url = $"{apiUrl}account/svrtime"; string url = $"{apiUrl}account/svrtime";
var svrTimeStr = await client.GetStringAsync(url); var svrTimeStr = await client.GetStringAsync(url);
// var svrTimeStr = await client.GetStringAsync(timeUrl); // var svrTimeStr = await client.GetStringAsync(timeUrl);

View File

@@ -27,6 +27,11 @@ namespace Script.RuntimeScript
private DiggingGameModel _dataModel; private DiggingGameModel _dataModel;
public DiggingGameModel DataModel => _dataModel; public DiggingGameModel DataModel => _dataModel;
private Animation _digAni; private Animation _digAni;
private CinemachineVirtualCamera _cameraVir; private CinemachineVirtualCamera _cameraVir;

View File

@@ -1,4 +1,4 @@
using asap.core; using asap.core;
using cfg; using cfg;
using GameCore; using GameCore;
using Newtonsoft.Json; using Newtonsoft.Json;
@@ -15,6 +15,8 @@ using UnityEngine.ResourceManagement.AsyncOperations;
public class DiggingGameModel public class DiggingGameModel
{ {
readonly Core.KV.IPlayerSaveStore saveStore = GContext.container.Resolve<Core.KV.IPlayerSaveStore>();
#region 协议Const区域 #region 协议Const区域
//工具个数 //工具个数
@@ -78,8 +80,8 @@ public class DiggingGameModel
redDot = cycle.RedDot; redDot = cycle.RedDot;
} }
DiggingGameManager.LogError("沙滩寻宝开启"); DiggingGameManager.LogError("沙滩寻宝开启");
string countJson = PlayFabMgr.Instance.GetLocalData(DigCountKey); string countJson = saveStore.Read(DigCountKey).ValueOrThrow();
string digGridDataJson = PlayFabMgr.Instance.GetLocalData(DigGridDataKey); string digGridDataJson = saveStore.Read(DigGridDataKey).ValueOrThrow();
if (string.IsNullOrEmpty(countJson)) if (string.IsNullOrEmpty(countJson))
{ {
SaveNull(); SaveNull();
@@ -105,8 +107,8 @@ public class DiggingGameModel
MaxCount = digActivityConfig.StageCount; MaxCount = digActivityConfig.StageCount;
} }
string countJson = PlayFabMgr.Instance.GetLocalData(DigCountKey); string countJson = saveStore.Read(DigCountKey).ValueOrThrow();
string digGridDataJson = PlayFabMgr.Instance.GetLocalData(DigGridDataKey); string digGridDataJson = saveStore.Read(DigGridDataKey).ValueOrThrow();
DoServerData(countJson, digGridDataJson); DoServerData(countJson, digGridDataJson);
await InitClientData(ClientDataHandleEnum.Init); await InitClientData(ClientDataHandleEnum.Init);
} }
@@ -460,7 +462,7 @@ public class DiggingGameModel
dataJson.ActivityId = _curActivityId; dataJson.ActivityId = _curActivityId;
dataJson.DigCount = _serverDigCount; dataJson.DigCount = _serverDigCount;
string json = JsonConvert.SerializeObject(dataJson); string json = JsonConvert.SerializeObject(dataJson);
PlayFabMgr.Instance.UpdateUserDataValue(DigCountKey, json); saveStore.Write(DigCountKey, json).RequireAccepted();
GContext.container.Resolve<FishingEventData>().SaveTransitionData(dataJson.ActivityId, dataJson.DigCount); GContext.container.Resolve<FishingEventData>().SaveTransitionData(dataJson.ActivityId, dataJson.DigCount);
} }
void SetRedPoint() void SetRedPoint()
@@ -493,7 +495,7 @@ public class DiggingGameModel
string json = JsonConvert.SerializeObject(dataJson); string json = JsonConvert.SerializeObject(dataJson);
//DiggingGameManager.LogError(json); //DiggingGameManager.LogError(json);
PlayFabMgr.Instance.UpdateUserDataValue(DigGridDataKey, json); saveStore.Write(DigGridDataKey, json).RequireAccepted();
} }
public void SaveAll() public void SaveAll()
@@ -509,7 +511,7 @@ public class DiggingGameModel
{ {
_serverDigCount = GContext.container.Resolve<FishingEventData>().GetInitWelcomeGift(_curActivityId); _serverDigCount = GContext.container.Resolve<FishingEventData>().GetInitWelcomeGift(_curActivityId);
SaveDigCount(); SaveDigCount();
PlayFabMgr.Instance.UpdateUserDataValue(DigGridDataKey, string.Empty); saveStore.Write(DigGridDataKey, string.Empty).RequireAccepted();
} }
#endregion #endregion

View File

@@ -0,0 +1,271 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using asap.core;
using tysdk;
using System.Linq;
using game.PlayerDataStorage;
namespace game
{
public class AggKvUserService : KvUserService
{
private bool channelLoginRunning;
protected override string NewPlayerName
{
get
{
if (TYSdkFacade.TYAccountInfo == null)
{
return base.NewPlayerName;
}
if (TYSdkFacade.TYAccountInfo.channel == EAccoutType.hwFacebook
|| TYSdkFacade.TYAccountInfo.channel == EAccoutType.hwGoogle)
{
if (!string.IsNullOrEmpty(TYSdkFacade.TYAccountInfo.userName))
{
return TYSdkFacade.TYAccountInfo.userName;
}
}
return base.NewPlayerName;
}
}
protected override string NewPlayerAvatar
{
get
{
if (TYSdkFacade.TYAccountInfo == null)
{
return base.NewPlayerAvatar;
}
if (TYSdkFacade.TYAccountInfo.channel == EAccoutType.hwFacebook
/*|| TYSdkFacade.TYAccountInfo.channel == EAccoutType.hwGoogle*/)
{
if (!string.IsNullOrEmpty(TYSdkFacade.TYAccountInfo.avatar))
{
return TYSdkFacade.TYAccountInfo.avatar;
}
}
return base.NewPlayerAvatar;
}
}
public override async Task<ELoginStep> Login(Action<int> progress, bool tryLoginByToken = true)
{
if (channelLoginRunning || LoginLifetime.IsCancellationRequested)
{
return ELoginStep.Sdk;
}
channelLoginRunning = true;
try
{
if (IsLogin)
{
progress?.Invoke(3);
return ELoginStep.Done;
}
UnityEngine.Debug.Log("AggUserService Login");
LoginInfo loginInfo = new LoginInfo();
if (!TYSdkFacade.IsLoggedIn)
{
loginInfo = await TYSdkFacade.Instance.LoginBySns();
if (LoginLifetime.IsCancellationRequested)
{
return ELoginStep.Sdk;
}
if (!loginInfo.isSuccess && tryLoginByToken)
{
loginInfo = await TYSdkFacade.Instance.LoginByToken();
if (LoginLifetime.IsCancellationRequested)
{
return ELoginStep.Sdk;
}
}
if (!loginInfo.isSuccess)
{
var evt = new LoginTypeEvt();
GContext.Publish(evt);
var loginType = await evt.loginType.Task;
if (LoginLifetime.IsCancellationRequested)
{
return ELoginStep.Sdk;
}
loginInfo = await TYSdkFacade.Instance.Login(loginType);
if (LoginLifetime.IsCancellationRequested)
{
return ELoginStep.Sdk;
}
}
}
else
{
loginInfo.isSuccess = true;
loginInfo.userId = TYSdkFacade.TYAccountInfo.userId;
}
if (!loginInfo.isSuccess)
{
return ELoginStep.Sdk;
}
progress.Invoke(1);
CustomId = loginInfo.StrUserId;
var step = await base.Login(step => progress?.Invoke(step + 1));
if (LoginLifetime.IsCancellationRequested)
{
return ELoginStep.Api;
}
if (step != ELoginStep.Done)
{
return step;
}
if (ChannelManager.IsRustoreStore)
{
GEvent.SetProp("sdk_channel", "ru_store");
}
GEvent.SetProps(new Dictionary<string, object>()
{
{"pf_id", UserId},
{"CreateTime",CreateTime.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss")}
});
GEvent.SetUserProps(new Dictionary<string, object>()
{
{"pf_id", UserId},
{"CreateTime",CreateTime.ToString("yyyy-MM-dd HH:mm:ss")}
});
AvatarToKV(TYSdkFacade.TYAccountInfo);
GEvent.OnLogin(UserId, CustomId);
if (config.Get<bool>("NewlyCreated", false))
{
using (var e = GEvent.CompleteRegistrationEvent())
{
e.AddContent("login_type", TYSdkFacade.TYAccountInfo.channel.ToString());
}
GEvent.SetProps(new Dictionary<string, object>()
{
{"vip_level", 0}
});
GEvent.SetUserProps(new Dictionary<string, object>()
{
{"vip_level", 0},
{"total_consumption", 0}
});
if (AFHelper.Instance.GetDLReferer(out var pid, out var uid))
{
UnityEngine.Debug.Log("[AggUserService] deeplink referer:" + pid + " " + uid);
using (var e = GEvent.GameEvent("deeplink_invited"))
{
e.AddContent("referrer_uid", uid);
e.AddContent("referrer_pid", pid);
}
var refererData = new Dictionary<string, object>()
{
{"refererPid", pid},
{"refererUid", uid},
{"playerUid", CustomId}
};
_ = customServerMgr.CustomServerPost("friend/RecoreReferer",
Newtonsoft.Json.JsonConvert.SerializeObject(refererData));
}
}
using (var e = GEvent.LoginEvent())
{
e.AddContent("login_type", TYSdkFacade.TYAccountInfo.channel.ToString());
e.AddContent("display_name", DisplayName);
e.AddContent("avatar_url", AvatarUrl);
e.AddContent("linkedAccout", string.Join('#', TYSdkFacade.TYAccountInfo.linkedAccout));
}
return step;
}
finally
{
channelLoginRunning = false;
}
}
public override void Logout()
{
TYSdkFacade.Instance.Logout();
base.Logout();
}
public override async Task<bool> GetLinkState(EAccoutType accoutType)
{
return await TYSdkFacade.Instance.LinkCheck(accoutType);
}
public override async Task<(int code, int userid)> LinkAccount(EAccoutType accoutType)
{
var linkResult = await TYSdkFacade.Instance.LinkAccount(accoutType);
if (linkResult.code == 0 && !LoginLifetime.IsCancellationRequested)
{
var _accountInfo = TYSdkFacade.TYAccountInfo;
if (_accountInfo != null)
{
AvatarToKV(_accountInfo);
if (_accountInfo.channel == EAccoutType.hwFacebook)
{
_ = UpdateAvatarUrl(_accountInfo.avatar);
}
}
}
return (linkResult.code, linkResult.userId);
}
protected override void InitData(IDictionary<string, string> data)
{
if (data.TryGetValue(EAccoutType.hwGoogle.ToString(), out var google) && google == "1")
{
TYSdkFacade.TYAccountInfo.linkedAccout.Add(EAccoutType.hwGoogle);
}
if (data.TryGetValue(EAccoutType.hwFacebook.ToString(), out var facebook) && facebook == "1")
{
TYSdkFacade.TYAccountInfo.linkedAccout.Add(EAccoutType.hwFacebook);
}
if (data.TryGetValue(EAccoutType.Apple.ToString(), out var apple) && apple == "1")
{
TYSdkFacade.TYAccountInfo.linkedAccout.Add(EAccoutType.Apple);
}
if (TYSdkFacade.TYAccountInfo.channel != EAccoutType.hwGuest && TYSdkFacade.TYAccountInfo.channel != EAccoutType.none)
{
var channel = TYSdkFacade.TYAccountInfo.channel.ToString();
if (!data.ContainsKey(channel))
{
data.Add(channel, "1");
SaveStore.Write(channel, "1").RequireAccepted();
}
}
if (data.TryGetValue("EAccoutUrl", out var data_value))
{
EAccoutUrl = Newtonsoft.Json.JsonConvert.DeserializeObject<Dictionary<string, string>>(data_value);
}
}
}
}

View File

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

View File

@@ -45,6 +45,9 @@ namespace game
public override async Task<ELoginStep> Login(Action<int> progress, bool tryLoginByToken = true) public override async Task<ELoginStep> Login(Action<int> progress, bool tryLoginByToken = true)
{ {
var backend=CaptureSaveBackend();
if(RejectSaveBackend(backend)) return ELoginStep.Api;
var generation = logoutGeneration;
if (IsLogin) if (IsLogin)
{ {
progress?.Invoke(3); progress?.Invoke(3);
@@ -58,10 +61,12 @@ namespace game
if (!TYSdkFacade.IsLoggedIn) if (!TYSdkFacade.IsLoggedIn)
{ {
loginInfo = await TYSdkFacade.Instance.LoginBySns(); loginInfo = await TYSdkFacade.Instance.LoginBySns();
if (generation != logoutGeneration) return ELoginStep.Sdk;
if (!loginInfo.isSuccess && tryLoginByToken) if (!loginInfo.isSuccess && tryLoginByToken)
{ {
loginInfo = await TYSdkFacade.Instance.LoginByToken(); loginInfo = await TYSdkFacade.Instance.LoginByToken();
if (generation != logoutGeneration) return ELoginStep.Sdk;
} }
if (!loginInfo.isSuccess) if (!loginInfo.isSuccess)
@@ -69,7 +74,9 @@ namespace game
var evt = new LoginTypeEvt(); var evt = new LoginTypeEvt();
GContext.Publish(evt); GContext.Publish(evt);
var loginType = await evt.loginType.Task; var loginType = await evt.loginType.Task;
if (generation != logoutGeneration) return ELoginStep.Sdk;
loginInfo = await TYSdkFacade.Instance.Login(loginType); loginInfo = await TYSdkFacade.Instance.Login(loginType);
if (generation != logoutGeneration) return ELoginStep.Sdk;
} }
} }
else else
@@ -87,14 +94,11 @@ namespace game
OnInitData += InitData; OnInitData += InitData;
var step = await base.Login(step => progress?.Invoke(step + 1)); ELoginStep step;
if (step != ELoginStep.Done) try { step = await LoginWithBackend(value => progress?.Invoke(value + 1),backend); }
{ finally { OnInitData -= InitData; }
OnInitData -= InitData; if (generation != logoutGeneration) return ELoginStep.Sdk;
return step; if (step != ELoginStep.Done) return step;
}
OnInitData -= InitData;
if (ChannelManager.IsRustoreStore) if (ChannelManager.IsRustoreStore)
{ {

Some files were not shown because too many files have changed in this diff Show More