Files
AIGame/Doc/NetProtocol.md
T

163 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 游戏服协议
## 生产
Base: `https://game.xworld.ren`(Caddy → 后端)
## 本地 AIServer(推荐开发)
路径:`AIServer/`(C++ + SQLite)
| Base | `http://127.0.0.1:8080` |
| WS | `ws://127.0.0.1:8080/ws?pid={pid}&token={token}` |
构建与运行见 [`AIServer/README.md`](../AIServer/README.md)。
## HTTP
| 方法 | 路径 | Body | 成功 | 失败 |
| --- | --- | --- | --- | --- |
| POST | `/register` | `{"username","password"}` | `{token,pid}` | `invalid_input` / `user_exists` |
| POST | `/login` | `{"username","password"}` | `{token,pid}` | `invalid_credentials` |
| GET | `/health` | — | `{ok,service,maintenance,gameProtocol:1}` | — |
| GET | `/maintenance` | — | `{enabled,message}` | — |
| GET | `/game/state` | — | 游戏快照、服务端时间、待完成活动 | 401 / 503 / 500 |
| POST | `/game/command` | 见下文 | 幂等操作结果和最新快照 | 400 / 401 / 409 / 503 / 500 |
### G1 教学玩法 API(本地已实现)
玩法接口统一使用 `Authorization: Bearer {token}`。玩家身份从 token 解析,Body 内的 `pid` 不参与寻址。维护期间读写玩法均返回 HTTP 503。此版本仅在本地 AIServer 联调通过,未部署生产服。
```json
{"requestId":"32位唯一请求号","action":"shot.begin","revision":0,"aim":5}
```
`requestId` 为 16~64 位字母、数字、下划线或短横线;客户端使用 GUID。`revision` 是最近一次快照的修订号。同一玩家、同一请求号、相同完整 JSON 内容再次提交,返回原操作结果及当前最新快照,并标记 `replayed:true`;对象字段顺序不影响比较。同一请求号更换内容返回 HTTP 409 `request_id_conflict`。已记录的业务拒绝也保持原结果,修正条件后必须创建新请求号。
| action | 额外字段 | 行为 |
| --- | --- | --- |
| `shot.begin` | `aim`:整数 -35~35 | 扣1弹珠,保存待结算活动,返回完整服务端轨迹与奖励 |
| `shot.finish` | `activityId`:begin 请求号 | 服务端到期后结算一次,首次及每十次赠券 |
| `capture` | 无 | 消耗1券;服务端随机并推进保底,返回 `result.id/pet/duplicate/tutorial/pity/shards/poolVersion` |
| `train` | 无 | 消耗 `30×level` 糖果;共享训练最高20级 |
| `evolve` | `pet`:0~4 | T1、Lv.5、碎片40、金币500;成长到T2 |
| `deploy` | `pet`:0~4,`slot`:0~1 | 唯一伙伴、占位交换,只开放中央路线 |
| `autoDeploy` | 无 | 恢复岩团团前排、火喵后排 |
| `stage.select` | `stageId`:1~5 | 保存选中关卡,只允许已解锁关卡 |
| `battle.begin` | 可选 `stageId`,省略/0使用已选关卡 | 检查解锁,冻结编队、成长和完整配置,服务端计算并保存战斗结果 |
| `battle.finish` | `activityId`:begin 请求号 | 校验活动与最短时间;服务端胜利才发一次首通奖 |
| `battle.abandon` | `activityId` | 立即结束,不发胜利奖励 |
| `goal.claim` | 无 | 通关后只发一次5弹珠 |
| `camp.place` | `item`:0~2,`slot`:0~2 | 通过1-5后购买并放置;已拥有则免费移动,目标被其他装饰占用时拒绝 |
| `camp.store` | `slot`:0~2 | 通过1-5后收回装饰,保留拥有记录,无扣费/退款 |
| `camp.interact` | `pet`:0~4 | 通过1-3后与已拥有伙伴互动,保存伙伴和次数,不发资源 |
| `daily.claim` | `item`:0~2,`day`:最近快照的 `dailyDay` | 领取已完成当日目标;跨日旧请求的新提交拒绝,已提交成功的原请求可重放 |
| `signin.claim` | `item`:0~6 | 领取累计登录已解锁项,逐项只发一次,已解锁未领奖励保留 |
| `shop.buy` | `item`:0~2,`day`:最近快照的 `dailyDay` | 按服务器价格领取/购买一份补给,校验解锁、余额和当日限购 |
| `chest.claimDaily` | 无 | 领取当日每日宝箱,只增加1个箱库存,不发箱内容 |
| `chest.open` | `chest`:0~2;自选箱另带 `choice`:0~4 | 扣除1个箱并在同一事务发放固定/自选内容 |
回应公共字段:`ok/error/replayed/action/state/serverTime/configVersion/config/result/capture/rewards`;读取时 `action` 为空,命令与重放响应保留动作名。`serverTime` 是 UTC Unix 秒。`config` 是唯一配置源 `Config/Gameplay/greenfield.json` 的内容,包含经济、弹珠、五只伙伴、捕捉池、敌人、五关波次与首通奖励、五元素关系;服务端 CMake 编译时嵌入,版本为 `greenfield-1-{源文件SHA256前12位}`,修改 JSON 后须重新构建。客户端使用返回配置,战斗使用开始时冻结配置,不能混用新旧数值。
`state` 对应客户端 `PrototypeSave`:当前schema=9,保留钱包、阵容、恢复锚点、修订号、首通/目标标志、`pendingShot/pendingBattle/lastShot/lastBattle`、`selectedStage` 和长度5的0/1数组 `clearedStages`。伙伴 `stages/shards` 长度为5,顺序为岩团团、火喵、芽芽、闪闪雀、泡泡龟;保留 `captureTutorialDone/captureCount/duplicateStreak`、营地及任务/签到字段。按顺序首次通过才解锁下一关,每关首通独立只发一次,重复战斗不发经济奖励;`cleared` 保留为1-1完成标志,用于原教学目标。
商店新增 `shopBought:[0,0,0]`,为当前 `dailyDay` 各商品已领取/购买次数,和每日任务在同一次日切事务清零。`config.shop.goods` 定义商品ID、名称/说明、金币价、每日上限、解锁关卡与 `{coins,candy,balls,tickets}` 奖励。当前ID0为免费5弹珠/每日1次;ID1为100金币换20糖果/1-1解锁/每日3次;ID2为300金币换1券/1-3解锁/每日1次。装饰仍使用`camp.place/store`,不从商店重复购买。
`shop.buy` 不接受客户端价格、奖励、解锁或库存数值,回执为 `{id,item,day,chargedCoins,reward:{coins,candy,balls,tickets}}`。金币扣除、原钱包到账、限购次数与请求日志同事务提交;新请求须携带当前日键,过期确认拒绝。原成功请求跨日重放保留原费用/奖励,不重新发放或消耗新日库存;客户端恢复“补给已到账”弹窗,标明原费用。打开确认、取消、分类切换均不提交消费请求。限购不按离线天数累积,时钟回退不刷新。免费补给可令弹珠超过自然恢复上限,但不改变原恢复规则。
背包没有新增库存接口或镜像余额,读取同一快照的`tickets/shards/decorations/campLayout`,钱包只展示原`coins/candy/balls`。当前物品均永久有效,捕捉、训练和装饰操作直接影响这些原字段;通用物品/批量使用/活动道具/宝箱协议尚未接入。
任务/签到新增 `dailyDay`、`dailyProgress:[0,0,0]`、`dailyClaimed:[0,0,0]`、`signinDays:1`、`signinClaimed:[0,0,0,0,0,0,0]`。公共响应附 `daily:{day,resetAt}`,`resetAt` 为下次重置的UTC Unix秒;`config.daily` 含 `utcOffsetSeconds:28800`、三项任务目标/奖励与七项签到奖励。服务器日键为 `floor((now+28800)/86400)`;首次进入当天的玩法读写事务会清空上日每日进度/领取标志,并将累计登录增加1(最多7),跳过多天仍只增加1。时钟回退时保留已记录的较大日键,不回滚标志或多计登录。
只有首次执行成功的 `shot.finish`、正常 `battle.finish`(胜/负均可)、`camp.interact` 分别增加三项进度,封顶3/1/1。`shot.begin/battle.begin`、主动放弃、页面打开、重放和客户端自报进度不计入;跨日活动按实际结算日计数。每日奖励未领取则在重置后失效,`daily.claim` 的 `day` 必须匹配当前日键;旧日已成功请求重放仍返回原回执,不改写新日进度/领取标志。签到按累计登录日解锁,不要求连续登录;已解锁项任意顺序领取,无付费补签和七日循环。
`daily.claim/signin.claim` 回执为 `{id,item,reward:{coins,candy,balls}}`,每日任务另含 `day`;`goal.claim` 新回执也携带 `id/reward`。领取标志、钱包和请求日志同事务提交;写入失败整体回滚,同一请求重试只返回原奖励,客户端恢复“奖励已领取”弹窗并清理已确认队列。奖励允许弹珠超过恢复上限,恢复规则不另改。旧版本已保存的教学回执可没有reward,客户端按原教学奖励兼容展示。
营地新增 `decorations:[0,0,0]` 拥有标记、`campLayout:[-1,-1,-1]` 固定位置(值为装饰ID,-1为空)、`restPet:0` 和 `campInteractions:0`。同一装饰最多放置一次,收回后进入仓库。`config.camp` 定义解锁关卡、槽数、三件装饰的名称/说明/价格;以服务端价格扣费。`camp.place/store` 回执为 `{id,item,slot,chargedCoins}`,购买/移动/收回都与钱包和日志同事务提交;重放返回原费用和原结果。预览与撤回仅改变界面,不发消费请求。确认后丢失响应时保留请求重试,客户端展示“营地已更新”回执。营地操作沿用修订号、鉴权与在途活动互斥规则。
弹珠树共用 `state.balls/ballRecoveryUnix` 与原 `recover` 规则,没有额外生产/领取接口;显示的倒计时用最近服务端时间加客户端单调时间推算,余额只在服务端快照确认后更新。伙伴互动只更新选择和累计次数,客户端驱动已制作肖像节点的缩放反馈;无资源产出。
`capture` 为当前快照的池展示:`tutorial/completed/pityReady/count/duplicateStreak/entries`,每项为 `{pet,baseProbability,probability}`,概率是0~1的小数。当前普通池五种等权,各20%;首次教学固定芽芽,不推进普通次数或保底。普通捕捉连续4次重复后,下次仅在未拥有伙伴中按权重抽取;获得新伙伴清零重复计数,集齐后保底暂停、重复计数归零。重复发20对应物种碎片。扣券、随机结果、次数、保底、碎片和操作日志同事务提交,客户端不能指定结果或随机种子。
最近一次已提交捕捉附 `lastCaptureResult`,结构与捕捉 `result` 相同;不存在时省略,不把默认pet=0视为历史结果。重放返回原回执及最新快照,不重新抽取;客户端恢复待确认的 `capture` 后展示原结果。`poolVersion` 是抽取时的配置版本,`id` 是原请求号。
尚未完成的弹珠附 `shot:{id,readyAt,path:[{x,y}],bin,candy,coins}`;挑战附 `battle:{id,readyAt,snapshot:{level,stages,formation,stageId,config,legacy}}`。最后一次战斗结果附 `lastBattleResult:{id,stageId,won,firstClear,duration,durability,defeated,reward:{coins,candy,balls}}`。`reward` 是该关首通基准,只有 `firstClear=true` 时实际发放。
schema=1/2服务端存档在首次读写事务内迁移到3。schema=1旧 `cleared=true` 只映射为1-1已通关;schema=2既有关卡进度不变。保留余额、目标、前三伙伴的拥有/进化/碎片和阵容,新增两种伙伴初始未拥有;已拥有芽芽视为完成教学,普通捕捉/保底从0开始,不补发伙伴或奖励。旧未结算弹珠继续用已保存轨迹/奖励;G1旧挑战标为 `legacy:true`,按原G1表现与原80/40/3奖励结算;G2三伙伴挑战保留冻结的三伙伴配置和原关卡奖励,新五伙伴配置不改写在途活动。未知未来schema拒绝处理,不重置账号。客户端PlayerPrefs的G0余额仍不迁入服务端。
上述schema1/2迁移后继续执行3→4迁移;既有schema3保留钱包、捕捉次数/保底/回执、伙伴、关卡和在途活动,初始化空装饰库存/布局、默认休息伙伴0和互动次数0,不扣费也不补发奖励。该阶段得到schema4,随后继续下述任务迁移。旧冻结战斗配置允许没有 `camp` 字段,新实时配置必须带营地数据。
4→5迁移保留全部钱包、教学/关卡、捕捉、营地、回执和在途活动;初始化新的每日进度/领取标志及七日领取标志,当次访问登记为首个累计登录日,不追溯旧发射/战斗/互动总数,不自动发签到奖励。该阶段得到schema5,随后继续下述商店迁移。旧冻结战斗配置允许没有 `daily` 字段,按保存的战斗数值与奖励继续结算。
商店阶段迁移执行5→6:只新增空的`shopBought`,不自动领补给、不清空每日/签到/捕捉/营地或钱包;旧未完成活动与回执保留。旧冻结战斗配置允许缺少`shop`字段,战斗仍按原配置与奖励结算。随后执行下述邮件迁移与宝箱迁移,再执行阵容扩展,当前服务端实时快照为schema9;客户端兼容schema6~9服务滚动更新,低于对应版本时邮件/宝箱入口提示稍后重进。
邮件扩展(2026-10-18):6→7只新增`mails`,按`config.mail.initial`保存初始通知/附件,`sentAt=now`、`expiresAt=now+lifetimeSeconds`。新旧账号首次迁移均只建记录,不自动发附件;schema7后读取不补发、不重建邮件。原钱包、伙伴、营地、捕捉、日常、购买次数、回执与在途活动保留。旧冻结战斗无需mail字段,仍按保存数值与奖励结算;实时schema7要求完整mail配置。
`state.mails`最多100封,每封为`{mailId,title,body,reward:{coins,candy,balls,tickets},sentAt,expiresAt,read,claimed,expired}`。ID在账号内唯一,服务端时间达到`expiresAt`即过期,已记录的`expired`不因时钟回退复活。到期和已领取记录仍保留供阅读;首页红点仅表示有效未读邮件或有效未领附件,客户端倒计时使用服务端时间加单调时钟,实际可领性仍以服务端为准。
- `mail.read`携带`mailId`:只标已读,不发附件,回执`{id,mailId}`。
- `mail.claim`携带`mailId`:领取保存的附件,忽略客户端奖励;同时标记已读/已领取,回执`{id,mailId,claimedCount:1,reward}`。
- `mail.claimAll`携带1~20个不重复的`mailIds`:逐封处理,回执`{id,claimedCount,reward,entries:[{mailId,status}]}`。`status`为`claimed/expired/already_claimed/no_attachment/not_found`,失效邮件不阻止有效附件到账;参数重复、空数组、超容量或错误类型整单拒绝为`invalid_input`。
邮件操作继续使用鉴权、revision和在途活动互斥。单封业务错误为`mail_not_found/mail_expired/mail_already_claimed/mail_no_attachment`;拒绝也持久化,条件变化须新请求。钱包、标志、批量结果及操作日志在同一事务提交;存储失败全部回滚。原成功请求在重启、跨日或邮件到期后重放仍返回原结果,不二次发附件。客户端仅在完整有效快照和匹配的邮件回执确认后清理请求,并恢复领取结果弹窗。
当前初始邮件为7天有效的欢迎补给(糖果20、弹珠5)和无附件营地通知,属于本地开发基线;尚无运营投递/群发/删除邮件接口,修改initial不会向已有schema7账号自动追加新邮件。
宝箱扩展(2026-10-19):7→8只新增`chests:[0,0,0]`与`chestDailyClaimed:0`,保留原钱包、伙伴、关卡、捕捉、营地、日常、购买次数、邮件、回执与在途活动;迁移不发放任何免费宝箱。`config.rewards`定义三种箱(`id/name/kind/description/contents/choices`)、每日箱ID与首通掉落表。当前ID0「绿野补给箱」固定内容金币60/糖果30/弹珠2,同时作为每日宝箱;ID1「伙伴碎片自选箱」为自选箱,五个选项各对应一种伙伴碎片×10;ID2「溪口大礼箱」固定内容金币150/糖果80/弹珠5/捕捉券1。首通掉落:1-1→箱0、1-3→箱1、1-5→箱2,每次首通只掉一个,重复挑战不再掉落。
箱内容使用`{coins,candy,balls,tickets,shardsPet,shards}`:`shards>0`时`shardsPet`为伙伴ID并把碎片计入`state.shards`,否则`shardsPet=-1`且`shards=0`。公共响应附`rewards:{dailyClaimed}`,与`state.chestDailyClaimed`一致。
- `chest.claimDaily`只把每日箱加入库存(回执`{id,chest,day,count:1}`),不发放箱内容;每服务器日一次,与商店限购共用UTC+8日切,时钟回退不刷新。
- `chest.open`回执`{id,chest,kind,choice?,remaining,chestReward}`,`remaining`为同类箱剩余数量;固定箱忽略`choice`,自选箱缺少或越界的选择返回`invalid_choice`。扣箱、发奖(含碎片)与操作日志同事务提交;失败整体回滚,同一请求重放返回原回执,不重复发放。
宝箱业务错误为`invalid_chest/chest_missing/chest_claimed/invalid_choice`,沿用鉴权、revision与在途活动互斥。`battle.finish`首通回执在掉落时新增`chest`字段表示获得的箱ID。客户端校验回执ID、箱ID与箱内容;JsonUtility会把缺失的嵌套奖励物化为全零对象,因此打开的箱必须包含至少一项非零奖励,每日箱回执则不得携带非零奖励。
阵容与关卡扩展(2026-10-20):8→9只把`stages/shards`扩到10项、`clearedStages`扩到20项,新伙伴全部未拥有、新关卡全部未通关,不发放伙伴、关卡首通奖或宝箱。`config.pets`现为10只:岩团团、火喵、芽芽、闪闪雀、泡泡龟、岩甲犀(岩·控制·减速)、烈焰狮(火·输出·双目标)、藤蔓灵(草·支援·治疗)、雷鸣豹(电·输出·双目标)、潮汐鲸(水·守护·护盾)。捕捉池权重十项等权10%,教学仍固定芽芽;自选箱十个选项各对应一种伙伴碎片×10。
`config.stages`现为20关,每关新增`chapter`:1-1~1-10为「绿野初程」,2-1~2-10为「溪谷守望」。每关仍为三波九个敌人、固定五类敌人;`hpScale`由1.0递增到4.0、`attackScale`由1.0递增到2.35,1-5起三路线。首通掉落扩展为1-1→箱0、1-3→箱1、1-5→箱2、1-10→箱1、1-15→箱0、1-20→箱2,仍为每关首通只掉一次。
`autoDeploy`改为按配置选择:前排优先射程最短的已拥有伙伴,后排优先射程最长的,按路线依次填充,不再依赖固定的五个伙伴ID。`camp.interact`与`evolve`的伙伴边界改为`config.pets`长度。
成功与业务拒绝均返回 HTTP 200,业务拒绝 `ok:false`,同时给出最新快照。常见错误:`stale_revision`、`pending_activity`、`insufficient_balls/candy/tickets`、`evolve_requirements`、`invalid_formation`、`activity_missing/not_ready`、`goal_incomplete/claimed`、`level_cap`、`stage_locked`、`invalid_stage`。格式错误400、鉴权失败401、请求号冲突409、维护503和存储失败500不确认业务操作;客户端保留未确认请求重试。
SQLite `game_states` 与 `game_operations` 在同一 `BEGIN IMMEDIATE` 事务中写入。请求号按玩家唯一,扣费与日志同时提交;写入失败整体回滚。客户端请求队列只保存原始操作、不保存 token,按端点与玩家隔离;401 会重新登录同一游客身份,再提交同一请求。客户端收到完整有效快照才清理请求队列,失败时禁用业务操作并提供重试。禁止回退到 G0 本地钱包。
营地业务错误包括 `camp_build_locked/camp_rest_locked`、`invalid_decoration/invalid_camp_slot/invalid_pet`、`camp_slot_occupied/camp_slot_empty`、`insufficient_coins`;缺失/错误类型字段返回已记录的 `invalid_input` 业务拒绝。
任务/签到业务错误包括 `daily_expired`、`task_incomplete`、`reward_claimed`、`signin_locked`、`invalid_task/invalid_signin`;同样持久化业务拒绝,条件变化后须新建请求号。领取沿用在途活动互斥、鉴权与修订号规则。
商店业务错误包括 `shop_expired`、`shop_limit`、`goods_locked`、`invalid_goods`、`insufficient_coins`,同样沿用修订号、在途活动互斥与持久业务拒绝;参数类型错误返回`invalid_input`业务拒绝。条件变化后重新确认并使用新请求号。
弹珠只能按服务端轨迹播放;奖励不接受客户端数值。战斗在开始时由服务端按冻结快照计算结果,完成请求不接受客户端 `won`;最早结算时间为模拟时长的一半(最高2倍速)。暂停或重进时可以重新播放已授权挑战,不再创建新挑战。此时间门槛不验证逐帧操作,当前仅适用没有战中决策的教学自动战斗;加入技能输入或新战斗规则时须扩展验证协议。
## WebSocket
```
/ws?pid={pid}&token={token}
```
- 无 token / token 无效 → HTTP 401
- 应用层:`{"op":"hello"}` → `{"op":"hello_ack","ok":true}`;`{"op":"ping"}` → `{"op":"pong"}`
- 维护:若 `data/maintenance.json` 中 `enabled=true`,升级后立刻下发 `{"op":"maintenance","message":"..."}` 并断开
- 协议层 Ping/Pong 由服务端应答
## 维护开关
文件:`AIServer/data/maintenance.json`(与 `--db` 同目录,可用 `--maintenance` 覆盖)
```json
{ "enabled": true, "message": "今晚 2:00-4:00 维护" }
```
| GET | `/maintenance` | — | `{enabled,message}` |
| GET | `/health` | — | 增加 `maintenance` 字段 |
CDN `version.json.notice` 也会在客户端启动时弹公告(不依赖游戏服)。
## 客户端
- `HotUpdate.Net.AuthApi` / `GameNet` / `WsClient`
- Boot:`HttpBase` + `WsUrlTemplate`
- 菜单:**AIGame/Boot/Server/Local AIServer :8080** 或 Production
- 编辑器自动 `guest_{deviceId}` 账号