Skip to content

05 · 玩家数据、物品与图书馆 ​

登录成功后客户端会立刻并发拉几路数据。这些接口大多只要返回结构正确、内容可以为空,但有两个必须给真数据:卡牌库(否则收藏界面空白、卡组编辑器打不开)和 server_options(第 3 章已讲)。

卡牌库 /players/{id}/library ​

返回全量卡牌清单。客户端据此渲染收藏、判断"你有几张"、以及在卡组编辑器里校验卡组码。

官服实测的响应外层是对象(cards + new_cards),条目以 card_type 为主键:

jsonc
// GET /players/{id}/library
{
  "cards": [
    { "card_type": "card_unit_1st_infantry", "count": 40, "gold_card_count": 0,
      "player_id": 1, "recently_crafted_count": 0 }
  ],
  "new_cards": []
}
字段含义
card_type卡牌资产名(card_unit_* / card_event_*)——条目主键
count普通卡数量(40 就是"全都有",参考实现统一给 40)
gold_card_count金卡数量
player_id该行归属的玩家
recently_crafted_count近期合成数(用于 UI 高亮,可全 0)

条目里没有数字 id,是 player_id

本文早期示例给过 "id": 1024,那是误读:官服条目只有 card_type 资产名与 player_id,没有卡牌数字 id。数字编码只存在于客户端内置的卡组码映射表里(见下),不需要出现在接口响应中。外层 { "cards": […], "new_cards": [] } 形态与官服一致。

id 必须和卡组码表对得上

这是最容易出错的地方:库里 id 与客户端内置的卡牌表不一致,卡组编辑器会显示"未知卡牌",甚至直接崩。采集方式:从客户端导出 deckCodeIDsTable(fyserver 用的是 library/deckCodeIDsTable2.json,TS 版直接内联成 src/library.ts,Go 版用 //go:embed 塞进二进制),它是 {deck_code_id, card, ID} 的数组:

jsonc
[ { "deck_code_id": "1a", "card": "card_unit_1st_infantry", "ID": 1024 }, … ]

两个接口名都实现更稳(老版本客户端会请求 librarynew)。但要注意:官服上并不存在 /librarynew(实测 404),它只是参考实现为旧客户端准备的兼容别名:

csharp
app.MapGet("/players/{id}/library",    (string id, PlayerLibraryService lib) => Results.Ok(lib.Library));
app.MapGet("/players/{id}/librarynew", (string id, PlayerLibraryService lib) => Results.Ok(lib.Library));
typescript
@Get('players/:id/library')
library(@Param('id') id: string) {
  return { cards: LIBRARY, new_cards: [] };   // LIBRARY 由 deckCodeIDsTable 生成
}

:::

物品与装备 /items/{id} ​

item 是桌饰、头像、卡背、表情这类装饰。返回全量库 + 玩家已装备项:

jsonc
// GET /items/{id}
{
  "date": "2025-07-06 04:06:03",
  "equipped_items": [ { "faction": "germany", "item_id": "cardback_germany_1", "slot": "cardback" } ],
  "items": [ { "details": "{}", "item_id": "emote_appreciate", "cnt": 0 } ]
}

装备时客户端把整个对象 POST 回来,服务端保存即可:

jsonc
// POST /items/{id}
{ "faction": "germany", "item_id": "cardback_germany_1", "slot": "cardback" }
// → 200 { "player_id": "1", "equipped_items": [...] }

去重必须按 (slot, faction) 而不是只按 slot

每个国家各有独立的卡背/桌面装饰槽。Go 实现的判断是关键细节:

go
for _, eq := range user.EquippedItems {
    if !(eq.Slot == item.Slot && eq.Faction == item.Faction) {   // slot 和 faction 都相同才删
        newEquipped = append(newEquipped, eq)
    }
}

C# 版只按 slot 去重(RemoveAll(i => i.Slot == item.Slot))——切换国家时会把别国的装备顺手清掉。建议按 (slot, faction) 实现。

心跳与在线状态 ​

http
PUT /players/{id}/heartbeat      → {}
DELETE /players/{id}/heartbeat   → {}   # 下线

两个方法都注册到同一路由,返回空对象即可:

csharp
app.MapMethods("/players/{id}/heartbeat", new[] { "PUT", "DELETE" }, (string id) => Results.Ok(new EmptyResponseDto()));
typescript
@Put('players/:id/heartbeat')  @Delete('players/:id/heartbeat')
heartbeat() { return {}; }

真正的在线状态由 WebSocket 连接维护(第 9 章):连着就是在线,断开就下线并判负。心跳只是保活,不影响业务。

其余"占位即可"的接口 ​

这些接口客户端会请求,但返回结构对就行,内容空着不影响进游戏:

接口返回
GET /{a}/players/{id}/friends{ "friends": [], "previous_opponents": [] }
GET /entitlements/{id}[ { "entitlement_type": "emote", "name": "emote_appreciate" } ]
PUT DELETE /players/notifications/{id}{}
GET /players/{id}/packs未开卡包列表(空数组即可)
GET /store/v2/(旧版 /store/)商店,见下
POST /store/v2/txn(旧版 /store/txn)空 200
GET /fp/首页配置,{} 即可

注意 friends 路径里的 /{a}/ 段

真实路径形如 /{任意段}/players/{id}/friends,前缀段是客户端自带的(不同版本不同)。用通配段匹配,不要写死:

csharp
app.MapGet("/{a}/players/{player_id}/friends", (string a, string player_id) =>
    Results.Ok(new FriendsReponse(new List<int>(), new List<int>())));

商店的最小形态 ​

商店接口的响应形状固定,内容来自你自己的配置:

jsonc
{
  "currency": "USD",
  "groups":        [ … ],     // 商品分组
  "always_featured": [ … ],   // 常驻推荐位
  "message": "Offers for 2025-07-06T04:06:03.000000Z",
  "status": 200,
  "ts": 1751774763.5          // Unix 秒(带小数)
}

旧版本客户端请求的是 /store/(没有 v2)、下单走 /store/txn,两条都注册上,成本极低但能省掉一轮"客户端黑屏"排查。

开箱(PUT /crate/claim)返回的奖励结构里两处字段是 camelCase,与全局的 snake_case 策略不同:

jsonc
{
  "dust": 1000,
  "items": [
    { "data": { "dust": 1000, "isGold": null, "itemType": "card", "name": "card_wildcard_elite" }, "qty": 5 },
    { "data": { "dust": 1000, "isGold": null, "itemType": "gold", "name": "" }, "qty": 30 }
  ]
}
序列化策略:全局 snake_case + 局部覆盖

fyserver 的做法值得照抄:全局用 JsonNamingPolicy.SnakeCaseLower,个别字段用特性覆盖:

csharp
public record ClaimItemData(
    int Dust,
    [property: JsonPropertyName("isGold")]   bool? IsGold,
    [property: JsonPropertyName("itemType")] string ItemType,
    string Name
);

TS 侧用装饰器或直接返回对象字面量;Go 侧用 json:"isGold" tag。别为了几个字段把全局策略改掉,否则 equipped_items 会变成 equippedItems,客户端直接解析失败。


数据接口齐了,下一章做卡组——包括那个自成一体的卡组码格式。