修正 M3 族谱口径注释并抽出图装配纯函数(第二批)
- 修正 spacetime-module 里 get_game_distribution_lineage_and_return 的文档注释:锚点不存在/已软删除/未公开一律 found=false(api-server 映射 404),并写明理由——用空标题占位或空树代替 404 等于确认该 gameId 的存在、代际与血缘位置 - 修正 shared-contracts 里节点 status 的注释:当前只有已公开作品会成为节点(未公开节点被过滤、未公开锚点直接 404),线上取值实际恒为 published;保留该字段是为将来允许非公开节点进入树时不必再改契约 - 新增纯函数 assemble_lineage_tree_entries:家族归属校验(只保留 root_game_id 命中的血缘行)、根行以 0 代入集合、游戏行缺失的行安全跳过、重复行去重(根行优先);module 侧只负责取行与按根查索引 - 新增纯函数 resolve_lineage_author_name:账号昵称优先(trim 后非空)→ 退回发布时冻结的作者名 → 两者都空白则 None(不占位) - 新增纯函数 lineage_entry_from_game_facts:族谱装配与衍生列表共用同一份字段映射 - module 删除 game_distribution_lineage_entry,衍生列表 tx 改用共用构造函数并复用同一份行事实折叠 - 新增 6 条单测:整族装配、旁支排除、缺行跳过、无根行回退、重复行去重、作者名归一三态 - 本批未改前端;package.json 与 lineage e2e 脚本属第三个 session,未触碰
This commit is contained in:
@@ -30,8 +30,10 @@ pub use domain::{
|
||||
pub use errors::{GameDistributionError, GameDistributionFieldError};
|
||||
pub use events::GameDistributionEvent;
|
||||
pub use lineage::{
|
||||
GAME_DISTRIBUTION_LINEAGE_NODE_LIMIT, LineageNodeList, LineageTree, LineageTreeEntry,
|
||||
LineageTreeNode, build_derived_node_list, build_lineage_tree, lineage_anchor_readable,
|
||||
GAME_DISTRIBUTION_LINEAGE_NODE_LIMIT, LineageGameFacts, LineageLink, LineageNodeList,
|
||||
LineageResolvedLink, LineageTree, LineageTreeEntry, LineageTreeNode,
|
||||
assemble_lineage_tree_entries, build_derived_node_list, build_lineage_tree,
|
||||
lineage_anchor_readable, lineage_entry_from_game_facts, resolve_lineage_author_name,
|
||||
};
|
||||
pub use package::{
|
||||
MAX_COMPRESSION_RATIO, MAX_EXPANDED_BYTES, MAX_FILE_BYTES, MAX_FILE_COUNT, MAX_PACKAGE_BYTES,
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
//! 展示层遇到「父不在可见集合里」就按 M1 已确立的口径标注「原作品已不可用」,
|
||||
//! 不猜测、不回显父作品内容。
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::domain::{can_serve_as_fork_source, counts_as_public_derivative};
|
||||
|
||||
/// 单次响应最多返回的树节点数;超出时如实标注 `truncated`,不静默丢弃。
|
||||
@@ -132,6 +134,116 @@ pub fn build_derived_node_list(entries: &[LineageTreeEntry], limit: usize) -> Li
|
||||
LineageNodeList { nodes, truncated }
|
||||
}
|
||||
|
||||
/// 游戏行的事实:调用方从游戏表取出后折叠成这些字段,不携带任何 `ReducerContext` 依赖。
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct LineageGameFacts {
|
||||
pub game_id: String,
|
||||
pub title: String,
|
||||
pub author_name: Option<String>,
|
||||
pub play_count: u64,
|
||||
pub is_deleted: bool,
|
||||
pub is_published: bool,
|
||||
pub status: String,
|
||||
pub created_at_micros: i64,
|
||||
}
|
||||
|
||||
/// 血缘行的事实:谁改编自谁、属于哪个根、第几代。
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct LineageLink {
|
||||
/// 子作品。
|
||||
pub game_id: String,
|
||||
pub parent_game_id: String,
|
||||
pub root_game_id: String,
|
||||
pub generation: u32,
|
||||
}
|
||||
|
||||
/// 血缘行 + 它指向的游戏行事实;游戏行缺失(脏数据 / 历史遗留)时 `game` 为 `None`。
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct LineageResolvedLink {
|
||||
pub link: LineageLink,
|
||||
pub game: Option<LineageGameFacts>,
|
||||
}
|
||||
|
||||
/// 把「根作品行 + 以该根为祖先的血缘行」装配成族谱树输入行。
|
||||
///
|
||||
/// 这一层是树形状正确性的根,规则全部收在这里并脱离 SpacetimeDB 可单测:
|
||||
/// - **家族归属**:只保留 `root_game_id` 命中的血缘行(调用方已按根索引取过一次,这里是显式
|
||||
/// 不变量,避免将来换查询写法时把旁支混进树里);
|
||||
/// - **根自身也在集合里**:根行以 0 代、无父进入;根行缺失时不占位(由展示层判「母版不可用」);
|
||||
/// - **游戏行缺失就跳过**:不发只有 ID 的半残节点,也不 panic;
|
||||
/// - **重复行去重**:同一作品只保留第一次出现,根行优先于血缘行。
|
||||
pub fn assemble_lineage_tree_entries(
|
||||
root_game_id: &str,
|
||||
root: Option<LineageGameFacts>,
|
||||
links: &[LineageResolvedLink],
|
||||
) -> Vec<LineageTreeEntry> {
|
||||
let mut entries = Vec::new();
|
||||
if let Some(root) = root {
|
||||
entries.push(lineage_entry_from_game_facts(root, 0, None));
|
||||
}
|
||||
for resolved in links {
|
||||
if resolved.link.root_game_id != root_game_id {
|
||||
continue;
|
||||
}
|
||||
let Some(game) = resolved.game.clone() else {
|
||||
continue;
|
||||
};
|
||||
entries.push(lineage_entry_from_game_facts(
|
||||
game,
|
||||
resolved.link.generation,
|
||||
Some(resolved.link.parent_game_id.clone()),
|
||||
));
|
||||
}
|
||||
let mut seen = HashSet::new();
|
||||
entries.retain(|entry| seen.insert(entry.game_id.clone()));
|
||||
entries
|
||||
}
|
||||
|
||||
/// 族谱节点的作者名:账号昵称优先(非空才用),否则退回发布时冻结的作者名;
|
||||
/// 两者都是空串或纯空白时返回 `None`——节点上没有可渲染的名字就不占位。
|
||||
///
|
||||
/// 注意与 M1 的 `fork_lineage_visible_identity` 的差异:那个函数刻意保留作者名三态
|
||||
/// (`Some("")` / `Some(" ")` 原样透传),因为溯源卡由展示层按 falsy 判断是否渲染
|
||||
/// 「由 X 制作」,两种取值渲染结果等价;节点侧则不该把空串当成一个名字发出去。
|
||||
pub fn resolve_lineage_author_name(
|
||||
account_display_name: Option<&str>,
|
||||
frozen_author_name: Option<&str>,
|
||||
) -> Option<String> {
|
||||
if let Some(display_name) = account_display_name {
|
||||
let trimmed = display_name.trim();
|
||||
if !trimmed.is_empty() {
|
||||
return Some(trimmed.to_string());
|
||||
}
|
||||
}
|
||||
frozen_author_name
|
||||
.map(str::trim)
|
||||
.filter(|name| !name.is_empty())
|
||||
.map(str::to_string)
|
||||
}
|
||||
|
||||
/// 由行事实 + 血缘关系构造一条树输入行。
|
||||
///
|
||||
/// 公开给调用方复用:族谱树走 [`assemble_lineage_tree_entries`],衍生列表则由调用方按
|
||||
/// `parent_game_id` 取直接子代后逐条调用本函数——两处的字段映射因此只有一份。
|
||||
pub fn lineage_entry_from_game_facts(
|
||||
facts: LineageGameFacts,
|
||||
generation: u32,
|
||||
parent_game_id: Option<String>,
|
||||
) -> LineageTreeEntry {
|
||||
LineageTreeEntry {
|
||||
game_id: facts.game_id,
|
||||
title: facts.title,
|
||||
author_name: facts.author_name,
|
||||
generation,
|
||||
parent_game_id,
|
||||
play_count: facts.play_count,
|
||||
is_deleted: facts.is_deleted,
|
||||
is_published: facts.is_published,
|
||||
status: facts.status,
|
||||
created_at_micros: facts.created_at_micros,
|
||||
}
|
||||
}
|
||||
|
||||
/// 稳定排序:代际升序 → 创建时间升序 → gameId 升序。
|
||||
///
|
||||
/// 最后一级是按 ID 的确定性兜底,避免同代同刻的作品在两次读之间换序(截断点会随之漂移)。
|
||||
@@ -422,4 +534,143 @@ mod lineage_tree_tests {
|
||||
GAME_DISTRIBUTION_LINEAGE_NODE_LIMIT
|
||||
);
|
||||
}
|
||||
|
||||
fn facts(game_id: &str, is_deleted: bool, is_published: bool) -> LineageGameFacts {
|
||||
LineageGameFacts {
|
||||
game_id: game_id.to_string(),
|
||||
title: format!("作品 {game_id}"),
|
||||
author_name: Some(format!("作者 {game_id}")),
|
||||
play_count: 1,
|
||||
is_deleted,
|
||||
is_published,
|
||||
status: if is_published {
|
||||
"published"
|
||||
} else {
|
||||
"unpublished"
|
||||
}
|
||||
.to_string(),
|
||||
created_at_micros: 10,
|
||||
}
|
||||
}
|
||||
|
||||
fn link(game_id: &str, parent: &str, root: &str, generation: u32) -> LineageResolvedLink {
|
||||
LineageResolvedLink {
|
||||
link: LineageLink {
|
||||
game_id: game_id.to_string(),
|
||||
parent_game_id: parent.to_string(),
|
||||
root_game_id: root.to_string(),
|
||||
generation,
|
||||
},
|
||||
game: Some(facts(game_id, false, true)),
|
||||
}
|
||||
}
|
||||
|
||||
fn assembled_ids(entries: &[LineageTreeEntry]) -> Vec<&str> {
|
||||
entries.iter().map(|entry| entry.game_id.as_str()).collect()
|
||||
}
|
||||
|
||||
/// 装配是树形状正确性的根:根行进集合(0 代、无父),同根的整族(各分支 + 深层后代)
|
||||
/// 都进去,且后代保留自己的代际与父引用。
|
||||
#[test]
|
||||
fn assembly_keeps_root_and_whole_family_of_its_root() {
|
||||
let entries = assemble_lineage_tree_entries(
|
||||
"root",
|
||||
Some(facts("root", false, true)),
|
||||
&[
|
||||
link("child-a", "root", "root", 1),
|
||||
link("child-b", "root", "root", 1),
|
||||
link("grandchild", "child-a", "root", 2),
|
||||
],
|
||||
);
|
||||
assert_eq!(
|
||||
assembled_ids(&entries),
|
||||
vec!["root", "child-a", "child-b", "grandchild"]
|
||||
);
|
||||
assert_eq!(entries[0].generation, 0, "根自身是 0 代");
|
||||
assert_eq!(entries[0].parent_game_id, None, "根自身没有父");
|
||||
let grandchild = entries
|
||||
.iter()
|
||||
.find(|entry| entry.game_id == "grandchild")
|
||||
.expect("深层后代应进入集合");
|
||||
assert_eq!(grandchild.generation, 2);
|
||||
assert_eq!(grandchild.parent_game_id.as_deref(), Some("child-a"));
|
||||
}
|
||||
|
||||
/// 不属于该根的血缘行一律排除,避免把旁支混进树里。
|
||||
#[test]
|
||||
fn assembly_excludes_links_from_other_roots() {
|
||||
let entries = assemble_lineage_tree_entries(
|
||||
"root",
|
||||
Some(facts("root", false, true)),
|
||||
&[
|
||||
link("child-a", "root", "root", 1),
|
||||
link("outsider", "other-root", "other-root", 1),
|
||||
],
|
||||
);
|
||||
assert_eq!(assembled_ids(&entries), vec!["root", "child-a"]);
|
||||
}
|
||||
|
||||
/// 游戏行缺失(脏数据 / 历史遗留)时跳过该行:不 panic,也不产生只有 ID 的半残节点。
|
||||
#[test]
|
||||
fn assembly_skips_links_whose_game_row_is_missing() {
|
||||
let mut broken = link("ghost", "root", "root", 1);
|
||||
broken.game = None;
|
||||
let entries = assemble_lineage_tree_entries(
|
||||
"root",
|
||||
Some(facts("root", false, true)),
|
||||
&[broken, link("child-a", "root", "root", 1)],
|
||||
);
|
||||
assert_eq!(assembled_ids(&entries), vec!["root", "child-a"]);
|
||||
assert!(entries.iter().all(|entry| entry.game_id != "ghost"));
|
||||
}
|
||||
|
||||
/// 根行缺失(历史数据)时仍装配出后代:根不占位,由展示层按 `rootGameId` 判「母版不可用」。
|
||||
#[test]
|
||||
fn assembly_works_without_the_root_row() {
|
||||
let entries =
|
||||
assemble_lineage_tree_entries("root", None, &[link("child-a", "root", "root", 1)]);
|
||||
assert_eq!(assembled_ids(&entries), vec!["child-a"]);
|
||||
}
|
||||
|
||||
/// 重复行去重且保留第一次出现:同一作品既有根行又有血缘行时,根的身份(0 代、无父)优先。
|
||||
#[test]
|
||||
fn assembly_dedupes_keeping_the_first_occurrence() {
|
||||
let entries = assemble_lineage_tree_entries(
|
||||
"root",
|
||||
Some(facts("root", false, true)),
|
||||
&[
|
||||
link("root", "someone", "root", 3),
|
||||
link("root", "someone", "root", 3),
|
||||
],
|
||||
);
|
||||
assert_eq!(assembled_ids(&entries), vec!["root"]);
|
||||
assert_eq!(entries[0].generation, 0, "根行优先,不被血缘行改写");
|
||||
assert_eq!(entries[0].parent_game_id, None);
|
||||
}
|
||||
|
||||
/// 作者名归一:账号昵称优先(并 trim)→ 退回冻结名 → 两者都空白则 `None`(节点不占位)。
|
||||
#[test]
|
||||
fn author_name_prefers_account_then_frozen_then_none() {
|
||||
assert_eq!(
|
||||
resolve_lineage_author_name(Some("账号昵称"), Some("冻结名")),
|
||||
Some("账号昵称".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_lineage_author_name(Some(" 账号昵称 "), Some("冻结名")),
|
||||
Some("账号昵称".to_string()),
|
||||
"账号昵称去首尾空白后透传"
|
||||
);
|
||||
// 账号信息不可得(或昵称为空白)时退回发布时冻结的作者名。
|
||||
assert_eq!(
|
||||
resolve_lineage_author_name(None, Some("冻结名")),
|
||||
Some("冻结名".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_lineage_author_name(Some(" "), Some("冻结名")),
|
||||
Some("冻结名".to_string())
|
||||
);
|
||||
// 两者都没有可渲染的名字 → 不占位。
|
||||
assert_eq!(resolve_lineage_author_name(None, None), None);
|
||||
assert_eq!(resolve_lineage_author_name(Some(""), Some(" ")), None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -270,7 +270,9 @@ pub struct GameDistributionLineageNode {
|
||||
#[serde(default)]
|
||||
pub parent_game_id: Option<String>,
|
||||
pub play_count: u64,
|
||||
/// 可见性;未公开的锚点节点原样回传 `unpublished` / `suspended`。
|
||||
/// 可见性。当前**只有已公开作品会成为节点**(未公开节点被过滤、未公开锚点直接 404),
|
||||
/// 所以线上取值实际恒为 `published`;保留该字段是为了将来允许非公开节点进入树时
|
||||
/// 不必再改契约,届时展示层按此渲染状态。
|
||||
pub status: GameDistributionVisibility,
|
||||
}
|
||||
|
||||
@@ -363,6 +365,14 @@ pub struct GameDistributionCreateGameRequest {
|
||||
pub device_support: GameDistributionDeviceSupport,
|
||||
pub input_modes: Vec<GameDistributionInputMode>,
|
||||
pub orientation: GameDistributionOrientation,
|
||||
/// 上架时选择的共创授权档位;缺省按「禁止共创」解释(与表列默认一致,旧客户端不受影响)。
|
||||
///
|
||||
/// 写成非 `Option` + `#[serde(default)]` 而不是 `Option<..>`:省略与显式传 `forbidden`
|
||||
/// 得到同一个值,因此 `create_game` 的幂等摘要(对整个请求体取摘要)也相同——省略与显式
|
||||
/// 传默认不会变成两个不同的请求;未知取值由反序列化直接拒绝(api-server 映射 400),
|
||||
/// 不会静默落成 `forbidden`。
|
||||
#[serde(default)]
|
||||
pub fork_authorization: GameDistributionForkAuthorization,
|
||||
/// 改编来源声明;只在全新作品上生效,复用既有身份时会被拒绝。
|
||||
#[serde(default)]
|
||||
pub fork: Option<GameDistributionForkDeclaration>,
|
||||
|
||||
@@ -2013,8 +2013,11 @@ pub fn get_public_game_distribution_game_and_return(
|
||||
|
||||
/// 返回以该作品根为顶的整棵创作族谱树;公开只读,匿名可读。
|
||||
///
|
||||
/// 目标作品不存在或已软删除时 `found = false`(api-server 映射 404);未公开但未删除的作品
|
||||
/// 仍可读(作者页入口),自身作为锚点保留、身份字段按可见性降级。
|
||||
/// 锚点必须公开可读:不存在、已软删除或未公开(`visibility != published`)一律 `found = false`
|
||||
/// (api-server 映射 404)。**不允许**用「标题降级为空的锚点节点」或空树代替 404——那等于对外
|
||||
/// 确认该 gameId 存在、它是第几代、它在血缘里的位置,本身就是未公开作品的信息泄露。
|
||||
/// 树内也只出现未软删除且已公开的作品;父/祖辈被排除时孩子照常出现并保留 `generation` 与
|
||||
/// `parent_game_id`,不补占位节点。
|
||||
#[spacetimedb::procedure]
|
||||
pub fn get_game_distribution_lineage_and_return(
|
||||
ctx: &mut ProcedureContext,
|
||||
@@ -4173,9 +4176,9 @@ fn game_distribution_derived_games_tx(
|
||||
.game_distribution_game()
|
||||
.game_id()
|
||||
.find(&lineage.game_id)?;
|
||||
Some(game_distribution_lineage_entry(
|
||||
ctx,
|
||||
&child,
|
||||
// 字段映射只有一份:复用与族谱装配同一个构造函数,避免两条路径各写一套。
|
||||
Some(module_game_distribution::lineage_entry_from_game_facts(
|
||||
game_distribution_lineage_game_facts(ctx, &child),
|
||||
lineage.generation,
|
||||
Some(lineage.parent_game_id.clone()),
|
||||
))
|
||||
@@ -4234,57 +4237,52 @@ fn get_game_distribution_fork_source_tx(
|
||||
|
||||
/// 折叠族谱树的输入行:根作品自身 + 所有以该根为祖先的血缘行。
|
||||
///
|
||||
/// 只读行事实,不做任何可见性判断——「哪些节点能出现、身份字段要不要降级」全部由
|
||||
/// `module_game_distribution::lineage` 的纯函数决定,避免同一口径长出第二套实现。
|
||||
/// 这里只负责取行(根游戏行、按根索引命中的血缘行、以及每行引用的游戏行),
|
||||
/// 「家族归属校验、根自身入集合、游戏行缺失跳过、重复行去重」全部交给
|
||||
/// `module_game_distribution::assemble_lineage_tree_entries` 这个纯函数,
|
||||
/// 既避免同一口径长出第二套实现,也让树形状能在不起 `ReducerContext` 的前提下被单测钉住。
|
||||
fn game_distribution_lineage_entries(
|
||||
ctx: &ReducerContext,
|
||||
root_game_id: &str,
|
||||
) -> Vec<module_game_distribution::LineageTreeEntry> {
|
||||
let mut entries = Vec::new();
|
||||
if let Some(root) = ctx
|
||||
let root = ctx
|
||||
.db
|
||||
.game_distribution_game()
|
||||
.game_id()
|
||||
.find(&root_game_id.to_string())
|
||||
{
|
||||
entries.push(game_distribution_lineage_entry(ctx, &root, 0, None));
|
||||
}
|
||||
for lineage in ctx
|
||||
.map(|game| game_distribution_lineage_game_facts(ctx, &game));
|
||||
let resolved_links = ctx
|
||||
.db
|
||||
.game_distribution_lineage()
|
||||
.by_game_distribution_lineage_root_game_id()
|
||||
.filter(&root_game_id.to_string())
|
||||
{
|
||||
let Some(game) = ctx
|
||||
.db
|
||||
.game_distribution_game()
|
||||
.game_id()
|
||||
.find(&lineage.game_id)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
entries.push(game_distribution_lineage_entry(
|
||||
ctx,
|
||||
&game,
|
||||
lineage.generation,
|
||||
Some(lineage.parent_game_id.clone()),
|
||||
));
|
||||
}
|
||||
entries
|
||||
.map(|lineage| module_game_distribution::LineageResolvedLink {
|
||||
link: module_game_distribution::LineageLink {
|
||||
game_id: lineage.game_id.clone(),
|
||||
parent_game_id: lineage.parent_game_id.clone(),
|
||||
root_game_id: lineage.root_game_id.clone(),
|
||||
generation: lineage.generation,
|
||||
},
|
||||
game: ctx
|
||||
.db
|
||||
.game_distribution_game()
|
||||
.game_id()
|
||||
.find(&lineage.game_id)
|
||||
.map(|game| game_distribution_lineage_game_facts(ctx, &game)),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
module_game_distribution::assemble_lineage_tree_entries(root_game_id, root, &resolved_links)
|
||||
}
|
||||
|
||||
fn game_distribution_lineage_entry(
|
||||
/// 把一个游戏行折叠成族谱装配用的行事实(含作者名的读时联账号)。
|
||||
fn game_distribution_lineage_game_facts(
|
||||
ctx: &ReducerContext,
|
||||
game: &GameDistributionGame,
|
||||
generation: u32,
|
||||
parent_game_id: Option<String>,
|
||||
) -> module_game_distribution::LineageTreeEntry {
|
||||
module_game_distribution::LineageTreeEntry {
|
||||
) -> module_game_distribution::LineageGameFacts {
|
||||
module_game_distribution::LineageGameFacts {
|
||||
game_id: game.game_id.clone(),
|
||||
title: game.title.clone(),
|
||||
author_name: game_distribution_lineage_author_name(ctx, game),
|
||||
generation,
|
||||
parent_game_id,
|
||||
play_count: game.play_count,
|
||||
is_deleted: game.deleted_at.is_some(),
|
||||
is_published: game.visibility == GAME_DISTRIBUTION_VISIBILITY_PUBLISHED,
|
||||
@@ -4296,20 +4294,22 @@ fn game_distribution_lineage_entry(
|
||||
/// 族谱节点的作者名:与公开详情同样读时联账号表(账号改名/换头像立即跟随),
|
||||
/// 账号信息不可得时退回发布时冻结的作者名。
|
||||
///
|
||||
/// 与公开投影的差别只有一处:空串与纯空白归一成 `None`(节点上没有可渲染的名字就不占位)。
|
||||
/// 归一规则(账号昵称优先、两者都空白则 `None`)放在
|
||||
/// `module_game_distribution::resolve_lineage_author_name`,这里只做账号查询。
|
||||
fn game_distribution_lineage_author_name(
|
||||
ctx: &ReducerContext,
|
||||
game: &GameDistributionGame,
|
||||
) -> Option<String> {
|
||||
if let Some(account) = ctx.db.user_account().user_id().find(&game.owner_user_id) {
|
||||
let display_name = account.display_name.trim().to_string();
|
||||
if !display_name.is_empty() {
|
||||
return Some(display_name);
|
||||
}
|
||||
}
|
||||
game.author_name
|
||||
.clone()
|
||||
.filter(|name| !name.trim().is_empty())
|
||||
let account_display_name = ctx
|
||||
.db
|
||||
.user_account()
|
||||
.user_id()
|
||||
.find(&game.owner_user_id)
|
||||
.map(|account| account.display_name);
|
||||
module_game_distribution::resolve_lineage_author_name(
|
||||
account_display_name.as_deref(),
|
||||
game.author_name.as_deref(),
|
||||
)
|
||||
}
|
||||
|
||||
fn game_distribution_lineage_node_snapshot(
|
||||
|
||||
Reference in New Issue
Block a user