fix:修复无消息草稿保存
优化首次加载速度
This commit is contained in:
@@ -0,0 +1,18 @@
|
|||||||
|
# Agent 空会话草稿可见性修正 2026-04-26
|
||||||
|
|
||||||
|
用户从创作中心点进 RPG 或大鱼吃小鱼工作台时,后端会立即创建 Agent session,并写入一条助手欢迎消息。但在用户尚未发送任何消息、也没有传入种子文本时,这个 session 只是临时工作区,不应进入“我的创作”草稿列表。
|
||||||
|
|
||||||
|
本次规则:
|
||||||
|
|
||||||
|
1. 只有存在用户消息、非空 seedText、真实草稿数据或已发布状态时,Agent session 才算作品草稿。
|
||||||
|
2. 助手欢迎消息、默认 anchorPack、空 `{}` draftProfile 不算用户创作内容。
|
||||||
|
3. 过滤必须落在后端 works 聚合层,前端创作中心只消费结果,不负责隐藏空草稿。
|
||||||
|
4. RPG 仍保留已发布 profile 和孤立持久草稿 profile 的展示;未发布且仍有活跃 Agent session 的编译 profile 继续去重。
|
||||||
|
|
||||||
|
涉及入口:
|
||||||
|
|
||||||
|
- `server-rs/crates/spacetime-module/src/lib.rs`
|
||||||
|
- `server-rs/crates/spacetime-module/src/custom_world/mod.rs`
|
||||||
|
- `server-rs/crates/spacetime-module/src/big_fish/session.rs`
|
||||||
|
|
||||||
|
后续如果新增玩法创作 Agent,也必须复用同一判断:创建会话不等于创建草稿,作品列表只展示已经被用户实际开始编辑或已经生成结果的会话。
|
||||||
@@ -29,3 +29,4 @@
|
|||||||
- [RPG_DRAFT_IMAGE_PARALLEL_GENERATION_2026-04-24.md](./RPG_DRAFT_IMAGE_PARALLEL_GENERATION_2026-04-24.md):记录 RPG 底稿阶段角色主形象与场景背景图并行生成约束。
|
- [RPG_DRAFT_IMAGE_PARALLEL_GENERATION_2026-04-24.md](./RPG_DRAFT_IMAGE_PARALLEL_GENERATION_2026-04-24.md):记录 RPG 底稿阶段角色主形象与场景背景图并行生成约束。
|
||||||
- [PLATFORM_HOME_BANNER_IMAGE_SIZE_FIX_2026-04-25.md](./PLATFORM_HOME_BANNER_IMAGE_SIZE_FIX_2026-04-25.md):记录首页 banner 背景图不能进入普通布局流的修复经验。
|
- [PLATFORM_HOME_BANNER_IMAGE_SIZE_FIX_2026-04-25.md](./PLATFORM_HOME_BANNER_IMAGE_SIZE_FIX_2026-04-25.md):记录首页 banner 背景图不能进入普通布局流的修复经验。
|
||||||
- [RPG_PUBLISH_GALLERY_REFRESH_FIX_2026-04-25.md](./RPG_PUBLISH_GALLERY_REFRESH_FIX_2026-04-25.md):记录 RPG 发布后首页 / 分类页公开作品列表刷新链路。
|
- [RPG_PUBLISH_GALLERY_REFRESH_FIX_2026-04-25.md](./RPG_PUBLISH_GALLERY_REFRESH_FIX_2026-04-25.md):记录 RPG 发布后首页 / 分类页公开作品列表刷新链路。
|
||||||
|
- [AGENT_EMPTY_SESSION_DRAFT_VISIBILITY_2026-04-26.md](./AGENT_EMPTY_SESSION_DRAFT_VISIBILITY_2026-04-26.md):记录 Agent 空会话不应进入作品草稿列表的后端判定规则。
|
||||||
|
|||||||
@@ -24,6 +24,8 @@
|
|||||||
- `PlatformEntryFlowShellImpl` 将拼图 Agent、拼图结果页、拼图详情页、拼图运行态、创作货架等非默认首屏组件改为 `lazy`。
|
- `PlatformEntryFlowShellImpl` 将拼图 Agent、拼图结果页、拼图详情页、拼图运行态、创作货架等非默认首屏组件改为 `lazy`。
|
||||||
- 平台首页 Tab 保留已访问页面的挂载状态,但首访只挂载当前 Tab,避免隐藏的创作页提前触发创作中心等懒加载模块。
|
- 平台首页 Tab 保留已访问页面的挂载状态,但首访只挂载当前 Tab,避免隐藏的创作页提前触发创作中心等懒加载模块。
|
||||||
- RPG 运行态画布和 overlay host 只在已经进入 RPG 世界后挂载,平台首页不再同步拉取运行态画布链路。
|
- RPG 运行态画布和 overlay host 只在已经进入 RPG 世界后挂载,平台首页不再同步拉取运行态画布链路。
|
||||||
|
- 默认 `App` 不再首屏调用 `useRpgRuntimeSession`。平台首页先挂载轻量 `PlatformEntryFlowShell`,用户选择世界、恢复存档或进入 RPG 运行态深链后,才懒加载完整 `RpgRuntimeApp` 和故事/战斗/NPC 交互 hooks。
|
||||||
|
- 平台入口 props 移除未使用的 `gameState`,避免轻量首页为了兼容旧签名初始化完整 RPG `GameState`。
|
||||||
- 平台首页资料服务直连 `rpgProfileClient`,避免经过 `services/rpg-entry/index.ts` 把同域其它 client 一并纳入冷转译链路。
|
- 平台首页资料服务直连 `rpgProfileClient`,避免经过 `services/rpg-entry/index.ts` 把同域其它 client 一并纳入冷转译链路。
|
||||||
|
|
||||||
### 3.2 首屏图片门控
|
### 3.2 首屏图片门控
|
||||||
@@ -47,7 +49,8 @@ Vite dev server 只对前端真实运行入口保持热更新敏感:
|
|||||||
|
|
||||||
1. Vite ready 后,默认站点首屏不再一次性转译明显非首屏的拼图/玩法结果/运行态组件。
|
1. Vite ready 后,默认站点首屏不再一次性转译明显非首屏的拼图/玩法结果/运行态组件。
|
||||||
2. 默认首页冷加载 `.tsx` 请求数量下降,创作、拼图、运行态等阶段在用户进入时再加载对应 chunk。
|
2. 默认首页冷加载 `.tsx` 请求数量下降,创作、拼图、运行态等阶段在用户进入时再加载对应 chunk。
|
||||||
3. 慢图片、失败图片或生成资源代理慢时,页面主体仍能先显示并保持可操作。
|
3. 默认首页不再同步加载 RPG story / combat / NPC interaction 运行态 hooks;进入自定义世界或恢复存档后再加载完整运行态。
|
||||||
4. 修改 `docs/`、`server-rs/`、`scripts/` 或测试文件时,不再触发前端页面 reload。
|
4. 慢图片、失败图片或生成资源代理慢时,页面主体仍能先显示并保持可操作。
|
||||||
5. `RouteImageReadyGate` 工具测试覆盖慢图片仍会放行首屏的行为。
|
5. 修改 `docs/`、`server-rs/`、`scripts/` 或测试文件时,不再触发前端页面 reload。
|
||||||
6. 修改中文文件后运行编码检查,确保没有破坏 UTF-8 文本。
|
6. `RouteImageReadyGate` 工具测试覆盖慢图片仍会放行首屏的行为。
|
||||||
|
7. 修改中文文件后运行编码检查,确保没有破坏 UTF-8 文本。
|
||||||
|
|||||||
@@ -182,8 +182,7 @@ pub(crate) fn create_big_fish_session_tx(
|
|||||||
owner_user_id: input.owner_user_id.clone(),
|
owner_user_id: input.owner_user_id.clone(),
|
||||||
seed_text: input.seed_text.trim().to_string(),
|
seed_text: input.seed_text.trim().to_string(),
|
||||||
current_turn: 0,
|
current_turn: 0,
|
||||||
// 中文注释:欢迎语和种子推断只是初始上下文,不代表创作者已经推进了共创流程。
|
progress_percent: 20,
|
||||||
progress_percent: 0,
|
|
||||||
stage: BigFishCreationStage::CollectingAnchors,
|
stage: BigFishCreationStage::CollectingAnchors,
|
||||||
anchor_pack_json: serialize_anchor_pack(&anchor_pack)
|
anchor_pack_json: serialize_anchor_pack(&anchor_pack)
|
||||||
.map_err(|error| error.to_string())?,
|
.map_err(|error| error.to_string())?,
|
||||||
@@ -239,7 +238,9 @@ pub(crate) fn list_big_fish_works_tx(
|
|||||||
.db
|
.db
|
||||||
.big_fish_creation_session()
|
.big_fish_creation_session()
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|row| row.owner_user_id == input.owner_user_id)
|
.filter(|row| {
|
||||||
|
row.owner_user_id == input.owner_user_id && should_include_big_fish_work(ctx, row)
|
||||||
|
})
|
||||||
.map(|row| build_big_fish_work_summary(ctx, &row))
|
.map(|row| build_big_fish_work_summary(ctx, &row))
|
||||||
.collect::<Result<Vec<_>, _>>()?;
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
|
||||||
@@ -252,6 +253,24 @@ pub(crate) fn list_big_fish_works_tx(
|
|||||||
Ok(items)
|
Ok(items)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn should_include_big_fish_work(ctx: &ReducerContext, row: &BigFishCreationSession) -> bool {
|
||||||
|
if big_fish_session_has_direct_work_content(row) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.db.big_fish_agent_message().iter().any(|message| {
|
||||||
|
message.session_id == row.session_id
|
||||||
|
&& matches!(message.role, BigFishAgentMessageRole::User)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn big_fish_session_has_direct_work_content(row: &BigFishCreationSession) -> bool {
|
||||||
|
// 助手欢迎语和默认 anchorPack 只是工作台初始状态,不应被当成草稿作品。
|
||||||
|
!row.seed_text.trim().is_empty()
|
||||||
|
|| row.draft_json.is_some()
|
||||||
|
|| row.stage == BigFishCreationStage::Published
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn delete_big_fish_work_tx(
|
pub(crate) fn delete_big_fish_work_tx(
|
||||||
ctx: &ReducerContext,
|
ctx: &ReducerContext,
|
||||||
input: BigFishWorkDeleteInput,
|
input: BigFishWorkDeleteInput,
|
||||||
@@ -688,3 +707,53 @@ pub(crate) fn append_big_fish_system_message(
|
|||||||
created_at: Timestamp::from_micros_since_unix_epoch(created_at_micros),
|
created_at: Timestamp::from_micros_since_unix_epoch(created_at_micros),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn build_test_big_fish_session(
|
||||||
|
seed_text: &str,
|
||||||
|
draft_json: Option<&str>,
|
||||||
|
stage: BigFishCreationStage,
|
||||||
|
) -> BigFishCreationSession {
|
||||||
|
BigFishCreationSession {
|
||||||
|
session_id: "big-fish-session-1".to_string(),
|
||||||
|
owner_user_id: "user-1".to_string(),
|
||||||
|
seed_text: seed_text.to_string(),
|
||||||
|
current_turn: 0,
|
||||||
|
progress_percent: 20,
|
||||||
|
stage,
|
||||||
|
anchor_pack_json: "{}".to_string(),
|
||||||
|
draft_json: draft_json.map(str::to_string),
|
||||||
|
asset_coverage_json: "{}".to_string(),
|
||||||
|
last_assistant_reply: Some("欢迎来到大鱼吃小鱼共创。".to_string()),
|
||||||
|
publish_ready: false,
|
||||||
|
created_at: Timestamp::from_micros_since_unix_epoch(1),
|
||||||
|
updated_at: Timestamp::from_micros_since_unix_epoch(1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn big_fish_direct_work_content_ignores_empty_created_session() {
|
||||||
|
let empty_session =
|
||||||
|
build_test_big_fish_session("", None, BigFishCreationStage::CollectingAnchors);
|
||||||
|
let seeded_session = build_test_big_fish_session(
|
||||||
|
"想做深海吞噬成长",
|
||||||
|
None,
|
||||||
|
BigFishCreationStage::CollectingAnchors,
|
||||||
|
);
|
||||||
|
let drafted_session = build_test_big_fish_session(
|
||||||
|
"",
|
||||||
|
Some(r#"{"title":"深海吞噬"}"#),
|
||||||
|
BigFishCreationStage::DraftReady,
|
||||||
|
);
|
||||||
|
let published_session =
|
||||||
|
build_test_big_fish_session("", None, BigFishCreationStage::Published);
|
||||||
|
|
||||||
|
assert!(!big_fish_session_has_direct_work_content(&empty_session));
|
||||||
|
assert!(big_fish_session_has_direct_work_content(&seeded_session));
|
||||||
|
assert!(big_fish_session_has_direct_work_content(&drafted_session));
|
||||||
|
assert!(big_fish_session_has_direct_work_content(&published_session));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
#[spacetimedb::table(
|
#[spacetimedb::table(
|
||||||
accessor = custom_world_profile,
|
accessor = custom_world_profile,
|
||||||
index(accessor = by_custom_world_profile_owner_user_id, btree(columns = [owner_user_id])),
|
index(accessor = by_custom_world_profile_owner_user_id, btree(columns = [owner_user_id])),
|
||||||
@@ -1457,10 +1459,14 @@ fn list_custom_world_work_snapshots(
|
|||||||
validate_custom_world_works_list_input(&input).map_err(|error| error.to_string())?;
|
validate_custom_world_works_list_input(&input).map_err(|error| error.to_string())?;
|
||||||
|
|
||||||
let mut items = Vec::new();
|
let mut items = Vec::new();
|
||||||
|
let mut active_agent_session_ids = HashSet::new();
|
||||||
|
|
||||||
for session in ctx.db.custom_world_agent_session().iter().filter(|row| {
|
for session in ctx.db.custom_world_agent_session().iter().filter(|row| {
|
||||||
row.owner_user_id == input.owner_user_id && row.stage != RpgAgentStage::Published
|
row.owner_user_id == input.owner_user_id
|
||||||
|
&& row.stage != RpgAgentStage::Published
|
||||||
|
&& should_include_custom_world_agent_session_work(ctx, row)
|
||||||
}) {
|
}) {
|
||||||
|
active_agent_session_ids.insert(session.session_id.clone());
|
||||||
let gate = build_custom_world_publish_gate_from_session(&session);
|
let gate = build_custom_world_publish_gate_from_session(&session);
|
||||||
let draft_profile = parse_optional_session_object(session.draft_profile_json.as_deref());
|
let draft_profile = parse_optional_session_object(session.draft_profile_json.as_deref());
|
||||||
let title = resolve_session_work_title(&session, draft_profile.as_ref());
|
let title = resolve_session_work_title(&session, draft_profile.as_ref());
|
||||||
@@ -1504,6 +1510,7 @@ fn list_custom_world_work_snapshots(
|
|||||||
.custom_world_profile()
|
.custom_world_profile()
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|row| row.owner_user_id == input.owner_user_id && row.deleted_at.is_none())
|
.filter(|row| row.owner_user_id == input.owner_user_id && row.deleted_at.is_none())
|
||||||
|
.filter(|row| should_include_custom_world_profile_work(row, &active_agent_session_ids))
|
||||||
{
|
{
|
||||||
items.push(CustomWorldWorkSummarySnapshot {
|
items.push(CustomWorldWorkSummarySnapshot {
|
||||||
work_id: format!("published:{}", profile.profile_id),
|
work_id: format!("published:{}", profile.profile_id),
|
||||||
@@ -1558,6 +1565,63 @@ fn list_custom_world_work_snapshots(
|
|||||||
Ok(items)
|
Ok(items)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn should_include_custom_world_agent_session_work(
|
||||||
|
ctx: &ReducerContext,
|
||||||
|
session: &CustomWorldAgentSession,
|
||||||
|
) -> bool {
|
||||||
|
if custom_world_agent_session_has_direct_work_content(session) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ctx.db.custom_world_agent_message().iter().any(|message| {
|
||||||
|
message.session_id == session.session_id && matches!(message.role, RpgAgentMessageRole::User)
|
||||||
|
}) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.db
|
||||||
|
.custom_world_draft_card()
|
||||||
|
.iter()
|
||||||
|
.any(|card| card.session_id == session.session_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn custom_world_agent_session_has_direct_work_content(
|
||||||
|
session: &CustomWorldAgentSession,
|
||||||
|
) -> bool {
|
||||||
|
// 创建会话时写入的助手欢迎语和空 `{}` draftProfile 不算草稿内容;
|
||||||
|
// 这里只承认用户显式输入的 seed 或已经生成出的真实草稿阶段。
|
||||||
|
!session.seed_text.trim().is_empty()
|
||||||
|
|| matches!(
|
||||||
|
session.stage,
|
||||||
|
RpgAgentStage::ObjectRefining
|
||||||
|
| RpgAgentStage::VisualRefining
|
||||||
|
| RpgAgentStage::LongTailReview
|
||||||
|
| RpgAgentStage::ReadyToPublish
|
||||||
|
| RpgAgentStage::Published
|
||||||
|
)
|
||||||
|
|| parse_optional_session_object(session.draft_profile_json.as_deref())
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|profile| !profile.is_empty())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_include_custom_world_profile_work(
|
||||||
|
row: &CustomWorldProfile,
|
||||||
|
active_agent_session_ids: &HashSet<String>,
|
||||||
|
) -> bool {
|
||||||
|
// 已发布 profile 是正式作品;即使来源会话还存在,也必须保留独立入口。
|
||||||
|
if row.publication_status == CustomWorldPublicationStatus::Published {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 未发布 profile 若来源于仍可继续聊天的 Agent 会话,只是同一草稿的编译产物,
|
||||||
|
// works 里保留 agent_session 即可,避免草稿分组显示两份同名作品。
|
||||||
|
row.source_agent_session_id
|
||||||
|
.as_ref()
|
||||||
|
.map_or(true, |session_id| {
|
||||||
|
!active_agent_session_ids.contains(session_id)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn delete_custom_world_agent_session_tx(
|
fn delete_custom_world_agent_session_tx(
|
||||||
ctx: &ReducerContext,
|
ctx: &ReducerContext,
|
||||||
input: CustomWorldAgentSessionGetInput,
|
input: CustomWorldAgentSessionGetInput,
|
||||||
@@ -3708,7 +3772,7 @@ fn parse_json_array_or_empty(raw: &str) -> Vec<JsonValue> {
|
|||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_first_payload_text(payload: &JsonMap<String, JsonValue>, array_key: &str, scalar_key: &str) -> Option<String> {
|
fn read_first_payload_text(payload: &JsonMap<String, JsonValue>, array_key: &str, scalar_key: &str) -> Option<String> {
|
||||||
payload.get(array_key).and_then(JsonValue::as_array).and_then(|values| values.first()).and_then(JsonValue::as_str)
|
payload.get(array_key).and_then(JsonValue::as_array).and_then(|values| values.first()).and_then(JsonValue::as_str)
|
||||||
.or_else(|| payload.get(scalar_key).and_then(JsonValue::as_str))
|
.or_else(|| payload.get(scalar_key).and_then(JsonValue::as_str))
|
||||||
.map(str::trim).filter(|value| !value.is_empty()).map(ToOwned::to_owned)
|
.map(str::trim).filter(|value| !value.is_empty()).map(ToOwned::to_owned)
|
||||||
|
|||||||
@@ -2930,7 +2930,9 @@ fn list_custom_world_work_snapshots(
|
|||||||
let mut active_agent_session_ids = HashSet::new();
|
let mut active_agent_session_ids = HashSet::new();
|
||||||
|
|
||||||
for session in ctx.db.custom_world_agent_session().iter().filter(|row| {
|
for session in ctx.db.custom_world_agent_session().iter().filter(|row| {
|
||||||
row.owner_user_id == input.owner_user_id && row.stage != RpgAgentStage::Published
|
row.owner_user_id == input.owner_user_id
|
||||||
|
&& row.stage != RpgAgentStage::Published
|
||||||
|
&& should_include_custom_world_agent_session_work(ctx, row)
|
||||||
}) {
|
}) {
|
||||||
active_agent_session_ids.insert(session.session_id.clone());
|
active_agent_session_ids.insert(session.session_id.clone());
|
||||||
let gate = build_custom_world_publish_gate_from_session(&session);
|
let gate = build_custom_world_publish_gate_from_session(&session);
|
||||||
@@ -3031,6 +3033,44 @@ fn list_custom_world_work_snapshots(
|
|||||||
Ok(items)
|
Ok(items)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn should_include_custom_world_agent_session_work(
|
||||||
|
ctx: &ReducerContext,
|
||||||
|
session: &CustomWorldAgentSession,
|
||||||
|
) -> bool {
|
||||||
|
if custom_world_agent_session_has_direct_work_content(session) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ctx.db.custom_world_agent_message().iter().any(|message| {
|
||||||
|
message.session_id == session.session_id
|
||||||
|
&& matches!(message.role, RpgAgentMessageRole::User)
|
||||||
|
}) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.db
|
||||||
|
.custom_world_draft_card()
|
||||||
|
.iter()
|
||||||
|
.any(|card| card.session_id == session.session_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn custom_world_agent_session_has_direct_work_content(session: &CustomWorldAgentSession) -> bool {
|
||||||
|
// 创建会话时写入的助手欢迎语和空 `{}` draftProfile 不算草稿内容;
|
||||||
|
// 这里只承认用户显式输入的 seed 或已经生成出的真实草稿阶段。
|
||||||
|
!session.seed_text.trim().is_empty()
|
||||||
|
|| matches!(
|
||||||
|
session.stage,
|
||||||
|
RpgAgentStage::ObjectRefining
|
||||||
|
| RpgAgentStage::VisualRefining
|
||||||
|
| RpgAgentStage::LongTailReview
|
||||||
|
| RpgAgentStage::ReadyToPublish
|
||||||
|
| RpgAgentStage::Published
|
||||||
|
)
|
||||||
|
|| parse_optional_session_object(session.draft_profile_json.as_deref())
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|profile| !profile.is_empty())
|
||||||
|
}
|
||||||
|
|
||||||
fn should_include_custom_world_profile_work(
|
fn should_include_custom_world_profile_work(
|
||||||
row: &CustomWorldProfile,
|
row: &CustomWorldProfile,
|
||||||
active_agent_session_ids: &HashSet<String>,
|
active_agent_session_ids: &HashSet<String>,
|
||||||
@@ -6248,25 +6288,25 @@ fn build_npc_state_snapshot_from_row(row: &NpcState) -> NpcStateSnapshot {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
fn build_test_custom_world_agent_session(
|
||||||
fn resolve_stable_agent_draft_profile_id_prefers_legacy_result_profile_id() {
|
seed_text: &str,
|
||||||
let session = CustomWorldAgentSession {
|
stage: RpgAgentStage,
|
||||||
|
draft_profile_json: Option<&str>,
|
||||||
|
) -> CustomWorldAgentSession {
|
||||||
|
CustomWorldAgentSession {
|
||||||
session_id: "session-1".to_string(),
|
session_id: "session-1".to_string(),
|
||||||
owner_user_id: "user-1".to_string(),
|
owner_user_id: "user-1".to_string(),
|
||||||
seed_text: "seed".to_string(),
|
seed_text: seed_text.to_string(),
|
||||||
current_turn: 1,
|
current_turn: 0,
|
||||||
progress_percent: 100,
|
progress_percent: 0,
|
||||||
stage: RpgAgentStage::ObjectRefining,
|
stage,
|
||||||
focus_card_id: None,
|
focus_card_id: None,
|
||||||
anchor_content_json: "{}".to_string(),
|
anchor_content_json: "{}".to_string(),
|
||||||
creator_intent_json: None,
|
creator_intent_json: None,
|
||||||
creator_intent_readiness_json: "{}".to_string(),
|
creator_intent_readiness_json: "{}".to_string(),
|
||||||
anchor_pack_json: None,
|
anchor_pack_json: None,
|
||||||
lock_state_json: None,
|
lock_state_json: None,
|
||||||
draft_profile_json: Some(
|
draft_profile_json: draft_profile_json.map(str::to_string),
|
||||||
r#"{"id":"drifted-profile","legacyResultProfile":{"id":"stable-profile"}}"#
|
|
||||||
.to_string(),
|
|
||||||
),
|
|
||||||
last_assistant_reply: None,
|
last_assistant_reply: None,
|
||||||
publish_gate_json: None,
|
publish_gate_json: None,
|
||||||
result_preview_json: None,
|
result_preview_json: None,
|
||||||
@@ -6278,7 +6318,16 @@ mod tests {
|
|||||||
checkpoints_json: "[]".to_string(),
|
checkpoints_json: "[]".to_string(),
|
||||||
created_at: Timestamp::from_micros_since_unix_epoch(1),
|
created_at: Timestamp::from_micros_since_unix_epoch(1),
|
||||||
updated_at: Timestamp::from_micros_since_unix_epoch(1),
|
updated_at: Timestamp::from_micros_since_unix_epoch(1),
|
||||||
};
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolve_stable_agent_draft_profile_id_prefers_legacy_result_profile_id() {
|
||||||
|
let session = build_test_custom_world_agent_session(
|
||||||
|
"seed",
|
||||||
|
RpgAgentStage::ObjectRefining,
|
||||||
|
Some(r#"{"id":"drifted-profile","legacyResultProfile":{"id":"stable-profile"}}"#),
|
||||||
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resolve_stable_agent_draft_profile_id(&session),
|
resolve_stable_agent_draft_profile_id(&session),
|
||||||
@@ -6286,6 +6335,37 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn custom_world_agent_session_direct_work_content_ignores_empty_created_session() {
|
||||||
|
let empty_session =
|
||||||
|
build_test_custom_world_agent_session("", RpgAgentStage::CollectingIntent, Some("{}"));
|
||||||
|
let seeded_session = build_test_custom_world_agent_session(
|
||||||
|
"想做一个海雾群岛",
|
||||||
|
RpgAgentStage::CollectingIntent,
|
||||||
|
Some("{}"),
|
||||||
|
);
|
||||||
|
let drafted_session =
|
||||||
|
build_test_custom_world_agent_session("", RpgAgentStage::ObjectRefining, Some("{}"));
|
||||||
|
let profile_session = build_test_custom_world_agent_session(
|
||||||
|
"",
|
||||||
|
RpgAgentStage::CollectingIntent,
|
||||||
|
Some(r#"{"worldHook":"海雾会吞掉记错航线的人。"}"#),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(!custom_world_agent_session_has_direct_work_content(
|
||||||
|
&empty_session,
|
||||||
|
));
|
||||||
|
assert!(custom_world_agent_session_has_direct_work_content(
|
||||||
|
&seeded_session,
|
||||||
|
));
|
||||||
|
assert!(custom_world_agent_session_has_direct_work_content(
|
||||||
|
&drafted_session,
|
||||||
|
));
|
||||||
|
assert!(custom_world_agent_session_has_direct_work_content(
|
||||||
|
&profile_session,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn same_agent_draft_profile_candidate_requires_same_owner_active_draft_and_session() {
|
fn same_agent_draft_profile_candidate_requires_same_owner_active_draft_and_session() {
|
||||||
let matching = CustomWorldProfile {
|
let matching = CustomWorldProfile {
|
||||||
|
|||||||
125
src/App.tsx
125
src/App.tsx
@@ -1,8 +1,125 @@
|
|||||||
import { RpgRuntimeShell } from './components/rpg-runtime-shell/RpgRuntimeShell';
|
import { lazy, Suspense, useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { useRpgRuntimeSession } from './hooks/rpg-session/useRpgRuntimeSession';
|
|
||||||
|
import { useAuthUi } from './components/auth/AuthUiContext';
|
||||||
|
import { PlatformEntryFlowShell } from './components/platform-entry/PlatformEntryFlowShell';
|
||||||
|
import type { SelectionStage } from './components/platform-entry/platformEntryTypes';
|
||||||
|
import type { HydratedSavedGameSnapshot } from './persistence/runtimeSnapshotTypes';
|
||||||
|
import {
|
||||||
|
APP_RUNTIME_ROUTES,
|
||||||
|
normalizeAppPath,
|
||||||
|
pushAppHistoryPath,
|
||||||
|
resolvePathForSelectionStage,
|
||||||
|
resolveSelectionStageFromPath,
|
||||||
|
} from './routing/appPageRoutes';
|
||||||
|
import type { RpgRuntimeAppIntent } from './RpgRuntimeApp';
|
||||||
|
import type { CustomWorldProfile } from './types';
|
||||||
|
|
||||||
|
const RpgRuntimeApp = lazy(async () => {
|
||||||
|
const module = await import('./RpgRuntimeApp');
|
||||||
|
return {
|
||||||
|
default: module.RpgRuntimeApp,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
function isRpgRuntimeRoute(pathname: string) {
|
||||||
|
const normalizedPath = normalizeAppPath(pathname);
|
||||||
|
return (
|
||||||
|
normalizedPath === APP_RUNTIME_ROUTES['rpg-character-select'] ||
|
||||||
|
normalizedPath === APP_RUNTIME_ROUTES['rpg-adventure']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const gameShellProps = useRpgRuntimeSession();
|
const authUi = useAuthUi();
|
||||||
|
const runtimeIntentTokenRef = useRef(0);
|
||||||
|
const [runtimeIntent, setRuntimeIntent] =
|
||||||
|
useState<RpgRuntimeAppIntent | null>(null);
|
||||||
|
const [isRuntimeActive, setIsRuntimeActive] = useState(() =>
|
||||||
|
isRpgRuntimeRoute(window.location.pathname),
|
||||||
|
);
|
||||||
|
const [selectionStage, setRawSelectionStage] = useState<SelectionStage>(() =>
|
||||||
|
resolveSelectionStageFromPath(window.location.pathname),
|
||||||
|
);
|
||||||
|
|
||||||
return <RpgRuntimeShell {...gameShellProps} />;
|
const setSelectionStage = useCallback((stage: SelectionStage) => {
|
||||||
|
setRawSelectionStage(stage);
|
||||||
|
pushAppHistoryPath(resolvePathForSelectionStage(stage));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const syncStageFromHistory = () => {
|
||||||
|
if (isRpgRuntimeRoute(window.location.pathname)) {
|
||||||
|
setIsRuntimeActive(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsRuntimeActive(false);
|
||||||
|
setRawSelectionStage(
|
||||||
|
resolveSelectionStageFromPath(window.location.pathname),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('popstate', syncStageFromHistory);
|
||||||
|
return () => window.removeEventListener('popstate', syncStageFromHistory);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const createRuntimeIntent = useCallback(
|
||||||
|
(intent: Omit<RpgRuntimeAppIntent, 'token'>) => {
|
||||||
|
runtimeIntentTokenRef.current += 1;
|
||||||
|
setRuntimeIntent({
|
||||||
|
...intent,
|
||||||
|
token: runtimeIntentTokenRef.current,
|
||||||
|
});
|
||||||
|
setIsRuntimeActive(true);
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleContinueGame = useCallback(
|
||||||
|
(snapshot?: HydratedSavedGameSnapshot | null) => {
|
||||||
|
createRuntimeIntent({
|
||||||
|
kind: 'snapshot',
|
||||||
|
snapshot: snapshot ?? null,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[createRuntimeIntent],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleCustomWorldSelect = useCallback(
|
||||||
|
(customWorldProfile: CustomWorldProfile) => {
|
||||||
|
createRuntimeIntent({
|
||||||
|
kind: 'custom-world',
|
||||||
|
profile: customWorldProfile,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[createRuntimeIntent],
|
||||||
|
);
|
||||||
|
const platformThemeClass =
|
||||||
|
authUi?.platformTheme === 'dark'
|
||||||
|
? 'platform-theme--dark'
|
||||||
|
: 'platform-theme--light';
|
||||||
|
|
||||||
|
if (isRuntimeActive) {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={null}>
|
||||||
|
<RpgRuntimeApp initialIntent={runtimeIntent} />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`platform-ui-shell platform-theme ${platformThemeClass} flex h-screen max-h-screen flex-col overflow-hidden bg-[image:var(--platform-body-fill)] p-2 font-sans text-[var(--platform-text-strong)] sm:p-4`}
|
||||||
|
>
|
||||||
|
<PlatformEntryFlowShell
|
||||||
|
selectionStage={selectionStage}
|
||||||
|
setSelectionStage={setSelectionStage}
|
||||||
|
hasSavedGame={false}
|
||||||
|
savedSnapshot={null}
|
||||||
|
handleContinueGame={handleContinueGame}
|
||||||
|
handleStartNewGame={() => {}}
|
||||||
|
handleCustomWorldSelect={handleCustomWorldSelect}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
45
src/RpgRuntimeApp.tsx
Normal file
45
src/RpgRuntimeApp.tsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
import { RpgRuntimeShell } from './components/rpg-runtime-shell/RpgRuntimeShell';
|
||||||
|
import { useRpgRuntimeSession } from './hooks/rpg-session/useRpgRuntimeSession';
|
||||||
|
import type { HydratedSavedGameSnapshot } from './persistence/runtimeSnapshotTypes';
|
||||||
|
import type { CustomWorldProfile } from './types';
|
||||||
|
|
||||||
|
export type RpgRuntimeAppIntent =
|
||||||
|
| {
|
||||||
|
token: number;
|
||||||
|
kind: 'custom-world';
|
||||||
|
profile: CustomWorldProfile;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
token: number;
|
||||||
|
kind: 'snapshot';
|
||||||
|
snapshot: HydratedSavedGameSnapshot | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function RpgRuntimeApp({
|
||||||
|
initialIntent,
|
||||||
|
}: {
|
||||||
|
initialIntent: RpgRuntimeAppIntent | null;
|
||||||
|
}) {
|
||||||
|
const gameShellProps = useRpgRuntimeSession();
|
||||||
|
const handledIntentTokenRef = useRef<number | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!initialIntent || handledIntentTokenRef.current === initialIntent.token) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handledIntentTokenRef.current = initialIntent.token;
|
||||||
|
if (initialIntent.kind === 'custom-world') {
|
||||||
|
gameShellProps.entry.handleCustomWorldSelect(initialIntent.profile);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
gameShellProps.entry.handleContinueGame(initialIntent.snapshot);
|
||||||
|
}, [gameShellProps.entry, initialIntent]);
|
||||||
|
|
||||||
|
return <RpgRuntimeShell {...gameShellProps} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RpgRuntimeApp;
|
||||||
@@ -2,7 +2,7 @@ import type {
|
|||||||
CustomWorldAgentSessionSnapshot,
|
CustomWorldAgentSessionSnapshot,
|
||||||
} from '../../../packages/shared/src/contracts/customWorldAgent';
|
} from '../../../packages/shared/src/contracts/customWorldAgent';
|
||||||
import type { HydratedSavedGameSnapshot } from '../../persistence/runtimeSnapshotTypes';
|
import type { HydratedSavedGameSnapshot } from '../../persistence/runtimeSnapshotTypes';
|
||||||
import type { CustomWorldProfile, GameState } from '../../types';
|
import type { CustomWorldProfile } from '../../types';
|
||||||
|
|
||||||
export type SelectionStage =
|
export type SelectionStage =
|
||||||
| 'platform'
|
| 'platform'
|
||||||
@@ -34,7 +34,6 @@ export type SyncedAgentDraftResult = {
|
|||||||
export type PlatformEntryFlowShellProps = {
|
export type PlatformEntryFlowShellProps = {
|
||||||
selectionStage: SelectionStage;
|
selectionStage: SelectionStage;
|
||||||
setSelectionStage: (stage: SelectionStage) => void;
|
setSelectionStage: (stage: SelectionStage) => void;
|
||||||
gameState: GameState;
|
|
||||||
hasSavedGame: boolean;
|
hasSavedGame: boolean;
|
||||||
savedSnapshot: HydratedSavedGameSnapshot | null;
|
savedSnapshot: HydratedSavedGameSnapshot | null;
|
||||||
handleContinueGame: (snapshot?: HydratedSavedGameSnapshot | null) => void;
|
handleContinueGame: (snapshot?: HydratedSavedGameSnapshot | null) => void;
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import type { AuthUser } from '../../services/authService';
|
|||||||
import { ApiClientError } from '../../services/apiClient';
|
import { ApiClientError } from '../../services/apiClient';
|
||||||
import {
|
import {
|
||||||
clearRpgProfileBrowseHistory as clearProfileBrowseHistory,
|
clearRpgProfileBrowseHistory as clearProfileBrowseHistory,
|
||||||
deleteRpgEntryWorldProfile,
|
|
||||||
getRpgEntryWorldGalleryDetail,
|
getRpgEntryWorldGalleryDetail,
|
||||||
getRpgProfileDashboard as getProfileDashboard,
|
getRpgProfileDashboard as getProfileDashboard,
|
||||||
listRpgEntryWorldGallery,
|
listRpgEntryWorldGallery,
|
||||||
@@ -47,8 +46,10 @@ import {
|
|||||||
listPuzzleGallery,
|
listPuzzleGallery,
|
||||||
} from '../../services/puzzle-gallery';
|
} from '../../services/puzzle-gallery';
|
||||||
import { listPuzzleWorks } from '../../services/puzzle-works';
|
import { listPuzzleWorks } from '../../services/puzzle-works';
|
||||||
import { getRpgEntryWorldGalleryDetailByCode } from '../../services/rpg-entry/rpgEntryLibraryClient';
|
import {
|
||||||
import type { GameState } from '../../types';
|
deleteRpgEntryWorldProfile,
|
||||||
|
getRpgEntryWorldGalleryDetailByCode,
|
||||||
|
} from '../../services/rpg-entry/rpgEntryLibraryClient';
|
||||||
import {
|
import {
|
||||||
AuthUiContext,
|
AuthUiContext,
|
||||||
type PlatformSettingsSection,
|
type PlatformSettingsSection,
|
||||||
@@ -130,6 +131,7 @@ vi.mock('../../services/puzzle-gallery', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../../services/rpg-entry/rpgEntryLibraryClient', () => ({
|
vi.mock('../../services/rpg-entry/rpgEntryLibraryClient', () => ({
|
||||||
|
deleteRpgEntryWorldProfile: vi.fn(),
|
||||||
getRpgEntryWorldGalleryDetailByCode: vi.fn(),
|
getRpgEntryWorldGalleryDetailByCode: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -522,7 +524,6 @@ function TestWrapper({
|
|||||||
<RpgEntryFlowShell
|
<RpgEntryFlowShell
|
||||||
selectionStage={selectionStage}
|
selectionStage={selectionStage}
|
||||||
setSelectionStage={setSelectionStage}
|
setSelectionStage={setSelectionStage}
|
||||||
gameState={{} as GameState}
|
|
||||||
hasSavedGame={false}
|
hasSavedGame={false}
|
||||||
savedSnapshot={null}
|
savedSnapshot={null}
|
||||||
handleContinueGame={onContinueGame ?? (() => {})}
|
handleContinueGame={onContinueGame ?? (() => {})}
|
||||||
@@ -574,7 +575,7 @@ beforeEach(() => {
|
|||||||
savedAt: '2026-04-19T12:00:00.000Z',
|
savedAt: '2026-04-19T12:00:00.000Z',
|
||||||
bottomTab: 'adventure',
|
bottomTab: 'adventure',
|
||||||
currentStory: null,
|
currentStory: null,
|
||||||
gameState: {} as GameState,
|
gameState: {},
|
||||||
} as HydratedSavedGameSnapshot,
|
} as HydratedSavedGameSnapshot,
|
||||||
});
|
});
|
||||||
vi.mocked(upsertProfileBrowseHistory).mockResolvedValue([]);
|
vi.mocked(upsertProfileBrowseHistory).mockResolvedValue([]);
|
||||||
@@ -1477,10 +1478,13 @@ test('published puzzle detail returns to the source platform tab', async () => {
|
|||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(document.getElementById('platform-tab-panel-category')).toBeTruthy();
|
expect(document.getElementById('platform-tab-panel-category')).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
await waitFor(() => {
|
||||||
|
const categoryPanel = getPlatformTabPanel('category');
|
||||||
|
expect(
|
||||||
|
within(categoryPanel).getAllByText('星桥机关').length,
|
||||||
|
).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
const categoryPanel = getPlatformTabPanel('category');
|
const categoryPanel = getPlatformTabPanel('category');
|
||||||
expect(
|
|
||||||
within(categoryPanel).getAllByText('星桥机关').length,
|
|
||||||
).toBeGreaterThan(0);
|
|
||||||
|
|
||||||
await user.click(
|
await user.click(
|
||||||
within(categoryPanel).getByRole('button', {
|
within(categoryPanel).getByRole('button', {
|
||||||
@@ -2114,7 +2118,6 @@ test('agent draft result publishes to gallery from publish panel', async () => {
|
|||||||
<RpgEntryFlowShell
|
<RpgEntryFlowShell
|
||||||
selectionStage={selectionStage}
|
selectionStage={selectionStage}
|
||||||
setSelectionStage={setSelectionStage}
|
setSelectionStage={setSelectionStage}
|
||||||
gameState={{} as GameState}
|
|
||||||
hasSavedGame={false}
|
hasSavedGame={false}
|
||||||
savedSnapshot={null}
|
savedSnapshot={null}
|
||||||
handleContinueGame={() => {}}
|
handleContinueGame={() => {}}
|
||||||
@@ -2189,7 +2192,6 @@ test('agent draft result test button enters current draft without publish gate',
|
|||||||
<RpgEntryFlowShell
|
<RpgEntryFlowShell
|
||||||
selectionStage={selectionStage}
|
selectionStage={selectionStage}
|
||||||
setSelectionStage={setSelectionStage}
|
setSelectionStage={setSelectionStage}
|
||||||
gameState={{} as GameState}
|
|
||||||
hasSavedGame={false}
|
hasSavedGame={false}
|
||||||
savedSnapshot={null}
|
savedSnapshot={null}
|
||||||
handleContinueGame={() => {}}
|
handleContinueGame={() => {}}
|
||||||
@@ -2827,7 +2829,7 @@ test('save tab can resume a selected archive directly into the game', async () =
|
|||||||
currentStory: null,
|
currentStory: null,
|
||||||
gameState: {
|
gameState: {
|
||||||
worldType: 'CUSTOM',
|
worldType: 'CUSTOM',
|
||||||
} as GameState,
|
},
|
||||||
} as HydratedSavedGameSnapshot,
|
} as HydratedSavedGameSnapshot,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
listRpgEntryWorldLibrary,
|
listRpgEntryWorldLibrary,
|
||||||
publishRpgEntryWorldProfile,
|
publishRpgEntryWorldProfile,
|
||||||
unpublishRpgEntryWorldProfile,
|
unpublishRpgEntryWorldProfile,
|
||||||
} from '../../services/rpg-entry';
|
} from '../../services/rpg-entry/rpgEntryLibraryClient';
|
||||||
import { ApiClientError } from '../../services/apiClient';
|
import { ApiClientError } from '../../services/apiClient';
|
||||||
import type { CustomWorldProfile } from '../../types';
|
import type { CustomWorldProfile } from '../../types';
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -172,7 +172,6 @@ export function RpgRuntimeStageRouter({
|
|||||||
<PlatformEntryFlowShell
|
<PlatformEntryFlowShell
|
||||||
selectionStage={selectionStage}
|
selectionStage={selectionStage}
|
||||||
setSelectionStage={setSelectionStage}
|
setSelectionStage={setSelectionStage}
|
||||||
gameState={gameState}
|
|
||||||
hasSavedGame={hasSavedGame}
|
hasSavedGame={hasSavedGame}
|
||||||
savedSnapshot={savedSnapshot}
|
savedSnapshot={savedSnapshot}
|
||||||
handleContinueGame={handleContinueGame}
|
handleContinueGame={handleContinueGame}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import type { HydratedSavedGameSnapshot } from '../../persistence/runtimeSnapsho
|
|||||||
import { useBackgroundMusic } from '../useBackgroundMusic';
|
import { useBackgroundMusic } from '../useBackgroundMusic';
|
||||||
import { useCombatFlow } from '../useCombatFlow';
|
import { useCombatFlow } from '../useCombatFlow';
|
||||||
import { useNpcInteractionFlow } from '../useNpcInteractionFlow';
|
import { useNpcInteractionFlow } from '../useNpcInteractionFlow';
|
||||||
import { useRpgRuntimeStory } from '../rpg-runtime-story';
|
import { useRpgRuntimeStory } from '../rpg-runtime-story/useRpgRuntimeStory';
|
||||||
import { useRpgSessionBootstrap } from './useRpgSessionBootstrap';
|
import { useRpgSessionBootstrap } from './useRpgSessionBootstrap';
|
||||||
import { useRpgSessionPersistence } from './useRpgSessionPersistence';
|
import { useRpgSessionPersistence } from './useRpgSessionPersistence';
|
||||||
|
|
||||||
|
|||||||
@@ -272,7 +272,7 @@ export function useRpgSessionPersistence({
|
|||||||
|
|
||||||
const continueSavedGame = useCallback(
|
const continueSavedGame = useCallback(
|
||||||
async (snapshotOverride?: HydratedSavedGameSnapshot | null) => {
|
async (snapshotOverride?: HydratedSavedGameSnapshot | null) => {
|
||||||
if (!authenticatedUserId) {
|
if (!authenticatedUserId && !snapshotOverride) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user