Compare commits
6 Commits
6813da149a
...
feat/upser
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e848b63d8 | ||
|
|
73ac2bf6f8 | ||
|
|
1a81c331e3 | ||
|
|
edb8a056c7 | ||
|
|
144e7f739f | ||
|
|
d3c5394a55 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -11,3 +11,6 @@ QWEN.md
|
|||||||
Client/.claude/
|
Client/.claude/
|
||||||
dev-ab-svr/debugconfig.json
|
dev-ab-svr/debugconfig.json
|
||||||
/Client/.uloop/
|
/Client/.uloop/
|
||||||
|
/Client/.codex/
|
||||||
|
/CodexOutput/
|
||||||
|
|
||||||
|
|||||||
61
AGENTS.md
61
AGENTS.md
@@ -1,5 +1,25 @@
|
|||||||
# 项目通用开发规则
|
# 项目通用开发规则
|
||||||
|
|
||||||
|
- 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或原邮件流程。已审同合同替换连续实施,确有新合同或架构差异时一次合并评审,避免逐功能送审。
|
- KV模式的迁移目标是由KV管理器及其接线承接原PlayFab相关能力,不只迁个人存档;等级榜、公开资料、好友和匹配是原PlayFab调用的既有功能,不得当作新增产品需求、默认延期或直接转交服务端。先核客户端已有KV能力及等价调用映射;只有明确证实缺少必要能力时,才把具体接口证据集中写入沟通文档。不得反复要求用户确认“是否存KV”、新ID或原邮件流程。已审同合同替换连续实施,确有新合同或架构差异时一次合并评审,避免逐功能送审。
|
||||||
- 整个迁移流程、所有阶段与批次均只做平移,不限于 M2。全部迁移完成前,仅修改完成迁移所必需的存储、身份和接口接线,保留原业务规则、调用顺序、同步/异步方式、返回值、异常处理和界面流程。不得夹带额外业务修正、优化或代码重构;不得以“更安全”“更合理”或“顺手修复”为由改变原逻辑。
|
- 整个迁移流程、所有阶段与批次均只做平移,不限于 M2。全部迁移完成前,仅修改完成迁移所必需的存储、身份和接口接线,保留原业务规则、调用顺序、同步/异步方式、返回值、异常处理和界面流程。不得夹带额外业务修正、优化或代码重构;不得以“更安全”“更合理”或“顺手修复”为由改变原逻辑。
|
||||||
- 发现原有业务缺陷、代码结构问题或可优化点,统一记录到迁移完成后的待办,不在任何迁移阶段穿插处理。必须等整个迁移流程全部完成后,再由用户另行授权修正;单个阶段完成不构成提前修正的许可。此前方案或评审包含此类扩展时,以此规则及用户最新要求为准。
|
- 发现原有业务缺陷、代码结构问题或可优化点,统一记录到迁移完成后的待办,不在任何迁移阶段穿插处理。必须等整个迁移流程全部完成后,再由用户另行授权修正;单个阶段完成不构成提前修正的许可。此前方案或评审包含此类扩展时,以此规则及用户最新要求为准。
|
||||||
@@ -7,3 +27,44 @@
|
|||||||
- 在满足已确认的功能、正确性和必要安全边界的前提下,优先选择最简单、最直观、最容易维护的实现。抽象应服务于实际调用者;不要为假设中的扩展增加接口、适配器、管理器、状态机或重复保护层。
|
- 在满足已确认的功能、正确性和必要安全边界的前提下,优先选择最简单、最直观、最容易维护的实现。抽象应服务于实际调用者;不要为假设中的扩展增加接口、适配器、管理器、状态机或重复保护层。
|
||||||
- 修改代码前完成相关方案的完整评审,记录裁决和开发边界。评审新增抽象时,说明它解决的具体问题、实际调用点,以及为何复用或小幅修改现有实现不足;无法说明必要性的层次不要引入。
|
- 修改代码前完成相关方案的完整评审,记录裁决和开发边界。评审新增抽象时,说明它解决的具体问题、实际调用点,以及为何复用或小幅修改现有实现不足;无法说明必要性的层次不要引入。
|
||||||
- 同一事实保持单一来源,同一业务流程避免多重开关和重复状态。迁移中的临时路径须登记,最终通关前恢复或清理;最终代码删除闲置实现,但保留仍在使用的原有功能。
|
- 同一事实保持单一来源,同一业务流程避免多重开关和重复状态。迁移中的临时路径须登记,最终通关前恢复或清理;最终代码删除闲置实现,但保留仍在使用的原有功能。
|
||||||
|
- 原设计是迁移基准,尽量做到 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;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
@@ -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}
|
||||||
|
|||||||
Binary file not shown.
@@ -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()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ public class DebugConfig
|
|||||||
private const string s101StaicResUrl = "http://s101.local:8080/{0}";
|
private const string s101StaicResUrl = "http://s101.local:8080/{0}";
|
||||||
private const string s101AddrUrl = "http://s101.local:8080/{0}/{1}";
|
private const string s101AddrUrl = "http://s101.local:8080/{0}/{1}";
|
||||||
private const string s101EventAPIUrl = "http://s101.local:18080/";
|
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";
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
#if UNITY_EDITOR
|
|
||||||
using System;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Reflection;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
public static class KVGameLoadValidation
|
|
||||||
{
|
|
||||||
// Fixed assembly/type/method and explicit case allow-list; no arbitrary reflection arguments.
|
|
||||||
public static async Task<string> RunAsync(string caseId)
|
|
||||||
{
|
|
||||||
if (caseId != "M08-M23" && caseId != "M21-M22-M24" && caseId != "M09-M10-M12" &&
|
|
||||||
caseId != "M14" && caseId != "M07" && caseId != "M20-M21-M22-M24-M28-M29" && caseId != "M16-M17" &&
|
|
||||||
caseId != "M06-M13" && caseId != "M15-M26-M27" && caseId != "M30" && caseId != "M19" && caseId != "M11" && caseId != "M25" &&
|
|
||||||
caseId != "M01-M02-M21-M22-M24" && caseId != "M04-M21-M22-M24" && caseId != "M05-M21-M22-M24" && caseId != "M03" && caseId != "M18" && caseId != "M31" && caseId != "B2-20") throw new ArgumentException("Unknown fixed validation case.");
|
|
||||||
var assembly = AppDomain.CurrentDomain.GetAssemblies().Single(a => a.GetName().Name == "HotUpdate");
|
|
||||||
var type = assembly.GetType("game.PlayerDataStorage.PlayerKVEditorValidationHost", true);
|
|
||||||
var method = type.GetMethod("RunAsync", BindingFlags.Static | BindingFlags.NonPublic, null, new[] { typeof(string) }, null);
|
|
||||||
if (method == null) throw new InvalidOperationException("Validation entry unavailable.");
|
|
||||||
return await (Task<string>)method.Invoke(null, new object[] { caseId });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 4ce05f2f22c2492f8a3c51a82a208647
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
#if UNITY_EDITOR
|
|
||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Reflection;
|
|
||||||
using System.Security.Cryptography;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
using UnityEditor;
|
|
||||||
using UnityEngine;
|
|
||||||
|
|
||||||
// Fixed CLI helpers; no arbitrary method names or process-marker deletion.
|
|
||||||
public static class KVGameplayLoopSetup
|
|
||||||
{
|
|
||||||
static string Root => Path.GetFullPath(Path.Combine(Application.dataPath,"../../CodexOutput/upserver/KVGameplayLoopTests"));
|
|
||||||
static Type Find(string name) => AppDomain.CurrentDomain.GetAssemblies().Select(a=>a.GetType("game.PlayerDataStorage."+name)).FirstOrDefault(t=>t!=null)
|
|
||||||
?? throw new InvalidOperationException("Gameplay assembly not loaded.");
|
|
||||||
static object ReadProperty(Type type,string name) => type.GetProperty(name,BindingFlags.Static|BindingFlags.NonPublic)?.GetValue(null);
|
|
||||||
|
|
||||||
public static async System.Threading.Tasks.Task<string> RunValidationAsync(string caseId)
|
|
||||||
{
|
|
||||||
if(caseId!="equipment-chain" && caseId!="fishing-consume" && caseId!="fishing-reward") throw new ArgumentException("Unknown fixed validation case.");
|
|
||||||
if(EditorApplication.isPlayingOrWillChangePlaymode || EditorApplication.isCompiling || EditorApplication.isUpdating)
|
|
||||||
throw new InvalidOperationException("Idle editor required.");
|
|
||||||
var method=Find("PlayerKVGameplayLoop").GetMethod(caseId=="equipment-chain"?"VerifyEquipmentForEditorAsync":caseId=="fishing-consume"?"VerifyFishingForEditorAsync":"VerifyFishingRewardForEditorAsync",BindingFlags.Static|BindingFlags.NonPublic);
|
|
||||||
if(method==null) throw new InvalidOperationException("Fixed equipment validation entry unavailable.");
|
|
||||||
return await (System.Threading.Tasks.Task<string>)method.Invoke(null,null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string State()
|
|
||||||
{
|
|
||||||
var type=Find("PlayerKVGameplayLoop");
|
|
||||||
var history=Path.Combine(Root,"run-control");
|
|
||||||
return JsonConvert.SerializeObject(new{project=Application.dataPath,playing=EditorApplication.isPlaying,
|
|
||||||
enabled=ReadProperty(type,"Enabled"),ready=ReadProperty(type,"Ready"),error=ReadProperty(type,"Error"),run=ReadProperty(type,"RunPath"),
|
|
||||||
permitExists=File.Exists(Path.Combine(Root,"permit.json")),
|
|
||||||
processMarkers=Directory.Exists(history)?Directory.GetFiles(history,"*.claimed").Select(Path.GetFileName).OrderBy(x=>x).ToArray():Array.Empty<string>()});
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string ValidatePermit(string path)
|
|
||||||
{
|
|
||||||
var type=Find("PlayerKVLoopPermit");
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var permit=type.GetMethod("Read",BindingFlags.Static|BindingFlags.NonPublic).Invoke(null,new object[]{Path.GetFullPath(path)});
|
|
||||||
return JsonConvert.SerializeObject(permit);
|
|
||||||
}
|
|
||||||
catch(TargetInvocationException e) { throw e.InnerException ?? e; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string InspectPFBackup(string path)
|
|
||||||
{
|
|
||||||
if(EditorApplication.isPlayingOrWillChangePlaymode || EditorApplication.isCompiling || EditorApplication.isUpdating)
|
|
||||||
throw new InvalidOperationException("Idle editor required.");
|
|
||||||
var method=Find("PlayerKVGameplayLoop").GetMethod("InspectPFBackupForEditor",BindingFlags.Static|BindingFlags.NonPublic);
|
|
||||||
try { return (string)method.Invoke(null,new object[]{path}); }
|
|
||||||
catch(TargetInvocationException e) { throw e.InnerException ?? e; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string PreparePermit(string account,string backupPath,string[] allowedKeys)
|
|
||||||
=> PreparePermitCore(account,backupPath,allowedKeys,false);
|
|
||||||
public static string PrepareFishingPermit(string account,string backupPath,string[] allowedKeys)
|
|
||||||
=> PreparePermitCore(account,backupPath,allowedKeys,true);
|
|
||||||
public static string PrepareFishingRewardPermit(string account,string backupPath,string[] allowedKeys)
|
|
||||||
=> PreparePermitCore(account,backupPath,allowedKeys,true,true);
|
|
||||||
static string PreparePermitCore(string account,string backupPath,string[] allowedKeys,bool fishing,bool reward=false)
|
|
||||||
{
|
|
||||||
if(EditorApplication.isPlayingOrWillChangePlaymode || EditorApplication.isCompiling)
|
|
||||||
throw new InvalidOperationException("Stop play before preparing a new process run.");
|
|
||||||
backupPath=Path.GetFullPath(backupPath);
|
|
||||||
var bytes=File.ReadAllBytes(backupPath);
|
|
||||||
string hash;
|
|
||||||
using(var sha=SHA256.Create()) hash=BitConverter.ToString(sha.ComputeHash(bytes)).Replace("-","");
|
|
||||||
var candidate=Path.Combine(Root,"permit.candidate.json");
|
|
||||||
Directory.CreateDirectory(Root);
|
|
||||||
var json=JObject.FromObject(new{Version=reward?3:fishing?2:1,Enabled=true,PlayerId=account,
|
|
||||||
Api="http://s101.local:18080/",BackupPath=backupPath,BackupSha256=hash,AllowedKeys=allowedKeys});
|
|
||||||
if(fishing) json["Operation"]=reward?"fishing-reward-once":"fishing-consume-once";
|
|
||||||
File.WriteAllText(candidate,json.ToString(Formatting.Indented));
|
|
||||||
var validated=ValidatePermit(candidate); // Empty/mismatched backups cannot enable a run.
|
|
||||||
File.WriteAllText(Path.Combine(Root,"permit.json"),validated);
|
|
||||||
return State();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: ff2728a480374b609aaed9ea0742215e
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
#if UNITY_EDITOR
|
|
||||||
using System;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Reflection;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
public static class KVSaveRouteValidation
|
|
||||||
{
|
|
||||||
public static async Task<string> RunAsync(string caseId)
|
|
||||||
{
|
|
||||||
if (caseId != "fixed-route") throw new ArgumentException("Unknown fixed validation case.");
|
|
||||||
var assembly = AppDomain.CurrentDomain.GetAssemblies().Single(a => a.GetName().Name == "HotUpdate");
|
|
||||||
var type = assembly.GetType("game.PlayerDataStorage.PlayerKVSaveRouteEditorValidationHost", true);
|
|
||||||
var method = type.GetMethod("RunAsync", BindingFlags.Static | BindingFlags.NonPublic, null, new[] { typeof(string) }, null);
|
|
||||||
if (method == null) throw new InvalidOperationException("Validation entry unavailable.");
|
|
||||||
return await (Task<string>)method.Invoke(null, new object[] { caseId });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: dc9b5dbb615a4746a95df66b46987589
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Threading;
|
|
||||||
using game.Account;
|
|
||||||
using UnityEditor;
|
|
||||||
using UnityEngine;
|
|
||||||
|
|
||||||
public sealed class S101LoginWindow : EditorWindow
|
|
||||||
{
|
|
||||||
string playerId = "101";
|
|
||||||
string status = "输入涂游测试 ID,验证 s101 认证及 Cookie。";
|
|
||||||
bool busy;
|
|
||||||
S101AccountSession session;
|
|
||||||
CancellationTokenSource cancellation;
|
|
||||||
|
|
||||||
[MenuItem("Tools/Debug/S101 Minimal Login")]
|
|
||||||
static void Open() => GetWindow<S101LoginWindow>("S101 最小登录");
|
|
||||||
|
|
||||||
void OnGUI()
|
|
||||||
{
|
|
||||||
EditorGUILayout.LabelField("API", S101AccountSession.DefaultApiUrl);
|
|
||||||
using (new EditorGUI.DisabledScope(busy))
|
|
||||||
{
|
|
||||||
playerId = EditorGUILayout.TextField("涂游测试 ID", playerId);
|
|
||||||
if (GUILayout.Button("登录并验证 Cookie")) Login();
|
|
||||||
}
|
|
||||||
EditorGUILayout.HelpBox(status, MessageType.Info);
|
|
||||||
EditorGUILayout.LabelField("此入口只验证认证,不初始化玩法或写入存档。");
|
|
||||||
}
|
|
||||||
|
|
||||||
async void Login()
|
|
||||||
{
|
|
||||||
busy = true;
|
|
||||||
session?.Dispose();
|
|
||||||
cancellation?.Dispose();
|
|
||||||
cancellation = new CancellationTokenSource();
|
|
||||||
var attempt = new S101AccountSession();
|
|
||||||
session = attempt;
|
|
||||||
var token = cancellation.Token;
|
|
||||||
status = "正在登录…";
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var login = await attempt.LoginAsync(playerId, token);
|
|
||||||
if (token.IsCancellationRequested) return;
|
|
||||||
if (!login.Success)
|
|
||||||
{
|
|
||||||
status = $"登录失败:{login.Error},HTTP {login.StatusCode}";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var probe = await attempt.CheckAuthorizationAsync(token);
|
|
||||||
if (token.IsCancellationRequested) return;
|
|
||||||
status = probe.Success
|
|
||||||
? $"认证通过:涂游 ID {attempt.PlayerId},内部 ID {attempt.InternalId};Cookie 授权读取通过。"
|
|
||||||
: $"登录成功,但授权读取未通过:{probe.Error},HTTP {probe.StatusCode}";
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
status = "验证异常:" + ex.GetType().Name;
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
busy = false;
|
|
||||||
if (this != null) Repaint();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void OnDisable()
|
|
||||||
{
|
|
||||||
cancellation?.Cancel();
|
|
||||||
session?.Dispose();
|
|
||||||
cancellation?.Dispose();
|
|
||||||
cancellation = null;
|
|
||||||
session = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -9,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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ namespace Activity.Data
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class EventDataRepository : IEventDataRepository
|
public class EventDataRepository : IEventDataRepository
|
||||||
{
|
{
|
||||||
private readonly game.PlayerDataStorage.IPlayerSaveStore store;
|
private readonly Core.KV.IPlayerSaveStore store;
|
||||||
public EventDataRepository(game.PlayerDataStorage.IPlayerSaveStore store)
|
public EventDataRepository(Core.KV.IPlayerSaveStore store)
|
||||||
{ this.store = store ?? throw new ArgumentNullException(nameof(store)); }
|
{ this.store = store ?? throw new ArgumentNullException(nameof(store)); }
|
||||||
|
|
||||||
public void Save<T>(T data) where T : IEventData
|
public void Save<T>(T data) where T : IEventData
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ namespace Activity.Services
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class EventTokenService : IEventTokenService
|
public class EventTokenService : IEventTokenService
|
||||||
{
|
{
|
||||||
private readonly game.PlayerDataStorage.IPlayerSaveStore store;
|
private readonly Core.KV.IPlayerSaveStore store;
|
||||||
public EventTokenService(game.PlayerDataStorage.IPlayerSaveStore store)
|
public EventTokenService(Core.KV.IPlayerSaveStore store)
|
||||||
{ this.store = store ?? throw new ArgumentNullException(nameof(store)); }
|
{ this.store = store ?? throw new ArgumentNullException(nameof(store)); }
|
||||||
|
|
||||||
#region 常量与字段
|
#region 常量与字段
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ using UnityEngine;
|
|||||||
public class AquariumManager
|
public class AquariumManager
|
||||||
{
|
{
|
||||||
public const string AquariumEventDataNewKey = "AquariumEventDataNewKey";
|
public const string AquariumEventDataNewKey = "AquariumEventDataNewKey";
|
||||||
readonly game.PlayerDataStorage.IPlayerSaveStore saveStore;
|
readonly Core.KV.IPlayerSaveStore saveStore;
|
||||||
readonly IUserService loginUser;
|
readonly IUserService loginUser;
|
||||||
private cfg.Tables _tables;
|
private cfg.Tables _tables;
|
||||||
private ICustomServerMgr customServerMgr;
|
private ICustomServerMgr customServerMgr;
|
||||||
@@ -23,14 +23,14 @@ public class AquariumManager
|
|||||||
public List<AquariumSlotData> sellList;
|
public List<AquariumSlotData> sellList;
|
||||||
public AquariumManager()
|
public AquariumManager()
|
||||||
{
|
{
|
||||||
saveStore = GContext.container.Resolve<game.PlayerDataStorage.IPlayerSaveStore>();
|
saveStore = GContext.container.Resolve<Core.KV.IPlayerSaveStore>();
|
||||||
loginUser = GContext.container.Resolve<IUserService>();
|
loginUser = GContext.container.Resolve<IUserService>();
|
||||||
_tables = GContext.container.Resolve<Tables>();
|
_tables = GContext.container.Resolve<Tables>();
|
||||||
customServerMgr = GContext.container.Resolve<ICustomServerMgr>();
|
customServerMgr = GContext.container.Resolve<ICustomServerMgr>();
|
||||||
}
|
}
|
||||||
public static AquariumManagerData GetAquariumManagerData(game.PlayerDataStorage.IPlayerSaveStore store = null)
|
public static AquariumManagerData GetAquariumManagerData(Core.KV.IPlayerSaveStore store = null)
|
||||||
{
|
{
|
||||||
var source = store ?? GContext.container.Resolve<game.PlayerDataStorage.IPlayerSaveStore>();
|
var source = store ?? GContext.container.Resolve<Core.KV.IPlayerSaveStore>();
|
||||||
string dataStr = source.Read(AquariumEventDataNewKey).ValueOrThrow();
|
string dataStr = source.Read(AquariumEventDataNewKey).ValueOrThrow();
|
||||||
if (!string.IsNullOrEmpty(dataStr))
|
if (!string.IsNullOrEmpty(dataStr))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using asap.core;
|
using asap.core;
|
||||||
using cfg;
|
using cfg;
|
||||||
using game;
|
using game;
|
||||||
using GameCore;
|
using GameCore;
|
||||||
@@ -10,7 +10,7 @@ using UnityEngine;
|
|||||||
|
|
||||||
public class CampDataMM
|
public class CampDataMM
|
||||||
{
|
{
|
||||||
[asap.core.Inject] public game.PlayerDataStorage.IPlayerSaveStore SaveStore { get; set; }
|
[asap.core.Inject] public Core.KV.IPlayerSaveStore SaveStore { get; set; }
|
||||||
|
|
||||||
private cfg.Tables _tables;
|
private cfg.Tables _tables;
|
||||||
MMTService _MMService;
|
MMTService _MMService;
|
||||||
@@ -104,23 +104,10 @@ public class CampDataMM
|
|||||||
public bool IsFix => _isFix && AreaIndex < AreaList.Count;
|
public bool IsFix => _isFix && AreaIndex < AreaList.Count;
|
||||||
bool _isFix;
|
bool _isFix;
|
||||||
|
|
||||||
internal static game.PlayerDataStorage.PlayerKVCampState DecodeForKVLoad(IReadOnlyDictionary<string,string> raw,
|
|
||||||
game.PlayerDataStorage.PlayerKVSchemaDiagnostics errors) => game.PlayerDataStorage.PlayerKVSaveSchema.DecodeCamp(raw,errors);
|
|
||||||
internal void ResetForKVLoad()
|
|
||||||
{
|
|
||||||
CampDataDic=new Dictionary<string,int>(StringComparer.Ordinal);
|
|
||||||
_isFix=false; _isOpenSkyscraper="0"; _id=101;
|
|
||||||
PreID=AreaIndex=AreaStep=AreaId=AreaStepCount=TimelineId=0;
|
|
||||||
AreaIcon=null; constructionArea=null; StepInfo=null; AreaStepList=null; IsUpdataArea=false;
|
|
||||||
}
|
|
||||||
internal void ReplaceFromKVDecoded(game.PlayerDataStorage.PlayerKVCampState state)
|
|
||||||
{
|
|
||||||
if (state == null) throw new ArgumentNullException(nameof(state));
|
|
||||||
CampDataDic=state.Values.ToDictionary(p=>p.Key,p=>p.Value,StringComparer.Ordinal);
|
|
||||||
_isFix=state.IsFix; _isOpenSkyscraper=state.Skyscraper;
|
|
||||||
}
|
|
||||||
internal game.PlayerDataStorage.PlayerKVCampState SnapshotForKVValidation() =>
|
|
||||||
new game.PlayerDataStorage.PlayerKVCampState(_isFix,_isOpenSkyscraper,CampDataDic);
|
|
||||||
public void InitData(Dictionary<string, string> userData)
|
public void InitData(Dictionary<string, string> userData)
|
||||||
{
|
{
|
||||||
string value;
|
string value;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ using System.Threading;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using game.Account;
|
using game.Account;
|
||||||
using game.PlayerDataStorage;
|
using game.KV;
|
||||||
|
|
||||||
public interface ICustomServerMgr
|
public interface ICustomServerMgr
|
||||||
{
|
{
|
||||||
@@ -27,12 +27,14 @@ public interface ICustomServerMgr
|
|||||||
Uri ApiBaseUri { get; }
|
Uri ApiBaseUri { get; }
|
||||||
int SessionVersion { get; }
|
int SessionVersion { get; }
|
||||||
event Action SessionChanged;
|
event Action SessionChanged;
|
||||||
void UseAccountSession(S101AccountSession session);
|
void UseAccountSession(GameServerAccountSession session);
|
||||||
void ClearAccountSession();
|
void ClearAccountSession();
|
||||||
}
|
}
|
||||||
|
|
||||||
public class CustomServerMgr : ICustomServerMgr
|
public class CustomServerMgr : ICustomServerMgr
|
||||||
{
|
{
|
||||||
|
public const string DefaultApiUrl = "http://s101.local:18080/";
|
||||||
|
|
||||||
IConfig config { get; set; }
|
IConfig config { get; set; }
|
||||||
|
|
||||||
//网络客户端
|
//网络客户端
|
||||||
@@ -43,34 +45,31 @@ public class CustomServerMgr : ICustomServerMgr
|
|||||||
private readonly SynchronizationContext ownerContext;
|
private readonly SynchronizationContext ownerContext;
|
||||||
public int SessionVersion { get; private set; }
|
public int SessionVersion { get; private set; }
|
||||||
public event Action SessionChanged;
|
public event Action SessionChanged;
|
||||||
PlayerKVAccountLease kvLease;
|
|
||||||
bool kvOnly;
|
private KvAccountLease kvLease;
|
||||||
bool kvBusinessReady;
|
private bool kvOnly;
|
||||||
bool kvBusinessPublished;
|
private bool kvBusinessReady;
|
||||||
int pfRequests;
|
private bool kvBusinessPublished;
|
||||||
|
private int pfRequests;
|
||||||
internal event Action<int> KVUnauthorized;
|
internal event Action<int> KVUnauthorized;
|
||||||
internal bool IsFreshForKVLoad => pfRequests==0 && kvLease==null;
|
internal bool IsFreshForKVLoad => pfRequests==0 && kvLease==null;
|
||||||
internal void BindKVForValidation(PlayerKVAccountLease lease)
|
internal void BindKVForGame(KvAccountLease lease)
|
||||||
{
|
{
|
||||||
if(lease==null || lease.IsFormal || !lease.IsCurrent || lease.ApiBaseUri!=ApiBaseUri || !IsFreshForKVLoad)
|
if (lease == null || !lease.IsCurrent ||
|
||||||
throw new InvalidOperationException("Fresh matching KV lease required.");
|
lease.ApiBaseUri.AbsoluteUri != ApiBaseUri.AbsoluteUri || !IsFreshForKVLoad)
|
||||||
kvOnly=true; kvBusinessReady=false; kvBusinessPublished=false; SessionVersion++; kvLease=lease; _cookie=null;
|
{
|
||||||
var previous=client; client=null; previous?.Dispose(); SessionChanged?.Invoke();
|
|
||||||
}
|
|
||||||
internal void BindKVForGame(PlayerKVAccountLease lease)
|
|
||||||
{
|
|
||||||
if(lease==null || !lease.IsFormal || !lease.IsCurrent || lease.ApiBaseUri.AbsoluteUri!=PlayerKVRuntimeSelection.FormalApiUrl ||
|
|
||||||
ApiBaseUri.AbsoluteUri!=PlayerKVRuntimeSelection.FormalApiUrl || !IsFreshForKVLoad)
|
|
||||||
throw new InvalidOperationException("Fresh formal KV lease required.");
|
throw new InvalidOperationException("Fresh formal KV lease required.");
|
||||||
|
}
|
||||||
kvOnly=true; kvBusinessReady=false; kvBusinessPublished=false; SessionVersion++; kvLease=lease; _cookie=null;
|
kvOnly=true; kvBusinessReady=false; kvBusinessPublished=false; SessionVersion++; kvLease=lease; _cookie=null;
|
||||||
var previous=client; client=null; previous?.Dispose(); SessionChanged?.Invoke();
|
var previous=client; client=null; previous?.Dispose(); SessionChanged?.Invoke();
|
||||||
}
|
}
|
||||||
internal void EnableKVBusiness(long epoch)
|
internal void EnableKVBusiness(long epoch)
|
||||||
{
|
{
|
||||||
if(!kvOnly || kvBusinessReady || kvBusinessPublished || kvLease==null || !kvLease.IsFormal || !kvLease.IsCurrent ||
|
if (!kvOnly || kvBusinessReady || kvBusinessPublished || kvLease == null || !kvLease.IsCurrent ||
|
||||||
kvLease.Epoch!=epoch || kvLease.ApiBaseUri.AbsoluteUri!=PlayerKVRuntimeSelection.FormalApiUrl ||
|
kvLease.Epoch != epoch || kvLease.ApiBaseUri.AbsoluteUri != ApiBaseUri.AbsoluteUri)
|
||||||
ApiBaseUri.AbsoluteUri!=PlayerKVRuntimeSelection.FormalApiUrl)
|
{
|
||||||
throw new InvalidOperationException("Current formal KV lease required for business transport.");
|
throw new InvalidOperationException("Current formal KV lease required for business transport.");
|
||||||
|
}
|
||||||
kvBusinessPublished=true; kvBusinessReady=true;
|
kvBusinessPublished=true; kvBusinessReady=true;
|
||||||
}
|
}
|
||||||
internal void DisableKVBusiness()
|
internal void DisableKVBusiness()
|
||||||
@@ -80,30 +79,21 @@ public class CustomServerMgr : ICustomServerMgr
|
|||||||
try { SessionChanged?.Invoke(); }
|
try { SessionChanged?.Invoke(); }
|
||||||
catch(Exception ex) { Debug.LogError($"KV session change failed: {ex.GetType().Name}"); }
|
catch(Exception ex) { Debug.LogError($"KV session change failed: {ex.GetType().Name}"); }
|
||||||
}
|
}
|
||||||
internal Task ClearKVForGame() => ClearKVForValidation();
|
internal Task ClearKVForGame()
|
||||||
internal Task ClearKVForValidation()
|
|
||||||
{
|
{
|
||||||
kvBusinessReady=false; SessionVersion++; var previous=kvLease; kvLease=null; _cookie=null;
|
kvBusinessReady = false;
|
||||||
var closing=previous?.CloseAsync() ?? Task.CompletedTask;
|
SessionVersion++;
|
||||||
SessionChanged?.Invoke(); return closing;
|
var previous = kvLease;
|
||||||
}
|
kvLease = null;
|
||||||
internal async Task<PlayerKVAccountResponse> SendKVForValidationAsync(string route)
|
_cookie = null;
|
||||||
{
|
var closing = previous?.CloseAsync() ?? Task.CompletedTask;
|
||||||
var lease=kvLease; int version=SessionVersion;
|
SessionChanged?.Invoke();
|
||||||
if(!kvOnly || lease==null) return new PlayerKVAccountResponse(false);
|
return closing;
|
||||||
var response=await lease.SendAsync(HttpMethod.Get,route);
|
|
||||||
if(version!=SessionVersion || !ReferenceEquals(lease,kvLease) || !response.IsCurrent) return new PlayerKVAccountResponse(false);
|
|
||||||
if(response.StatusCode==401 || response.StatusCode==403)
|
|
||||||
{
|
|
||||||
KVUnauthorized?.Invoke(version);
|
|
||||||
return new PlayerKVAccountResponse(false,response.StatusCode);
|
|
||||||
}
|
|
||||||
return response;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public CookieContainer Cookie => kvOnly
|
public CookieContainer Cookie => kvOnly
|
||||||
? (kvBusinessReady && kvLease?.IsFormal==true ? kvLease.FormalCookies : null) ?? throw new NullReferenceException()
|
? (kvBusinessReady && kvLease != null ? kvLease.FormalCookies : null) ?? throw new NullReferenceException()
|
||||||
: _cookie ?? throw new NullReferenceException();
|
: _cookie ?? throw new NullReferenceException();
|
||||||
public Uri ApiBaseUri { get; }
|
public Uri ApiBaseUri { get; }
|
||||||
|
|
||||||
@@ -118,13 +108,16 @@ public class CustomServerMgr : ICustomServerMgr
|
|||||||
});
|
});
|
||||||
this.config = config;
|
this.config = config;
|
||||||
|
|
||||||
var url = config.Get<string>(GConstant.K_Event_API_URL, S101AccountSession.DefaultApiUrl);
|
var url = config.Get<string>(GConstant.K_Event_API_URL, DefaultApiUrl);
|
||||||
if (string.IsNullOrWhiteSpace(url)) url = S101AccountSession.DefaultApiUrl;
|
if (string.IsNullOrWhiteSpace(url))
|
||||||
|
{
|
||||||
|
url = DefaultApiUrl;
|
||||||
|
}
|
||||||
ApiBaseUri = new Uri(url.TrimEnd('/') + "/");
|
ApiBaseUri = new Uri(url.TrimEnd('/') + "/");
|
||||||
ClearAccountSession();
|
ClearAccountSession();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UseAccountSession(S101AccountSession session)
|
public void UseAccountSession(GameServerAccountSession session)
|
||||||
{
|
{
|
||||||
if(kvOnly) throw new InvalidOperationException("Controlled container cannot switch to PlayFab.");
|
if(kvOnly) throw new InvalidOperationException("Controlled container cannot switch to PlayFab.");
|
||||||
if (session == null || !session.IsAuthenticated || session.ApiBaseUri != ApiBaseUri)
|
if (session == null || !session.IsAuthenticated || session.ApiBaseUri != ApiBaseUri)
|
||||||
@@ -134,7 +127,11 @@ public class CustomServerMgr : ICustomServerMgr
|
|||||||
|
|
||||||
public void ClearAccountSession()
|
public void ClearAccountSession()
|
||||||
{
|
{
|
||||||
if(kvOnly) { PlayerKVShutdownRegistry.Process.Register(ClearKVForValidation()); return; }
|
if (kvOnly)
|
||||||
|
{
|
||||||
|
KvShutdownRegistry.Process.Register(ClearKVForGame());
|
||||||
|
return;
|
||||||
|
}
|
||||||
ReplaceTransport(new CookieContainer());
|
ReplaceTransport(new CookieContainer());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,7 +167,10 @@ public class CustomServerMgr : ICustomServerMgr
|
|||||||
}
|
}
|
||||||
if(!kvBusinessReady) throw new InvalidOperationException("Formal KV business transport is not ready.");
|
if(!kvBusinessReady) throw new InvalidOperationException("Formal KV business transport is not ready.");
|
||||||
var lease=kvLease; var kvVersion=SessionVersion;
|
var lease=kvLease; var kvVersion=SessionVersion;
|
||||||
if(lease==null || !lease.IsFormal || !lease.IsCurrent) return null;
|
if (lease == null || !lease.IsCurrent)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var response=await lease.SendAsync(method,url,body,contentType);
|
var response=await lease.SendAsync(method,url,body,contentType);
|
||||||
@@ -286,3 +286,4 @@ public class CustomServerMgr : ICustomServerMgr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,54 +11,101 @@ using Newtonsoft.Json.Linq;
|
|||||||
|
|
||||||
namespace game.Account
|
namespace game.Account
|
||||||
{
|
{
|
||||||
public enum AccountLoginError { None, Validation, Http, Rejected, Protocol, Network, Timeout, Cancelled }
|
public enum AccountLoginError
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
Validation,
|
||||||
|
Http,
|
||||||
|
Rejected,
|
||||||
|
Protocol,
|
||||||
|
Network,
|
||||||
|
Timeout,
|
||||||
|
Cancelled
|
||||||
|
}
|
||||||
|
|
||||||
public sealed class AccountLoginResult
|
public sealed class AccountLoginResult
|
||||||
{
|
{
|
||||||
public bool Success => Error == AccountLoginError.None;
|
public bool Success => Error == AccountLoginError.None;
|
||||||
public AccountLoginError Error { get; }
|
public AccountLoginError Error
|
||||||
public int? StatusCode { get; }
|
{
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
public int? StatusCode
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
}
|
||||||
internal AccountLoginResult(AccountLoginError error, int? status = null)
|
internal AccountLoginResult(AccountLoginError error, int? status = null)
|
||||||
{ Error = error; StatusCode = status; }
|
{
|
||||||
|
Error = error;
|
||||||
|
StatusCode = status;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Minimal Tuyou-ID authentication, independent of gameplay and PlayFab storage.
|
/// Minimal Tuyou-ID authentication, independent of gameplay and PlayFab storage.
|
||||||
/// Each login owns a fresh cookie jar; dispose the session on logout.
|
/// Each login owns a fresh cookie jar; dispose the session on logout.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class S101AccountSession : IDisposable
|
public sealed class GameServerAccountSession : IDisposable
|
||||||
{
|
{
|
||||||
public const string DefaultApiUrl = "http://s101.local:18080/";
|
private readonly object gate = new object();
|
||||||
readonly object gate = new object();
|
private readonly Uri api;
|
||||||
readonly Uri api;
|
private readonly Func<CookieContainer, HttpMessageHandler> handlerFactory;
|
||||||
readonly Func<CookieContainer, HttpMessageHandler> handlerFactory;
|
private HttpClient http;
|
||||||
HttpClient http;
|
private int generation;
|
||||||
int generation;
|
private bool disposed;
|
||||||
bool disposed;
|
|
||||||
|
|
||||||
public bool IsAuthenticated { get; private set; }
|
public bool IsAuthenticated
|
||||||
public string PlayerId { get; private set; }
|
{
|
||||||
public uint InternalId { get; private set; }
|
get;
|
||||||
public DateTime CreateTimeUtc { get; private set; }
|
private set;
|
||||||
public DateTime ServerTimeUtc { get; private set; }
|
}
|
||||||
public bool NewlyCreated { get; private set; }
|
public string PlayerId
|
||||||
public CookieContainer Cookies { get; private set; }
|
{
|
||||||
|
get;
|
||||||
|
private set;
|
||||||
|
}
|
||||||
|
public uint InternalId
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
private set;
|
||||||
|
}
|
||||||
|
public DateTime CreateTimeUtc
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
private set;
|
||||||
|
}
|
||||||
|
public DateTime ServerTimeUtc
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
private set;
|
||||||
|
}
|
||||||
|
public bool NewlyCreated
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
private set;
|
||||||
|
}
|
||||||
|
public CookieContainer Cookies
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
private set;
|
||||||
|
}
|
||||||
public Uri ApiBaseUri => api;
|
public Uri ApiBaseUri => api;
|
||||||
|
|
||||||
public S101AccountSession() : this(new Uri(DefaultApiUrl)) { }
|
// KV supplies its owned transport; the PF path uses the default handler.
|
||||||
|
public GameServerAccountSession(Uri api, Func<CookieContainer, HttpMessageHandler> handlerFactory = null)
|
||||||
// The factory is optional for transport tests. Returned handlers are owned by this session.
|
|
||||||
public S101AccountSession(Uri api, Func<CookieContainer, HttpMessageHandler> handlerFactory = null)
|
|
||||||
{
|
{
|
||||||
if (api == null || !api.IsAbsoluteUri ||
|
if (api == null || !api.IsAbsoluteUri ||
|
||||||
(api.Scheme != "http" && api.Scheme != "https") ||
|
(api.Scheme != "http" && api.Scheme != "https") ||
|
||||||
!string.IsNullOrEmpty(api.Query) || !string.IsNullOrEmpty(api.Fragment) ||
|
!string.IsNullOrEmpty(api.Query) || !string.IsNullOrEmpty(api.Fragment) ||
|
||||||
!string.IsNullOrEmpty(api.UserInfo))
|
!string.IsNullOrEmpty(api.UserInfo))
|
||||||
|
{
|
||||||
throw new ArgumentException("An absolute HTTP(S) API base URI is required.", nameof(api));
|
throw new ArgumentException("An absolute HTTP(S) API base URI is required.", nameof(api));
|
||||||
|
}
|
||||||
this.api = new Uri(api.AbsoluteUri.TrimEnd('/') + "/");
|
this.api = new Uri(api.AbsoluteUri.TrimEnd('/') + "/");
|
||||||
this.handlerFactory = handlerFactory ?? (jar => new HttpClientHandler
|
this.handlerFactory = handlerFactory ?? (jar => new HttpClientHandler
|
||||||
{
|
{
|
||||||
CookieContainer = jar, UseCookies = true, AllowAutoRedirect = false
|
CookieContainer = jar,
|
||||||
|
UseCookies = true,
|
||||||
|
AllowAutoRedirect = false
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,7 +117,10 @@ namespace game.Account
|
|||||||
string id;
|
string id;
|
||||||
lock (gate)
|
lock (gate)
|
||||||
{
|
{
|
||||||
if (disposed) return Result(AccountLoginError.Cancelled);
|
if (disposed)
|
||||||
|
{
|
||||||
|
return Result(AccountLoginError.Cancelled);
|
||||||
|
}
|
||||||
generation++;
|
generation++;
|
||||||
ClearIdentity();
|
ClearIdentity();
|
||||||
http?.Dispose();
|
http?.Dispose();
|
||||||
@@ -78,11 +128,19 @@ namespace game.Account
|
|||||||
Cookies = null;
|
Cookies = null;
|
||||||
if (string.IsNullOrEmpty(tuyooId) ||
|
if (string.IsNullOrEmpty(tuyooId) ||
|
||||||
!long.TryParse(tuyooId, NumberStyles.None, CultureInfo.InvariantCulture, out var numericId) || numericId <= 0)
|
!long.TryParse(tuyooId, NumberStyles.None, CultureInfo.InvariantCulture, out var numericId) || numericId <= 0)
|
||||||
|
{
|
||||||
return Result(AccountLoginError.Validation);
|
return Result(AccountLoginError.Validation);
|
||||||
if (token.IsCancellationRequested) return Result(AccountLoginError.Cancelled);
|
}
|
||||||
|
if (token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
return Result(AccountLoginError.Cancelled);
|
||||||
|
}
|
||||||
id = numericId.ToString(CultureInfo.InvariantCulture);
|
id = numericId.ToString(CultureInfo.InvariantCulture);
|
||||||
cookies = new CookieContainer();
|
cookies = new CookieContainer();
|
||||||
client = new HttpClient(handlerFactory(cookies)) { Timeout = TimeSpan.FromSeconds(15) };
|
client = new HttpClient(handlerFactory(cookies))
|
||||||
|
{
|
||||||
|
Timeout = TimeSpan.FromSeconds(15)
|
||||||
|
};
|
||||||
http = client;
|
http = client;
|
||||||
Cookies = cookies;
|
Cookies = cookies;
|
||||||
requestGeneration = generation;
|
requestGeneration = generation;
|
||||||
@@ -91,24 +149,40 @@ namespace game.Account
|
|||||||
int? status = null;
|
int? status = null;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using (var request = Request("account/Login", new JObject { ["playerId"] = id }))
|
using (var request = Request("account/Login", new JObject
|
||||||
|
{
|
||||||
|
["playerId"] = id
|
||||||
|
}))
|
||||||
using (var response = await client.SendAsync(request, token))
|
using (var response = await client.SendAsync(request, token))
|
||||||
{
|
{
|
||||||
status = (int)response.StatusCode;
|
status = (int)response.StatusCode;
|
||||||
if (response.StatusCode != HttpStatusCode.OK) return Result(AccountLoginError.Http, status);
|
if (response.StatusCode != HttpStatusCode.OK)
|
||||||
|
{
|
||||||
|
return Result(AccountLoginError.Http, status);
|
||||||
|
}
|
||||||
var body = Parse(await response.Content.ReadAsStringAsync());
|
var body = Parse(await response.Content.ReadAsStringAsync());
|
||||||
if (body["state"]?.Type != JTokenType.Integer) return Result(AccountLoginError.Protocol, status);
|
if (body["state"]?.Type != JTokenType.Integer)
|
||||||
if (body["state"].Value<long>() != 0) return Result(AccountLoginError.Rejected, status);
|
{
|
||||||
|
return Result(AccountLoginError.Protocol, status);
|
||||||
|
}
|
||||||
|
if (body["state"].Value<long>() != 0)
|
||||||
|
{
|
||||||
|
return Result(AccountLoginError.Rejected, status);
|
||||||
|
}
|
||||||
if (body["internalId"]?.Type != JTokenType.Integer ||
|
if (body["internalId"]?.Type != JTokenType.Integer ||
|
||||||
!uint.TryParse(body["internalId"].ToString(), out var internalId) || internalId == 0 ||
|
!uint.TryParse(body["internalId"].ToString(), out var internalId) || internalId == 0 ||
|
||||||
body["newCreate"]?.Type != JTokenType.Boolean ||
|
body["newCreate"]?.Type != JTokenType.Boolean ||
|
||||||
!ReadUtc(body["createTime"], out var created) || !ReadUtc(body["svrTime"], out var serverTime) ||
|
!ReadUtc(body["createTime"], out var created) || !ReadUtc(body["svrTime"], out var serverTime) ||
|
||||||
cookies.GetCookies(api).Count == 0)
|
cookies.GetCookies(api).Count == 0)
|
||||||
|
{
|
||||||
return Result(AccountLoginError.Protocol, status);
|
return Result(AccountLoginError.Protocol, status);
|
||||||
|
}
|
||||||
lock (gate)
|
lock (gate)
|
||||||
{
|
{
|
||||||
if (disposed || generation != requestGeneration || token.IsCancellationRequested)
|
if (disposed || generation != requestGeneration || token.IsCancellationRequested)
|
||||||
|
{
|
||||||
return Result(AccountLoginError.Cancelled, status);
|
return Result(AccountLoginError.Cancelled, status);
|
||||||
|
}
|
||||||
PlayerId = id;
|
PlayerId = id;
|
||||||
InternalId = internalId;
|
InternalId = internalId;
|
||||||
CreateTimeUtc = created;
|
CreateTimeUtc = created;
|
||||||
@@ -122,82 +196,59 @@ namespace game.Account
|
|||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
lock (gate)
|
lock (gate)
|
||||||
return Result(disposed || generation != requestGeneration || token.IsCancellationRequested
|
|
||||||
? AccountLoginError.Cancelled : AccountLoginError.Timeout, status);
|
|
||||||
}
|
|
||||||
catch (ObjectDisposedException) { return Result(AccountLoginError.Cancelled, status); }
|
|
||||||
catch (HttpRequestException) { return Result(AccountLoginError.Network, status); }
|
|
||||||
catch (JsonException) { return Result(AccountLoginError.Protocol, status); }
|
|
||||||
catch (OverflowException) { return Result(AccountLoginError.Protocol, status); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Read-only probe: verifies that the server accepts this session cookie; never writes KV.</summary>
|
|
||||||
public async Task<AccountLoginResult> CheckAuthorizationAsync(CancellationToken token = default)
|
|
||||||
{
|
|
||||||
HttpClient client;
|
|
||||||
int requestGeneration;
|
|
||||||
lock (gate)
|
|
||||||
{
|
|
||||||
if (disposed || !IsAuthenticated) return Result(AccountLoginError.Rejected);
|
|
||||||
client = http;
|
|
||||||
requestGeneration = generation;
|
|
||||||
}
|
|
||||||
int? status = null;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using (var request = Request("playerdata/Get", new JObject()))
|
|
||||||
using (var response = await client.SendAsync(request, token))
|
|
||||||
{
|
{
|
||||||
status = (int)response.StatusCode;
|
return Result(disposed || generation != requestGeneration || token.IsCancellationRequested
|
||||||
lock (gate)
|
? AccountLoginError.Cancelled : AccountLoginError.Timeout, status);
|
||||||
{
|
|
||||||
if (disposed || generation != requestGeneration || token.IsCancellationRequested)
|
|
||||||
return Result(AccountLoginError.Cancelled, status);
|
|
||||||
if (status == 401 || status == 403) ClearIdentity();
|
|
||||||
}
|
|
||||||
if (response.StatusCode != HttpStatusCode.OK) return Result(AccountLoginError.Http, status);
|
|
||||||
var body = Parse(await response.Content.ReadAsStringAsync());
|
|
||||||
if (!(body["data"] is JObject data)) return Result(AccountLoginError.Protocol, status);
|
|
||||||
foreach (var item in data.Properties())
|
|
||||||
if (item.Value.Type != JTokenType.String) return Result(AccountLoginError.Protocol, status);
|
|
||||||
lock (gate)
|
|
||||||
return Result(disposed || generation != requestGeneration || token.IsCancellationRequested
|
|
||||||
? AccountLoginError.Cancelled : AccountLoginError.None, status);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (ObjectDisposedException)
|
||||||
{
|
{
|
||||||
lock (gate)
|
return Result(AccountLoginError.Cancelled, status);
|
||||||
return Result(disposed || generation != requestGeneration || token.IsCancellationRequested
|
}
|
||||||
? AccountLoginError.Cancelled : AccountLoginError.Timeout, status);
|
catch (HttpRequestException)
|
||||||
|
{
|
||||||
|
return Result(AccountLoginError.Network, status);
|
||||||
|
}
|
||||||
|
catch (JsonException)
|
||||||
|
{
|
||||||
|
return Result(AccountLoginError.Protocol, status);
|
||||||
|
}
|
||||||
|
catch (OverflowException)
|
||||||
|
{
|
||||||
|
return Result(AccountLoginError.Protocol, status);
|
||||||
}
|
}
|
||||||
catch (ObjectDisposedException) { return Result(AccountLoginError.Cancelled, status); }
|
|
||||||
catch (HttpRequestException) { return Result(AccountLoginError.Network, status); }
|
|
||||||
catch (JsonException) { return Result(AccountLoginError.Protocol, status); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
HttpRequestMessage Request(string path, JObject body) => new HttpRequestMessage(HttpMethod.Post, new Uri(api, path))
|
private HttpRequestMessage Request(string path, JObject body) => new HttpRequestMessage(HttpMethod.Post, new Uri(api, path))
|
||||||
{ Content = new StringContent(body.ToString(Formatting.None), Encoding.UTF8, "application/json") };
|
|
||||||
|
|
||||||
static JObject Parse(string text)
|
|
||||||
{
|
{
|
||||||
using (var reader = new JsonTextReader(new StringReader(text)) { DateParseHandling = DateParseHandling.None })
|
Content = new StringContent(body.ToString(Formatting.None), Encoding.UTF8, "application/json")
|
||||||
|
};
|
||||||
|
|
||||||
|
private static JObject Parse(string text)
|
||||||
|
{
|
||||||
|
using (var reader = new JsonTextReader(new StringReader(text))
|
||||||
|
{
|
||||||
|
DateParseHandling = DateParseHandling.None
|
||||||
|
})
|
||||||
{
|
{
|
||||||
var value = JObject.Load(reader);
|
var value = JObject.Load(reader);
|
||||||
if (reader.Read()) throw new JsonReaderException("Trailing JSON content.");
|
if (reader.Read())
|
||||||
|
{
|
||||||
|
throw new JsonReaderException("Trailing JSON content.");
|
||||||
|
}
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool ReadUtc(JToken value, out DateTime date)
|
private static bool ReadUtc(JToken value, out DateTime date)
|
||||||
{
|
{
|
||||||
date = default;
|
date = default;
|
||||||
return value?.Type == JTokenType.String && DateTime.TryParse(value.Value<string>(), CultureInfo.InvariantCulture,
|
return value?.Type == JTokenType.String && DateTime.TryParse(value.Value<string>(), CultureInfo.InvariantCulture,
|
||||||
DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out date);
|
DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out date);
|
||||||
}
|
}
|
||||||
|
|
||||||
static AccountLoginResult Result(AccountLoginError error, int? status = null) => new AccountLoginResult(error, status);
|
private static AccountLoginResult Result(AccountLoginError error, int? status = null) => new AccountLoginResult(error, status);
|
||||||
void ClearIdentity()
|
private void ClearIdentity()
|
||||||
{
|
{
|
||||||
IsAuthenticated = false;
|
IsAuthenticated = false;
|
||||||
PlayerId = null;
|
PlayerId = null;
|
||||||
@@ -211,7 +262,10 @@ namespace game.Account
|
|||||||
{
|
{
|
||||||
lock (gate)
|
lock (gate)
|
||||||
{
|
{
|
||||||
if (disposed) return;
|
if (disposed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
disposed = true;
|
disposed = true;
|
||||||
generation++;
|
generation++;
|
||||||
ClearIdentity();
|
ClearIdentity();
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: a8f49f7440c141e89d8519ff3aa1c034
|
guid: 47cad15649b346799afbe5e5cafb4bb3
|
||||||
MonoImporter:
|
MonoImporter:
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: 4930c4a417ba4c908416717d26695848
|
guid: f3f810de2c56488e979e3e585c94b35a
|
||||||
folderAsset: yes
|
folderAsset: yes
|
||||||
DefaultImporter:
|
DefaultImporter:
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
76
Client/Assets/Scripts/Core/KV/IPlayerSaveStore.cs
Normal file
76
Client/Assets/Scripts/Core/KV/IPlayerSaveStore.cs
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Core.KV
|
||||||
|
{
|
||||||
|
// A store belongs to one login. Callers retain this instance across awaits.
|
||||||
|
public interface IPlayerSaveStore
|
||||||
|
{
|
||||||
|
bool IsCurrent
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
PlayerSaveReadResult Read(string key);
|
||||||
|
PlayerSaveWriteResult Write(string key, string value, bool remoteStore = true);
|
||||||
|
Task<bool> SyncDataImmidiate(string key, string value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class PlayerSaveReadResult
|
||||||
|
{
|
||||||
|
public bool Success => Error == null;
|
||||||
|
public bool? Found
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
public string Value
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
public string Error
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
internal PlayerSaveReadResult(bool found, string value)
|
||||||
|
{
|
||||||
|
Found = found;
|
||||||
|
Value = value;
|
||||||
|
}
|
||||||
|
internal PlayerSaveReadResult(string error)
|
||||||
|
{
|
||||||
|
Error = error ?? "ReadFailed";
|
||||||
|
}
|
||||||
|
public string ValueOrThrow()
|
||||||
|
{
|
||||||
|
if (!Success)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Player save read failed: " + Error);
|
||||||
|
}
|
||||||
|
return Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class PlayerSaveWriteResult
|
||||||
|
{
|
||||||
|
public bool Accepted => Error == null;
|
||||||
|
public string Error
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
internal PlayerSaveWriteResult()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
internal PlayerSaveWriteResult(string error)
|
||||||
|
{
|
||||||
|
Error = error ?? "WriteRejected";
|
||||||
|
}
|
||||||
|
public void RequireAccepted()
|
||||||
|
{
|
||||||
|
if (!Accepted)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Player save write rejected: " + Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
132
Client/Assets/Scripts/Core/KV/KvCacheStore.cs
Normal file
132
Client/Assets/Scripts/Core/KV/KvCacheStore.cs
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Security;
|
||||||
|
using System.Text;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace Core.KV
|
||||||
|
{
|
||||||
|
internal sealed class KvCacheStore : IDisposable
|
||||||
|
{
|
||||||
|
private const long MaxBytes = 64L * 1024 * 1024;
|
||||||
|
private readonly KvIdentity identity;
|
||||||
|
private readonly FileStream lease;
|
||||||
|
private readonly string main, temporary, backup;
|
||||||
|
private bool disposed;
|
||||||
|
|
||||||
|
private KvCacheStore(KvIdentity identity, FileStream lease)
|
||||||
|
{
|
||||||
|
this.identity = identity;
|
||||||
|
this.lease = lease;
|
||||||
|
main = Path.Combine(identity.DirectoryPath, "state.json");
|
||||||
|
temporary = main + ".tmp";
|
||||||
|
backup = main + ".bak";
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static KVOpenError Open(KvIdentity identity,
|
||||||
|
out KvCacheStore store, out KvSnapshot snapshot)
|
||||||
|
{
|
||||||
|
store = null;
|
||||||
|
snapshot = null;
|
||||||
|
FileStream lease = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(identity.DirectoryPath);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
lease = new FileStream(Path.Combine(identity.DirectoryPath, "state.lock"), FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None);
|
||||||
|
}
|
||||||
|
catch (IOException ex) when ((ex.HResult & 0xffff) == 32 || (ex.HResult & 0xffff) == 33)
|
||||||
|
{
|
||||||
|
return KVOpenError.Busy;
|
||||||
|
}
|
||||||
|
store = new KvCacheStore(identity, lease);
|
||||||
|
if (!File.Exists(store.main))
|
||||||
|
{
|
||||||
|
if (File.Exists(store.backup) || File.Exists(store.temporary))
|
||||||
|
{
|
||||||
|
throw KvSnapshot.Invalid();
|
||||||
|
}
|
||||||
|
snapshot = new KvSnapshot();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (new FileInfo(store.main).Length > MaxBytes)
|
||||||
|
{
|
||||||
|
throw KvSnapshot.Invalid();
|
||||||
|
}
|
||||||
|
snapshot = KvSnapshot.Decode(File.ReadAllText(store.main, new UTF8Encoding(false, true)), identity);
|
||||||
|
}
|
||||||
|
return KVOpenError.None;
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex is JsonException || ex is OverflowException || ex is FormatException || ex is DecoderFallbackException)
|
||||||
|
{
|
||||||
|
store?.Dispose();
|
||||||
|
lease?.Dispose();
|
||||||
|
store = null;
|
||||||
|
return KVOpenError.RecoveryRequired;
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (DiskError(ex))
|
||||||
|
{
|
||||||
|
store?.Dispose();
|
||||||
|
lease?.Dispose();
|
||||||
|
store = null;
|
||||||
|
return KVOpenError.StorageError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal bool Save(KvSnapshot snapshot, out bool mayHaveCommitted)
|
||||||
|
{
|
||||||
|
mayHaveCommitted = false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (disposed)
|
||||||
|
{
|
||||||
|
// Preserve the existing diagnostic ObjectName when renaming the implementation.
|
||||||
|
throw new ObjectDisposedException("PlayerKVCacheStore");
|
||||||
|
}
|
||||||
|
snapshot.Validate();
|
||||||
|
var bytes = new UTF8Encoding(false, true).GetBytes(snapshot.Encode(identity));
|
||||||
|
if (bytes.LongLength > MaxBytes)
|
||||||
|
{
|
||||||
|
throw new IOException("KV cache capacity exceeded.");
|
||||||
|
}
|
||||||
|
using (var stream = new FileStream(temporary, FileMode.Create, FileAccess.Write, FileShare.None))
|
||||||
|
{
|
||||||
|
stream.Write(bytes, 0, bytes.Length);
|
||||||
|
stream.Flush(true);
|
||||||
|
}
|
||||||
|
mayHaveCommitted = true;
|
||||||
|
// The temp file is on the same filesystem. Never delete the main file first.
|
||||||
|
if (File.Exists(main))
|
||||||
|
{
|
||||||
|
File.Replace(temporary, main, backup);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
File.Move(temporary, main);
|
||||||
|
}
|
||||||
|
mayHaveCommitted = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (DiskError(ex) || ex is JsonException || ex is EncoderFallbackException)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool DiskError(Exception ex) => ex is IOException || ex is UnauthorizedAccessException ||
|
||||||
|
ex is SecurityException || ex is NotSupportedException || ex is ObjectDisposedException;
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (disposed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
disposed = true;
|
||||||
|
lease.Dispose();
|
||||||
|
// Keep files, including failed temporary writes, for explicit recovery diagnosis.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
247
Client/Assets/Scripts/Core/KV/KvClient.cs
Normal file
247
Client/Assets/Scripts/Core/KV/KvClient.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
461
Client/Assets/Scripts/Core/KV/KvSessionOwner.cs
Normal file
461
Client/Assets/Scripts/Core/KV/KvSessionOwner.cs
Normal file
@@ -0,0 +1,461 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using game.Account;
|
||||||
|
|
||||||
|
namespace Core.KV
|
||||||
|
{
|
||||||
|
public enum KVSessionOpenError
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
Validation,
|
||||||
|
Unauthenticated,
|
||||||
|
Busy,
|
||||||
|
RecoveryRequired,
|
||||||
|
StorageError,
|
||||||
|
ReadFailed,
|
||||||
|
Unauthorized,
|
||||||
|
Stale,
|
||||||
|
Cancelled
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class KvSessionOpenResult
|
||||||
|
{
|
||||||
|
public bool Success => Error == KVSessionOpenError.None;
|
||||||
|
public KVSessionOpenError Error
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
public KvSessionOwner Owner
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
internal KvSessionOpenResult(KVSessionOpenError error, KvSessionOwner owner = null)
|
||||||
|
{
|
||||||
|
Error = error;
|
||||||
|
Owner = owner;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Owns one immutable account/epoch/cache/transport tuple.</summary>
|
||||||
|
public sealed class KvSessionOwner
|
||||||
|
{
|
||||||
|
private sealed class OperationLease : IDisposable
|
||||||
|
{
|
||||||
|
private KvSessionOwner owner;
|
||||||
|
internal OperationLease(KvSessionOwner owner)
|
||||||
|
{
|
||||||
|
this.owner = owner;
|
||||||
|
}
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Interlocked.Exchange(ref owner, null)?.ExitOperation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly object admission = new object();
|
||||||
|
private readonly GameServerAccountSession session;
|
||||||
|
private readonly CookieContainer cookies;
|
||||||
|
private readonly CancellationTokenSource lifetime;
|
||||||
|
private readonly Func<long, bool> isCurrent;
|
||||||
|
private readonly KvSyncState state;
|
||||||
|
|
||||||
|
private readonly KvSyncCoordinator coordinator;
|
||||||
|
private readonly TaskCompletionSource<bool> drained =
|
||||||
|
new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
private readonly string ownerId = Guid.NewGuid().ToString("N");
|
||||||
|
private int activeOperations;
|
||||||
|
private bool closing, closed;
|
||||||
|
private TaskCompletionSource<bool> closeCompletion;
|
||||||
|
|
||||||
|
public string OwnerInstanceId => ownerId;
|
||||||
|
public Uri ApiBaseUri
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
public string PlayerId
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
public long Epoch
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
public KVSyncStatus Status => state.Status;
|
||||||
|
|
||||||
|
private KvSessionOwner(GameServerAccountSession session, CookieContainer cookies, Uri api, string playerId,
|
||||||
|
long epoch, Func<long, bool> isCurrent, CancellationTokenSource lifetime,
|
||||||
|
KvSyncState state, HttpClient http)
|
||||||
|
{
|
||||||
|
this.session = session;
|
||||||
|
this.cookies = cookies;
|
||||||
|
ApiBaseUri = api;
|
||||||
|
PlayerId = playerId;
|
||||||
|
Epoch = epoch;
|
||||||
|
this.isCurrent = isCurrent;
|
||||||
|
this.lifetime = lifetime;
|
||||||
|
this.state = state;
|
||||||
|
coordinator = new KvSyncCoordinator(state, new KvClient(http, api), http,
|
||||||
|
IsCapturedOwnerCurrent, lifetime.Token);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static async Task<KvSessionOpenResult> CreateAsync(GameServerAccountSession session, string cacheRoot,
|
||||||
|
string expectedPlayerId, Uri expectedApiBaseUri, long epoch, Func<long, bool> isCurrent,
|
||||||
|
CancellationToken lifetimeToken, Func<CookieContainer, HttpMessageHandler> handlerFactory)
|
||||||
|
{
|
||||||
|
if (session == null || expectedApiBaseUri == null || isCurrent == null || handlerFactory == null || epoch < 0 ||
|
||||||
|
!long.TryParse(expectedPlayerId, NumberStyles.None, CultureInfo.InvariantCulture, out var numericId) || numericId <= 0)
|
||||||
|
{
|
||||||
|
return new KvSessionOpenResult(KVSessionOpenError.Validation);
|
||||||
|
}
|
||||||
|
var playerId = numericId.ToString(CultureInfo.InvariantCulture);
|
||||||
|
Uri expectedApi;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
expectedApi = NormalizeApi(expectedApiBaseUri);
|
||||||
|
}
|
||||||
|
catch (ArgumentException)
|
||||||
|
{
|
||||||
|
return new KvSessionOpenResult(KVSessionOpenError.Validation);
|
||||||
|
}
|
||||||
|
if (lifetimeToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
return new KvSessionOpenResult(KVSessionOpenError.Cancelled);
|
||||||
|
}
|
||||||
|
if (!session.IsAuthenticated || session.Cookies == null)
|
||||||
|
{
|
||||||
|
return new KvSessionOpenResult(KVSessionOpenError.Unauthenticated);
|
||||||
|
}
|
||||||
|
var capturedCookies = session.Cookies;
|
||||||
|
var capturedApi = NormalizeApi(session.ApiBaseUri);
|
||||||
|
if (session.PlayerId != playerId || capturedApi != expectedApi || !isCurrent(epoch))
|
||||||
|
{
|
||||||
|
return new KvSessionOpenResult(KVSessionOpenError.Stale);
|
||||||
|
}
|
||||||
|
|
||||||
|
var opened = KvSyncState.Open(cacheRoot, capturedApi, playerId);
|
||||||
|
if (!opened.Success)
|
||||||
|
{
|
||||||
|
return new KvSessionOpenResult(MapOpen(opened.Error));
|
||||||
|
}
|
||||||
|
HttpClient http = null;
|
||||||
|
CancellationTokenSource lifetime = null;
|
||||||
|
KvSessionOwner owner = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var handler = handlerFactory(capturedCookies);
|
||||||
|
if (handler == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Transport factory returned null.");
|
||||||
|
}
|
||||||
|
http = new HttpClient(handler)
|
||||||
|
{
|
||||||
|
Timeout = TimeSpan.FromSeconds(15)
|
||||||
|
};
|
||||||
|
lifetime = CancellationTokenSource.CreateLinkedTokenSource(lifetimeToken);
|
||||||
|
owner = new KvSessionOwner(session, capturedCookies, capturedApi, playerId, epoch,
|
||||||
|
isCurrent, lifetime, opened.State, http);
|
||||||
|
http = null;
|
||||||
|
lifetime = null;
|
||||||
|
KvResult read;
|
||||||
|
using (var lease = owner.TryEnter())
|
||||||
|
{
|
||||||
|
if (lease == null)
|
||||||
|
{
|
||||||
|
return await owner.FailCreate(KVSessionOpenError.Cancelled);
|
||||||
|
}
|
||||||
|
read = await owner.coordinator.InitializeAsync();
|
||||||
|
}
|
||||||
|
if (!read.Success)
|
||||||
|
{
|
||||||
|
return await owner.FailCreate(read.Error == KVError.Unauthorized
|
||||||
|
? KVSessionOpenError.Unauthorized
|
||||||
|
: read.Error == KVError.Cancelled ? KVSessionOpenError.Cancelled
|
||||||
|
: KVSessionOpenError.ReadFailed);
|
||||||
|
}
|
||||||
|
if (!owner.IsCapturedOwnerCurrent())
|
||||||
|
{
|
||||||
|
return await owner.FailCreate(KVSessionOpenError.Stale);
|
||||||
|
}
|
||||||
|
return new KvSessionOpenResult(KVSessionOpenError.None, owner: owner);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
if (owner != null)
|
||||||
|
{
|
||||||
|
return await owner.FailCreate(KVSessionOpenError.Cancelled);
|
||||||
|
}
|
||||||
|
opened.State.Dispose();
|
||||||
|
return new KvSessionOpenResult(KVSessionOpenError.Cancelled);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
if (owner != null)
|
||||||
|
{
|
||||||
|
return await owner.FailCreate(KVSessionOpenError.Validation);
|
||||||
|
}
|
||||||
|
http?.Dispose();
|
||||||
|
lifetime?.Dispose();
|
||||||
|
opened.State.Dispose();
|
||||||
|
return new KvSessionOpenResult(KVSessionOpenError.Validation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Reads the existing state under an operation lease; does not open another cache.</summary>
|
||||||
|
public KvLocalReadResult ReadLocalSnapshot()
|
||||||
|
{
|
||||||
|
using (var lease = TryEnter())
|
||||||
|
{
|
||||||
|
if (lease == null)
|
||||||
|
{
|
||||||
|
return ReadFailure(KVLocalReadError.StaleOrClosing);
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var data = state.ReadLocal();
|
||||||
|
lock (admission)
|
||||||
|
{
|
||||||
|
if (!IsCapturedOwnerCurrent() || closing || closed)
|
||||||
|
{
|
||||||
|
return ReadFailure(KVLocalReadError.StaleOrClosing);
|
||||||
|
}
|
||||||
|
return new KvLocalReadResult(KVLocalReadError.None, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException)
|
||||||
|
{
|
||||||
|
lock (admission)
|
||||||
|
return ReadFailure(!IsCapturedOwnerCurrent() || closing || closed
|
||||||
|
? KVLocalReadError.StaleOrClosing : KVLocalReadError.NotReady);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private KvLocalReadResult ReadFailure(KVLocalReadError error) =>
|
||||||
|
new KvLocalReadResult(error);
|
||||||
|
|
||||||
|
internal KvStateResult CompareDatas(long nowTicks)
|
||||||
|
{
|
||||||
|
using (var lease = TryEnter())
|
||||||
|
{
|
||||||
|
return lease == null ? new KvStateResult(KVChangeError.Blocked) : coordinator.CompareDatas(nowTicks);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal KvStateResult Update(string key, string value, bool remoteStore, long updateTicks)
|
||||||
|
{
|
||||||
|
using (var lease = TryEnter())
|
||||||
|
{
|
||||||
|
return lease == null ? ClosedEdit() : coordinator.Update(key, value, remoteStore, updateTicks);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<KvFlushResult> SyncDataAsync()
|
||||||
|
{
|
||||||
|
var lease = TryEnter();
|
||||||
|
if (lease == null)
|
||||||
|
{
|
||||||
|
return Task.FromResult(new KvFlushResult(1, 0, KVError.Cancelled));
|
||||||
|
}
|
||||||
|
return RunOrdinarySyncAsync(lease);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<KvFlushResult> RunOrdinarySyncAsync(OperationLease lease)
|
||||||
|
{
|
||||||
|
using (lease)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await coordinator.SyncDataAsync();
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
return new KvFlushResult(1, 0, KVError.Protocol);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> SyncDataImmidiate(string key, string value)
|
||||||
|
{
|
||||||
|
using (var lease = TryEnter())
|
||||||
|
{
|
||||||
|
if (lease == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return await coordinator.SyncDataImmidiate(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal async Task<bool> SyncDataImmidiate(IReadOnlyDictionary<string, string> values)
|
||||||
|
{
|
||||||
|
using (var lease = TryEnter())
|
||||||
|
{
|
||||||
|
if (lease == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return await coordinator.SyncDataImmidiate(values);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private OperationLease TryEnter()
|
||||||
|
{
|
||||||
|
lock (admission)
|
||||||
|
{
|
||||||
|
if (closing || closed || !IsCapturedOwnerCurrent())
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
activeOperations++;
|
||||||
|
return new OperationLease(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ExitOperation()
|
||||||
|
{
|
||||||
|
lock (admission)
|
||||||
|
{
|
||||||
|
activeOperations--;
|
||||||
|
if (closing && activeOperations == 0)
|
||||||
|
{
|
||||||
|
drained.TrySetResult(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task CloseAsync()
|
||||||
|
{
|
||||||
|
TaskCompletionSource<bool> completion;
|
||||||
|
lock (admission)
|
||||||
|
{
|
||||||
|
if (closeCompletion != null)
|
||||||
|
{
|
||||||
|
return closeCompletion.Task;
|
||||||
|
}
|
||||||
|
closing = true;
|
||||||
|
completion = closeCompletion = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
if (activeOperations == 0)
|
||||||
|
{
|
||||||
|
drained.TrySetResult(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = CloseCoreAsync(completion);
|
||||||
|
return completion.Task;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CloseCoreAsync(TaskCompletionSource<bool> completion)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
lifetime.Cancel();
|
||||||
|
coordinator.CloseTickets();
|
||||||
|
await drained.Task;
|
||||||
|
coordinator.Dispose();
|
||||||
|
state.Dispose();
|
||||||
|
lifetime.Dispose();
|
||||||
|
lock (admission)
|
||||||
|
closed = true;
|
||||||
|
completion.TrySetResult(true);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
completion.TrySetException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<KvSessionOpenResult> FailCreate(KVSessionOpenError error)
|
||||||
|
{
|
||||||
|
await CloseAsync();
|
||||||
|
return new KvSessionOpenResult(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsCapturedOwnerCurrent()
|
||||||
|
{
|
||||||
|
if (lifetime.IsCancellationRequested || !isCurrent(Epoch) || state.AuthenticationInvalid)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return session.IsAuthenticated && ReferenceEquals(session.Cookies, cookies) &&
|
||||||
|
session.PlayerId == PlayerId && NormalizeApi(session.ApiBaseUri) == ApiBaseUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Uri NormalizeApi(Uri api)
|
||||||
|
{
|
||||||
|
if (api == null || !api.IsAbsoluteUri || (api.Scheme != "http" && api.Scheme != "https") ||
|
||||||
|
api.UserInfo.Length != 0 || api.Query.Length != 0 || api.Fragment.Length != 0)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Invalid API base URI.");
|
||||||
|
}
|
||||||
|
return new Uri(api.AbsoluteUri.TrimEnd('/') + "/");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static KVSessionOpenError MapOpen(KVOpenError error)
|
||||||
|
{
|
||||||
|
if (error == KVOpenError.Busy)
|
||||||
|
{
|
||||||
|
return KVSessionOpenError.Busy;
|
||||||
|
}
|
||||||
|
if (error == KVOpenError.RecoveryRequired)
|
||||||
|
{
|
||||||
|
return KVSessionOpenError.RecoveryRequired;
|
||||||
|
}
|
||||||
|
if (error == KVOpenError.StorageError)
|
||||||
|
{
|
||||||
|
return KVSessionOpenError.StorageError;
|
||||||
|
}
|
||||||
|
return KVSessionOpenError.Validation;
|
||||||
|
}
|
||||||
|
|
||||||
|
private KvStateResult ClosedEdit()
|
||||||
|
{
|
||||||
|
return new KvStateResult(KVChangeError.Blocked);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum KVLocalReadError
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
StaleOrClosing,
|
||||||
|
NotReady
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>A detached snapshot. Failed reads have no data, never a pretend empty save.</summary>
|
||||||
|
public sealed class KvLocalReadResult
|
||||||
|
{
|
||||||
|
public bool Success => Error == KVLocalReadError.None;
|
||||||
|
public KVLocalReadError Error
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
public IReadOnlyDictionary<string, string> Data
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal KvLocalReadResult(KVLocalReadError error, IReadOnlyDictionary<string, string> data = null)
|
||||||
|
{
|
||||||
|
Error = error;
|
||||||
|
if (error != KVLocalReadError.None)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(data));
|
||||||
|
}
|
||||||
|
var copy = new Dictionary<string, string>(StringComparer.Ordinal);
|
||||||
|
foreach (var item in data)
|
||||||
|
{
|
||||||
|
copy.Add(item.Key, item.Value);
|
||||||
|
}
|
||||||
|
Data = new ReadOnlyDictionary<string, string>(copy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
671
Client/Assets/Scripts/Core/KV/KvSyncCoordinator.cs
Normal file
671
Client/Assets/Scripts/Core/KV/KvSyncCoordinator.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
665
Client/Assets/Scripts/Core/KV/KvSyncModels.cs
Normal file
665
Client/Assets/Scripts/Core/KV/KvSyncModels.cs
Normal 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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
739
Client/Assets/Scripts/Core/KV/KvSyncState.cs
Normal file
739
Client/Assets/Scripts/Core/KV/KvSyncState.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using game.PlayerDataStorage;
|
|
||||||
|
|
||||||
internal static class KVMgr
|
|
||||||
{
|
|
||||||
internal static PlayerKVKeyReadResult ReadKV(PlayerKVGameSessionHandle handle, string key)
|
|
||||||
{
|
|
||||||
if (PlayerKVGameLoadGuard.Reject(PlayerKVGuardPoint.ReadLocal))
|
|
||||||
return new PlayerKVKeyReadResult(PlayerKVKeyReadError.LoadGuardActive, handle);
|
|
||||||
if (handle == null || !KVSnapshot.ValidKey(key))
|
|
||||||
return new PlayerKVKeyReadResult(PlayerKVKeyReadError.InvalidInput, handle);
|
|
||||||
var read = handle.ReadLocalSnapshot();
|
|
||||||
if (!read.Success)
|
|
||||||
return new PlayerKVKeyReadResult(read.Error == PlayerKVLocalReadError.StaleOrClosing
|
|
||||||
? PlayerKVKeyReadError.StaleOrClosing : PlayerKVKeyReadError.NotReady, handle);
|
|
||||||
var found = read.Data.TryGetValue(key, out var value);
|
|
||||||
return new PlayerKVKeyReadResult(PlayerKVKeyReadError.None, handle, found, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static PlayerKVEditResult WriteKV(PlayerKVGameSessionHandle handle,
|
|
||||||
string key, string value, bool remoteStore = true)
|
|
||||||
{
|
|
||||||
if (PlayerKVGameLoadGuard.Reject(PlayerKVGuardPoint.Update))
|
|
||||||
return new PlayerKVEditResult(new PlayerKVStateResult(PlayerKVChangeError.Blocked));
|
|
||||||
if (handle == null)
|
|
||||||
return new PlayerKVEditResult(new PlayerKVStateResult(PlayerKVChangeError.Validation));
|
|
||||||
return handle.Update(key, value, remoteStore);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static Task<PlayerKVFlushResult> FlushKVAsync(PlayerKVGameSessionHandle handle,
|
|
||||||
CancellationToken waitToken = default)
|
|
||||||
{
|
|
||||||
if (PlayerKVGameLoadGuard.Reject(PlayerKVGuardPoint.Sync))
|
|
||||||
return Task.FromResult(new PlayerKVFlushResult(0, 0, 1, 0, PlayerKVError.Cancelled,
|
|
||||||
handle == null ? PlayerKVSyncStatus.NotLoaded : handle.Status));
|
|
||||||
if (handle == null)
|
|
||||||
return Task.FromResult(new PlayerKVFlushResult(0, 0, 1, 0, PlayerKVError.Validation, PlayerKVSyncStatus.NotLoaded));
|
|
||||||
return handle.FlushAsync(waitToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static Task<PlayerKVTicketResult> WaitForKVTicketAsync(PlayerKVGameSessionHandle handle,
|
|
||||||
PlayerKVSyncTicket ticket, CancellationToken waitToken = default)
|
|
||||||
{
|
|
||||||
if (PlayerKVGameLoadGuard.Reject(PlayerKVGuardPoint.Sync))
|
|
||||||
return Task.FromResult(new PlayerKVTicketResult(PlayerKVTicketStatus.Blocked, PlayerKVError.Cancelled,
|
|
||||||
handle == null ? PlayerKVSyncStatus.NotLoaded : handle.Status));
|
|
||||||
if (handle == null)
|
|
||||||
return Task.FromResult(new PlayerKVTicketResult(PlayerKVTicketStatus.Blocked, PlayerKVError.Validation, PlayerKVSyncStatus.NotLoaded));
|
|
||||||
return handle.WaitForTicketAsync(ticket, waitToken);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
329
Client/Assets/Scripts/Core/KvMgr.cs
Normal file
329
Client/Assets/Scripts/Core/KvMgr.cs
Normal file
@@ -0,0 +1,329 @@
|
|||||||
|
using Core.KV;
|
||||||
|
using game.KV;
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using UnityEngine;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
internal class KvMgr : MonoBehaviour
|
||||||
|
{
|
||||||
|
private static KvMgr instance;
|
||||||
|
private KvSaveStore store;
|
||||||
|
private CancellationTokenSource tokenSource;
|
||||||
|
|
||||||
|
private void Awake()
|
||||||
|
{
|
||||||
|
instance = this;
|
||||||
|
tokenSource = new CancellationTokenSource();
|
||||||
|
UpdateUserDataValue(tokenSource.Token);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void Bind(KvSaveStore original)
|
||||||
|
{
|
||||||
|
if (instance != null)
|
||||||
|
{
|
||||||
|
instance.store = original;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void Unbind(KvSaveStore original)
|
||||||
|
{
|
||||||
|
if (instance != null && ReferenceEquals(instance.store, original))
|
||||||
|
{
|
||||||
|
instance.store = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void Stop(KvSaveStore original)
|
||||||
|
{
|
||||||
|
if (instance != null && ReferenceEquals(instance.store, original))
|
||||||
|
{
|
||||||
|
instance.Stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Stop()
|
||||||
|
{
|
||||||
|
tokenSource?.Cancel();
|
||||||
|
tokenSource?.Dispose();
|
||||||
|
tokenSource = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void UpdateUserDataValue(CancellationToken token)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (!token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
SyncData();
|
||||||
|
await Task.Delay(40000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Debug.LogError("KV save schedule failed: " + ex.GetType().Name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SyncData()
|
||||||
|
{
|
||||||
|
var original = store;
|
||||||
|
if (original != null && original.IsCurrent)
|
||||||
|
{
|
||||||
|
ObserveSync(original.SyncDataAsync());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async void ObserveSync(Task<KvFlushResult> task)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await task;
|
||||||
|
if (result.TransportError != KVError.None || result.Blocked != 0 || result.Unknown != 0)
|
||||||
|
{
|
||||||
|
Debug.LogError("KV ordinary save failed: " + result.TransportError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Debug.LogError("KV ordinary save failed: " + ex.GetType().Name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnApplicationPause(bool paused)
|
||||||
|
{
|
||||||
|
if (paused)
|
||||||
|
{
|
||||||
|
Stop();
|
||||||
|
SyncData();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
tokenSource = new CancellationTokenSource();
|
||||||
|
UpdateUserDataValue(tokenSource.Token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnApplicationQuit()
|
||||||
|
{
|
||||||
|
Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDestroy()
|
||||||
|
{
|
||||||
|
Stop();
|
||||||
|
SyncData();
|
||||||
|
store = null;
|
||||||
|
if (ReferenceEquals(instance, this))
|
||||||
|
{
|
||||||
|
instance = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace game.KV
|
||||||
|
{
|
||||||
|
public sealed class KvSaveStore : IPlayerSaveStore, IDisposable
|
||||||
|
{
|
||||||
|
private KvSessionOwner handle;
|
||||||
|
private CancellationToken lifetime;
|
||||||
|
private bool bound, disposed;
|
||||||
|
private readonly object receiptsGate = new object();
|
||||||
|
public bool IsCurrent => bound && !disposed && !lifetime.IsCancellationRequested && handle.Status != KVSyncStatus.Closed;
|
||||||
|
|
||||||
|
internal void Bind(KvSessionOwner original, CancellationToken loginLifetime)
|
||||||
|
{
|
||||||
|
if (bound || disposed || original == null || loginLifetime.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Player save store cannot be rebound.");
|
||||||
|
}
|
||||||
|
if (!ReadKV(original, "LastLoginTimeUtc").Success)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Player save handle is not ready.");
|
||||||
|
}
|
||||||
|
handle = original;
|
||||||
|
lifetime = loginLifetime;
|
||||||
|
bound = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void ReleaseFailedBinding(KvSessionOwner original)
|
||||||
|
{
|
||||||
|
lock (receiptsGate)
|
||||||
|
{
|
||||||
|
if (!bound)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (disposed || !ReferenceEquals(handle, original) || original.Status != KVSyncStatus.Closed)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Failed save binding has not closed.");
|
||||||
|
}
|
||||||
|
handle = null;
|
||||||
|
bound = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public PlayerSaveReadResult Read(string key)
|
||||||
|
{
|
||||||
|
if (!IsCurrent)
|
||||||
|
{
|
||||||
|
return new PlayerSaveReadResult("StaleOrNotReady");
|
||||||
|
}
|
||||||
|
return ReadKV(handle, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PlayerSaveWriteResult Write(string key, string value, bool remoteStore = true)
|
||||||
|
{
|
||||||
|
lock (receiptsGate)
|
||||||
|
{
|
||||||
|
if (!IsCurrent)
|
||||||
|
{
|
||||||
|
var rejected = new PlayerSaveWriteResult("StaleOrNotReady");
|
||||||
|
return rejected;
|
||||||
|
}
|
||||||
|
var result = WriteKV(handle, key, value, remoteStore);
|
||||||
|
var write = result.Success ? new PlayerSaveWriteResult() :
|
||||||
|
new PlayerSaveWriteResult(result.Error.ToString());
|
||||||
|
return write;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> SyncDataImmidiate(string key, string value)
|
||||||
|
{
|
||||||
|
KvSessionOwner original;
|
||||||
|
CancellationToken originalLifetime;
|
||||||
|
Task<bool> save;
|
||||||
|
lock (receiptsGate)
|
||||||
|
{
|
||||||
|
if (!IsCurrent)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Preserve the former current-store dictionary lookup's null-key behavior.
|
||||||
|
if (key == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(key));
|
||||||
|
}
|
||||||
|
original = handle;
|
||||||
|
originalLifetime = lifetime;
|
||||||
|
save = SyncDataImmidiate(original, key, value);
|
||||||
|
}
|
||||||
|
var success = await save;
|
||||||
|
lock (receiptsGate)
|
||||||
|
{
|
||||||
|
return success && IsCurrent && !originalLifetime.IsCancellationRequested && ReferenceEquals(handle, original);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal async Task<bool> SyncDataImmidiate(IReadOnlyDictionary<string, string> values)
|
||||||
|
{
|
||||||
|
KvSessionOwner original;
|
||||||
|
CancellationToken originalLifetime;
|
||||||
|
Task<bool> save;
|
||||||
|
lock (receiptsGate)
|
||||||
|
{
|
||||||
|
if (!IsCurrent || values == null || values.Count == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var data = values.ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.Ordinal);
|
||||||
|
original = handle;
|
||||||
|
originalLifetime = lifetime;
|
||||||
|
save = SyncDataImmidiate(original, data);
|
||||||
|
}
|
||||||
|
var success = await save;
|
||||||
|
lock (receiptsGate)
|
||||||
|
{
|
||||||
|
return success && IsCurrent && !originalLifetime.IsCancellationRequested && ReferenceEquals(handle, original);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal Task<KvFlushResult> SyncDataAsync()
|
||||||
|
{
|
||||||
|
lock (receiptsGate)
|
||||||
|
{
|
||||||
|
var original = handle;
|
||||||
|
if (!IsCurrent)
|
||||||
|
{
|
||||||
|
return Task.FromResult(new KvFlushResult(1, 0, KVError.Cancelled));
|
||||||
|
}
|
||||||
|
if (original == null)
|
||||||
|
{
|
||||||
|
return Task.FromResult(new KvFlushResult(1, 0, KVError.Validation));
|
||||||
|
}
|
||||||
|
return original.SyncDataAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
lock (receiptsGate)
|
||||||
|
{
|
||||||
|
disposed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static PlayerSaveReadResult ReadKV(KvSessionOwner handle, string key)
|
||||||
|
{
|
||||||
|
if (handle == null || !KvSnapshot.ValidKey(key))
|
||||||
|
{
|
||||||
|
return new PlayerSaveReadResult("InvalidInput");
|
||||||
|
}
|
||||||
|
var read = handle.ReadLocalSnapshot();
|
||||||
|
if (!read.Success)
|
||||||
|
{
|
||||||
|
return new PlayerSaveReadResult(read.Error == KVLocalReadError.StaleOrClosing
|
||||||
|
? "StaleOrClosing" : "NotReady");
|
||||||
|
}
|
||||||
|
var found = read.Data.TryGetValue(key, out var value);
|
||||||
|
if (found && value == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("A successful key observation requires an existence verdict.");
|
||||||
|
}
|
||||||
|
return new PlayerSaveReadResult(found, found ? value : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static KvStateResult WriteKV(KvSessionOwner handle,
|
||||||
|
string key, string value, bool remoteStore = true)
|
||||||
|
{
|
||||||
|
if (handle == null)
|
||||||
|
{
|
||||||
|
return new KvStateResult(KVChangeError.Validation);
|
||||||
|
}
|
||||||
|
return handle.Update(key, value, remoteStore, ZZTimeHelper.UtcNow().Ticks);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static async Task<bool> SyncDataImmidiate(KvSessionOwner handle, string key, string value)
|
||||||
|
{
|
||||||
|
var success = false;
|
||||||
|
if (handle != null &&
|
||||||
|
KvSnapshot.ValidKey(key) && value != null)
|
||||||
|
{
|
||||||
|
success = await handle.SyncDataImmidiate(key, value);
|
||||||
|
}
|
||||||
|
if (!success)
|
||||||
|
{
|
||||||
|
UnityEngine.Debug.LogError("[KVMgr::SyncDataImmidiate] Save failed.");
|
||||||
|
}
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static async Task<bool> SyncDataImmidiate(KvSessionOwner handle,
|
||||||
|
IReadOnlyDictionary<string, string> values)
|
||||||
|
{
|
||||||
|
var success = false;
|
||||||
|
if (handle != null && values != null && values.Count > 0)
|
||||||
|
{
|
||||||
|
success = await handle.SyncDataImmidiate(values);
|
||||||
|
}
|
||||||
|
if (!success)
|
||||||
|
{
|
||||||
|
UnityEngine.Debug.LogError("[KVMgr::SyncDataImmidiate] Save failed.");
|
||||||
|
}
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,7 +7,6 @@ using System.Linq;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using game.PlayerDataStorage;
|
|
||||||
|
|
||||||
public class PlayFabMgr : MonoBehaviour
|
public class PlayFabMgr : MonoBehaviour
|
||||||
{
|
{
|
||||||
@@ -26,8 +25,6 @@ public class PlayFabMgr : MonoBehaviour
|
|||||||
private CancellationTokenSource tokenSource;
|
private CancellationTokenSource tokenSource;
|
||||||
|
|
||||||
static PlayFabMgr _instance = null;
|
static PlayFabMgr _instance = null;
|
||||||
// An inactive, never-awakened component proves that no timer or PF send has ever been started.
|
|
||||||
internal bool IsUnstartedForKVLoad => tokenSource==null && dataBuffer==null && swapData==null && localData==null && lastUpdateTime==null && _instance!=this;
|
|
||||||
internal static bool HasLiveInstanceForKVLoad => _instance!=null;
|
internal static bool HasLiveInstanceForKVLoad => _instance!=null;
|
||||||
|
|
||||||
public static PlayFabMgr Instance
|
public static PlayFabMgr Instance
|
||||||
@@ -45,10 +42,6 @@ public class PlayFabMgr : MonoBehaviour
|
|||||||
|
|
||||||
public Dictionary<string, string> CompareDatas(Dictionary<string, UserDataRecord> remoteDatas, string userId)
|
public Dictionary<string, string> CompareDatas(Dictionary<string, UserDataRecord> remoteDatas, string userId)
|
||||||
{
|
{
|
||||||
#if UNITY_EDITOR && !SERVICE_HOST
|
|
||||||
if(PlayerKVGameplayLoop.Enabled) throw PlayerKVGameplayLoop.Stop("PF archive comparison is disabled in KV loop.");
|
|
||||||
#endif
|
|
||||||
PlayerKVGameLoadGuard.RequireLegacyAllowed(PlayerKVGuardPoint.Compare);
|
|
||||||
Last_Update_Time_Key = $"{Last_Update_Time_Key_Prefix}_{userId}";
|
Last_Update_Time_Key = $"{Last_Update_Time_Key_Prefix}_{userId}";
|
||||||
Local_Data_Key = $"{Local_Data_Key_prefix}_{userId}";
|
Local_Data_Key = $"{Local_Data_Key_prefix}_{userId}";
|
||||||
var lastUpdateTimeStr = PlayerPrefs.GetString(Last_Update_Time_Key, string.Empty);
|
var lastUpdateTimeStr = PlayerPrefs.GetString(Last_Update_Time_Key, string.Empty);
|
||||||
@@ -122,26 +115,12 @@ public class PlayFabMgr : MonoBehaviour
|
|||||||
|
|
||||||
void Awake()
|
void Awake()
|
||||||
{
|
{
|
||||||
if(PlayerKVGameLoadGuard.Reject(PlayerKVGuardPoint.Lifecycle)) return;
|
|
||||||
_instance = this;
|
_instance = this;
|
||||||
#if UNITY_EDITOR && !SERVICE_HOST
|
|
||||||
if(PlayerKVGameplayLoop.Enabled) return;
|
|
||||||
#endif
|
|
||||||
InitBuffer();
|
InitBuffer();
|
||||||
tokenSource = new CancellationTokenSource();
|
tokenSource = new CancellationTokenSource();
|
||||||
UpdateUserDataValue(tokenSource.Token);
|
UpdateUserDataValue(tokenSource.Token);
|
||||||
}
|
}
|
||||||
|
|
||||||
#if UNITY_EDITOR && !SERVICE_HOST
|
|
||||||
internal void StartKVGameplayLoopSync()
|
|
||||||
{
|
|
||||||
if(!PlayerKVGameplayLoop.Ready || tokenSource!=null) return;
|
|
||||||
tokenSource=new CancellationTokenSource();
|
|
||||||
UpdateUserDataValue(tokenSource.Token);
|
|
||||||
}
|
|
||||||
internal void StopKVGameplayLoopSync()
|
|
||||||
{ tokenSource?.Cancel(); tokenSource?.Dispose(); tokenSource=null; }
|
|
||||||
#endif
|
|
||||||
void InitBuffer()
|
void InitBuffer()
|
||||||
{
|
{
|
||||||
dataBuffer = new LinkedList<Dictionary<string, string>>();
|
dataBuffer = new LinkedList<Dictionary<string, string>>();
|
||||||
@@ -154,15 +133,6 @@ public class PlayFabMgr : MonoBehaviour
|
|||||||
|
|
||||||
void OnApplicationPause(bool is_pause)
|
void OnApplicationPause(bool is_pause)
|
||||||
{
|
{
|
||||||
#if UNITY_EDITOR && !SERVICE_HOST
|
|
||||||
if(PlayerKVGameplayLoop.Enabled)
|
|
||||||
{
|
|
||||||
if(is_pause) { StopKVGameplayLoopSync(); _=PlayerKVGameplayLoop.FlushObservedAsync(); }
|
|
||||||
else StartKVGameplayLoopSync();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
if(PlayerKVGameLoadGuard.Reject(PlayerKVGuardPoint.Lifecycle)) return;
|
|
||||||
if (is_pause)
|
if (is_pause)
|
||||||
{
|
{
|
||||||
tokenSource.Cancel();
|
tokenSource.Cancel();
|
||||||
@@ -178,10 +148,6 @@ public class PlayFabMgr : MonoBehaviour
|
|||||||
|
|
||||||
void OnApplicationQuit()
|
void OnApplicationQuit()
|
||||||
{
|
{
|
||||||
#if UNITY_EDITOR && !SERVICE_HOST
|
|
||||||
if(PlayerKVGameplayLoop.Enabled) { StopKVGameplayLoopSync(); _=PlayerKVGameplayLoop.CloseAsync(); return; }
|
|
||||||
#endif
|
|
||||||
if(PlayerKVGameLoadGuard.Reject(PlayerKVGuardPoint.Lifecycle)) return;
|
|
||||||
tokenSource.Cancel();
|
tokenSource.Cancel();
|
||||||
tokenSource = null;
|
tokenSource = null;
|
||||||
SyncData();
|
SyncData();
|
||||||
@@ -189,19 +155,14 @@ public class PlayFabMgr : MonoBehaviour
|
|||||||
|
|
||||||
void OnDestroy()
|
void OnDestroy()
|
||||||
{
|
{
|
||||||
#if UNITY_EDITOR && !SERVICE_HOST
|
|
||||||
if(PlayerKVGameplayLoop.Enabled) { StopKVGameplayLoopSync(); _=PlayerKVGameplayLoop.CloseAsync(); return; }
|
|
||||||
#endif
|
|
||||||
if(PlayerKVGameLoadGuard.Reject(PlayerKVGuardPoint.Lifecycle)) return;
|
|
||||||
tokenSource?.Cancel();
|
tokenSource?.Cancel();
|
||||||
tokenSource = null;
|
tokenSource = null;
|
||||||
if(!PlayerKVGameLoadGuard.Reject(PlayerKVGuardPoint.PrefsSave)) PlayerPrefs.Save();
|
PlayerPrefs.Save();
|
||||||
SyncData();
|
SyncData();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void UpdateUserDataValue(CancellationToken token)
|
private async void UpdateUserDataValue(CancellationToken token)
|
||||||
{
|
{
|
||||||
if(PlayerKVGameLoadGuard.Reject(PlayerKVGuardPoint.Lifecycle)) return;
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
while (!token.IsCancellationRequested)
|
while (!token.IsCancellationRequested)
|
||||||
@@ -215,10 +176,6 @@ public class PlayFabMgr : MonoBehaviour
|
|||||||
|
|
||||||
public void UpdateUserDataValue(string key, string data, bool remoteStore = true)
|
public void UpdateUserDataValue(string key, string data, bool remoteStore = true)
|
||||||
{
|
{
|
||||||
#if UNITY_EDITOR && !SERVICE_HOST
|
|
||||||
if(PlayerKVGameplayLoop.Enabled) { PlayerKVGameplayLoop.Write(key,data,remoteStore); return; }
|
|
||||||
#endif
|
|
||||||
PlayerKVGameLoadGuard.RequireLegacyAllowed(PlayerKVGuardPoint.Update);
|
|
||||||
if (remoteStore)
|
if (remoteStore)
|
||||||
{
|
{
|
||||||
var cbuff = dataBuffer.First;
|
var cbuff = dataBuffer.First;
|
||||||
@@ -248,10 +205,6 @@ public class PlayFabMgr : MonoBehaviour
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public string GetLocalData(string key)
|
public string GetLocalData(string key)
|
||||||
{
|
{
|
||||||
#if UNITY_EDITOR && !SERVICE_HOST
|
|
||||||
if(PlayerKVGameplayLoop.Enabled) return PlayerKVGameplayLoop.Read(key);
|
|
||||||
#endif
|
|
||||||
PlayerKVGameLoadGuard.RequireLegacyAllowed(PlayerKVGuardPoint.ReadLocal);
|
|
||||||
if (localData.ContainsKey(key))
|
if (localData.ContainsKey(key))
|
||||||
{
|
{
|
||||||
return localData[key];
|
return localData[key];
|
||||||
@@ -264,10 +217,6 @@ public class PlayFabMgr : MonoBehaviour
|
|||||||
|
|
||||||
private void SyncData()
|
private void SyncData()
|
||||||
{
|
{
|
||||||
#if UNITY_EDITOR && !SERVICE_HOST
|
|
||||||
if(PlayerKVGameplayLoop.Enabled) { _=PlayerKVGameplayLoop.FlushObservedAsync(); return; }
|
|
||||||
#endif
|
|
||||||
if(PlayerKVGameLoadGuard.Reject(PlayerKVGuardPoint.Sync)) return;
|
|
||||||
// Logout clears credentials before RootCtx is destroyed. Keep pending data local.
|
// Logout clears credentials before RootCtx is destroyed. Keep pending data local.
|
||||||
if (!PlayFabClientAPI.IsClientLoggedIn()) return;
|
if (!PlayFabClientAPI.IsClientLoggedIn()) return;
|
||||||
var buf = dataBuffer.First;
|
var buf = dataBuffer.First;
|
||||||
@@ -320,14 +269,9 @@ public class PlayFabMgr : MonoBehaviour
|
|||||||
GameDebug.LogError("[PlayFabMgr]UpdateUserData Error: {0}", error.ToString());
|
GameDebug.LogError("[PlayFabMgr]UpdateUserData Error: {0}", error.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void FlushBufferedData() => SyncData();
|
|
||||||
|
|
||||||
public async Task<bool> SyncDataImmidiate(string key, string data)
|
public async Task<bool> SyncDataImmidiate(string key, string data)
|
||||||
{
|
{
|
||||||
#if UNITY_EDITOR && !SERVICE_HOST
|
|
||||||
if(PlayerKVGameplayLoop.Enabled) throw PlayerKVGameplayLoop.Stop("Immediate/consuming saves are outside the equipment loop.");
|
|
||||||
#endif
|
|
||||||
if(PlayerKVGameLoadGuard.Reject(PlayerKVGuardPoint.Immediate)) return false;
|
|
||||||
foreach (var buff in dataBuffer)
|
foreach (var buff in dataBuffer)
|
||||||
{
|
{
|
||||||
if (buff.ContainsKey(key))
|
if (buff.ContainsKey(key))
|
||||||
|
|||||||
@@ -32,30 +32,14 @@ public class AchievementDataManager : IDisposable
|
|||||||
{
|
{
|
||||||
_tables = tables;
|
_tables = tables;
|
||||||
}
|
}
|
||||||
internal static game.PlayerDataStorage.PlayerKVAchievementState DecodeForKVLoad(
|
|
||||||
IReadOnlyDictionary<string, string> raw, IReadOnlyDictionary<string, string> modern,
|
|
||||||
game.PlayerDataStorage.PlayerKVSchemaSnapshot snapshot, game.PlayerDataStorage.PlayerKVSchemaDiagnostics errors)
|
|
||||||
=> game.PlayerDataStorage.PlayerKVSaveSchema.DecodeAchievements(raw, modern, snapshot, errors);
|
|
||||||
|
|
||||||
internal void ResetForKVLoad()
|
|
||||||
{
|
|
||||||
_achievementDatas = new Dictionary<ConditionType, AchievementData>();
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void ReplaceFromKVDecoded(game.PlayerDataStorage.PlayerKVAchievementState state)
|
|
||||||
{
|
|
||||||
if (state == null) throw new ArgumentNullException(nameof(state));
|
|
||||||
var replacement = new Dictionary<ConditionType, AchievementData>();
|
|
||||||
foreach (var entry in state.Values)
|
|
||||||
replacement.Add((ConditionType)entry.Key, new AchievementData
|
|
||||||
{ Id = entry.Value.Id, Target = entry.Value.Target, day = entry.Value.Day });
|
|
||||||
_achievementDatas = replacement;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal bool IsEmptyForKVValidation => _achievementDatas.Count == 0;
|
|
||||||
internal AchievementData FindForKVValidation(ConditionType key)
|
|
||||||
=> _achievementDatas.TryGetValue(key, out var value)
|
|
||||||
? new AchievementData { Id = value.Id, Target = value.Target, day = value.day } : null;
|
|
||||||
|
|
||||||
|
|
||||||
public void LoadAchievementData(string value)
|
public void LoadAchievementData(string value)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using asap.core;
|
using asap.core;
|
||||||
using cfg;
|
using cfg;
|
||||||
using GameCore;
|
using GameCore;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
@@ -8,68 +8,20 @@ using System.Collections.Generic;
|
|||||||
|
|
||||||
public class BargainPackData
|
public class BargainPackData
|
||||||
{
|
{
|
||||||
[asap.core.Inject] public game.PlayerDataStorage.IPlayerSaveStore SaveStore { get; set; }
|
[asap.core.Inject] public Core.KV.IPlayerSaveStore SaveStore { get; set; }
|
||||||
|
|
||||||
private readonly Tables _tables = GContext.container.Resolve<Tables>();
|
private readonly Tables _tables = GContext.container.Resolve<Tables>();
|
||||||
private readonly TbEventPackManager _epm = GContext.container.Resolve<Tables>().TbEventPackManager;
|
private readonly TbEventPackManager _epm = GContext.container.Resolve<Tables>().TbEventPackManager;
|
||||||
private readonly TbSpecialPack _spp = GContext.container.Resolve<Tables>().TbSpecialPack;
|
private readonly TbSpecialPack _spp = GContext.container.Resolve<Tables>().TbSpecialPack;
|
||||||
private GeneralEventPackData _data;
|
private GeneralEventPackData _data;
|
||||||
|
|
||||||
private bool _kvLoadPresent;
|
|
||||||
#if UNITY_EDITOR
|
|
||||||
internal bool SelectionsAreIndependentForKVValidation()
|
|
||||||
{
|
|
||||||
var source = new List<int> { 7, 9 };
|
|
||||||
var state = new game.PlayerDataStorage.PlayerKVBargainState(true,0,_data.redirectID,0,0,0,0,0,0,0,0,0,false,false,0,source);
|
|
||||||
ReplaceFromKVDecoded(state);
|
|
||||||
source.Clear();
|
|
||||||
if (_data.selections.Count != 2 || ReferenceEquals(_data.selections, source) || ReferenceEquals(_data.selections, state.selections)) return false;
|
|
||||||
var previous = _data.selections;
|
|
||||||
_data.selections[0] = 10;
|
|
||||||
if (state.selections[0] != 7) return false;
|
|
||||||
ResetForKVLoad();
|
|
||||||
return _data.selections.Count == 0 && !ReferenceEquals(previous, _data.selections) && !_kvLoadPresent;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
internal bool KVLoadPresent => _kvLoadPresent;
|
|
||||||
internal static game.PlayerDataStorage.PlayerKVBargainState DecodeForKVLoad(
|
|
||||||
IReadOnlyDictionary<string, string> raw, game.PlayerDataStorage.PlayerKVSchemaSnapshot snapshot,
|
|
||||||
game.PlayerDataStorage.PlayerKVSchemaDiagnostics errors) =>
|
|
||||||
game.PlayerDataStorage.PlayerKVSaveSchema.DecodeBargain(raw, snapshot, errors);
|
|
||||||
|
|
||||||
internal void ResetForKVLoad()
|
|
||||||
{
|
|
||||||
_kvLoadPresent = false;
|
|
||||||
_data = default(GeneralEventPackData);
|
|
||||||
_data.selections = new List<int>();
|
|
||||||
}
|
|
||||||
internal void ReplaceFromKVDecoded(game.PlayerDataStorage.PlayerKVBargainState state)
|
|
||||||
{
|
|
||||||
if (state == null) throw new ArgumentNullException(nameof(state));
|
|
||||||
if (!state.IsPresent) { ResetForKVLoad(); return; }
|
|
||||||
var replacement = new GeneralEventPackData
|
|
||||||
{
|
|
||||||
currentEventID = state.currentEventID,
|
|
||||||
redirectID = state.redirectID,
|
|
||||||
VIPLvlWhenEventActivate = state.VIPLvlWhenEventActivate,
|
|
||||||
purchaseCount = state.purchaseCount,
|
|
||||||
savingProgress = state.savingProgress,
|
|
||||||
discountLvl = state.discountLvl,
|
|
||||||
visualProgress = state.visualProgress,
|
|
||||||
visualDiscountLvl = state.visualDiscountLvl,
|
|
||||||
rodID = state.rodID,
|
|
||||||
lastPackIDBought = state.lastPackIDBought,
|
|
||||||
packID = state.packID,
|
|
||||||
doNeedTrigger = state.doNeedTrigger,
|
|
||||||
isTriggered = state.isTriggered,
|
|
||||||
inflation = state.inflation,
|
|
||||||
selections = state.selections == null ? null : new List<int>(state.selections),
|
|
||||||
};
|
|
||||||
_data = replacement;
|
|
||||||
_kvLoadPresent = true;
|
|
||||||
}
|
|
||||||
internal game.PlayerDataStorage.PlayerKVBargainState SnapshotForKVValidation() =>
|
|
||||||
new game.PlayerDataStorage.PlayerKVBargainState(_kvLoadPresent, _data.currentEventID, _data.redirectID, _data.VIPLvlWhenEventActivate, _data.purchaseCount, _data.savingProgress, _data.discountLvl, _data.visualProgress, _data.visualDiscountLvl, _data.rodID, _data.lastPackIDBought, _data.packID, _data.doNeedTrigger, _data.isTriggered, _data.inflation, _data.selections);
|
|
||||||
|
|
||||||
public int CurrentEventID => _data.currentEventID;
|
public int CurrentEventID => _data.currentEventID;
|
||||||
|
|
||||||
|
|||||||
@@ -33,32 +33,13 @@ public struct BpVipUnlock
|
|||||||
|
|
||||||
public class BattlePassData
|
public class BattlePassData
|
||||||
{
|
{
|
||||||
[asap.core.Inject] public game.PlayerDataStorage.IPlayerSaveStore playerSaveStore { get; set; }
|
[asap.core.Inject] public Core.KV.IPlayerSaveStore playerSaveStore { get; set; }
|
||||||
public BattlePassRecord BattlePassRecord;
|
public BattlePassRecord BattlePassRecord;
|
||||||
public BattleTask BattleTask;
|
public BattleTask BattleTask;
|
||||||
|
|
||||||
internal static game.PlayerDataStorage.PlayerKVBattlePassState DecodeForKVLoad(IReadOnlyDictionary<string,string> raw,
|
|
||||||
game.PlayerDataStorage.PlayerKVSchemaSnapshot snapshot,game.PlayerDataStorage.PlayerKVSchemaDiagnostics errors) =>
|
|
||||||
game.PlayerDataStorage.PlayerKVSaveSchema.DecodeBattlePass(raw,snapshot,errors);
|
|
||||||
internal void ResetForKVLoad()
|
|
||||||
{
|
|
||||||
BattlePassRecord=new BattlePassRecord();
|
|
||||||
BattleTask=new BattleTask();
|
|
||||||
}
|
|
||||||
internal void ReplaceFromKVDecoded(game.PlayerDataStorage.PlayerKVBattlePassState state)
|
|
||||||
{
|
|
||||||
if(state==null) throw new ArgumentNullException(nameof(state));
|
|
||||||
var record=new BattlePassRecord {
|
|
||||||
ID=state.Record.ID,Level=state.Record.Level,Exp=state.Record.Exp,BuyVipToastIndex=state.Record.BuyVipToastIndex,
|
|
||||||
IsUnlockVip=state.Record.IsUnlockVip,NormalRewardGet=new List<int>(state.Record.Normal),VipRewardGet=new List<int>(state.Record.Vip),
|
|
||||||
LastDropIDs=new List<int>(state.Record.LastDrops),InflationRate=state.Record.Inflation
|
|
||||||
};
|
|
||||||
var task=new BattleTask {
|
|
||||||
dic_dailyTasks=state.Task.Daily.ToDictionary(p=>p.Key,p=>p.Value),dic_weeklyTasks=state.Task.Weekly.ToDictionary(p=>p.Key,p=>p.Value),
|
|
||||||
loginDays=new List<int>(state.Task.LoginDays),DailyTaskEndTime=state.Task.DailyEnd,WeeklyTaskEndTime=state.Task.WeeklyEnd
|
|
||||||
};
|
|
||||||
BattlePassRecord=record; BattleTask=task;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Tables _tables;
|
private Tables _tables;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using asap.core;
|
using asap.core;
|
||||||
using cfg;
|
using cfg;
|
||||||
using GameCore;
|
using GameCore;
|
||||||
using System;
|
using System;
|
||||||
@@ -6,7 +6,7 @@ using System.Collections.Generic;
|
|||||||
|
|
||||||
public class BuffDataCenter
|
public class BuffDataCenter
|
||||||
{
|
{
|
||||||
[asap.core.Inject] public game.PlayerDataStorage.IPlayerSaveStore SaveStore { get; set; }
|
[asap.core.Inject] public Core.KV.IPlayerSaveStore SaveStore { get; set; }
|
||||||
|
|
||||||
private cfg.Tables _tables;
|
private cfg.Tables _tables;
|
||||||
public BuffDataCenter(cfg.Tables tables)
|
public BuffDataCenter(cfg.Tables tables)
|
||||||
@@ -19,38 +19,10 @@ public class BuffDataCenter
|
|||||||
FishBuffTimeData fishDailyBuff = null;
|
FishBuffTimeData fishDailyBuff = null;
|
||||||
public List<FishBuffTimeData> showFishBuffIds = new List<FishBuffTimeData>();
|
public List<FishBuffTimeData> showFishBuffIds = new List<FishBuffTimeData>();
|
||||||
|
|
||||||
internal static game.PlayerDataStorage.PlayerKVBuffState DecodeForKVLoad(IReadOnlyDictionary<string,string> raw,
|
|
||||||
game.PlayerDataStorage.PlayerKVSchemaSnapshot snapshot,game.PlayerDataStorage.PlayerKVSchemaDiagnostics errors) =>
|
|
||||||
game.PlayerDataStorage.PlayerKVSaveSchema.DecodeBuffs(raw,snapshot,errors);
|
|
||||||
internal void ResetForKVLoad()
|
|
||||||
{
|
|
||||||
mapBuffDataList=new Dictionary<int,FishBuffTimeData>();
|
|
||||||
targetBuffDataList=new List<FishBuffTimeData>();
|
|
||||||
globalBuffDataList=new Dictionary<int,FishBuffTimeData>();
|
|
||||||
showFishBuffIds=new List<FishBuffTimeData>(); fishDailyBuff=null;
|
|
||||||
}
|
|
||||||
static FishBuffTimeData MaterializeKVBuff(game.PlayerDataStorage.PlayerKVBuffValue value) => value==null ? null : new FishBuffTimeData
|
|
||||||
{ SortID=value.SortID,sourceID=value.sourceID,buffID=value.buffID,isEnd=value.isEnd,buffEndTime=value.buffEndTime,isNew=false,addDown=0 };
|
|
||||||
internal void ReplaceFromKVDecoded(game.PlayerDataStorage.PlayerKVBuffState state)
|
|
||||||
{
|
|
||||||
if(state==null) throw new ArgumentNullException(nameof(state));
|
|
||||||
var map=new Dictionary<int,FishBuffTimeData>(); var global=new Dictionary<int,FishBuffTimeData>(); var target=new List<FishBuffTimeData>();
|
|
||||||
foreach(var item in state.Map) map.Add(item.Key,MaterializeKVBuff(item.Value));
|
|
||||||
foreach(var item in state.Global) global.Add(item.Key,MaterializeKVBuff(item.Value));
|
|
||||||
foreach(var item in state.Target) target.Add(MaterializeKVBuff(item));
|
|
||||||
mapBuffDataList=map; globalBuffDataList=global; targetBuffDataList=target; fishDailyBuff=MaterializeKVBuff(state.Daily);
|
|
||||||
}
|
|
||||||
#if UNITY_EDITOR
|
|
||||||
internal FishBuffTimeData BuffForKVValidation(int kind,int key)
|
|
||||||
{
|
|
||||||
if(kind==0) return mapBuffDataList.TryGetValue(key,out var value) ? value : null;
|
|
||||||
if(kind==1) return targetBuffDataList.Count>key ? targetBuffDataList[key] : null;
|
|
||||||
if(kind==2) return fishDailyBuff;
|
|
||||||
return globalBuffDataList.TryGetValue(key,out var global) ? global : null;
|
|
||||||
}
|
|
||||||
internal bool IsEmptyForKVValidation => mapBuffDataList.Count==0 && targetBuffDataList.Count==0 &&
|
|
||||||
globalBuffDataList.Count==0 && showFishBuffIds.Count==0 && fishDailyBuff==null;
|
|
||||||
#endif
|
|
||||||
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))
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using Activity.Services;
|
using Activity.Services;
|
||||||
using asap.core;
|
using asap.core;
|
||||||
using Assets.Scripts.UI.SocialRush;
|
using Assets.Scripts.UI.SocialRush;
|
||||||
using cfg;
|
using cfg;
|
||||||
@@ -56,8 +56,8 @@ namespace GameCore
|
|||||||
{
|
{
|
||||||
public partial class FishingEventData : IDisposable
|
public partial class FishingEventData : IDisposable
|
||||||
{
|
{
|
||||||
game.PlayerDataStorage.IPlayerSaveStore boundSaveStore;
|
Core.KV.IPlayerSaveStore boundSaveStore;
|
||||||
[Inject] public game.PlayerDataStorage.IPlayerSaveStore playerSaveStore
|
[Inject] public Core.KV.IPlayerSaveStore playerSaveStore
|
||||||
{
|
{
|
||||||
get => boundSaveStore;
|
get => boundSaveStore;
|
||||||
set
|
set
|
||||||
@@ -141,45 +141,9 @@ namespace GameCore
|
|||||||
//待释放
|
//待释放
|
||||||
IDisposable disposable;
|
IDisposable disposable;
|
||||||
|
|
||||||
internal static game.PlayerDataStorage.PlayerKVEventBaseState DecodeForKVLoad(IReadOnlyDictionary<string,string> raw,
|
|
||||||
IReadOnlyDictionary<string,string> modern,game.PlayerDataStorage.PlayerKVSchemaSnapshot snapshot,game.PlayerDataStorage.PlayerKVSchemaDiagnostics errors)
|
|
||||||
=> game.PlayerDataStorage.PlayerKVSaveSchema.DecodeEventBase(raw,modern,snapshot,errors);
|
|
||||||
internal void ResetForKVLoad()
|
|
||||||
{
|
|
||||||
limitedTimeEventDic=new Dictionary<int,LimitedTimeEvent>(); fixedTimeEventDic=new Dictionary<int,FixedTimeEvent>();
|
|
||||||
ETDropAfterDropDic=new Dictionary<int,int>(); RankTargetCount=new Dictionary<int,int>(); WheelLevelCount=new Dictionary<int,int>();
|
|
||||||
TurntableCoupon=_tables.TbGlobalConfig.InitWheelTicket; WheelOperateCount=0; AllWheelOperateCount=0; freecount=0; PVPToken=0;
|
|
||||||
collectingTargetEventID=-1; virtualCollectingTargetEventID=-1; NewPlayerEvent=default;
|
|
||||||
collectingTargetInit=null; collectingTargetInitTokenID=0; collectingTargetInitTaskID=0;
|
|
||||||
TargetList=null; collectingTarget=null; preCollectingTarget=null; targetItemDatas=null; preRankTarget=null; rankItemDatas=null;
|
|
||||||
rankEvent=null; rankInit=null; rankTarget=null; turntableEvent=null; wheelInit=null; wheelLevel=null;
|
|
||||||
SystemOpen=new Dictionary<int,bool>(); _unlocked=new HashSet<int>(); _isTargetGuidance=true;
|
|
||||||
IsJoinUsPanelOpen=false; collectingTargetResoure=true; IsAquariumOpen=false; IsOpenDrop=false; OpenAquariumID=0;
|
|
||||||
requiredCount=0; requiredRankCount=0; curActivateItemId=0;
|
|
||||||
duelData=null; rankTierChangeState=0; curSolomain=null; Pack1A1Data=null; Pack1A2Data=null;
|
|
||||||
}
|
|
||||||
internal void ReplaceFromKVDecoded(game.PlayerDataStorage.PlayerKVEventBaseState state)
|
|
||||||
{
|
|
||||||
if(state==null) throw new ArgumentNullException(nameof(state));
|
|
||||||
limitedTimeEventDic=state.Limited.ToDictionary(p=>p.Key,p=>new LimitedTimeEvent { id=p.Value.Id,duration=p.Value.Duration,startTime=p.Value.StartTime });
|
|
||||||
fixedTimeEventDic=state.Fixed.ToDictionary(p=>p.Key,p=>new FixedTimeEvent { id=p.Value.id,endTime=p.Value.endTime });
|
|
||||||
ETDropAfterDropDic=state.Target.ToDictionary(p=>p.Key,p=>p.Value); RankTargetCount=state.Rank.ToDictionary(p=>p.Key,p=>p.Value);
|
|
||||||
WheelLevelCount=state.Wheel.ToDictionary(p=>p.Key,p=>p.Value); TurntableCoupon=state.Coupon;
|
|
||||||
WheelOperateCount=state.Operate; AllWheelOperateCount=state.AllOperate; freecount=state.Free; PVPToken=state.PvpToken;
|
|
||||||
}
|
|
||||||
#if UNITY_EDITOR
|
|
||||||
internal game.PlayerDataStorage.PlayerKVEventBaseState SnapshotForKVValidation() => new game.PlayerDataStorage.PlayerKVEventBaseState(
|
|
||||||
limitedTimeEventDic.ToDictionary(p=>p.Key,p=>new game.PlayerDataStorage.PlayerKVLimitedValue(p.Value.id,p.Value.duration,p.Value.startTime)),
|
|
||||||
fixedTimeEventDic.ToDictionary(p=>p.Key,p=>new game.PlayerDataStorage.PlayerKVFixedTimeValue(p.Value.id,p.Value.endTime)),
|
|
||||||
ETDropAfterDropDic,RankTargetCount,WheelLevelCount,TurntableCoupon,WheelOperateCount,AllWheelOperateCount,freecount,PVPToken);
|
|
||||||
internal void MutateCollectionsForKVValidation()
|
|
||||||
{ limitedTimeEventDic.Values.First().duration=99; fixedTimeEventDic.Values.First().id=99; ETDropAfterDropDic.Clear(); RankTargetCount.Clear(); WheelLevelCount.Clear(); }
|
|
||||||
internal bool IsInactiveForKVValidation => collectingTargetEventID==-1 && virtualCollectingTargetEventID==-1 && NewPlayerEvent==default &&
|
|
||||||
collectingTargetInit==null && collectingTargetInitTokenID==0 && collectingTargetInitTaskID==0 && TargetList==null && collectingTarget==null &&
|
|
||||||
preCollectingTarget==null && targetItemDatas==null && preRankTarget==null && rankItemDatas==null && rankEvent==null && rankInit==null && rankTarget==null &&
|
|
||||||
turntableEvent==null && wheelInit==null && wheelLevel==null && SystemOpen.Count==0 && _unlocked.Count==0 && duelData==null && curSolomain==null &&
|
|
||||||
Pack1A1Data==null && Pack1A2Data==null && !IsJoinUsPanelOpen && !IsAquariumOpen && !IsOpenDrop && OpenAquariumID==0 && requiredCount==0 && requiredRankCount==0 && curActivateItemId==0;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
public void InitData(Dictionary<string, string> userDatas)
|
public void InitData(Dictionary<string, string> userDatas)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ namespace GameCore
|
|||||||
}
|
}
|
||||||
public class GuideDataCenter : IDisposable
|
public class GuideDataCenter : IDisposable
|
||||||
{
|
{
|
||||||
[asap.core.Inject] public game.PlayerDataStorage.IPlayerSaveStore playerSaveStore { get; set; }
|
[asap.core.Inject] public Core.KV.IPlayerSaveStore playerSaveStore { get; set; }
|
||||||
Tables _tables;
|
Tables _tables;
|
||||||
PlayerData playerData;
|
PlayerData playerData;
|
||||||
public GuideDataCenter(cfg.Tables tables, PlayerData playerData)
|
public GuideDataCenter(cfg.Tables tables, PlayerData playerData)
|
||||||
@@ -58,19 +58,9 @@ namespace GameCore
|
|||||||
public GuidanceGroupDefine curGroupDefine;
|
public GuidanceGroupDefine curGroupDefine;
|
||||||
public GuidanceDefineSetting guidanceDefineSetting;
|
public GuidanceDefineSetting guidanceDefineSetting;
|
||||||
|
|
||||||
internal static game.PlayerDataStorage.PlayerKVGuideState DecodeForKVLoad(IReadOnlyDictionary<string,string> raw,
|
|
||||||
game.PlayerDataStorage.PlayerKVSchemaDiagnostics errors) => game.PlayerDataStorage.PlayerKVSaveSchema.DecodeGuides(raw,errors);
|
|
||||||
internal void ResetForKVLoad()
|
|
||||||
{
|
|
||||||
GuideSaveDataDic = new Dictionary<string,GuideSaveData>(StringComparer.Ordinal);
|
|
||||||
curSaveData = null; curDefine = null; curGroupDefine = null; popupPanelNode = null; CurPanelName = null;
|
|
||||||
}
|
|
||||||
internal void ReplaceFromKVDecoded(game.PlayerDataStorage.PlayerKVGuideState state)
|
|
||||||
{
|
|
||||||
if (state == null) throw new ArgumentNullException(nameof(state));
|
|
||||||
GuideSaveDataDic = state.Values.ToDictionary(p=>p.Key,
|
|
||||||
p=>new GuideSaveData { guideSubIndex=p.Value.Index, isFinish=p.Value.Finished, groupName=p.Value.Group },StringComparer.Ordinal);
|
|
||||||
}
|
|
||||||
//待释放
|
//待释放
|
||||||
IDisposable disposable;
|
IDisposable disposable;
|
||||||
IDisposable disposable2;
|
IDisposable disposable2;
|
||||||
@@ -119,13 +109,7 @@ namespace GameCore
|
|||||||
|
|
||||||
if (GuideSaveDataDic.TryGetValue(item.groupName.ToString(), out _saveData))
|
if (GuideSaveDataDic.TryGetValue(item.groupName.ToString(), out _saveData))
|
||||||
{
|
{
|
||||||
#if UNITY_EDITOR && !SERVICE_HOST
|
|
||||||
if (DeferUnavailablePotionGuide(item, _saveData))
|
|
||||||
{
|
|
||||||
keyValuePairs[item.groupName.ToString()] = _saveData;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
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++)
|
||||||
@@ -178,29 +162,7 @@ namespace GameCore
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#if UNITY_EDITOR && !SERVICE_HOST
|
|
||||||
private static bool DeferUnavailablePotionGuide(GuidanceGroupDefine group, GuideSaveData saved)
|
|
||||||
{
|
|
||||||
if (!game.PlayerDataStorage.PlayerKVGameplayLoop.Enabled || !game.PlayerDataStorage.PlayerKVGameplayLoop.Ready ||
|
|
||||||
group.groupName != GroupName.EventPotion01 || saved == null || saved.isFinish || saved.guideSubIndex <= 0 ||
|
|
||||||
group.guidanceDefines == null || saved.guideSubIndex >= group.guidanceDefines.Length ||
|
|
||||||
group.guidanceDefines[saved.guideSubIndex] == null ||
|
|
||||||
group.guidanceDefines[saved.guideSubIndex].panelName != "EventPotionPanel")
|
|
||||||
return false;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Existing potion activity classification; availability query does not initialize activity data.
|
|
||||||
const int potionEventType = 4;
|
|
||||||
const int potionEventSubType = 5;
|
|
||||||
return GContext.container.Resolve<FishingEventData>().GetEvent(potionEventType, potionEventSubType) == -1;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Debug.LogWarning("Potion guide availability query failed; retaining original guide recovery: " + ex);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
public bool GetIsFinish(string groupName)
|
public bool GetIsFinish(string groupName)
|
||||||
{
|
{
|
||||||
return GuideSaveDataDic.ContainsKey(groupName) && GuideSaveDataDic[groupName].isFinish;
|
return GuideSaveDataDic.ContainsKey(groupName) && GuideSaveDataDic[groupName].isFinish;
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
|
using Core.KV;
|
||||||
using asap.core;
|
using asap.core;
|
||||||
using asap.playfab.async;
|
using asap.playfab.async;
|
||||||
using cfg;
|
using cfg;
|
||||||
using game;
|
using game;
|
||||||
using game.PlayerDataStorage;
|
using game.KV;
|
||||||
using PlayFab.ClientModels;
|
using PlayFab.ClientModels;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
@@ -164,24 +165,30 @@ namespace GameCore
|
|||||||
{
|
{
|
||||||
Statistics.Add(new StatisticUpdate() { StatisticName = names[i], Value = value[i] });
|
Statistics.Add(new StatisticUpdate() { StatisticName = names[i], Value = value[i] });
|
||||||
}
|
}
|
||||||
if (userService is KVUserService kvUser)
|
if (userService is KvUserService kvUser)
|
||||||
{
|
{
|
||||||
var store = kvUser.SaveStore;
|
var store = kvUser.SaveStore as KvSaveStore;
|
||||||
var writtenNames = new HashSet<string>(StringComparer.Ordinal);
|
var writtenNames = new HashSet<string>(StringComparer.Ordinal);
|
||||||
bool accepted = false;
|
var data = new Dictionary<string, string>(StringComparer.Ordinal);
|
||||||
foreach (var statistic in Statistics)
|
foreach (var statistic in Statistics)
|
||||||
{
|
{
|
||||||
if (!writtenNames.Add(statistic.StatisticName)) continue;
|
if (!writtenNames.Add(statistic.StatisticName))
|
||||||
var write = store.Write(statistic.StatisticName, statistic.Value.ToString(CultureInfo.InvariantCulture));
|
{
|
||||||
if (write.Accepted) accepted = true;
|
continue;
|
||||||
else Debug.LogError("KV statistic write failed: " + statistic.StatisticName + ": " + write.Error);
|
}
|
||||||
|
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 (accepted)
|
if (data.Count > 0)
|
||||||
{
|
{
|
||||||
var confirmation = await store.FlushAsync();
|
if (store == null || !await store.SyncDataImmidiate(data))
|
||||||
if (confirmation.Status != PlayerSaveStatus.Confirmed)
|
{
|
||||||
Debug.LogError("KV statistic save not confirmed: " + string.Join(",", writtenNames) +
|
Debug.LogError("KV statistic save failed: " + string.Join(",", data.Keys));
|
||||||
": " + confirmation.Status + ": " + confirmation.Error);
|
}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -193,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,
|
||||||
@@ -209,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;
|
||||||
|
|||||||
@@ -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,7 +17,7 @@ namespace GameCore
|
|||||||
{
|
{
|
||||||
public class PlayerData
|
public class PlayerData
|
||||||
{
|
{
|
||||||
[asap.core.Inject] public game.PlayerDataStorage.IPlayerSaveStore playerSaveStore { get; set; }
|
[asap.core.Inject] public Core.KV.IPlayerSaveStore playerSaveStore { get; set; }
|
||||||
private cfg.Tables _tables;
|
private cfg.Tables _tables;
|
||||||
MMTService _MMService;
|
MMTService _MMService;
|
||||||
public PlayerData(cfg.Tables tables, MMTService mmService)
|
public PlayerData(cfg.Tables tables, MMTService mmService)
|
||||||
@@ -36,41 +36,9 @@ namespace GameCore
|
|||||||
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;
|
||||||
|
|
||||||
internal static game.PlayerDataStorage.PlayerKVPlayerState DecodeForKVLoad(IReadOnlyDictionary<string,string> raw,
|
|
||||||
IReadOnlyDictionary<string,string> modern,game.PlayerDataStorage.PlayerKVSchemaSnapshot snapshot,
|
|
||||||
game.PlayerDataStorage.PlayerKVSchemaDiagnostics errors) => game.PlayerDataStorage.PlayerKVSaveSchema.DecodePlayer(raw,modern,snapshot,errors);
|
|
||||||
internal void ResetForKVLoad()
|
|
||||||
{
|
|
||||||
NewPlayerDataDic=new Dictionary<string,string>(StringComparer.Ordinal); vipScoreDic=new Dictionary<int,int>();
|
|
||||||
newRodList=new List<int>();
|
|
||||||
lv=vip=vipAllScore=0; vipLastScore=PriceLv=0; priceLevel=null;
|
|
||||||
energy=pearl=diamond=equipRodID=magnification=seasonId=rewardNewMap=0;
|
|
||||||
gold=0; goldRM=0; energyRM=0; _currentMapId=_lastMapId=0;
|
|
||||||
_fishingFrequency=_allFishingFrequency=0; _lastEnergyRecoveryTime=DateTime.MinValue;
|
|
||||||
IsOpenAppraise=false; InfiniteBuildingData=null;
|
|
||||||
}
|
|
||||||
internal void ReplaceFromKVDecoded(game.PlayerDataStorage.PlayerKVPlayerState state)
|
|
||||||
{
|
|
||||||
if(state==null || state.Core==null) throw new ArgumentNullException(nameof(state));
|
|
||||||
var modern=state.Modern.ToDictionary(p=>p.Key,p=>p.Value,StringComparer.Ordinal);
|
|
||||||
var scores=state.VipScores.ToDictionary(p=>p.Key,p=>p.Value);
|
|
||||||
var rods=new List<int>(state.NewRods);
|
|
||||||
var core=state.Core;
|
|
||||||
lv=core.Integer(0); gold=core.Unsigned(1); vipAllScore=core.Integer(2); energy=core.Integer(3);
|
|
||||||
pearl=core.Integer(4); magnification=core.Integer(5); equipRodID=core.Integer(6); _lastEnergyRecoveryTime=core.EnergyTime;
|
|
||||||
rewardNewMap=core.Integer(8); energyRM=core.Integer(9); goldRM=core.Unsigned(10); diamond=core.Integer(11);
|
|
||||||
_currentMapId=core.Integer(12); _lastMapId=core.Integer(13); _fishingFrequency=core.Integer(14); _allFishingFrequency=core.Integer(15);
|
|
||||||
NewPlayerDataDic=modern; vipScoreDic=scores; newRodList=rods;
|
|
||||||
seasonId=state.Season; IsOpenAppraise=state.Appraise; InfiniteBuildingData=state.Building;
|
|
||||||
}
|
|
||||||
#if UNITY_EDITOR
|
|
||||||
internal game.PlayerDataStorage.PlayerKVPlayerState SnapshotForKVValidation() =>
|
|
||||||
new game.PlayerDataStorage.PlayerKVPlayerState(new game.PlayerDataStorage.PlayerKVCoreState(new object[] {
|
|
||||||
(int)lv,(ulong)gold,(int)vipAllScore,(int)energy,(int)pearl,(int)magnification,(int)equipRodID,_lastEnergyRecoveryTime,
|
|
||||||
(int)rewardNewMap,energyRM,goldRM,(int)diamond,(int)_currentMapId,(int)_lastMapId,_fishingFrequency,_allFishingFrequency,0,0,0
|
|
||||||
}),NewPlayerDataDic,vipScoreDic,newRodList,seasonId,IsOpenAppraise,InfiniteBuildingData);
|
|
||||||
internal bool IsDerivedEmptyForKVValidation => (int)vip==0 && vipLastScore==0 && PriceLv==0 && priceLevel==null;
|
|
||||||
#endif
|
|
||||||
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; }
|
||||||
@@ -1140,57 +1108,6 @@ namespace GameCore
|
|||||||
public void SavePlayerData(bool remote = true)
|
public void SavePlayerData(bool remote = true)
|
||||||
{
|
{
|
||||||
GContext.Publish<PlayerData>(this);
|
GContext.Publish<PlayerData>(this);
|
||||||
SerializeAndSavePlayerData(remote);
|
|
||||||
}
|
|
||||||
|
|
||||||
#if UNITY_EDITOR && !SERVICE_HOST
|
|
||||||
internal async System.Threading.Tasks.Task ConsumeEnergyForKVLoopAsync(int cost,
|
|
||||||
game.PlayerDataStorage.PlayerKVGameplayLoop.FishingAction action)
|
|
||||||
{
|
|
||||||
if(cost<=0 || Energy<cost) throw game.PlayerDataStorage.PlayerKVGameplayLoop.Stop("Invalid fishing energy cost or insufficient energy.");
|
|
||||||
int before=Energy;
|
|
||||||
var batch=game.PlayerDataStorage.PlayerKVGameplayLoop.BeginFishingWrites(action);
|
|
||||||
game.PlayerDataStorage.PlayerKVEditResult primary;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using(batch)
|
|
||||||
{
|
|
||||||
ReduceEnergy(cost,false);
|
|
||||||
GContext.Publish(new ConditionTypeEvent(ConditionType.ConsumeEnergy,cost));
|
|
||||||
primary=SerializeAndSavePlayerData(true);
|
|
||||||
}
|
|
||||||
game.PlayerDataStorage.PlayerKVGameplayLoop.Audit("fishing-energy-applied",new{before,cost,after=(int)Energy,writes=batch.Records.Count,localOnly=batch.Records.Count(r=>!r.Remote)});
|
|
||||||
await game.PlayerDataStorage.PlayerKVGameplayLoop.ConfirmFishingAsync(action,batch,primary);
|
|
||||||
}
|
|
||||||
catch(System.Exception e) { game.PlayerDataStorage.PlayerKVGameplayLoop.Fault(e.Message); throw; }
|
|
||||||
}
|
|
||||||
internal async System.Threading.Tasks.Task SetEquipRodIDForKVLoopAsync(int id)
|
|
||||||
{
|
|
||||||
var batch=game.PlayerDataStorage.PlayerKVGameplayLoop.BeginEquip();
|
|
||||||
game.PlayerDataStorage.PlayerKVEditResult primary;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using(batch)
|
|
||||||
{
|
|
||||||
equipRodID=id;
|
|
||||||
GContext.container.Resolve<PlayerFishData>().SetRedPoint();
|
|
||||||
GContext.Publish<PlayerData>(this);
|
|
||||||
primary=SerializeAndSavePlayerData(true);
|
|
||||||
}
|
|
||||||
await game.PlayerDataStorage.PlayerKVGameplayLoop.ConfirmEquipAsync(batch,primary);
|
|
||||||
}
|
|
||||||
catch(System.Exception e) { game.PlayerDataStorage.PlayerKVGameplayLoop.Fault(e.Message); throw; }
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
#if UNITY_EDITOR && !SERVICE_HOST
|
|
||||||
internal game.PlayerDataStorage.PlayerKVEditResult SaveFishingRewardForKVLoop()
|
|
||||||
{
|
|
||||||
game.PlayerDataStorage.PlayerKVGameplayLoop.CheckRewardMainSave();
|
|
||||||
return SerializeAndSavePlayerData(true);
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
private game.PlayerDataStorage.PlayerKVEditResult SerializeAndSavePlayerData(bool remote)
|
|
||||||
{
|
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
sb.Append(lv).Append(",");
|
sb.Append(lv).Append(",");
|
||||||
sb.Append(gold).Append(",");
|
sb.Append(gold).Append(",");
|
||||||
@@ -1231,11 +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(); ;
|
||||||
#if UNITY_EDITOR && !SERVICE_HOST
|
|
||||||
if(game.PlayerDataStorage.PlayerKVGameplayLoop.Enabled) return game.PlayerDataStorage.PlayerKVGameplayLoop.Write("NewPlayerDataDic",Newtonsoft.Json.JsonConvert.SerializeObject(NewPlayerDataDic),remote);
|
|
||||||
#endif
|
|
||||||
playerSaveStore.Write("NewPlayerDataDic", Newtonsoft.Json.JsonConvert.SerializeObject(NewPlayerDataDic), remote).RequireAccepted();
|
playerSaveStore.Write("NewPlayerDataDic", Newtonsoft.Json.JsonConvert.SerializeObject(NewPlayerDataDic), remote).RequireAccepted();
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
public void Init()
|
public void Init()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using cfg;
|
using cfg;
|
||||||
@@ -52,7 +52,7 @@ namespace GameCore
|
|||||||
}
|
}
|
||||||
public partial class PlayerFishData
|
public partial class PlayerFishData
|
||||||
{
|
{
|
||||||
[asap.core.Inject] public game.PlayerDataStorage.IPlayerSaveStore playerSaveStore { get; set; }
|
[asap.core.Inject] public Core.KV.IPlayerSaveStore playerSaveStore { get; set; }
|
||||||
private cfg.Tables _tables;
|
private cfg.Tables _tables;
|
||||||
private ICustomServerMgr customServerMgr;
|
private ICustomServerMgr customServerMgr;
|
||||||
public PlayerFishData(cfg.Tables tables, ICustomServerMgr customServerMgr)
|
public PlayerFishData(cfg.Tables tables, ICustomServerMgr customServerMgr)
|
||||||
@@ -86,45 +86,9 @@ 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;
|
||||||
internal static game.PlayerDataStorage.PlayerKVFishState DecodeForKVLoad(IReadOnlyDictionary<string,string> raw,
|
|
||||||
IReadOnlyDictionary<string,string> modern,game.PlayerDataStorage.PlayerKVSchemaSnapshot snapshot,game.PlayerDataStorage.PlayerKVSchemaDiagnostics errors)
|
|
||||||
=> game.PlayerDataStorage.PlayerKVSaveSchema.DecodeFish(raw,modern,snapshot,errors);
|
|
||||||
internal void ResetForKVLoad()
|
|
||||||
{
|
|
||||||
mapFishData=new Dictionary<int,Dictionary<int,FishWeightData>>(); fishData=new Dictionary<int,FishWeightData>(); fishCardData=new Dictionary<int,int>();
|
|
||||||
mapProgress=new Dictionary<int,int>(); rodData=new Dictionary<int,int>(); rodLevelData=new Dictionary<int,int>();
|
|
||||||
fishSwimToMap=new Dictionary<int,int>(); fishSwimToMapCount=new Dictionary<int,int>(); _honorlevel=new Dictionary<int,int>();
|
|
||||||
LuckDataDic=new Dictionary<string,float>(StringComparer.Ordinal); rodAccessoriesData=null;
|
|
||||||
InitBotLuck=0; InitFishLuck=0; BotCount=0; _isRedPoint_up=false; RodCanUp=false; RodCanUpLevel=false;
|
|
||||||
MapTipforUnlocked=0; IsOpenMap=false; IsOpenRod=false; IsOpenPiercing=false; RodSelectedInPack=0;
|
|
||||||
}
|
|
||||||
internal void ReplaceFromKVDecoded(game.PlayerDataStorage.PlayerKVFishState state)
|
|
||||||
{
|
|
||||||
if(state==null) throw new ArgumentNullException(nameof(state));
|
|
||||||
var maps=state.Maps.ToDictionary(m=>m.Key,m=>m.Value.ToDictionary(p=>p.Key,p=>new FishWeightData {
|
|
||||||
Level=p.Value.Level,MaxG=p.Value.Weight,TotalCard=p.Value.Cards,TotalNum=p.Value.Number,NewFish=p.Value.New }));
|
|
||||||
var flat=new Dictionary<int,FishWeightData>();
|
|
||||||
foreach(var map in maps.Values) foreach(var p in map) flat.Add(p.Key,p.Value);
|
|
||||||
mapFishData=maps; fishData=flat; fishCardData=state.Cards.ToDictionary(p=>p.Key,p=>p.Value);
|
|
||||||
rodData=state.Rods.ToDictionary(p=>p.Key,p=>p.Value); rodLevelData=state.RodLevels.ToDictionary(p=>p.Key,p=>p.Value);
|
|
||||||
fishSwimToMap=state.Swim.ToDictionary(p=>p.Key,p=>p.Value); fishSwimToMapCount=state.SwimCount.ToDictionary(p=>p.Key,p=>p.Value);
|
|
||||||
mapProgress=state.Progress.ToDictionary(p=>p.Key,p=>p.Value); _honorlevel=state.Honor.ToDictionary(p=>p.Key,p=>p.Value);
|
|
||||||
LuckDataDic=state.Luck.ToDictionary(p=>p.Key,p=>p.Value,StringComparer.Ordinal);
|
|
||||||
var a=state.Accessories;
|
|
||||||
rodAccessoriesData=a==null ? null : new RodGSAData { GloveID=a.Glove,RodToSkin=a.RodSkin.ToDictionary(p=>p.Key,p=>p.Value),
|
|
||||||
GloveIds=new List<int>(a.Gloves),SkinIds=new List<int>(a.Skins),GloveNewIds=new List<int>(a.NewGloves),SkinNewIds=new List<int>(a.NewSkins) };
|
|
||||||
}
|
|
||||||
#if UNITY_EDITOR
|
|
||||||
internal FishWeightData FishForKVValidation(int map,int id) => mapFishData[map][id];
|
|
||||||
internal bool IndexesMatchForKVValidation(int map,int id,int card) => ReferenceEquals(mapFishData[map][id],fishData[id]) && fishCardData[card]==fishData[id].TotalCard;
|
|
||||||
internal bool EmptyForKVValidation => mapFishData.Count==0 && fishData.Count==0 && fishCardData.Count==0 && mapProgress.Count==0 && rodData.Count==0 && rodLevelData.Count==0 &&
|
|
||||||
fishSwimToMap.Count==0 && fishSwimToMapCount.Count==0 && _honorlevel.Count==0 && LuckDataDic.Count==0 && rodAccessoriesData==null &&
|
|
||||||
InitBotLuck==0 && InitFishLuck==0 && BotCount==0 && !_isRedPoint_up && !RodCanUp && !RodCanUpLevel && !IsOpenMap && !IsOpenRod && !IsOpenPiercing && MapTipforUnlocked==0 && RodSelectedInPack==0;
|
|
||||||
internal bool DictionariesMatchForKVValidation(game.PlayerDataStorage.PlayerKVFishState s) =>
|
|
||||||
rodData.OrderBy(p=>p.Key).SequenceEqual(s.Rods.OrderBy(p=>p.Key)) && rodLevelData.OrderBy(p=>p.Key).SequenceEqual(s.RodLevels.OrderBy(p=>p.Key)) &&
|
|
||||||
fishSwimToMap.OrderBy(p=>p.Key).SequenceEqual(s.Swim.OrderBy(p=>p.Key)) && fishSwimToMapCount.OrderBy(p=>p.Key).SequenceEqual(s.SwimCount.OrderBy(p=>p.Key)) &&
|
|
||||||
mapProgress.OrderBy(p=>p.Key).SequenceEqual(s.Progress.OrderBy(p=>p.Key)) && _honorlevel.OrderBy(p=>p.Key).SequenceEqual(s.Honor.OrderBy(p=>p.Key));
|
|
||||||
#endif
|
|
||||||
|
|
||||||
public void InitData(Dictionary<string, string> userDatas)
|
public void InitData(Dictionary<string, string> userDatas)
|
||||||
{
|
{
|
||||||
@@ -1195,14 +1159,6 @@ namespace GameCore
|
|||||||
SetFishData(id, fishData[id]);
|
SetFishData(id, fishData[id]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#if UNITY_EDITOR && !SERVICE_HOST
|
|
||||||
internal void SaveCaughtFishForKVLoop(int fishId)
|
|
||||||
{
|
|
||||||
if(!fishData.TryGetValue(fishId,out var data)) throw game.PlayerDataStorage.PlayerKVGameplayLoop.Stop("Caught fish data is missing.");
|
|
||||||
game.PlayerDataStorage.PlayerKVGameplayLoop.CheckCaughtFishSave(fishId,data.TotalNum);
|
|
||||||
SetFishData(fishId,data);
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
void SetFishData(int id, FishWeightData data)
|
void SetFishData(int id, FishWeightData data)
|
||||||
{
|
{
|
||||||
int mapID = _tables.TbFishData.GetOrDefault(id).AreaID;
|
int mapID = _tables.TbFishData.GetOrDefault(id).AreaID;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using asap.core;
|
using asap.core;
|
||||||
using cfg;
|
using cfg;
|
||||||
using CodeStage.AntiCheat.ObscuredTypes;
|
using CodeStage.AntiCheat.ObscuredTypes;
|
||||||
using game;
|
using game;
|
||||||
@@ -73,8 +73,8 @@ namespace GameCore
|
|||||||
}
|
}
|
||||||
public partial class PlayerShopData : IDisposable
|
public partial class PlayerShopData : IDisposable
|
||||||
{
|
{
|
||||||
game.PlayerDataStorage.IPlayerSaveStore boundSaveStore;
|
Core.KV.IPlayerSaveStore boundSaveStore;
|
||||||
[asap.core.Inject] public game.PlayerDataStorage.IPlayerSaveStore SaveStore
|
[asap.core.Inject] public Core.KV.IPlayerSaveStore SaveStore
|
||||||
{
|
{
|
||||||
get => boundSaveStore;
|
get => boundSaveStore;
|
||||||
set { CouponData.BindSaveStore(value); boundSaveStore = value; }
|
set { CouponData.BindSaveStore(value); boundSaveStore = value; }
|
||||||
@@ -133,43 +133,10 @@ namespace GameCore
|
|||||||
Dictionary<int, int> advertCount = new Dictionary<int, int>();
|
Dictionary<int, int> advertCount = new Dictionary<int, int>();
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
internal static game.PlayerDataStorage.PlayerKVShopState DecodeForKVLoad(IReadOnlyDictionary<string,string> raw,
|
|
||||||
game.PlayerDataStorage.PlayerKVSchemaSnapshot snapshot,game.PlayerDataStorage.PlayerKVSchemaDiagnostics errors)
|
|
||||||
=> game.PlayerDataStorage.PlayerKVSaveSchema.DecodeShop(raw,snapshot,errors);
|
|
||||||
internal void ResetForKVLoad()
|
|
||||||
{
|
|
||||||
shopPackDailyBuyCount=new Dictionary<string,int>(StringComparer.Ordinal); shopAllPackBuyCount=new Dictionary<int,int>();
|
|
||||||
shopGiftBuyCount=new Dictionary<int,int>(); buyData=new List<float>(); advertCount=new Dictionary<int,int>();
|
|
||||||
Voucher=0; ChainPackData=null; VIPAdPackData=null; TurntableAdData=null;
|
|
||||||
_diamondShopItemRecords=new Dictionary<int,ItemShopRecord>(); _diamondShopRefreshCount=0; _diamondShopRefreshTime=default;
|
|
||||||
CouponData.ResetForKVLoad();
|
|
||||||
}
|
|
||||||
static DailySignEvent KVDaily(game.PlayerDataStorage.PlayerKVDailyValue value) => value==null ? null :
|
|
||||||
new DailySignEvent { ID=value.Id,index=value.Index,lastID=value.LastId,time=value.Time };
|
|
||||||
internal void ReplaceFromKVDecoded(game.PlayerDataStorage.PlayerKVShopState state)
|
|
||||||
{
|
|
||||||
if(state==null) throw new ArgumentNullException(nameof(state));
|
|
||||||
shopPackDailyBuyCount=state.Daily.ToDictionary(p=>p.Key,p=>p.Value,StringComparer.Ordinal);
|
|
||||||
shopAllPackBuyCount=state.All.ToDictionary(p=>p.Key,p=>p.Value); shopGiftBuyCount=state.Gift.ToDictionary(p=>p.Key,p=>p.Value);
|
|
||||||
advertCount=state.Advert.ToDictionary(p=>p.Key,p=>p.Value); buyData=new List<float>(state.Buy); Voucher=state.Voucher;
|
|
||||||
ChainPackData=KVDaily(state.Chain); TurntableAdData=KVDaily(state.Turntable);
|
|
||||||
VIPAdPackData=state.Vip==null ? null : new AdvertData { ID=state.Vip.Id,index=state.Vip.Index,day=state.Vip.Day,cdTime=state.Vip.Time,isOpen=state.Vip.Open };
|
|
||||||
_diamondShopItemRecords=state.Items.ToDictionary(p=>p.Key,p=>new ItemShopRecord(p.Value.Id,p.Value.Discount,p.Value.Bought));
|
|
||||||
_diamondShopRefreshCount=state.RefreshCount; _diamondShopRefreshTime=state.RefreshTime; CouponData.ReplaceFromKVDecoded(state.Coupon);
|
|
||||||
}
|
|
||||||
#if UNITY_EDITOR
|
|
||||||
internal game.PlayerDataStorage.PlayerKVShopState SnapshotForKVValidation()
|
|
||||||
{
|
|
||||||
game.PlayerDataStorage.PlayerKVDailyValue Daily(DailySignEvent v) => v==null ? null : new game.PlayerDataStorage.PlayerKVDailyValue(v.ID,v.index,v.lastID,v.time);
|
|
||||||
return new game.PlayerDataStorage.PlayerKVShopState(shopPackDailyBuyCount,shopAllPackBuyCount,shopGiftBuyCount,advertCount,
|
|
||||||
_diamondShopItemRecords.ToDictionary(p=>p.Key,p=>new game.PlayerDataStorage.PlayerKVShopItemValue(p.Value.ItemID,p.Value.Discount,p.Value.hasBought)),
|
|
||||||
buyData,Voucher,_diamondShopRefreshCount,_diamondShopRefreshTime,Daily(ChainPackData),Daily(TurntableAdData),
|
|
||||||
VIPAdPackData==null ? null : new game.PlayerDataStorage.PlayerKVAdvertValue(VIPAdPackData.ID,VIPAdPackData.index,VIPAdPackData.day,VIPAdPackData.cdTime,VIPAdPackData.isOpen),
|
|
||||||
new game.PlayerDataStorage.PlayerKVCouponState(CouponData.ActivateTime,CouponData.EventId,CouponData.RedirectId,CouponData.IsBought));
|
|
||||||
}
|
|
||||||
internal void MutateCollectionsForKVValidation()
|
|
||||||
{ buyData.Clear(); advertCount.Clear(); _diamondShopItemRecords.Values.First().Discount=99; }
|
|
||||||
#endif
|
|
||||||
|
|
||||||
public void InitData(Dictionary<string, string> userDatas)
|
public void InitData(Dictionary<string, string> userDatas)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using asap.core;
|
using asap.core;
|
||||||
using cfg;
|
using cfg;
|
||||||
using game;
|
using game;
|
||||||
using LitJson;
|
using LitJson;
|
||||||
@@ -29,7 +29,7 @@ namespace GameCore
|
|||||||
}
|
}
|
||||||
public class SigninData
|
public class SigninData
|
||||||
{
|
{
|
||||||
[asap.core.Inject] public game.PlayerDataStorage.IPlayerSaveStore SaveStore { get; set; }
|
[asap.core.Inject] public Core.KV.IPlayerSaveStore SaveStore { get; set; }
|
||||||
|
|
||||||
private cfg.Tables _tables;
|
private cfg.Tables _tables;
|
||||||
public SigninData(cfg.Tables tables)
|
public SigninData(cfg.Tables tables)
|
||||||
@@ -39,18 +39,9 @@ namespace GameCore
|
|||||||
|
|
||||||
MonthlySignEvent _monthlySignEvent = null;
|
MonthlySignEvent _monthlySignEvent = null;
|
||||||
|
|
||||||
internal static game.PlayerDataStorage.PlayerKVMonthlySignState DecodeForKVLoad(IReadOnlyDictionary<string,string> raw,
|
|
||||||
game.PlayerDataStorage.PlayerKVSchemaDiagnostics errors) => game.PlayerDataStorage.PlayerKVSaveSchema.DecodeMonthlySign(raw,errors);
|
|
||||||
internal void ResetForKVLoad() { _monthlySignEvent = null; }
|
|
||||||
internal void ReplaceFromKVDecoded(game.PlayerDataStorage.PlayerKVMonthlySignState state)
|
|
||||||
{
|
|
||||||
if (state == null) throw new ArgumentNullException(nameof(state));
|
|
||||||
_monthlySignEvent = !state.IsPresent ? null : new MonthlySignEvent
|
|
||||||
{
|
|
||||||
index = state.Index,
|
|
||||||
dailySignEvent = new DailySignEvent { ID=state.DailyId, index=state.DailyIndex, lastID=state.LastId, time=state.Time }
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public DailySignEvent dailySignEvent
|
public DailySignEvent dailySignEvent
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ namespace GameCore
|
|||||||
}
|
}
|
||||||
public class SmallGameData
|
public class SmallGameData
|
||||||
{
|
{
|
||||||
[asap.core.Inject] public game.PlayerDataStorage.IPlayerSaveStore playerSaveStore { get; set; }
|
[asap.core.Inject] public Core.KV.IPlayerSaveStore playerSaveStore { get; set; }
|
||||||
private cfg.Tables _tables;
|
private cfg.Tables _tables;
|
||||||
IUserService userService;
|
IUserService userService;
|
||||||
ICustomServerMgr customServerMgr;
|
ICustomServerMgr customServerMgr;
|
||||||
@@ -48,25 +48,9 @@ namespace GameCore
|
|||||||
public int FishHuntGameCount => _fishHuntGameCount;
|
public int FishHuntGameCount => _fishHuntGameCount;
|
||||||
public int BombGameCount => _bombGameCount;
|
public int BombGameCount => _bombGameCount;
|
||||||
|
|
||||||
internal void ResetForKVLoad()
|
|
||||||
{
|
|
||||||
_heistGameCount=_bombGameCount=_fishHuntGameCount=0;
|
|
||||||
opponentItemDatas=new List<OpponentItemData>(); opponentFriendItemDatas=new List<OpponentItemData>();
|
|
||||||
opponentNotFriendItemDatas=new List<OpponentItemData>(); opponentFriend=new List<OpponentItemData>();
|
|
||||||
opponentFriendDic=new Dictionary<string,OpponentItemData>(); opponentEnemy=new Dictionary<EnemyType,List<OpponentItemData>>();
|
|
||||||
opponentItemDatasPre=new List<OpponentItemData>(); opponentFriendItemDatasPre=new List<OpponentItemData>();
|
|
||||||
opponentNotFriendItemDatasPre=new List<OpponentItemData>(); powerData=new Dictionary<string,float>();
|
|
||||||
opponentData=null; nextOpponent=null; curEnemyType=EnemyType.Aquarium; RobotMapId=nextRobotMapId=0;
|
|
||||||
}
|
|
||||||
internal void ReplaceFromKVDecoded(game.PlayerDataStorage.PlayerKVCoreState state)
|
|
||||||
{
|
|
||||||
if(state==null) throw new ArgumentNullException(nameof(state));
|
|
||||||
_heistGameCount=state.Integer(16); _bombGameCount=state.Integer(17); _fishHuntGameCount=state.Integer(18);
|
|
||||||
}
|
|
||||||
internal bool IsOpponentEmptyForKVValidation => opponentItemDatas.Count==0 && opponentFriendItemDatas.Count==0 &&
|
|
||||||
opponentNotFriendItemDatas.Count==0 && opponentFriend.Count==0 && opponentFriendDic.Count==0 && opponentEnemy.Count==0 &&
|
|
||||||
opponentItemDatasPre.Count==0 && opponentFriendItemDatasPre.Count==0 && opponentNotFriendItemDatasPre.Count==0 &&
|
|
||||||
powerData.Count==0 && opponentData==null && nextOpponent==null && RobotMapId==0 && nextRobotMapId==0;
|
|
||||||
|
|
||||||
//public UnityEngine.Vector3 image_extra_pos { set; get; }
|
//public UnityEngine.Vector3 image_extra_pos { set; get; }
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using asap.core;
|
using asap.core;
|
||||||
using cfg;
|
using cfg;
|
||||||
using game;
|
using game;
|
||||||
using LitJson;
|
using LitJson;
|
||||||
@@ -38,7 +38,7 @@ namespace GameCore
|
|||||||
}
|
}
|
||||||
public class TriggerPackData
|
public class TriggerPackData
|
||||||
{
|
{
|
||||||
[asap.core.Inject] public game.PlayerDataStorage.IPlayerSaveStore SaveStore { get; set; }
|
[asap.core.Inject] public Core.KV.IPlayerSaveStore SaveStore { get; set; }
|
||||||
|
|
||||||
Tables _tables;
|
Tables _tables;
|
||||||
PlayerShopData playerShopData;
|
PlayerShopData playerShopData;
|
||||||
@@ -55,28 +55,9 @@ namespace GameCore
|
|||||||
//VIP 等级叠加
|
//VIP 等级叠加
|
||||||
public Dictionary<string, GiftVIPDData> giftVIPDData = new Dictionary<string, GiftVIPDData>();
|
public Dictionary<string, GiftVIPDData> giftVIPDData = new Dictionary<string, GiftVIPDData>();
|
||||||
|
|
||||||
internal static game.PlayerDataStorage.PlayerKVTriggerState DecodeForKVLoad(IReadOnlyDictionary<string,string> raw,
|
|
||||||
game.PlayerDataStorage.PlayerKVSchemaDiagnostics errors) => game.PlayerDataStorage.PlayerKVSaveSchema.DecodeTriggers(raw,errors);
|
|
||||||
internal void ResetForKVLoad()
|
|
||||||
{
|
|
||||||
triggerPackList=new List<TriggerPackBuyData>();
|
|
||||||
giftVIPDData=new Dictionary<string,GiftVIPDData>(StringComparer.Ordinal);
|
|
||||||
fixedTimeShopDic=new Dictionary<int,FixedTimeEvent>();
|
|
||||||
}
|
|
||||||
internal void ReplaceFromKVDecoded(game.PlayerDataStorage.PlayerKVTriggerState state)
|
|
||||||
{
|
|
||||||
if(state==null) throw new ArgumentNullException(nameof(state));
|
|
||||||
var packs=state.Packs.Select(p=>new TriggerPackBuyData {
|
|
||||||
ID=p.ID,index=p.index,lastID=p.lastID,vipLevel=p.vipLevel,mapID=p.mapID,time=p.time }).ToList();
|
|
||||||
var gifts=state.Gifts.ToDictionary(p=>p.Key,p=>new GiftVIPDData {
|
|
||||||
oldVipLevel=p.Value.oldVipLevel,curVipLevel=p.Value.curVipLevel,timer=p.Value.timer },StringComparer.Ordinal);
|
|
||||||
var fixedValues=state.Fixed.ToDictionary(p=>p.Key,p=>new FixedTimeEvent { id=p.Value.id,endTime=p.Value.endTime });
|
|
||||||
triggerPackList=packs; giftVIPDData=gifts; fixedTimeShopDic=fixedValues;
|
|
||||||
}
|
|
||||||
#if UNITY_EDITOR
|
|
||||||
internal FixedTimeEvent FixedForKVValidation(int key) => fixedTimeShopDic.TryGetValue(key,out var value) ? value : null;
|
|
||||||
internal bool IsEmptyForKVValidation => triggerPackList.Count==0 && giftVIPDData.Count==0 && fixedTimeShopDic.Count==0;
|
|
||||||
#endif
|
|
||||||
public void Init()
|
public void Init()
|
||||||
{
|
{
|
||||||
playerShopData = GContext.container.Resolve<PlayerShopData>();
|
playerShopData = GContext.container.Resolve<PlayerShopData>();
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ using GameCore;
|
|||||||
using cfg;
|
using cfg;
|
||||||
public class EventPackData
|
public class EventPackData
|
||||||
{
|
{
|
||||||
[asap.core.Inject] public game.PlayerDataStorage.IPlayerSaveStore playerSaveStore { get; set; }
|
[asap.core.Inject] public Core.KV.IPlayerSaveStore playerSaveStore { get; set; }
|
||||||
private Tables _tables = GContext.container.Resolve<Tables>();
|
private Tables _tables = GContext.container.Resolve<Tables>();
|
||||||
private struct Data
|
private struct Data
|
||||||
{
|
{
|
||||||
@@ -21,18 +21,10 @@ public class EventPackData
|
|||||||
}
|
}
|
||||||
private Data _data;
|
private Data _data;
|
||||||
|
|
||||||
internal static game.PlayerDataStorage.PlayerKVEventPackState DecodeForKVLoad(System.Collections.Generic.IReadOnlyDictionary<string,string> raw,
|
|
||||||
game.PlayerDataStorage.PlayerKVSchemaSnapshot snapshot,game.PlayerDataStorage.PlayerKVSchemaDiagnostics errors) =>
|
|
||||||
game.PlayerDataStorage.PlayerKVSaveSchema.DecodeEventPack(raw,snapshot,errors);
|
|
||||||
internal void ResetForKVLoad() { _data=default(Data); }
|
|
||||||
internal void ReplaceFromKVDecoded(game.PlayerDataStorage.PlayerKVEventPackState state)
|
|
||||||
{
|
|
||||||
if (state == null) throw new System.ArgumentNullException(nameof(state));
|
|
||||||
_data=new Data { currentEventID=state.EventId,purchaseCount=state.PurchaseCount,
|
|
||||||
VIPLvlWhenEventActivate=state.VipLevel,redirectID=state.RedirectId };
|
|
||||||
}
|
|
||||||
internal game.PlayerDataStorage.PlayerKVEventPackState SnapshotForKVValidation() =>
|
|
||||||
new game.PlayerDataStorage.PlayerKVEventPackState(_data.currentEventID,_data.purchaseCount,_data.VIPLvlWhenEventActivate,_data.redirectID);
|
|
||||||
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; } }
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
@@ -100,55 +100,14 @@ namespace DataCenter
|
|||||||
// EventPartnerGather 管理器
|
// EventPartnerGather 管理器
|
||||||
public class EventPartnerGatherManager : IAutoMatchManagerContext
|
public class EventPartnerGatherManager : IAutoMatchManagerContext
|
||||||
{
|
{
|
||||||
[asap.core.Inject] public game.PlayerDataStorage.IPlayerSaveStore SaveStore { get; set; }
|
[asap.core.Inject] public Core.KV.IPlayerSaveStore SaveStore { get; set; }
|
||||||
|
|
||||||
private const bool EnableTraceLogs = false;
|
private const bool EnableTraceLogs = false;
|
||||||
public const string SaveKey = "EventPartnerGatherData";
|
public const string SaveKey = "EventPartnerGatherData";
|
||||||
internal static game.PlayerDataStorage.PlayerKVPartnerState DecodeForKVLoad(
|
|
||||||
IReadOnlyDictionary<string,string> raw,game.PlayerDataStorage.PlayerKVSchemaSnapshot snapshot,
|
|
||||||
game.PlayerDataStorage.PlayerKVSchemaDiagnostics errors) => game.PlayerDataStorage.PlayerKVSaveSchema.DecodePartner(raw,snapshot,errors);
|
|
||||||
|
|
||||||
internal void ResetForKVLoad()
|
|
||||||
{
|
|
||||||
// Only a fresh controlled container is admitted. Never detach active gameplay work.
|
|
||||||
if(_isInit || _act!=null || _disposables!=null || _autoMatchStatusInFlight!=null || PartnerInfoManager.HasSubscriptionsForKVLoad)
|
|
||||||
throw new InvalidOperationException("Active partner module cannot enter KV load.");
|
|
||||||
var partner=new PartnerInfoManager();
|
|
||||||
var robot=new EventPartnerGatherRobotInvitationSystem();
|
|
||||||
GatherData=null; _isEventDataChanged=false; CurrentBuildPartner=null;
|
|
||||||
PartnerInfoManager=partner; RobotInvitationSystem=robot;
|
|
||||||
_fishingEvent=null; EventPartnerMineMainData=null; EventPartnerMineSpinPointList=null; EventPartnerMineInitData=null;
|
|
||||||
_nodeCumulativeSteps.Clear(); _nodeSteps.Clear(); _stageDropList.Clear(); StartTime=default; EndTime=default;
|
|
||||||
_isInit=false; _spinId=-1; _userGm=false; BonusPercent=0; BonusExpireDuration=0;
|
|
||||||
_dateTimeForGetEvent=default; startTime=0; delta=0; _lastAutoMatchStatusUtc=default; _cachedAutoMatchStatus=null;
|
|
||||||
}
|
|
||||||
internal void ReplaceFromKVDecoded(game.PlayerDataStorage.PlayerKVPartnerState state)
|
|
||||||
{
|
|
||||||
if(state==null) throw new ArgumentNullException(nameof(state));
|
|
||||||
if(!state.Present) { GatherData=null; return; }
|
|
||||||
GatherData=new EventPartnerGatherData
|
|
||||||
{
|
|
||||||
FirstOpen=state.FirstOpen,EventId=state.EventId,StartTime=state.StartTime,EndTime=state.EndTime,
|
|
||||||
CurrentPoints=state.CurrentPoints,CurrentMiningProgress=state.CurrentMiningProgress,Token=state.Token,
|
|
||||||
InvitedCount=state.InvitedCount,TicketCount=state.TicketCount,
|
|
||||||
CompletedNodesList=state.CompletedNodesList.Select(n=>new List<int>(n)).ToList(),
|
|
||||||
BuilderPartnerScore=new List<(int,int)>(state.BuilderPartnerScore),PartnerBonus=(state.BonusPercent,state.BonusExpireUtc),
|
|
||||||
FreeSpinNum=state.FreeSpinNum,Bots=state.Bots.ToDictionary(p=>p.Key,p=>new EventBuildBotInfo
|
|
||||||
{ Id=p.Value.Id,ScorePlayer=p.Value.ScorePlayer,ScoreBot=p.Value.ScoreBot,Grade=p.Value.Grade,
|
|
||||||
TargetScore=p.Value.TargetScore,WakeStartTime=p.Value.WakeStartTime,WakingSeconds=p.Value.WakingSeconds }),
|
|
||||||
IsRecvFinalReward=state.IsRecvFinalReward,UIFinishedPartners=new List<bool>(state.UIFinishedPartners),
|
|
||||||
IsFinished=state.IsFinished,SpinState=(ESpinState)state.SpinState,InflationRate=state.InflationRate,
|
|
||||||
IsEnableAutoMatch=state.IsEnableAutoMatch,LastAutoMatchTime=state.LastAutoMatchTime
|
|
||||||
};
|
|
||||||
}
|
|
||||||
#if UNITY_EDITOR
|
|
||||||
internal bool RuntimeEmptyForKVValidation => !_isInit && !_isEventDataChanged && _act==null && _disposables==null &&
|
|
||||||
_autoMatchStatusInFlight==null && _cachedAutoMatchStatus==null && _fishingEvent==null && CurrentBuildPartner==null &&
|
|
||||||
EventPartnerMineMainData==null && EventPartnerMineSpinPointList==null && EventPartnerMineInitData==null &&
|
|
||||||
_nodeCumulativeSteps.Count==0 && _nodeSteps.Count==0 && _stageDropList.Count==0 && StartTime==default && EndTime==default &&
|
|
||||||
_spinId==-1 && !_userGm && BonusPercent==0 && BonusExpireDuration==0 && _dateTimeForGetEvent==default &&
|
|
||||||
startTime==0 && delta==0 && _lastAutoMatchStatusUtc==default && !PartnerInfoManager.HasSubscriptionsForKVLoad;
|
|
||||||
#endif
|
|
||||||
[Inject] public Tables _tables { get; set; }
|
[Inject] public Tables _tables { get; set; }
|
||||||
|
|
||||||
[Inject] public EventPartnerService EventPartnerService { get; set; }
|
[Inject] public EventPartnerService EventPartnerService { get; set; }
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ namespace game
|
|||||||
{
|
{
|
||||||
public class EventPinballDataManager
|
public class EventPinballDataManager
|
||||||
{
|
{
|
||||||
[asap.core.Inject] public game.PlayerDataStorage.IPlayerSaveStore playerSaveStore { get; set; }
|
[asap.core.Inject] public Core.KV.IPlayerSaveStore playerSaveStore { get; set; }
|
||||||
|
|
||||||
#if UNITY_EDITOR
|
#if UNITY_EDITOR
|
||||||
private bool m_IsCheckCondition = true;
|
private bool m_IsCheckCondition = true;
|
||||||
@@ -450,38 +450,11 @@ namespace game
|
|||||||
|
|
||||||
#region 数据相关
|
#region 数据相关
|
||||||
private EventPinballData m_PinballData;
|
private EventPinballData m_PinballData;
|
||||||
internal bool IsInactiveForKVValidation => m_Tables==null && m_TbEventPinballMain==null && m_TbEventPinballPoint==null &&
|
|
||||||
m_TbEventPinballReward==null && m_UIPanel==null && m_InfoPanel==null && m_RewardList.Count==0 &&
|
|
||||||
m_PointWeightList.Count==0 && !m_IsSimulatorMode && !m_IsMultipleMax && m_CumulativeResponseNumber==0 &&
|
|
||||||
m_AccumulatedTriggerCount==0 && m_TemporaryScore==0 && m_TemporaryConsumedToken==0 && m_MagnificationListIndex==0 && m_BonusResponseNumber==10;
|
|
||||||
|
|
||||||
internal static game.PlayerDataStorage.PlayerKVPinballState DecodeForKVLoad(IReadOnlyDictionary<string,string> raw,
|
|
||||||
game.PlayerDataStorage.PlayerKVSchemaSnapshot snapshot,game.PlayerDataStorage.PlayerKVSchemaDiagnostics errors) =>
|
|
||||||
game.PlayerDataStorage.PlayerKVSaveSchema.DecodePinball(raw,snapshot,errors);
|
|
||||||
internal void ResetForKVLoad()
|
|
||||||
{
|
|
||||||
m_PinballData=null; m_BonusResponseNumber=10; m_CumulativeResponseNumber=m_AccumulatedTriggerCount=m_TemporaryScore=m_TemporaryConsumedToken=m_MagnificationListIndex=0;
|
|
||||||
m_IsMultipleMax=false; m_IsSimulatorMode=false; m_RewardList=new List<float>(); m_PointWeightList=new List<float>(); m_UIPanel=m_InfoPanel=null;
|
|
||||||
m_Tables=null; m_TbEventPinballMain=null; m_TbEventPinballPoint=null; m_TbEventPinballReward=null;
|
|
||||||
}
|
|
||||||
internal void ReplaceFromKVDecoded(game.PlayerDataStorage.PlayerKVPinballState state)
|
|
||||||
{
|
|
||||||
if(state==null) throw new ArgumentNullException(nameof(state));
|
|
||||||
m_PinballData=!state.IsPresent ? null : new EventPinballData
|
|
||||||
{
|
|
||||||
EventID=state.EventID,
|
|
||||||
PinballMainID=state.PinballMainID,
|
|
||||||
SpinPoint=state.SpinPoint,
|
|
||||||
Scores=state.Scores,
|
|
||||||
Multiple=state.Multiple,
|
|
||||||
Token=state.Token,
|
|
||||||
ConsumedToken=state.ConsumedToken,
|
|
||||||
RewardIndex=state.RewardIndex,
|
|
||||||
Force=state.Force,
|
|
||||||
LuckyValue=state.LuckyValue,
|
|
||||||
InflationRate=state.InflationRate
|
|
||||||
};
|
|
||||||
}
|
|
||||||
public EventPinballData PinballData { get { return m_PinballData; } }
|
public EventPinballData PinballData { get { return m_PinballData; } }
|
||||||
public int RewardMaxIndex
|
public int RewardMaxIndex
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ namespace game
|
|||||||
{
|
{
|
||||||
public class EventPotionDataManager
|
public class EventPotionDataManager
|
||||||
{
|
{
|
||||||
[asap.core.Inject] public game.PlayerDataStorage.IPlayerSaveStore playerSaveStore { get; set; }
|
[asap.core.Inject] public Core.KV.IPlayerSaveStore playerSaveStore { get; set; }
|
||||||
#if UNITY_EDITOR
|
#if UNITY_EDITOR
|
||||||
private bool m_IsCheckCondition = true;
|
private bool m_IsCheckCondition = true;
|
||||||
private bool m_IsSyncData = true;
|
private bool m_IsSyncData = true;
|
||||||
@@ -127,54 +127,11 @@ namespace game
|
|||||||
#region 数据相关
|
#region 数据相关
|
||||||
private EventPotionData m_PotionData;
|
private EventPotionData m_PotionData;
|
||||||
|
|
||||||
internal static PlayerDataStorage.PlayerKVPotionState DecodeForKVLoad(IReadOnlyDictionary<string,string> raw,
|
|
||||||
PlayerDataStorage.PlayerKVSchemaDiagnostics errors) => PlayerDataStorage.PlayerKVSaveSchema.DecodePotion(raw,errors);
|
|
||||||
internal void ResetForKVLoad()
|
|
||||||
{
|
|
||||||
m_PotionData=null; m_PendingFirstPlayedEventId=0; m_SpriteAtlas=null; m_Tables=null;
|
|
||||||
m_TbEventPotionMain=null; m_TbEventPotionStage=null; m_TbEventPotionBlock=null; m_TbEventPotionBottle=null;
|
|
||||||
m_PotionResDict=new Dictionary<int,EventPotionResource>(); m_WallsBefore=new List<PotionItemData>(); m_WallsAfter=new List<PotionItemData>();
|
|
||||||
m_UIPanel=m_InfoPanel=m_RewardPopupPanel=null; m_ScrollerDataList=new List<PotionItemData>();
|
|
||||||
}
|
|
||||||
static PotionItemData MaterializeKVPotionItem(PlayerDataStorage.PlayerKVPotionItem value) => new PotionItemData {
|
|
||||||
name=value.Name,index=value.Index,isOver=value.IsOver,DataType=(EventPotionDataType)value.DataType,
|
|
||||||
ColorTypeList=new List<int>(value.Colors),ShelfTypeList=value.Shelves.Select(n=>(EventPotionShelfType)n).ToList(),
|
|
||||||
GridStateList=value.Grids.Select(n=>(EventPotionGridState)n).ToList(),FromDisplayAreaIndexList=new List<int>(value.FromDisplay)
|
|
||||||
};
|
|
||||||
internal void ReplaceFromKVDecoded(PlayerDataStorage.PlayerKVPotionState state)
|
|
||||||
{
|
|
||||||
if(state==null) throw new ArgumentNullException(nameof(state));
|
|
||||||
m_PotionData=!state.IsPresent ? null : new EventPotionData {
|
|
||||||
EventID=state.EventID,
|
|
||||||
PotionID=state.PotionID,
|
|
||||||
StageID=state.StageID,
|
|
||||||
Stage1TotalLevel=state.Stage1TotalLevel,
|
|
||||||
Stage2TotalLevel=state.Stage2TotalLevel,
|
|
||||||
LevelID=state.LevelID,
|
|
||||||
LevelIndex=state.LevelIndex,
|
|
||||||
CorrectNum=state.CorrectNum,
|
|
||||||
SubmitNum=state.SubmitNum,
|
|
||||||
Token=state.Token,
|
|
||||||
ConsumedToken=state.ConsumedToken,
|
|
||||||
Rule=state.Rule,
|
|
||||||
PreEventID=state.PreEventID,
|
|
||||||
PrePotionID=state.PrePotionID,
|
|
||||||
PreStageID=state.PreStageID,
|
|
||||||
PreLevelID=state.PreLevelID,
|
|
||||||
Round=state.Round,
|
|
||||||
FirstPlayedEventId=state.FirstPlayedEventId,
|
|
||||||
IsOver=state.IsOver,
|
|
||||||
Name=state.Name,
|
|
||||||
InflationRate=state.InflationRate,
|
|
||||||
LevelTargetData=MaterializeKVPotionItem(state.LevelTargetData),
|
|
||||||
LevelDisplayData=MaterializeKVPotionItem(state.LevelDisplayData),
|
|
||||||
LevelProcessedList=state.LevelProcessedList.Select(MaterializeKVPotionItem).ToList()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
internal bool IsInactiveForKVValidation => m_PendingFirstPlayedEventId==0 && m_SpriteAtlas==null && m_Tables==null &&
|
|
||||||
m_TbEventPotionMain==null && m_TbEventPotionStage==null && m_TbEventPotionBlock==null && m_TbEventPotionBottle==null &&
|
|
||||||
m_PotionResDict.Count==0 && m_WallsBefore.Count==0 && m_WallsAfter.Count==0 && m_ScrollerDataList.Count==0 &&
|
|
||||||
m_UIPanel==null && m_InfoPanel==null && m_RewardPopupPanel==null;
|
|
||||||
public EventPotionData PotionData { get { return m_PotionData; } }
|
public EventPotionData PotionData { get { return m_PotionData; } }
|
||||||
|
|
||||||
public void InitLevel()
|
public void InitLevel()
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ namespace game
|
|||||||
{
|
{
|
||||||
public class EventScratchTicketDataManager
|
public class EventScratchTicketDataManager
|
||||||
{
|
{
|
||||||
[asap.core.Inject] public game.PlayerDataStorage.IPlayerSaveStore playerSaveStore { get; set; }
|
[asap.core.Inject] public Core.KV.IPlayerSaveStore playerSaveStore { get; set; }
|
||||||
#if UNITY_EDITOR
|
#if UNITY_EDITOR
|
||||||
private bool m_IsCheckCondition = true;
|
private bool m_IsCheckCondition = true;
|
||||||
private bool m_IsSyncData = true;
|
private bool m_IsSyncData = true;
|
||||||
@@ -95,45 +95,11 @@ namespace game
|
|||||||
|
|
||||||
#region 数据相关
|
#region 数据相关
|
||||||
private EventScratchTicketData m_ScratchTicketData;
|
private EventScratchTicketData m_ScratchTicketData;
|
||||||
internal bool IsInactiveForKVValidation => m_Tables==null && m_TbEventScratchMain==null && m_TbEventScratchCard==null &&
|
|
||||||
m_TbEventScratchReward==null && m_ChainPackData==null && m_NormalPackData==null && m_SpriteAtlas==null && m_SpriteDict==null &&
|
|
||||||
m_UIPanel==null && m_InfoPanel==null && m_RewardPanel==null && m_ChainPackPanel==null && m_PackPanel==null &&
|
|
||||||
m_RecordList.Count==0 && m_PointWeightList.Count==0 && m_ScratchStampList.Count==0 &&
|
|
||||||
TempRecordRewardTaskID==0 && TempRecordRewardTaskConsumedToken==0;
|
|
||||||
|
|
||||||
internal static game.PlayerDataStorage.PlayerKVScratchState DecodeForKVLoad(IReadOnlyDictionary<string,string> raw,
|
|
||||||
game.PlayerDataStorage.PlayerKVSchemaSnapshot snapshot,game.PlayerDataStorage.PlayerKVSchemaDiagnostics errors) =>
|
|
||||||
game.PlayerDataStorage.PlayerKVSaveSchema.DecodeScratch(raw,snapshot,errors);
|
|
||||||
internal void ResetForKVLoad()
|
|
||||||
{
|
|
||||||
m_ScratchTicketData=null; TempRecordRewardTaskID=TempRecordRewardTaskConsumedToken=0; m_RecordList=new List<EventScratchTicketRecordData>();
|
|
||||||
m_PointWeightList=new List<float>(); m_ScratchStampList=new List<int>(); m_ChainPackData=null; m_NormalPackData=null;
|
|
||||||
m_UIPanel=m_InfoPanel=m_RewardPanel=m_ChainPackPanel=m_PackPanel=null; m_SpriteAtlas=null; m_SpriteDict=null;
|
|
||||||
m_Tables=null; m_TbEventScratchMain=null; m_TbEventScratchCard=null; m_TbEventScratchReward=null;
|
|
||||||
}
|
|
||||||
internal void ReplaceFromKVDecoded(game.PlayerDataStorage.PlayerKVScratchState state)
|
|
||||||
{
|
|
||||||
if(state==null) throw new ArgumentNullException(nameof(state));
|
|
||||||
m_ScratchTicketData=!state.IsPresent ? null : new EventScratchTicketData
|
|
||||||
{
|
|
||||||
EventID=state.EventID,
|
|
||||||
ScratchMainID=state.ScratchMainID,
|
|
||||||
ChainPackID=state.ChainPackID,
|
|
||||||
Token=state.Token,
|
|
||||||
ConsumedToken=state.ConsumedToken,
|
|
||||||
RewardTaskConsumedToken=state.RewardTaskConsumedToken,
|
|
||||||
EventScratchRewardTaskID=state.EventScratchRewardTaskID,
|
|
||||||
EventScratchCardID=state.EventScratchCardID,
|
|
||||||
TotalConsumedToken=state.TotalConsumedToken,
|
|
||||||
ChainPackProgress=state.ChainPackProgress,
|
|
||||||
magIndex=state.magIndex,
|
|
||||||
IsOver=state.IsOver,
|
|
||||||
LuckyValue=state.LuckyValue,
|
|
||||||
InflationRate=state.InflationRate,
|
|
||||||
StampRewardList=new List<int>(state.StampRewardList),
|
|
||||||
StampRewardDict=state.StampRewardDict.ToDictionary(p=>p.Key,p=>new EventScratchTicketOwnedStampRewardData { EventScratchCardID=p.Value.EventScratchCardID,Count=p.Value.Count })
|
|
||||||
};
|
|
||||||
}
|
|
||||||
public EventScratchTicketData ScratchTicketData { get { return m_ScratchTicketData; } }
|
public EventScratchTicketData ScratchTicketData { get { return m_ScratchTicketData; } }
|
||||||
public EventScratchMain GetFreshScratchTicketData()
|
public EventScratchMain GetFreshScratchTicketData()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -13,19 +13,9 @@ 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();
|
||||||
|
|
||||||
internal static game.PlayerDataStorage.PlayerKVIntMapState DecodeForKVLoad(IReadOnlyDictionary<string,string> raw,
|
|
||||||
IReadOnlyDictionary<string,string> modern, game.PlayerDataStorage.PlayerKVSchemaDiagnostics errors) =>
|
|
||||||
game.PlayerDataStorage.PlayerKVSaveSchema.DecodeFishingBox(raw,modern,errors);
|
|
||||||
internal void ResetForKVLoad()
|
|
||||||
{
|
|
||||||
FishingNewBoxs = new Dictionary<int,int>();
|
|
||||||
FishingBoxOpenNum = new Dictionary<int,int>();
|
|
||||||
}
|
|
||||||
internal void ReplaceFromKVDecoded(game.PlayerDataStorage.PlayerKVIntMapState state)
|
|
||||||
{
|
|
||||||
if (state == null) throw new ArgumentNullException(nameof(state));
|
|
||||||
FishingBoxOpenNum = state.Values.ToDictionary(p=>p.Key,p=>p.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void InitData(string data)
|
public void InitData(string data)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using asap.core;
|
using asap.core;
|
||||||
using cfg;
|
using cfg;
|
||||||
using game;
|
using game;
|
||||||
using GameCore;
|
using GameCore;
|
||||||
@@ -40,7 +40,7 @@ public class FishingChallengeData
|
|||||||
|
|
||||||
public class FishingChallengeManager
|
public class FishingChallengeManager
|
||||||
{
|
{
|
||||||
[asap.core.Inject] public game.PlayerDataStorage.IPlayerSaveStore SaveStore { get; set; }
|
[asap.core.Inject] public Core.KV.IPlayerSaveStore SaveStore { get; set; }
|
||||||
|
|
||||||
//
|
//
|
||||||
private const string SaveKey = "FishingChallengeData";
|
private const string SaveKey = "FishingChallengeData";
|
||||||
@@ -63,44 +63,11 @@ public class FishingChallengeManager
|
|||||||
private DateTime EndTime;
|
private DateTime EndTime;
|
||||||
//
|
//
|
||||||
public FishingChallengeData fishingChallengeData { get; private set; }
|
public FishingChallengeData fishingChallengeData { get; private set; }
|
||||||
internal bool IsInactiveForKVValidation => !IsInit && CurFishingEventData==null && eventChallengeMain==null &&
|
|
||||||
eventChallengeConfig==null && challengeMapData==null && _stageDropList.Count==0 && _eliminateListScore.Count==0;
|
|
||||||
|
|
||||||
internal static game.PlayerDataStorage.PlayerKVChallengeState DecodeForKVLoad(IReadOnlyDictionary<string,string> raw,
|
|
||||||
game.PlayerDataStorage.PlayerKVSchemaSnapshot snapshot,game.PlayerDataStorage.PlayerKVSchemaDiagnostics errors) =>
|
|
||||||
game.PlayerDataStorage.PlayerKVSaveSchema.DecodeChallenge(raw,snapshot,errors);
|
|
||||||
internal void ResetForKVLoad()
|
|
||||||
{
|
|
||||||
fishingChallengeData=null; IsInit=false; PrefabName=null;
|
|
||||||
#if UNITY_EDITOR
|
|
||||||
_cheatingScore=0;
|
|
||||||
#endif
|
|
||||||
TopRankTier=Rank=FullStep=PlayerFullNumber=PreScore=0;
|
|
||||||
CurFishingEventData=null; StartTime=EndTime=default; eventChallengeMain=null; eventChallengeConfig=null; challengeMapData=null; itemDataList=null;
|
|
||||||
_stageDropList.Clear(); _eliminateListScore.Clear(); _eventTrackStageList=new List<int>(); _eventTrackNormalRewardList=new List<int>(); InVisibleReason=string.Empty;
|
|
||||||
}
|
|
||||||
internal void ReplaceFromKVDecoded(game.PlayerDataStorage.PlayerKVChallengeState state)
|
|
||||||
{
|
|
||||||
if(state==null) throw new ArgumentNullException(nameof(state));
|
|
||||||
fishingChallengeData=!state.IsPresent || state.IsExplicitNull ? null : new FishingChallengeData
|
|
||||||
{
|
|
||||||
FirstOpen=state.FirstOpen,
|
|
||||||
EventId=state.EventId,
|
|
||||||
RankTierLevel=state.RankTierLevel,
|
|
||||||
Finished=state.Finished,
|
|
||||||
RedirectID=state.RedirectID,
|
|
||||||
Score=state.Score,
|
|
||||||
ProcessedScore=state.ProcessedScore,
|
|
||||||
step=state.step,
|
|
||||||
uiStep=state.uiStep,
|
|
||||||
index=state.index,
|
|
||||||
StartTime=state.StartTime,
|
|
||||||
EndTime=state.EndTime,
|
|
||||||
eliminateNumber=new List<int>(state.eliminateNumber),
|
|
||||||
robotAccountList=new List<int>(state.robotAccountList),
|
|
||||||
InflationRate=state.InflationRate
|
|
||||||
};
|
|
||||||
}
|
|
||||||
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; }
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ namespace DataCenter
|
|||||||
|
|
||||||
public class OfferChainsChestManager
|
public class OfferChainsChestManager
|
||||||
{
|
{
|
||||||
[asap.core.Inject] public game.PlayerDataStorage.IPlayerSaveStore playerSaveStore { get; set; }
|
[asap.core.Inject] public Core.KV.IPlayerSaveStore playerSaveStore { get; set; }
|
||||||
|
|
||||||
public const string SaveKey = "OfferChainsChestData";
|
public const string SaveKey = "OfferChainsChestData";
|
||||||
[Inject] public Tables _tables { get; set; }
|
[Inject] public Tables _tables { get; set; }
|
||||||
@@ -53,27 +53,10 @@ namespace DataCenter
|
|||||||
// 当前可保存数据
|
// 当前可保存数据
|
||||||
public TOfferChainsData OfferChainsData { get; private set; }
|
public TOfferChainsData OfferChainsData { get; private set; }
|
||||||
|
|
||||||
internal static game.PlayerDataStorage.PlayerKVOfferState DecodeForKVLoad(IReadOnlyDictionary<string,string> raw,
|
|
||||||
game.PlayerDataStorage.PlayerKVSchemaSnapshot snapshot,game.PlayerDataStorage.PlayerKVSchemaDiagnostics errors) =>
|
|
||||||
game.PlayerDataStorage.PlayerKVSaveSchema.DecodeOffer(raw,false,snapshot,errors);
|
|
||||||
internal void ResetForKVLoad()
|
|
||||||
{
|
|
||||||
OfferChainsData=null; ValidIds=new List<int>(); _fishingEvent=null; _isInit=false;
|
|
||||||
OfferChainsChestMain=null; OfferChainsChestRewards=null;
|
|
||||||
}
|
|
||||||
internal void ReplaceFromKVDecoded(game.PlayerDataStorage.PlayerKVOfferState state)
|
|
||||||
{
|
|
||||||
if (state == null) throw new ArgumentNullException(nameof(state));
|
|
||||||
OfferChainsData=!state.IsPresent ? null : new TOfferChainsData
|
|
||||||
{
|
|
||||||
FirstOpen=state.FirstOpen,EventId=state.EventId,StartTime=state.StartTime,EndTime=state.EndTime,
|
|
||||||
ItemId=state.ItemId,LotteryNum=state.LotteryNum,
|
|
||||||
Items=state.Items.Select(item=>new TOfferChainsData.TItem {
|
|
||||||
Id=item.Id,Item=item.Item,Number=item.Number,Status=item.Status }).ToList(),
|
|
||||||
RewardList=new List<int>(state.RewardList),Finished=state.Finished,ChestReward=state.ChestReward
|
|
||||||
};
|
|
||||||
}
|
|
||||||
internal bool IsInactiveForKVValidation => !_isInit && _fishingEvent == null;
|
|
||||||
public List<int> ValidIds;
|
public List<int> ValidIds;
|
||||||
|
|
||||||
public bool IsFirstOpen
|
public bool IsFirstOpen
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ namespace DataCenter
|
|||||||
// Disco Manager
|
// Disco Manager
|
||||||
public class OfferDiscoPartyManager
|
public class OfferDiscoPartyManager
|
||||||
{
|
{
|
||||||
[asap.core.Inject] public game.PlayerDataStorage.IPlayerSaveStore playerSaveStore { get; set; }
|
[asap.core.Inject] public Core.KV.IPlayerSaveStore playerSaveStore { get; set; }
|
||||||
public const string SaveKey = "OfferDiscoPartyManager";
|
public const string SaveKey = "OfferDiscoPartyManager";
|
||||||
|
|
||||||
// 免费抽奖次数
|
// 免费抽奖次数
|
||||||
@@ -62,27 +62,10 @@ namespace DataCenter
|
|||||||
|
|
||||||
public TOfferDiscoPartyData OfferDiscoPartyData { get; private set; }
|
public TOfferDiscoPartyData OfferDiscoPartyData { get; private set; }
|
||||||
|
|
||||||
internal static game.PlayerDataStorage.PlayerKVOfferState DecodeForKVLoad(IReadOnlyDictionary<string,string> raw,
|
|
||||||
game.PlayerDataStorage.PlayerKVSchemaSnapshot snapshot,game.PlayerDataStorage.PlayerKVSchemaDiagnostics errors) =>
|
|
||||||
game.PlayerDataStorage.PlayerKVSaveSchema.DecodeOffer(raw,true,snapshot,errors);
|
|
||||||
internal void ResetForKVLoad()
|
|
||||||
{
|
|
||||||
OfferDiscoPartyData=null; ValidIds=new List<int>(); _fishingEvent=null; _isInit=false;
|
|
||||||
OfferDiscoParty=null; OfferDiscoPartyRewards=null; FreeTime=1;
|
|
||||||
}
|
|
||||||
internal void ReplaceFromKVDecoded(game.PlayerDataStorage.PlayerKVOfferState state)
|
|
||||||
{
|
|
||||||
if (state == null) throw new ArgumentNullException(nameof(state));
|
|
||||||
OfferDiscoPartyData=!state.IsPresent ? null : new TOfferDiscoPartyData
|
|
||||||
{
|
|
||||||
FirstOpen=state.FirstOpen,EventId=state.EventId,StartTime=state.StartTime,EndTime=state.EndTime,
|
|
||||||
ItemId=state.ItemId,LotteryNum=state.LotteryNum,
|
|
||||||
Items=state.Items.Select(item=>new TOfferDiscoPartyData.TItem {
|
|
||||||
Id=item.Id,Item=item.Item,Number=item.Number,Status=(TItemStatues) item.Status }).ToList(),
|
|
||||||
RewardList=new List<int>(state.RewardList)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
internal bool IsInactiveForKVValidation => !_isInit && _fishingEvent == null;
|
|
||||||
|
|
||||||
|
|
||||||
public List<int> ValidIds; //
|
public List<int> ValidIds; //
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ using game;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
public class PiggyBankPackData
|
public class PiggyBankPackData
|
||||||
{
|
{
|
||||||
[asap.core.Inject] public game.PlayerDataStorage.IPlayerSaveStore playerSaveStore { get; set; }
|
[asap.core.Inject] public Core.KV.IPlayerSaveStore playerSaveStore { get; set; }
|
||||||
private readonly Tables _tables = GContext.container.Resolve<Tables>();
|
private readonly Tables _tables = GContext.container.Resolve<Tables>();
|
||||||
private readonly TbEventPackManager _epm
|
private readonly TbEventPackManager _epm
|
||||||
= GContext.container.Resolve<Tables>().TbEventPackManager;
|
= GContext.container.Resolve<Tables>().TbEventPackManager;
|
||||||
@@ -36,38 +36,13 @@ public class PiggyBankPackData
|
|||||||
}
|
}
|
||||||
private Data _data;
|
private Data _data;
|
||||||
|
|
||||||
private bool _kvLoadPresent;
|
|
||||||
internal bool KVLoadPresent => _kvLoadPresent;
|
|
||||||
internal static game.PlayerDataStorage.PlayerKVPiggyBankState DecodeForKVLoad(
|
|
||||||
IReadOnlyDictionary<string, string> raw, game.PlayerDataStorage.PlayerKVSchemaSnapshot snapshot,
|
|
||||||
game.PlayerDataStorage.PlayerKVSchemaDiagnostics errors) =>
|
|
||||||
game.PlayerDataStorage.PlayerKVSaveSchema.DecodePiggyBank(raw, snapshot, errors);
|
|
||||||
|
|
||||||
internal void ResetForKVLoad()
|
|
||||||
{
|
|
||||||
_kvLoadPresent = false;
|
|
||||||
_data = default(Data);
|
|
||||||
}
|
|
||||||
internal void ReplaceFromKVDecoded(game.PlayerDataStorage.PlayerKVPiggyBankState state)
|
|
||||||
{
|
|
||||||
if (state == null) throw new ArgumentNullException(nameof(state));
|
|
||||||
if (!state.IsPresent) { ResetForKVLoad(); return; }
|
|
||||||
var replacement = new Data
|
|
||||||
{
|
|
||||||
currentEventID = state.currentEventID,
|
|
||||||
purchaseCount = state.purchaseCount,
|
|
||||||
VIPLvlWhenEventActivate = state.VIPLvlWhenEventActivate,
|
|
||||||
savingProgress = state.savingProgress,
|
|
||||||
redirectID = state.redirectID,
|
|
||||||
doNeedTrigger = state.doNeedTrigger,
|
|
||||||
isTriggered = state.isTriggered,
|
|
||||||
inflation = state.inflation,
|
|
||||||
};
|
|
||||||
_data = replacement;
|
|
||||||
_kvLoadPresent = true;
|
|
||||||
}
|
|
||||||
internal game.PlayerDataStorage.PlayerKVPiggyBankState SnapshotForKVValidation() =>
|
|
||||||
new game.PlayerDataStorage.PlayerKVPiggyBankState(_kvLoadPresent, _data.currentEventID, _data.purchaseCount, _data.VIPLvlWhenEventActivate, _data.savingProgress, _data.redirectID, _data.doNeedTrigger, _data.isTriggered, _data.inflation);
|
|
||||||
public bool IsWithinEventTime
|
public bool IsWithinEventTime
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ namespace GameCore
|
|||||||
}
|
}
|
||||||
public class TotalRechargeManager
|
public class TotalRechargeManager
|
||||||
{
|
{
|
||||||
readonly game.PlayerDataStorage.IPlayerSaveStore saveStore = GContext.container.Resolve<game.PlayerDataStorage.IPlayerSaveStore>();
|
readonly Core.KV.IPlayerSaveStore saveStore = GContext.container.Resolve<Core.KV.IPlayerSaveStore>();
|
||||||
|
|
||||||
public const string TotalRechargeDataKey = "EventTotalRecharge";
|
public const string TotalRechargeDataKey = "EventTotalRecharge";
|
||||||
public TotalRechargeData GetEventData()
|
public TotalRechargeData GetEventData()
|
||||||
|
|||||||
@@ -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()
|
||||||
{
|
{
|
||||||
GContext.container.Resolve<game.PlayerDataStorage.IPlayerSaveStore>().Write("LastPaymentTime", "").RequireAccepted();
|
GContext.container.Resolve<Core.KV.IPlayerSaveStore>().Write("LastPaymentTime", "").RequireAccepted();
|
||||||
}
|
}
|
||||||
static void HookWheelForecActive()
|
static void HookWheelForecActive()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -607,7 +607,8 @@ public class EventFishingDuelPanel : BasePanel
|
|||||||
}
|
}
|
||||||
void CreateMatchmakingTicket()
|
void CreateMatchmakingTicket()
|
||||||
{
|
{
|
||||||
if (GContext.container.Resolve<IUserService>() is not PlayFabUserService)
|
if (GContext.container.Resolve<IUserService>() is not PlayFabUserService &&
|
||||||
|
GContext.container.Resolve<IUserService>() is not KvUserService)
|
||||||
{
|
{
|
||||||
Debug.LogWarning("KV matchmaking service is not connected.");
|
Debug.LogWarning("KV matchmaking service is not connected.");
|
||||||
FDM.MachingEnd(null);
|
FDM.MachingEnd(null);
|
||||||
@@ -631,46 +632,58 @@ 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
|
Tier = matchTierListStr.ToArray(),
|
||||||
{
|
Map = mapIDsStr.ToArray(),
|
||||||
Entity = new EntityKey
|
Group = Group.ToArray(),
|
||||||
{
|
Details = details,
|
||||||
Id = ((PlayFabUserService)userService).entityKey.Id,
|
|
||||||
Type = ((PlayFabUserService)userService).entityKey.Type
|
|
||||||
},
|
|
||||||
Attributes = new MatchmakingPlayerAttributes
|
|
||||||
{
|
|
||||||
DataObject = new
|
|
||||||
{
|
|
||||||
Tier = matchTierListStr.ToArray(),
|
|
||||||
Map = mapIDsStr.ToArray(),
|
|
||||||
Group = Group.ToArray(),
|
|
||||||
Details = details,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
GiveUpAfterSeconds = MaxMatchTime,
|
|
||||||
QueueName = FishingDuelManager.QueueName // 需要根据实际的匹配队列名称修改
|
|
||||||
};
|
};
|
||||||
|
Action<CreateMatchmakingTicketResult> success = (result) =>
|
||||||
PlayFabMultiplayerAPI.CreateMatchmakingTicket(request,
|
{
|
||||||
(result) =>
|
currentTicketId = result.TicketId;
|
||||||
|
Debug.Log("匹配请求已创建,等待匹配...");
|
||||||
|
Debug.Log("匹配Ticket ID: " + result.TicketId);
|
||||||
|
//// 开始轮询匹配状态
|
||||||
|
StartCoroutine(CheckMatchmakingStatus());
|
||||||
|
//显示取消按钮
|
||||||
|
but_stop.gameObject.SetActive(true);
|
||||||
|
};
|
||||||
|
Action<string> failure = (error) =>
|
||||||
|
{
|
||||||
|
Debug.LogError("创建匹配请求失败: " + error);
|
||||||
|
FDM.MachingEnd(null);
|
||||||
|
};
|
||||||
|
if (userService is KvUserService)
|
||||||
|
{
|
||||||
|
RequestKVMatch("CreateMatchmakingTicket", new
|
||||||
{
|
{
|
||||||
currentTicketId = result.TicketId;
|
queueName = FishingDuelManager.QueueName,
|
||||||
Debug.Log("匹配请求已创建,等待匹配...");
|
giveUpAfterSeconds = MaxMatchTime,
|
||||||
Debug.Log("匹配Ticket ID: " + result.TicketId);
|
attributes
|
||||||
//// 开始轮询匹配状态
|
}, response => response.ToObject<CreateMatchmakingTicketResult>(), success, failure);
|
||||||
StartCoroutine(CheckMatchmakingStatus());
|
}
|
||||||
//显示取消按钮
|
else
|
||||||
but_stop.gameObject.SetActive(true);
|
{
|
||||||
},
|
var request = new CreateMatchmakingTicketRequest
|
||||||
(error) =>
|
|
||||||
{
|
{
|
||||||
Debug.LogError("创建匹配请求失败: " + error.GenerateErrorReport());
|
Creator = new MatchmakingPlayer
|
||||||
FDM.MachingEnd(null);
|
{
|
||||||
});
|
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()
|
||||||
@@ -680,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
|
||||||
{
|
{
|
||||||
@@ -689,41 +704,53 @@ public class EventFishingDuelPanel : BasePanel
|
|||||||
EscapeObject = false
|
EscapeObject = false
|
||||||
};
|
};
|
||||||
|
|
||||||
PlayFabMultiplayerAPI.GetMatchmakingTicket(request,
|
Action<GetMatchmakingTicketResult> success = (result) =>
|
||||||
(result) =>
|
{
|
||||||
|
string statusInfo = "匹配状态: " + result.Status;
|
||||||
|
if (result.MatchId != null)
|
||||||
{
|
{
|
||||||
string statusInfo = "匹配状态: " + result.Status;
|
statusInfo += ", Match ID: " + result.MatchId;
|
||||||
if (result.MatchId != null)
|
}
|
||||||
{
|
Debug.Log(statusInfo.ToString());
|
||||||
statusInfo += ", Match ID: " + result.MatchId;
|
|
||||||
}
|
|
||||||
Debug.Log(statusInfo.ToString());
|
|
||||||
|
|
||||||
if (result.Status == "Matched")
|
if (result.Status == "Matched")
|
||||||
{
|
|
||||||
but_stop.gameObject.SetActive(false);
|
|
||||||
Debug.Log("匹配成功,获取服务器信息: ");
|
|
||||||
GetMatchDetails(result.MatchId);
|
|
||||||
}
|
|
||||||
else if (result.Status == "Canceled")
|
|
||||||
{
|
|
||||||
Debug.Log("匹配已取消: Canceled " + result.CancellationReasonString);
|
|
||||||
//if (result.CancellationReasonString != "Requested")
|
|
||||||
//{
|
|
||||||
MachingEnd();
|
|
||||||
//}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
(error) =>
|
|
||||||
{
|
{
|
||||||
Debug.LogError("检查匹配状态失败: " + error.GenerateErrorReport());
|
but_stop.gameObject.SetActive(false);
|
||||||
|
Debug.Log("匹配成功,获取服务器信息: ");
|
||||||
|
GetMatchDetails(result.MatchId);
|
||||||
|
}
|
||||||
|
else if (result.Status == "Canceled")
|
||||||
|
{
|
||||||
|
Debug.Log("匹配已取消: Canceled " + result.CancellationReasonString);
|
||||||
|
//if (result.CancellationReasonString != "Requested")
|
||||||
|
//{
|
||||||
MachingEnd();
|
MachingEnd();
|
||||||
});
|
//}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Action<string> failure = (error) =>
|
||||||
|
{
|
||||||
|
Debug.LogError("检查匹配状态失败: " + error);
|
||||||
|
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)
|
if (GContext.container.Resolve<IUserService>() is not PlayFabUserService &&
|
||||||
|
GContext.container.Resolve<IUserService>() is not KvUserService)
|
||||||
{
|
{
|
||||||
Debug.LogWarning("KV matchmaking service is not connected.");
|
Debug.LogWarning("KV matchmaking service is not connected.");
|
||||||
FDM.MachingEnd(null);
|
FDM.MachingEnd(null);
|
||||||
@@ -737,52 +764,63 @@ public class EventFishingDuelPanel : BasePanel
|
|||||||
ReturnMemberAttributes = true
|
ReturnMemberAttributes = true
|
||||||
};
|
};
|
||||||
|
|
||||||
PlayFabMultiplayerAPI.GetMatch(request,
|
Action<GetMatchResult> success = (result) =>
|
||||||
(result) =>
|
{
|
||||||
|
Debug.Log("匹配成功: " + result.MatchId);
|
||||||
|
if (string.IsNullOrEmpty(currentTicketId))
|
||||||
{
|
{
|
||||||
Debug.Log("匹配成功: " + result.MatchId);
|
return;
|
||||||
if (string.IsNullOrEmpty(currentTicketId))
|
}
|
||||||
|
// 添加玩家信息
|
||||||
|
if (result.Members != null && result.Members.Count > 0)
|
||||||
|
{
|
||||||
|
//AddMatchmakingLog("玩家数量: " + result.Members.Count);
|
||||||
|
IUserService userService = GContext.container.Resolve<IUserService>();
|
||||||
|
MatchDetails matchDetails = null;
|
||||||
|
int[] mapArray = null;
|
||||||
|
try
|
||||||
{
|
{
|
||||||
return;
|
for (int i = 0; i < result.Members.Count; i++)
|
||||||
}
|
|
||||||
// 添加玩家信息
|
|
||||||
if (result.Members != null && result.Members.Count > 0)
|
|
||||||
{
|
|
||||||
//AddMatchmakingLog("玩家数量: " + result.Members.Count);
|
|
||||||
IUserService userService = GContext.container.Resolve<IUserService>();
|
|
||||||
MatchDetails matchDetails = null;
|
|
||||||
int[] mapArray = null;
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
for (int i = 0; i < result.Members.Count; i++)
|
MatchmakingPlayerWithTeamAssignment member = result.Members[i];
|
||||||
|
if (member.Entity.Id != (userService is KvUserService ? userService.UserId : ((PlayFabUserService)userService).entityKey.Id))
|
||||||
{
|
{
|
||||||
MatchmakingPlayerWithTeamAssignment member = result.Members[i];
|
JObject dataObject = JObject.FromObject(result.Members[i].Attributes.DataObject);
|
||||||
if (member.Entity.Id != ((PlayFabUserService)userService).entityKey.Id)
|
string[] tierArray = dataObject["Tier"].ToObject<string[]>();
|
||||||
{
|
mapArray = dataObject["Map"].ToObject<int[]>();
|
||||||
JObject dataObject = JObject.FromObject(result.Members[i].Attributes.DataObject);
|
string matchDetailsStr = dataObject["Details"].ToString();
|
||||||
string[] tierArray = dataObject["Tier"].ToObject<string[]>();
|
matchDetails = Newtonsoft.Json.JsonConvert.DeserializeObject<MatchDetails>(matchDetailsStr);
|
||||||
mapArray = dataObject["Map"].ToObject<int[]>();
|
break;
|
||||||
string matchDetailsStr = dataObject["Details"].ToString();
|
|
||||||
matchDetails = Newtonsoft.Json.JsonConvert.DeserializeObject<MatchDetails>(matchDetailsStr);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch
|
|
||||||
{
|
|
||||||
matchDetails = null;
|
|
||||||
mapArray = null;
|
|
||||||
}
|
|
||||||
currentTicketId = null;
|
|
||||||
FDM.MachingEnd(result.MatchId, matchDetails, mapArray);
|
|
||||||
StartCoroutine(CheckReady());
|
|
||||||
}
|
}
|
||||||
},
|
catch
|
||||||
(error) =>
|
{
|
||||||
|
matchDetails = null;
|
||||||
|
mapArray = null;
|
||||||
|
}
|
||||||
|
currentTicketId = null;
|
||||||
|
FDM.MachingEnd(result.MatchId, matchDetails, mapArray);
|
||||||
|
StartCoroutine(CheckReady());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Action<string> failure = (error) =>
|
||||||
|
{
|
||||||
|
Debug.LogError("获取匹配详情失败: " + error);
|
||||||
|
MachingEnd();
|
||||||
|
};
|
||||||
|
if (GContext.container.Resolve<IUserService>() is KvUserService)
|
||||||
|
{
|
||||||
|
RequestKVMatch("GetMatch", new
|
||||||
{
|
{
|
||||||
Debug.LogError("获取匹配详情失败: " + error.GenerateErrorReport());
|
queueName = request.QueueName,
|
||||||
MachingEnd();
|
matchId = request.MatchId
|
||||||
});
|
}, ReadKVMatch, success, failure);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
PlayFabMultiplayerAPI.GetMatch(request, success, error => failure(error.GenerateErrorReport()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
IEnumerator CheckReady()
|
IEnumerator CheckReady()
|
||||||
{
|
{
|
||||||
@@ -806,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
|
||||||
{
|
{
|
||||||
@@ -814,15 +854,98 @@ public class EventFishingDuelPanel : BasePanel
|
|||||||
QueueName = FishingDuelManager.QueueName // 需要根据实际的匹配队列名称修改
|
QueueName = FishingDuelManager.QueueName // 需要根据实际的匹配队列名称修改
|
||||||
};
|
};
|
||||||
currentTicketId = null;
|
currentTicketId = null;
|
||||||
PlayFabMultiplayerAPI.CancelMatchmakingTicket(request,
|
Action<CancelMatchmakingTicketResult> success = (result) =>
|
||||||
(result) =>
|
{
|
||||||
|
Debug.Log("匹配已取消: ");
|
||||||
|
};
|
||||||
|
Action<string> failure = (error) =>
|
||||||
|
{
|
||||||
|
Debug.LogError("取消匹配失败: " + error);
|
||||||
|
};
|
||||||
|
if (GContext.container.Resolve<IUserService>() is KvUserService)
|
||||||
|
{
|
||||||
|
RequestKVMatch("CancelMatchmakingTicket", new
|
||||||
{
|
{
|
||||||
Debug.Log("匹配已取消: ");
|
queueName = request.QueueName,
|
||||||
},
|
ticketId = request.TicketId
|
||||||
(error) =>
|
}, 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))
|
||||||
{
|
{
|
||||||
Debug.LogError("取消匹配失败: " + error.GenerateErrorReport());
|
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
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ namespace game
|
|||||||
GContext.OnEvent<TargetEvent>().Subscribe(OnEvent_TargetEvent).AddTo(Disposables);
|
GContext.OnEvent<TargetEvent>().Subscribe(OnEvent_TargetEvent).AddTo(Disposables);
|
||||||
}
|
}
|
||||||
private Tables _tables;
|
private Tables _tables;
|
||||||
[Inject] public PlayerDataStorage.IPlayerSaveStore SaveStore { get; set; }
|
[Inject] public Core.KV.IPlayerSaveStore SaveStore { get; set; }
|
||||||
bool disposed;
|
bool disposed;
|
||||||
|
|
||||||
[Inject]
|
[Inject]
|
||||||
|
|||||||
@@ -410,7 +410,7 @@ public class EventBingoStepState
|
|||||||
|
|
||||||
public class EventBingoPlayfabData
|
public class EventBingoPlayfabData
|
||||||
{
|
{
|
||||||
readonly game.PlayerDataStorage.IPlayerSaveStore saveStore = GContext.container.Resolve<game.PlayerDataStorage.IPlayerSaveStore>();
|
readonly Core.KV.IPlayerSaveStore saveStore = GContext.container.Resolve<Core.KV.IPlayerSaveStore>();
|
||||||
|
|
||||||
public int EventId, TicketCount, RoundCount, StepInRound, Seed, ChainProgress, ChainTaskTokenProgress;
|
public int EventId, TicketCount, RoundCount, StepInRound, Seed, ChainProgress, ChainTaskTokenProgress;
|
||||||
public float InflationRate;
|
public float InflationRate;
|
||||||
@@ -480,7 +480,7 @@ public class EventBingoPlayfabData
|
|||||||
|
|
||||||
public static EventBingoPlayfabData Load()
|
public static EventBingoPlayfabData Load()
|
||||||
{
|
{
|
||||||
var s = GContext.container.Resolve<game.PlayerDataStorage.IPlayerSaveStore>().Read(Key).ValueOrThrow();
|
var s = GContext.container.Resolve<Core.KV.IPlayerSaveStore>().Read(Key).ValueOrThrow();
|
||||||
return Deserialize(s);
|
return Deserialize(s);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ public class DrillModel : IHasChainPack
|
|||||||
|
|
||||||
public class EventBreakPfData
|
public class EventBreakPfData
|
||||||
{
|
{
|
||||||
readonly game.PlayerDataStorage.IPlayerSaveStore saveStore = GContext.container.Resolve<game.PlayerDataStorage.IPlayerSaveStore>();
|
readonly Core.KV.IPlayerSaveStore saveStore = GContext.container.Resolve<Core.KV.IPlayerSaveStore>();
|
||||||
|
|
||||||
public int EventId { get; set; }
|
public int EventId { get; set; }
|
||||||
public int TicketCount { get; set; }
|
public int TicketCount { get; set; }
|
||||||
|
|||||||
@@ -350,7 +350,7 @@ public class EventCanProgressTaskInfo
|
|||||||
|
|
||||||
public class EventCanPlayfabData
|
public class EventCanPlayfabData
|
||||||
{
|
{
|
||||||
readonly game.PlayerDataStorage.IPlayerSaveStore saveStore = GContext.container.Resolve<game.PlayerDataStorage.IPlayerSaveStore>();
|
readonly Core.KV.IPlayerSaveStore saveStore = GContext.container.Resolve<Core.KV.IPlayerSaveStore>();
|
||||||
|
|
||||||
public const string PlayFabKey = "EventCan";
|
public const string PlayFabKey = "EventCan";
|
||||||
private const char Splitter = '|', Comma = ',';
|
private const char Splitter = '|', Comma = ',';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// EventCubeMeltManager.cs
|
// EventCubeMeltManager.cs
|
||||||
// 锻刀大赛
|
// 锻刀大赛
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
@@ -22,7 +22,7 @@ using Random = UnityEngine.Random;
|
|||||||
|
|
||||||
public class EventCubeMeltManager
|
public class EventCubeMeltManager
|
||||||
{
|
{
|
||||||
[asap.core.Inject] public game.PlayerDataStorage.IPlayerSaveStore SaveStore { get; set; }
|
[asap.core.Inject] public Core.KV.IPlayerSaveStore SaveStore { get; set; }
|
||||||
|
|
||||||
//
|
//
|
||||||
public const string SaveKey = "EventCubeMeltData";
|
public const string SaveKey = "EventCubeMeltData";
|
||||||
@@ -171,33 +171,10 @@ public class EventCubeMeltManager
|
|||||||
|
|
||||||
public EventCubeMeltData EventData { get; private set; }
|
public EventCubeMeltData EventData { get; private set; }
|
||||||
|
|
||||||
internal static game.PlayerDataStorage.PlayerKVCubeState DecodeForKVLoad(IReadOnlyDictionary<string,string> raw,
|
|
||||||
game.PlayerDataStorage.PlayerKVSchemaSnapshot snapshot,game.PlayerDataStorage.PlayerKVSchemaDiagnostics errors) =>
|
|
||||||
game.PlayerDataStorage.PlayerKVSaveSchema.DecodeCube(raw,snapshot,errors);
|
|
||||||
internal void ResetForKVLoad()
|
|
||||||
{
|
|
||||||
EventData=null; _isInit=false; _fishingEvent=null;
|
|
||||||
EventCubeMeltMainData=null; EventCubeMeltConfig=null; EventCubeMeltStageData=null; EventCubeMeltCollectionData=null;
|
|
||||||
EventCubeMeltPieces=null; EventCubeMeltPieceItems=new List<PieceItem>(); EventCubeMeltItems=null; EventCubeMeltCollections=null;
|
|
||||||
EventStageList=null; StageCount=0; StartTime=EndTime=default; CurrentPieceItems=new List<PieceItem>(); _pieceDimension=default;
|
|
||||||
PrevData=null; NextData=null; PrevBoardData=null; PrevPieceDatas=null; CurrentBoardData=null; CurrentPieceDatas=null; GuideIndex=0;
|
|
||||||
}
|
|
||||||
internal void ReplaceFromKVDecoded(game.PlayerDataStorage.PlayerKVCubeState state)
|
|
||||||
{
|
|
||||||
if(state==null) throw new ArgumentNullException(nameof(state));
|
|
||||||
EventData=!state.IsPresent ? null : new EventCubeMeltData {
|
|
||||||
TicketCount=state.TicketCount,FirstOpen=state.FirstOpen,IsFinished=state.IsFinished,EventId=state.EventId,
|
|
||||||
IsInGuide=state.IsInGuide,StartTime=state.StartTime,EndTime=state.EndTime,CurrentStagetId=state.CurrentStagetId,
|
|
||||||
CollectItems=state.CollectItems.Select(p=>new CollectItem { ItemId=p.ItemId,Count=p.Count,MaxCount=p.MaxCount,ItemTrueId=p.ItemTrueId }).ToList(),
|
|
||||||
BoardData=new List<int>(state.BoardData),BoardViewSize=new Vector2Int(state.Width,state.Height),
|
|
||||||
CollectRewardsId=new List<int>(state.CollectRewardsId),IsFinalRewardGet=state.IsFinalRewardGet,
|
|
||||||
PieceDatas=state.PieceDatas.Select(p=>new List<int>(p)).ToList(),Cycle=state.Cycle,InflationRate=state.InflationRate
|
|
||||||
};
|
|
||||||
}
|
|
||||||
internal bool IsInactiveForKVValidation => !_isInit && _fishingEvent==null && EventCubeMeltMainData==null &&
|
|
||||||
EventCubeMeltStageData==null && EventCubeMeltCollectionData==null && EventCubeMeltPieces==null &&
|
|
||||||
EventCubeMeltPieceItems.Count==0 && CurrentPieceItems.Count==0 && PrevData==null && NextData==null &&
|
|
||||||
PrevBoardData==null && CurrentBoardData==null && PrevPieceDatas==null && CurrentPieceDatas==null;
|
|
||||||
|
|
||||||
public FishingEvent FishingEvent {get => _fishingEvent;set => _fishingEvent = value;}
|
public FishingEvent FishingEvent {get => _fishingEvent;set => _fishingEvent = value;}
|
||||||
|
|
||||||
|
|||||||
@@ -265,7 +265,7 @@ public class EventGatherCoreRewardInfo
|
|||||||
|
|
||||||
public class EventGatherCorePlayfabData
|
public class EventGatherCorePlayfabData
|
||||||
{
|
{
|
||||||
readonly game.PlayerDataStorage.IPlayerSaveStore saveStore = GContext.container.Resolve<game.PlayerDataStorage.IPlayerSaveStore>();
|
readonly Core.KV.IPlayerSaveStore saveStore = GContext.container.Resolve<Core.KV.IPlayerSaveStore>();
|
||||||
|
|
||||||
public int EventId { get; set; }
|
public int EventId { get; set; }
|
||||||
public int TicketCount { get; set; }
|
public int TicketCount { get; set; }
|
||||||
@@ -319,7 +319,7 @@ public class EventGatherCorePlayfabData
|
|||||||
|
|
||||||
public static EventGatherCorePlayfabData Load()
|
public static EventGatherCorePlayfabData Load()
|
||||||
{
|
{
|
||||||
var s = GContext.container.Resolve<game.PlayerDataStorage.IPlayerSaveStore>().Read(Key).ValueOrThrow();
|
var s = GContext.container.Resolve<Core.KV.IPlayerSaveStore>().Read(Key).ValueOrThrow();
|
||||||
var res = new EventGatherCorePlayfabData();
|
var res = new EventGatherCorePlayfabData();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ using UnityEngine;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class EventLuckyGameModel : ILuckyTaskGetData
|
public class EventLuckyGameModel : ILuckyTaskGetData
|
||||||
{
|
{
|
||||||
readonly game.PlayerDataStorage.IPlayerSaveStore saveStore = GContext.container.Resolve<game.PlayerDataStorage.IPlayerSaveStore>();
|
readonly Core.KV.IPlayerSaveStore saveStore = GContext.container.Resolve<Core.KV.IPlayerSaveStore>();
|
||||||
|
|
||||||
public static EventLuckyGameModel Get()
|
public static EventLuckyGameModel Get()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -261,7 +261,7 @@ public class EventOfferJourneyBlockData
|
|||||||
|
|
||||||
public class EventOfferJourneyPlayfabData
|
public class EventOfferJourneyPlayfabData
|
||||||
{
|
{
|
||||||
readonly game.PlayerDataStorage.IPlayerSaveStore saveStore = GContext.container.Resolve<game.PlayerDataStorage.IPlayerSaveStore>();
|
readonly Core.KV.IPlayerSaveStore saveStore = GContext.container.Resolve<Core.KV.IPlayerSaveStore>();
|
||||||
|
|
||||||
public int EventId, Progress, TicketCount, VipLvlOnTrigger, RoundCount;
|
public int EventId, Progress, TicketCount, VipLvlOnTrigger, RoundCount;
|
||||||
public float InflationRate;
|
public float InflationRate;
|
||||||
@@ -321,7 +321,7 @@ public class EventOfferJourneyPlayfabData
|
|||||||
|
|
||||||
public static EventOfferJourneyPlayfabData Load()
|
public static EventOfferJourneyPlayfabData Load()
|
||||||
{
|
{
|
||||||
var s = GContext.container.Resolve<game.PlayerDataStorage.IPlayerSaveStore>().Read(Key).ValueOrThrow();
|
var s = GContext.container.Resolve<Core.KV.IPlayerSaveStore>().Read(Key).ValueOrThrow();
|
||||||
return Deserialize(s);
|
return Deserialize(s);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ public class EventOfferJourneyLvlTriggerData : AEventOfferJourneyData
|
|||||||
|
|
||||||
public class EventOfferJourneyLvlTriggerPlayfabData
|
public class EventOfferJourneyLvlTriggerPlayfabData
|
||||||
{
|
{
|
||||||
readonly game.PlayerDataStorage.IPlayerSaveStore saveStore = GContext.container.Resolve<game.PlayerDataStorage.IPlayerSaveStore>();
|
readonly Core.KV.IPlayerSaveStore saveStore = GContext.container.Resolve<Core.KV.IPlayerSaveStore>();
|
||||||
|
|
||||||
public int Progress, TicketCount, RoundCount;
|
public int Progress, TicketCount, RoundCount;
|
||||||
private const string Key = "EventOfferJourneyLvlTriggerData";
|
private const string Key = "EventOfferJourneyLvlTriggerData";
|
||||||
@@ -197,7 +197,7 @@ public class EventOfferJourneyLvlTriggerPlayfabData
|
|||||||
|
|
||||||
public static EventOfferJourneyLvlTriggerPlayfabData Load()
|
public static EventOfferJourneyLvlTriggerPlayfabData Load()
|
||||||
{
|
{
|
||||||
var s = GContext.container.Resolve<game.PlayerDataStorage.IPlayerSaveStore>().Read(Key).ValueOrThrow();
|
var s = GContext.container.Resolve<Core.KV.IPlayerSaveStore>().Read(Key).ValueOrThrow();
|
||||||
return Deserialize(s);
|
return Deserialize(s);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,6 @@ namespace EventPartnerGather
|
|||||||
{
|
{
|
||||||
BuildPartners = new BuildPartner[_tables.TbEventPartnerConfig.InvitationCount[1]];
|
BuildPartners = new BuildPartner[_tables.TbEventPartnerConfig.InvitationCount[1]];
|
||||||
}
|
}
|
||||||
internal bool HasSubscriptionsForKVLoad => disposables != null;
|
|
||||||
//
|
//
|
||||||
public bool IsFullyMatched => BuildPartners.All(b => b != null && b.IsValid());
|
public bool IsFullyMatched => BuildPartners.All(b => b != null && b.IsValid());
|
||||||
|
|
||||||
|
|||||||
@@ -564,7 +564,7 @@ public class RewardItemNewFlyToStashRequest
|
|||||||
|
|
||||||
public class EventLuckMagicPlayfabData
|
public class EventLuckMagicPlayfabData
|
||||||
{
|
{
|
||||||
readonly game.PlayerDataStorage.IPlayerSaveStore saveStore = GContext.container.Resolve<game.PlayerDataStorage.IPlayerSaveStore>();
|
readonly Core.KV.IPlayerSaveStore saveStore = GContext.container.Resolve<Core.KV.IPlayerSaveStore>();
|
||||||
|
|
||||||
public int EventId;
|
public int EventId;
|
||||||
public int TicketCount;
|
public int TicketCount;
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ public class WelcomeBackData
|
|||||||
}
|
}
|
||||||
public class EventWelcomeBackManager
|
public class EventWelcomeBackManager
|
||||||
{
|
{
|
||||||
readonly game.PlayerDataStorage.IPlayerSaveStore saveStore = GContext.container.Resolve<game.PlayerDataStorage.IPlayerSaveStore>();
|
readonly Core.KV.IPlayerSaveStore saveStore = GContext.container.Resolve<Core.KV.IPlayerSaveStore>();
|
||||||
|
|
||||||
string DataKey_WelcomeBack = "WelcomeBack";
|
string DataKey_WelcomeBack = "WelcomeBack";
|
||||||
Tables tables;
|
Tables tables;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using game.PlayerDataStorage;
|
using Core.KV;
|
||||||
using asap.core;
|
using asap.core;
|
||||||
using cfg;
|
using cfg;
|
||||||
using DG.Tweening;
|
using DG.Tweening;
|
||||||
@@ -7,7 +7,6 @@ using GameCore;
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections;
|
using System.Collections;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using UniRx;
|
using UniRx;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
@@ -25,52 +24,6 @@ public class ShowHandEvent
|
|||||||
[DefaultExecutionOrder(10000)]
|
[DefaultExecutionOrder(10000)]
|
||||||
public partial class FishingBehaviorB : MonoBehaviour
|
public partial class FishingBehaviorB : MonoBehaviour
|
||||||
{
|
{
|
||||||
#if UNITY_EDITOR && !SERVICE_HOST
|
|
||||||
game.PlayerDataStorage.PlayerKVGameplayLoop.FishingAction kvFishingAction;
|
|
||||||
bool KVFishingCurrent()
|
|
||||||
{
|
|
||||||
if(!game.PlayerDataStorage.PlayerKVGameplayLoop.Enabled) return true;
|
|
||||||
return this!=null && isActiveAndEnabled && game.PlayerDataStorage.PlayerKVGameplayLoop.FishingCurrent(kvFishingAction);
|
|
||||||
}
|
|
||||||
void CancelKVFishing(string reason) => game.PlayerDataStorage.PlayerKVGameplayLoop.CancelFishing(kvFishingAction,reason);
|
|
||||||
void TrackKVCastTween(Tween tween)
|
|
||||||
{
|
|
||||||
if(!game.PlayerDataStorage.PlayerKVGameplayLoop.Enabled) return;
|
|
||||||
var action=kvFishingAction;
|
|
||||||
bool completed=false;
|
|
||||||
tween.onComplete+=()=>completed=true;
|
|
||||||
tween.onKill+=()=> { if(!completed) game.PlayerDataStorage.PlayerKVGameplayLoop.CancelFishing(action,"cast tween killed"); };
|
|
||||||
}
|
|
||||||
async Task ConsumeBaitOnKVAsync()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if(!KVFishingCurrent()) return;
|
|
||||||
if(!game.PlayerDataStorage.PlayerKVGameplayLoop.ClaimFishingConsumption(kvFishingAction)) return;
|
|
||||||
if(fishingData.fishItem==null || !fishingData.IsGeneralFish() || fishingData.IsFree || fishingData.IsRetry ||
|
|
||||||
fishingData.IsDuel || fishingData.IsPackItemFish() || fishingData.curFishData==null)
|
|
||||||
throw game.PlayerDataStorage.PlayerKVGameplayLoop.Stop("Only one ordinary paid fishing action is permitted. fishItem.ID="+(fishingData.fishItem==null?"null":fishingData.fishItem.ID.ToString())+", fishItem.SubType="+(fishingData.fishItem==null?"null":fishingData.fishItem.SubType.ToString())+", curFishData.ID="+(fishingData.curFishData==null?"null":fishingData.curFishData.ID.ToString())+", IsFree="+fishingData.IsFree+", IsRetry="+fishingData.IsRetry+", IsDuel="+fishingData.IsDuel);
|
|
||||||
if(Application.internetReachability==NetworkReachability.NotReachable)
|
|
||||||
throw game.PlayerDataStorage.PlayerKVGameplayLoop.Stop("Fishing stopped before consumption: offline.");
|
|
||||||
if(_tables.TbFishingEvent.DataList.Any(e=>e.ConditionList.Any(c=>c.Condition==ConditionType.ConsumeEnergy)))
|
|
||||||
throw game.PlayerDataStorage.PlayerKVGameplayLoop.Stop("ConsumeEnergy activity activation requires separate review.");
|
|
||||||
var player=GContext.container.Resolve<PlayerData>();
|
|
||||||
int mag=player.magnification;
|
|
||||||
if(mag<0 || mag>=_tables.TbEnergyDef.DataList.Count)
|
|
||||||
throw game.PlayerDataStorage.PlayerKVGameplayLoop.Stop("Invalid fishing magnification.");
|
|
||||||
int fish=fishingData.curFishData.ID, rod=fishingData.RodId, map=player.lastMapId;
|
|
||||||
int cost=_tables.TbEnergyDef.DataList[mag].Mag;
|
|
||||||
game.PlayerDataStorage.PlayerKVGameplayLoop.PrepareFishingReward(kvFishingAction,fishingData);
|
|
||||||
await player.ConsumeEnergyForKVLoopAsync(cost,kvFishingAction);
|
|
||||||
if(!KVFishingCurrent()) return;
|
|
||||||
if(fishingData.curFishData==null || fishingData.curFishData.ID!=fish || fishingData.RodId!=rod ||
|
|
||||||
player.lastMapId!=map || (int)player.magnification!=mag || fishingData.IsFree || fishingData.IsRetry || fishingData.IsDuel)
|
|
||||||
throw game.PlayerDataStorage.PlayerKVGameplayLoop.Stop("Fishing context changed during confirmation.");
|
|
||||||
game.PlayerDataStorage.PlayerKVGameplayLoop.ContinueFishing(kvFishingAction,WaitForFishBiteED);
|
|
||||||
}
|
|
||||||
catch(Exception e) { game.PlayerDataStorage.PlayerKVGameplayLoop.Fault(e.Message); }
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
IPlayerSaveStore saveStore;
|
IPlayerSaveStore saveStore;
|
||||||
Tables _tables;
|
Tables _tables;
|
||||||
[Header("RELATED OBJECT SETTINGS")]
|
[Header("RELATED OBJECT SETTINGS")]
|
||||||
@@ -762,25 +715,9 @@ public partial class FishingBehaviorB : MonoBehaviour
|
|||||||
//开始抛鱼线
|
//开始抛鱼线
|
||||||
public async void CastLineBehavior()
|
public async void CastLineBehavior()
|
||||||
{
|
{
|
||||||
#if UNITY_EDITOR && !SERVICE_HOST
|
|
||||||
if(game.PlayerDataStorage.PlayerKVGameplayLoop.Enabled)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var action=game.PlayerDataStorage.PlayerKVGameplayLoop.BeginFishing(this);
|
|
||||||
if(action==null) return;
|
|
||||||
kvFishingAction=action;
|
|
||||||
if(!KVFishingCurrent()) return;
|
|
||||||
}
|
|
||||||
catch(Exception e) { game.PlayerDataStorage.PlayerKVGameplayLoop.Fault(e.Message); return; }
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
//计算主画布分辨率下的宽高和屏幕的宽高的比列:
|
//计算主画布分辨率下的宽高和屏幕的宽高的比列:
|
||||||
fishingData.mainCamera.DOLocalRotateQuaternion(Quaternion.identity, 0.1f);
|
fishingData.mainCamera.DOLocalRotateQuaternion(Quaternion.identity, 0.1f);
|
||||||
await Awaiters.NextFrame;
|
await Awaiters.NextFrame;
|
||||||
#if UNITY_EDITOR && !SERVICE_HOST
|
|
||||||
if(!KVFishingCurrent()) return;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
CastBait();
|
CastBait();
|
||||||
//播放抛竿动画
|
//播放抛竿动画
|
||||||
@@ -791,18 +728,12 @@ public partial class FishingBehaviorB : MonoBehaviour
|
|||||||
rodHandleAni.SetBool("_idle", false);
|
rodHandleAni.SetBool("_idle", false);
|
||||||
rodHandleAni.SetTrigger("_drawing");
|
rodHandleAni.SetTrigger("_drawing");
|
||||||
await Awaiters.Seconds(fishingBehaviorConf.audioDelay);
|
await Awaiters.Seconds(fishingBehaviorConf.audioDelay);
|
||||||
#if UNITY_EDITOR && !SERVICE_HOST
|
|
||||||
if(!KVFishingCurrent()) return;
|
|
||||||
#endif
|
|
||||||
GContext.Publish(new EventFishingSound(SoundType.ThrowingAudio));
|
GContext.Publish(new EventFishingSound(SoundType.ThrowingAudio));
|
||||||
GContext.Publish(new VibrationData(1));
|
GContext.Publish(new VibrationData(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ConsumeBait()
|
private void ConsumeBait()
|
||||||
{
|
{
|
||||||
#if UNITY_EDITOR && !SERVICE_HOST
|
|
||||||
if(game.PlayerDataStorage.PlayerKVGameplayLoop.Enabled) { _=ConsumeBaitOnKVAsync(); return; }
|
|
||||||
#endif
|
|
||||||
if (fishingData.IsFree)
|
if (fishingData.IsFree)
|
||||||
{
|
{
|
||||||
WaitForFishBiteED();
|
WaitForFishBiteED();
|
||||||
@@ -855,8 +786,8 @@ public partial class FishingBehaviorB : MonoBehaviour
|
|||||||
{
|
{
|
||||||
GContext.Publish(new ConditionTypeEvent(ConditionType.ConsumeEnergy, energy));
|
GContext.Publish(new ConditionTypeEvent(ConditionType.ConsumeEnergy, energy));
|
||||||
}
|
}
|
||||||
var success = (await saveStore.WriteAndConfirmAsync("NewPlayerDataDic",
|
var success = await saveStore.SyncDataImmidiate("NewPlayerDataDic",
|
||||||
Newtonsoft.Json.JsonConvert.SerializeObject(commonDatas))).Confirmed;
|
Newtonsoft.Json.JsonConvert.SerializeObject(commonDatas));
|
||||||
Debug.Log($"[FishBehaviourB::ConsumeBaitOnPlayfab] {success}");
|
Debug.Log($"[FishBehaviourB::ConsumeBaitOnPlayfab] {success}");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1004,9 +935,6 @@ public partial class FishingBehaviorB : MonoBehaviour
|
|||||||
}
|
}
|
||||||
public void BaitMove()
|
public void BaitMove()
|
||||||
{
|
{
|
||||||
#if UNITY_EDITOR && !SERVICE_HOST
|
|
||||||
if(!KVFishingCurrent()) return;
|
|
||||||
#endif
|
|
||||||
flyEndPosTrue = _bait.transform.position;
|
flyEndPosTrue = _bait.transform.position;
|
||||||
|
|
||||||
GContext.Publish(new EventFishingSound(SoundType.audio_fishing_throwing_baitintowater));
|
GContext.Publish(new EventFishingSound(SoundType.audio_fishing_throwing_baitintowater));
|
||||||
@@ -1017,12 +945,9 @@ public partial class FishingBehaviorB : MonoBehaviour
|
|||||||
var fishEffect = _effectDic["fx_fishing_splashloopwaiting"];
|
var fishEffect = _effectDic["fx_fishing_splashloopwaiting"];
|
||||||
float z = WaitingZ();
|
float z = WaitingZ();
|
||||||
float timer = Mathf.Lerp(fishingBehaviorConf.waitingLineDrawMinTime, fishingBehaviorConf.waitingLineDrawMaxTime, Random.value);
|
float timer = Mathf.Lerp(fishingBehaviorConf.waitingLineDrawMinTime, fishingBehaviorConf.waitingLineDrawMaxTime, Random.value);
|
||||||
var waitingTween=_bait.transform.DOLocalMoveZ(z, timer).OnUpdate(
|
_bait.transform.DOLocalMoveZ(z, timer).OnUpdate(
|
||||||
(() =>
|
(() =>
|
||||||
{
|
{
|
||||||
#if UNITY_EDITOR && !SERVICE_HOST
|
|
||||||
if(!KVFishingCurrent()) return;
|
|
||||||
#endif
|
|
||||||
flyEndPosTrue = _bait.transform.position;
|
flyEndPosTrue = _bait.transform.position;
|
||||||
if (fishEffect)
|
if (fishEffect)
|
||||||
{
|
{
|
||||||
@@ -1031,9 +956,6 @@ public partial class FishingBehaviorB : MonoBehaviour
|
|||||||
fishEffect.transform.position = vector;
|
fishEffect.transform.position = vector;
|
||||||
}
|
}
|
||||||
})); ;
|
})); ;
|
||||||
#if UNITY_EDITOR && !SERVICE_HOST
|
|
||||||
TrackKVCastTween(waitingTween);
|
|
||||||
#endif
|
|
||||||
StartCoroutine(WaitForFishBite(timer));
|
StartCoroutine(WaitForFishBite(timer));
|
||||||
}
|
}
|
||||||
IEnumerator PlayWaitingFx()
|
IEnumerator PlayWaitingFx()
|
||||||
@@ -1056,9 +978,6 @@ public partial class FishingBehaviorB : MonoBehaviour
|
|||||||
}
|
}
|
||||||
private IEnumerator WaitForFishBite(float timer)
|
private IEnumerator WaitForFishBite(float timer)
|
||||||
{
|
{
|
||||||
#if UNITY_EDITOR && !SERVICE_HOST
|
|
||||||
if(!KVFishingCurrent()) yield break;
|
|
||||||
#endif
|
|
||||||
EventFishingSound eventFishingSound1 = new EventFishingSound(SoundType.audio_fishing_waiting);
|
EventFishingSound eventFishingSound1 = new EventFishingSound(SoundType.audio_fishing_waiting);
|
||||||
EventFishingSound eventFishingSound2 = new EventFishingSound(SoundType.ReelingAudio, loop: true);
|
EventFishingSound eventFishingSound2 = new EventFishingSound(SoundType.ReelingAudio, loop: true);
|
||||||
|
|
||||||
@@ -1068,9 +987,6 @@ public partial class FishingBehaviorB : MonoBehaviour
|
|||||||
timer += Mathf.Lerp(fishingBehaviorConf.waitingMinTime, fishingBehaviorConf.waitingMaxTime, Random.value) * (1 - EnhancedWaitingTime);
|
timer += Mathf.Lerp(fishingBehaviorConf.waitingMinTime, fishingBehaviorConf.waitingMaxTime, Random.value) * (1 - EnhancedWaitingTime);
|
||||||
|
|
||||||
yield return new WaitForSeconds(timer);
|
yield return new WaitForSeconds(timer);
|
||||||
#if UNITY_EDITOR && !SERVICE_HOST
|
|
||||||
if(!KVFishingCurrent()) yield break;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
if (eventFishingSound2.Sound != null)
|
if (eventFishingSound2.Sound != null)
|
||||||
{
|
{
|
||||||
@@ -1194,27 +1110,18 @@ public partial class FishingBehaviorB : MonoBehaviour
|
|||||||
}
|
}
|
||||||
private async void CastBait()
|
private async void CastBait()
|
||||||
{
|
{
|
||||||
#if UNITY_EDITOR && !SERVICE_HOST
|
|
||||||
if(!KVFishingCurrent()) return;
|
|
||||||
#endif
|
|
||||||
InitEffect();
|
InitEffect();
|
||||||
if (fishingBehaviorConf.fishFreeControlTime > 0f)
|
if (fishingBehaviorConf.fishFreeControlTime > 0f)
|
||||||
{
|
{
|
||||||
await new WaitForSeconds(fishingBehaviorConf.fishFreeControlTime);
|
await new WaitForSeconds(fishingBehaviorConf.fishFreeControlTime);
|
||||||
#if UNITY_EDITOR && !SERVICE_HOST
|
|
||||||
if(!KVFishingCurrent()) return;
|
|
||||||
#endif
|
|
||||||
_isCastLine = true;
|
_isCastLine = true;
|
||||||
_bait.transform.SetParent(transform);
|
_bait.transform.SetParent(transform);
|
||||||
var castX=_bait.transform.DOMoveX(0, fishingBehaviorConf.flyToWaterTime);
|
_bait.transform.DOMoveX(0, fishingBehaviorConf.flyToWaterTime);
|
||||||
var castZ=_bait.transform.DOMoveZ(fishingBehaviorConf.flyDistance, fishingBehaviorConf.flyToWaterTime).SetEase(fishingBehaviorConf.flyForwardCurve);
|
_bait.transform.DOMoveZ(fishingBehaviorConf.flyDistance, fishingBehaviorConf.flyToWaterTime).SetEase(fishingBehaviorConf.flyForwardCurve);
|
||||||
_cameraAnimator.SetTrigger("_waiting");
|
_cameraAnimator.SetTrigger("_waiting");
|
||||||
var castY=_bait.transform.DOMoveY(_waterHeight, fishingBehaviorConf.flyToWaterTime)
|
_bait.transform.DOMoveY(_waterHeight, fishingBehaviorConf.flyToWaterTime)
|
||||||
.SetEase(fishingBehaviorConf.flyDownCurve).OnComplete((() =>
|
.SetEase(fishingBehaviorConf.flyDownCurve).OnComplete((() =>
|
||||||
{
|
{
|
||||||
#if UNITY_EDITOR && !SERVICE_HOST
|
|
||||||
if(!KVFishingCurrent()) return;
|
|
||||||
#endif
|
|
||||||
fishingRodAnimator.SetTrigger("_waiting");
|
fishingRodAnimator.SetTrigger("_waiting");
|
||||||
GContext.Publish(ERightState.Waiting);
|
GContext.Publish(ERightState.Waiting);
|
||||||
LoadEffect("fx_fishing_splashinto");
|
LoadEffect("fx_fishing_splashinto");
|
||||||
@@ -1224,25 +1131,16 @@ public partial class FishingBehaviorB : MonoBehaviour
|
|||||||
BaitMove();
|
BaitMove();
|
||||||
_bait.SetActive(false);
|
_bait.SetActive(false);
|
||||||
}));
|
}));
|
||||||
#if UNITY_EDITOR && !SERVICE_HOST
|
|
||||||
TrackKVCastTween(castX); TrackKVCastTween(castZ); TrackKVCastTween(castY);
|
|
||||||
#endif
|
|
||||||
AddLinePos();
|
AddLinePos();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
await new WaitForSeconds(1.15f);
|
await new WaitForSeconds(1.15f);
|
||||||
#if UNITY_EDITOR && !SERVICE_HOST
|
|
||||||
if(!KVFishingCurrent()) return;
|
|
||||||
#endif
|
|
||||||
//当前动画_fishingRodAnimator播放时间
|
//当前动画_fishingRodAnimator播放时间
|
||||||
float time = fishingRodAnimator.GetCurrentAnimatorStateInfo(0).normalizedTime;
|
float time = fishingRodAnimator.GetCurrentAnimatorStateInfo(0).normalizedTime;
|
||||||
_isCastLine = true;
|
_isCastLine = true;
|
||||||
_cameraAnimator.SetTrigger("_waiting");
|
_cameraAnimator.SetTrigger("_waiting");
|
||||||
await new WaitForSeconds(time);
|
await new WaitForSeconds(time);
|
||||||
#if UNITY_EDITOR && !SERVICE_HOST
|
|
||||||
if(!KVFishingCurrent()) return;
|
|
||||||
#endif
|
|
||||||
_bait.transform.SetParent(transform);
|
_bait.transform.SetParent(transform);
|
||||||
_bait.SetActive(false);
|
_bait.SetActive(false);
|
||||||
//鱼漂位置
|
//鱼漂位置
|
||||||
@@ -2023,9 +1921,6 @@ public partial class FishingBehaviorB : MonoBehaviour
|
|||||||
_cameraAnimator.SetBool("_reeling", false);
|
_cameraAnimator.SetBool("_reeling", false);
|
||||||
fishingRodAnimator.SetBool("_pulling", false);
|
fishingRodAnimator.SetBool("_pulling", false);
|
||||||
CameraPullingEnd();
|
CameraPullingEnd();
|
||||||
#if UNITY_EDITOR && !SERVICE_HOST
|
|
||||||
if(game.PlayerDataStorage.PlayerKVGameplayLoop.Enabled && (!KVFishingCurrent() || !game.PlayerDataStorage.PlayerKVGameplayLoop.RewardCanAdvance)) yield break;
|
|
||||||
#endif
|
|
||||||
fishingData.IsCaughtFish = true;
|
fishingData.IsCaughtFish = true;
|
||||||
PlayCollectingEffect();
|
PlayCollectingEffect();
|
||||||
if (fx_fishing_splashwaveloop != null)
|
if (fx_fishing_splashwaveloop != null)
|
||||||
@@ -2438,9 +2333,6 @@ public partial class FishingBehaviorB : MonoBehaviour
|
|||||||
|
|
||||||
private void OnDisable()
|
private void OnDisable()
|
||||||
{
|
{
|
||||||
#if UNITY_EDITOR && !SERVICE_HOST
|
|
||||||
CancelKVFishing("disabled");
|
|
||||||
#endif
|
|
||||||
CloseAllEffect();
|
CloseAllEffect();
|
||||||
}
|
}
|
||||||
public void CloseAllEffect()
|
public void CloseAllEffect()
|
||||||
@@ -2464,9 +2356,6 @@ public partial class FishingBehaviorB : MonoBehaviour
|
|||||||
}
|
}
|
||||||
private void OnDestroy()
|
private void OnDestroy()
|
||||||
{
|
{
|
||||||
#if UNITY_EDITOR && !SERVICE_HOST
|
|
||||||
CancelKVFishing("destroyed");
|
|
||||||
#endif
|
|
||||||
disposables?.Dispose();
|
disposables?.Dispose();
|
||||||
disposables = null;
|
disposables = null;
|
||||||
foreach (var o in _effectDic)
|
foreach (var o in _effectDic)
|
||||||
|
|||||||
@@ -522,22 +522,6 @@ public class FishingUpdateSys : MonoBehaviour
|
|||||||
int isFailCount = 0;
|
int isFailCount = 0;
|
||||||
// 按了收线按钮
|
// 按了收线按钮
|
||||||
public void PiercingFish(int waitingZone)
|
public void PiercingFish(int waitingZone)
|
||||||
{
|
|
||||||
#if UNITY_EDITOR && !SERVICE_HOST
|
|
||||||
if(game.PlayerDataStorage.PlayerKVGameplayLoop.Enabled)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if(!game.PlayerDataStorage.PlayerKVGameplayLoop.BeginRewardDrawing(this)) return;
|
|
||||||
using(game.PlayerDataStorage.PlayerKVGameplayLoop.BeginRewardSegment("strike")) PiercingFishCore(waitingZone);
|
|
||||||
}
|
|
||||||
catch(System.Exception e) { game.PlayerDataStorage.PlayerKVGameplayLoop.Fault(e.Message); }
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
PiercingFishCore(waitingZone);
|
|
||||||
}
|
|
||||||
void PiercingFishCore(int waitingZone)
|
|
||||||
{
|
{
|
||||||
//waitingZone==100表示鱼没有开启刺鱼
|
//waitingZone==100表示鱼没有开启刺鱼
|
||||||
if (!fishingData.IsDrawing)
|
if (!fishingData.IsDrawing)
|
||||||
@@ -639,9 +623,6 @@ public class FishingUpdateSys : MonoBehaviour
|
|||||||
if (piercingToComboB > 0)
|
if (piercingToComboB > 0)
|
||||||
{
|
{
|
||||||
await Awaiters.Seconds(damageProtectionTime);
|
await Awaiters.Seconds(damageProtectionTime);
|
||||||
#if UNITY_EDITOR && !SERVICE_HOST
|
|
||||||
if(!game.PlayerDataStorage.PlayerKVGameplayLoop.RewardCanAdvance) return;
|
|
||||||
#endif
|
|
||||||
_comboMultiplayer += Random.Range(piercingToComboA, piercingToComboB);
|
_comboMultiplayer += Random.Range(piercingToComboA, piercingToComboB);
|
||||||
//新技能生效 刺鱼触发全力收线进度
|
//新技能生效 刺鱼触发全力收线进度
|
||||||
PublishPerkEvent(RodPerkType.BattlePerk_PiercingToCombo);
|
PublishPerkEvent(RodPerkType.BattlePerk_PiercingToCombo);
|
||||||
@@ -651,9 +632,6 @@ public class FishingUpdateSys : MonoBehaviour
|
|||||||
public async void EternalDeath()
|
public async void EternalDeath()
|
||||||
{
|
{
|
||||||
await Awaiters.Seconds(damageProtectionTime);
|
await Awaiters.Seconds(damageProtectionTime);
|
||||||
#if UNITY_EDITOR && !SERVICE_HOST
|
|
||||||
if(!game.PlayerDataStorage.PlayerKVGameplayLoop.RewardCanAdvance) return;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
StopAllCoroutines();
|
StopAllCoroutines();
|
||||||
OnFishingIsOver();
|
OnFishingIsOver();
|
||||||
@@ -1064,10 +1042,6 @@ public class FishingUpdateSys : MonoBehaviour
|
|||||||
|
|
||||||
private void Update()
|
private void Update()
|
||||||
{
|
{
|
||||||
#if UNITY_EDITOR && !SERVICE_HOST
|
|
||||||
if(!game.PlayerDataStorage.PlayerKVGameplayLoop.RewardCanAdvance) return;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
if (fishingData.IsDrawing)
|
if (fishingData.IsDrawing)
|
||||||
{
|
{
|
||||||
Vector3 fishPos = fishingBehavior.GetFishPos();
|
Vector3 fishPos = fishingBehavior.GetFishPos();
|
||||||
@@ -1245,34 +1219,6 @@ public class FishingUpdateSys : MonoBehaviour
|
|||||||
}
|
}
|
||||||
|
|
||||||
void FishEnd()
|
void FishEnd()
|
||||||
{
|
|
||||||
#if UNITY_EDITOR && !SERVICE_HOST
|
|
||||||
if(game.PlayerDataStorage.PlayerKVGameplayLoop.Enabled)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if(!game.PlayerDataStorage.PlayerKVGameplayLoop.BeginRewardEnding(this,fishingData.IsCaughtFish && !isFail)) return;
|
|
||||||
using(game.PlayerDataStorage.PlayerKVGameplayLoop.BeginRewardSegment("ending")) FishEndCore();
|
|
||||||
_=ShowKVRewardPanelAsync();
|
|
||||||
}
|
|
||||||
catch(System.Exception e) { game.PlayerDataStorage.PlayerKVGameplayLoop.Fault(e.Message); }
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
FishEndCore();
|
|
||||||
}
|
|
||||||
#if UNITY_EDITOR && !SERVICE_HOST
|
|
||||||
async Task ShowKVRewardPanelAsync()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var panel=await UIManager.Instance.ShowUI(UITypes.FishingRewardPanel);
|
|
||||||
if(!game.PlayerDataStorage.PlayerKVGameplayLoop.RewardRunCurrent && panel!=null) Destroy(panel);
|
|
||||||
}
|
|
||||||
catch(System.Exception e) { game.PlayerDataStorage.PlayerKVGameplayLoop.Fault(e.Message); }
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
void FishEndCore()
|
|
||||||
{
|
{
|
||||||
if (isFail)
|
if (isFail)
|
||||||
{
|
{
|
||||||
@@ -1351,9 +1297,6 @@ public class FishingUpdateSys : MonoBehaviour
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
#if UNITY_EDITOR && !SERVICE_HOST
|
|
||||||
if(game.PlayerDataStorage.PlayerKVGameplayLoop.Enabled) return; // Wrapper opens UI after S2 disposal.
|
|
||||||
#endif
|
|
||||||
_ = UIManager.Instance.ShowUI(UITypes.FishingRewardPanel);
|
_ = UIManager.Instance.ShowUI(UITypes.FishingRewardPanel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1803,15 +1746,6 @@ public class FishingUpdateSys : MonoBehaviour
|
|||||||
//全力收线
|
//全力收线
|
||||||
public async void FullCombo()
|
public async void FullCombo()
|
||||||
{
|
{
|
||||||
#if UNITY_EDITOR && !SERVICE_HOST
|
|
||||||
if(game.PlayerDataStorage.PlayerKVGameplayLoop.Enabled)
|
|
||||||
{
|
|
||||||
if(!game.PlayerDataStorage.PlayerKVGameplayLoop.RewardCanAdvance) return;
|
|
||||||
try { game.PlayerDataStorage.PlayerKVGameplayLoop.CheckRewardGuide(); }
|
|
||||||
catch(System.Exception e) { game.PlayerDataStorage.PlayerKVGameplayLoop.Fault(e.Message); return; }
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
if (_isCombo == 1 && curFishingStateName != FishingStateName.UIEating)
|
if (_isCombo == 1 && curFishingStateName != FishingStateName.UIEating)
|
||||||
{
|
{
|
||||||
if (comboToExDmg > 0 && comboToExDmgCount < curComboToExDmgCount)
|
if (comboToExDmg > 0 && comboToExDmgCount < curComboToExDmgCount)
|
||||||
@@ -1852,9 +1786,6 @@ public class FishingUpdateSys : MonoBehaviour
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
await Awaiters.Seconds(0.2f);
|
await Awaiters.Seconds(0.2f);
|
||||||
#if UNITY_EDITOR && !SERVICE_HOST
|
|
||||||
if(!game.PlayerDataStorage.PlayerKVGameplayLoop.RewardCanAdvance) return;
|
|
||||||
#endif
|
|
||||||
lockCombo = false;
|
lockCombo = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3199,10 +3130,6 @@ public class FishingUpdateSys : MonoBehaviour
|
|||||||
}
|
}
|
||||||
private void OnDisable()
|
private void OnDisable()
|
||||||
{
|
{
|
||||||
#if UNITY_EDITOR && !SERVICE_HOST
|
|
||||||
game.PlayerDataStorage.PlayerKVGameplayLoop.CancelReward(this,"controller disabled");
|
|
||||||
#endif
|
|
||||||
|
|
||||||
StopSoundCombo();
|
StopSoundCombo();
|
||||||
StopRodAudio();
|
StopRodAudio();
|
||||||
StopNotReeling();
|
StopNotReeling();
|
||||||
@@ -3249,10 +3176,6 @@ public class FishingUpdateSys : MonoBehaviour
|
|||||||
}
|
}
|
||||||
private void OnDestroy()
|
private void OnDestroy()
|
||||||
{
|
{
|
||||||
#if UNITY_EDITOR && !SERVICE_HOST
|
|
||||||
game.PlayerDataStorage.PlayerKVGameplayLoop.CancelReward(this,"controller destroyed");
|
|
||||||
#endif
|
|
||||||
|
|
||||||
disposables?.Dispose();
|
disposables?.Dispose();
|
||||||
_comboDisposable?.Dispose();
|
_comboDisposable?.Dispose();
|
||||||
_hpcoroutine = null;
|
_hpcoroutine = null;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ using UnityEngine;
|
|||||||
|
|
||||||
public class InfiniteBuildingData
|
public class InfiniteBuildingData
|
||||||
{
|
{
|
||||||
readonly game.PlayerDataStorage.IPlayerSaveStore saveStore = GContext.container.Resolve<game.PlayerDataStorage.IPlayerSaveStore>();
|
readonly Core.KV.IPlayerSaveStore saveStore = GContext.container.Resolve<Core.KV.IPlayerSaveStore>();
|
||||||
|
|
||||||
public int Lvl, Step, StepLvl = 1, LastHangingBlock = -1;
|
public int Lvl, Step, StepLvl = 1, LastHangingBlock = -1;
|
||||||
public List<BlockRecord> Record = new List<BlockRecord>();
|
public List<BlockRecord> Record = new List<BlockRecord>();
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ using UnityEngine;
|
|||||||
|
|
||||||
public class LuckyADTaskPopupPanel : LuckyTaskPanelBase
|
public class LuckyADTaskPopupPanel : LuckyTaskPanelBase
|
||||||
{
|
{
|
||||||
game.PlayerDataStorage.IPlayerSaveStore saveStore;
|
Core.KV.IPlayerSaveStore saveStore;
|
||||||
|
|
||||||
List<LuckyTaskItemData> taskList;
|
List<LuckyTaskItemData> taskList;
|
||||||
List<LuckyADTaskItem> taskItems = new List<LuckyADTaskItem>();
|
List<LuckyADTaskItem> taskItems = new List<LuckyADTaskItem>();
|
||||||
@@ -20,7 +20,7 @@ public class LuckyADTaskPopupPanel : LuckyTaskPanelBase
|
|||||||
System.DateTime refreshTime;
|
System.DateTime refreshTime;
|
||||||
protected override void Awake()
|
protected override void Awake()
|
||||||
{
|
{
|
||||||
saveStore = GContext.container.Resolve<game.PlayerDataStorage.IPlayerSaveStore>();
|
saveStore = GContext.container.Resolve<Core.KV.IPlayerSaveStore>();
|
||||||
base.Awake();
|
base.Awake();
|
||||||
luckyTaskItem = transform.Find("root/hook_missions_object").GetComponent<LuckyADTask>();
|
luckyTaskItem = transform.Find("root/hook_missions_object").GetComponent<LuckyADTask>();
|
||||||
text_time = transform.Find("root/text_time").GetComponent<TMP_Text>();
|
text_time = transform.Find("root/text_time").GetComponent<TMP_Text>();
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ using UnityEngine.UI;
|
|||||||
|
|
||||||
public class LuckyTaskBtn : MonoBehaviour, IUIRedPoint
|
public class LuckyTaskBtn : MonoBehaviour, IUIRedPoint
|
||||||
{
|
{
|
||||||
game.PlayerDataStorage.IPlayerSaveStore saveStore;
|
Core.KV.IPlayerSaveStore saveStore;
|
||||||
|
|
||||||
Button btn;
|
Button btn;
|
||||||
ILuckyTaskGetData model;
|
ILuckyTaskGetData model;
|
||||||
@@ -18,7 +18,7 @@ public class LuckyTaskBtn : MonoBehaviour, IUIRedPoint
|
|||||||
public TMP_Text text_redpoint_big;
|
public TMP_Text text_redpoint_big;
|
||||||
void Awake()
|
void Awake()
|
||||||
{
|
{
|
||||||
saveStore = GContext.container.Resolve<game.PlayerDataStorage.IPlayerSaveStore>();
|
saveStore = GContext.container.Resolve<Core.KV.IPlayerSaveStore>();
|
||||||
btn = GetComponent<Button>();
|
btn = GetComponent<Button>();
|
||||||
btn.onClick.AddListener(OnClick);
|
btn.onClick.AddListener(OnClick);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,11 +9,11 @@ public class LuckyTaskPanel : LuckyTaskPanelBase
|
|||||||
{
|
{
|
||||||
protected override void Awake()
|
protected override void Awake()
|
||||||
{
|
{
|
||||||
saveStore = GContext.container.Resolve<game.PlayerDataStorage.IPlayerSaveStore>();
|
saveStore = GContext.container.Resolve<Core.KV.IPlayerSaveStore>();
|
||||||
base.Awake();
|
base.Awake();
|
||||||
}
|
}
|
||||||
|
|
||||||
game.PlayerDataStorage.IPlayerSaveStore saveStore;
|
Core.KV.IPlayerSaveStore saveStore;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// luckyModel 也作为是否自动全部领奖的标志
|
/// luckyModel 也作为是否自动全部领奖的标志
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ public enum FlipResult
|
|||||||
|
|
||||||
public class EventMatchGameManager
|
public class EventMatchGameManager
|
||||||
{
|
{
|
||||||
readonly game.PlayerDataStorage.IPlayerSaveStore saveStore = GContext.container.Resolve<game.PlayerDataStorage.IPlayerSaveStore>();
|
readonly Core.KV.IPlayerSaveStore saveStore = GContext.container.Resolve<Core.KV.IPlayerSaveStore>();
|
||||||
|
|
||||||
public string EventMatchGameDataKey => "EventMatchGameDataKey";
|
public string EventMatchGameDataKey => "EventMatchGameDataKey";
|
||||||
public int ItemSubType => 3;
|
public int ItemSubType => 3;
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ public interface IMiniBattlePassDataProvider
|
|||||||
|
|
||||||
public class MiniBattlePassDataProvider : IMiniBattlePassDataProvider
|
public class MiniBattlePassDataProvider : IMiniBattlePassDataProvider
|
||||||
{
|
{
|
||||||
readonly game.PlayerDataStorage.IPlayerSaveStore saveStore = GContext.container.Resolve<game.PlayerDataStorage.IPlayerSaveStore>();
|
readonly Core.KV.IPlayerSaveStore saveStore = GContext.container.Resolve<Core.KV.IPlayerSaveStore>();
|
||||||
|
|
||||||
private Tables _tables;
|
private Tables _tables;
|
||||||
PlayerItemData _playerItemData;
|
PlayerItemData _playerItemData;
|
||||||
|
|||||||
@@ -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://s101.local:18080/");
|
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);
|
||||||
|
|||||||
@@ -27,25 +27,10 @@ namespace Script.RuntimeScript
|
|||||||
private DiggingGameModel _dataModel;
|
private DiggingGameModel _dataModel;
|
||||||
public DiggingGameModel DataModel => _dataModel;
|
public DiggingGameModel DataModel => _dataModel;
|
||||||
|
|
||||||
internal static game.PlayerDataStorage.PlayerKVEmptyModuleState DecodeForKVLoad() =>
|
|
||||||
game.PlayerDataStorage.PlayerKVEmptyModuleState.Instance;
|
|
||||||
internal bool CanResetForKVLoad => _monoBehaviour==null && _digAni==null && _cameraVir==null && _tables==null &&
|
|
||||||
fx_DgSanddig==null && fx_DgSanddighard==null && fx_DgSanddisappear==null && diggingMoveCurve==null &&
|
|
||||||
_broadLogic==null && _broadContainer==null && _gridContianer==null && _gridPropContianer==null &&
|
|
||||||
_propContainer==null && _showPropPositon==null && _explosionManager==null && sanddig_dig_sound==null &&
|
|
||||||
sanddig_dig_audio==null && sanddig_dighard_audio==null && _fxRoot==null && _onCloseAwardPanel==null &&
|
|
||||||
(_dataModel==null || (_dataModel.CurActivityId==0 && _dataModel.digActivityConfig==null && _dataModel.AllClamps.Count==0));
|
|
||||||
internal void ResetForKVLoad()
|
|
||||||
{
|
|
||||||
if(!CanResetForKVLoad) throw new InvalidOperationException("KV digging module is active.");
|
|
||||||
_dataModel=new DiggingGameModel(); _broadLogic=null; _gameState=SandDigGameState.Normal;
|
|
||||||
IsCanClick=true; ClampIsPass=false; itemDatas=new List<ItemData>(); _awardPanelIsOpen=false;
|
|
||||||
}
|
|
||||||
internal void ReplaceFromKVDecoded(game.PlayerDataStorage.PlayerKVEmptyModuleState state)
|
|
||||||
{
|
|
||||||
if(state==null) throw new ArgumentNullException(nameof(state));
|
|
||||||
if(!CanResetForKVLoad || _dataModel==null) throw new InvalidOperationException("KV digging reset required.");
|
|
||||||
}
|
|
||||||
|
|
||||||
private Animation _digAni;
|
private Animation _digAni;
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ using UnityEngine.ResourceManagement.AsyncOperations;
|
|||||||
|
|
||||||
public class DiggingGameModel
|
public class DiggingGameModel
|
||||||
{
|
{
|
||||||
readonly game.PlayerDataStorage.IPlayerSaveStore saveStore = GContext.container.Resolve<game.PlayerDataStorage.IPlayerSaveStore>();
|
readonly Core.KV.IPlayerSaveStore saveStore = GContext.container.Resolve<Core.KV.IPlayerSaveStore>();
|
||||||
|
|
||||||
#region 协议Const区域
|
#region 协议Const区域
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 748d56d3a1b2489d85e081bfed580314
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
271
Client/Assets/Scripts/Services/AggKvUserService.cs
Normal file
271
Client/Assets/Scripts/Services/AggKvUserService.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Client/Assets/Scripts/Services/AggKvUserService.cs.meta
Normal file
2
Client/Assets/Scripts/Services/AggKvUserService.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: f3774b22ff7e4fd894180c42bed01400
|
||||||
@@ -13,17 +13,6 @@ namespace game
|
|||||||
|
|
||||||
public class AggUserService : PlayFabUserService
|
public class AggUserService : PlayFabUserService
|
||||||
{
|
{
|
||||||
public AggUserService() { }
|
|
||||||
internal AggUserService(PlayerDataStorage.PlayerKVControlledLoadCapability capability) : base(capability) { }
|
|
||||||
internal IReadOnlyList<PlayerDataStorage.PlayerKVIdentityChannel> KVLinkedAccounts { get; private set; } =
|
|
||||||
Array.Empty<PlayerDataStorage.PlayerKVIdentityChannel>();
|
|
||||||
internal override void ResetForKVLoad()
|
|
||||||
{
|
|
||||||
base.ResetForKVLoad();
|
|
||||||
KVLinkedAccounts=Array.Empty<PlayerDataStorage.PlayerKVIdentityChannel>();
|
|
||||||
}
|
|
||||||
internal override void ReplaceKVLinked(PlayerDataStorage.PlayerKVLoginState state)
|
|
||||||
{ KVLinkedAccounts=new List<PlayerDataStorage.PlayerKVIdentityChannel>(state.Linked).AsReadOnly(); }
|
|
||||||
protected override string NewPlayerName
|
protected override string NewPlayerName
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
@@ -56,15 +45,6 @@ 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)
|
||||||
{
|
{
|
||||||
#if UNITY_EDITOR && !SERVICE_HOST
|
|
||||||
if(game.PlayerDataStorage.PlayerKVGameplayLoop.Enabled)
|
|
||||||
{
|
|
||||||
CustomId=game.PlayerDataStorage.PlayerKVGameplayLoop.Permit.PlayerId;
|
|
||||||
OnInitData+=InitData;
|
|
||||||
try { return await LoginWithBackend(progress,"kv-loop"); }
|
|
||||||
finally { OnInitData-=InitData; }
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
var backend=CaptureSaveBackend();
|
var backend=CaptureSaveBackend();
|
||||||
if(RejectSaveBackend(backend)) return ELoginStep.Api;
|
if(RejectSaveBackend(backend)) return ELoginStep.Api;
|
||||||
var generation = logoutGeneration;
|
var generation = logoutGeneration;
|
||||||
@@ -107,7 +87,7 @@ namespace game
|
|||||||
|
|
||||||
if (!loginInfo.isSuccess) return ELoginStep.Sdk;
|
if (!loginInfo.isSuccess) return ELoginStep.Sdk;
|
||||||
|
|
||||||
progress?.Invoke(1);
|
progress.Invoke(1);
|
||||||
|
|
||||||
CustomId = loginInfo.StrUserId;
|
CustomId = loginInfo.StrUserId;
|
||||||
|
|
||||||
@@ -190,9 +170,8 @@ namespace game
|
|||||||
|
|
||||||
public override void Logout()
|
public override void Logout()
|
||||||
{
|
{
|
||||||
base.Logout();
|
|
||||||
if(IsControlledKVInstance) return;
|
|
||||||
TYSdkFacade.Instance.Logout();
|
TYSdkFacade.Instance.Logout();
|
||||||
|
base.Logout();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task<bool> GetLinkState(EAccoutType accoutType)
|
public override async Task<bool> GetLinkState(EAccoutType accoutType)
|
||||||
@@ -218,25 +197,8 @@ namespace game
|
|||||||
return (linkResult.code, linkResult.userId);
|
return (linkResult.code, linkResult.userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal int KVNotificationCount { get; private set; }
|
|
||||||
internal override async Task<PlayerDataStorage.PlayerKVGameLoadResult> LoadKVForValidationAsync(PlayerDataStorage.PlayerKVGameLoadCapability capability)
|
|
||||||
{
|
|
||||||
EnterKVReceiver(ReceiveKVData);
|
|
||||||
try { return await base.LoadKVForValidationAsync(capability); }
|
|
||||||
finally { ExitKVReceiver(); }
|
|
||||||
}
|
|
||||||
void ReceiveKVData(IDictionary<string,string> data)
|
|
||||||
{
|
|
||||||
if(!IsControlledKVInstance || KVLoadingCopy==null || data.Count!=KVLoadingCopy.Count)
|
|
||||||
throw new InvalidOperationException("Controlled notification window invalid.");
|
|
||||||
KVNotificationCount++;
|
|
||||||
}
|
|
||||||
private void InitData(IDictionary<string, string> data)
|
private void InitData(IDictionary<string, string> data)
|
||||||
{
|
{
|
||||||
#if UNITY_EDITOR && !SERVICE_HOST
|
|
||||||
// Editor loop has no channel SDK login. M31 already replaced account URLs and linked flags.
|
|
||||||
if(PlayerDataStorage.PlayerKVGameplayLoop.Enabled) return;
|
|
||||||
#endif
|
|
||||||
if (data.TryGetValue(EAccoutType.hwGoogle.ToString(), out var google) && google == "1")
|
if (data.TryGetValue(EAccoutType.hwGoogle.ToString(), out var google) && google == "1")
|
||||||
{
|
{
|
||||||
TYSdkFacade.TYAccountInfo.linkedAccout.Add(EAccoutType.hwGoogle);
|
TYSdkFacade.TYAccountInfo.linkedAccout.Add(EAccoutType.hwGoogle);
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user