M4 runtime story Rust migration wrap-up

This commit is contained in:
2026-04-22 20:10:46 +08:00
parent 35958d5942
commit fa373f0575
31 changed files with 3257 additions and 1556 deletions

View File

@@ -9,17 +9,47 @@ use module_npc::{
build_relation_state as build_module_npc_relation_state,
};
use module_runtime::RuntimeSnapshotRecord;
use module_runtime_story_compat::{
CONTINUE_ADVENTURE_FUNCTION_ID, CurrentEncounterNpcQuestContext, GeneratedStoryPayload,
PendingQuestOfferContext, RuntimeStoryActionResponseParts,
StoryResolution, add_player_currency, add_player_inventory_items,
append_story_history, apply_equipment_loadout_to_state,
battle_mode_text, build_battle_runtime_story_options, build_current_build_toast,
build_status_patch,
build_npc_gift_result_text,
build_runtime_story_view_model,
clear_encounter_only, clear_encounter_state, clone_inventory_item_with_quantity,
current_encounter_id, current_encounter_name, current_world_type,
ensure_inventory_action_available, ensure_json_object, equipment_slot_label,
find_player_inventory_entry,
format_now_rfc3339, grant_player_progression_experience, has_giftable_player_inventory,
format_currency_text,
increment_runtime_stat, normalize_equipped_item,
normalize_equipment_slot_id, normalize_required_string, npc_buyback_price,
npc_purchase_price, read_array_field, read_bool_field, read_field, read_i32_field,
read_inventory_item_name, read_object_field, read_optional_string_field,
read_player_equipment_item, read_required_string_field, read_runtime_session_id,
read_u32_field, recruit_companion_to_party, remove_player_inventory_item,
restore_player_resource,
resolve_action_text, resolve_battle_action, resolve_equipment_slot_for_item,
resolve_forge_craft_action,
resolve_forge_dismantle_action, resolve_forge_reforge_action,
resolve_npc_gift_affinity_gain, simple_story_resolution, trade_quantity_suffix,
resolve_current_encounter_npc_state,
build_disabled_runtime_story_option, build_runtime_story_option_from_story_option,
build_static_runtime_story_option, build_story_option_from_runtime_option,
write_bool_field, write_i32_field, write_null_field, write_player_equipment_item,
write_string_field, write_u32_field,
};
use platform_llm::{LlmClient, LlmMessage, LlmTextRequest};
use serde_json::{Map, Value, json};
use shared_contracts::runtime_story::{
RuntimeBattlePresentation, RuntimeStoryActionRequest, RuntimeStoryActionResponse,
RuntimeStoryAiRequest, RuntimeStoryAiResponse, RuntimeStoryCompanionViewModel,
RuntimeStoryEncounterViewModel, RuntimeStoryOptionInteraction, RuntimeStoryOptionView,
RuntimeStoryPatch, RuntimeStoryPlayerViewModel, RuntimeStoryPresentation,
RuntimeStorySnapshotPayload, RuntimeStoryStateResolveRequest, RuntimeStoryStatusViewModel,
RuntimeStoryViewModel,
RuntimeStoryAiRequest, RuntimeStoryAiResponse, RuntimeStoryOptionInteraction,
RuntimeStoryOptionView, RuntimeStoryPatch, RuntimeStoryPresentation,
RuntimeStorySnapshotPayload, RuntimeStoryStateResolveRequest,
};
use shared_kernel::{format_rfc3339, offset_datetime_to_unix_micros, parse_rfc3339};
use shared_kernel::{offset_datetime_to_unix_micros, parse_rfc3339};
use spacetime_client::SpacetimeClientError;
use time::OffsetDateTime;
@@ -30,22 +60,10 @@ use crate::{
#[path = "compat/ai.rs"]
mod ai;
#[path = "compat/battle.rs"]
mod battle;
#[path = "compat/battle_actions.rs"]
mod battle_actions;
#[path = "compat/core.rs"]
mod core;
#[path = "compat/equipment_actions.rs"]
mod equipment_actions;
#[path = "compat/forge.rs"]
mod forge;
#[path = "compat/forge_actions.rs"]
mod forge_actions;
#[path = "compat/game_state.rs"]
mod game_state;
#[path = "compat/npc_support.rs"]
mod npc_support;
#[path = "compat/npc_actions.rs"]
mod npc_actions;
#[path = "compat/presentation.rs"]
@@ -54,50 +72,13 @@ mod presentation;
mod quest_actions;
use self::{
ai::*, battle::*, battle_actions::*, core::*, equipment_actions::*, forge::*,
forge_actions::*, game_state::*, npc_actions::*, npc_support::*, presentation::*,
quest_actions::*,
ai::*, equipment_actions::*, game_state::*, npc_actions::*, presentation::*, quest_actions::*,
};
#[cfg(test)]
#[path = "compat/tests.rs"]
mod tests;
const CONTINUE_ADVENTURE_FUNCTION_ID: &str = "story_continue_adventure";
const MAX_TASK5_COMPANIONS: usize = 2;
struct StoryResolution {
action_text: String,
result_text: String,
story_text: Option<String>,
presentation_options: Option<Vec<RuntimeStoryOptionView>>,
saved_current_story: Option<Value>,
patches: Vec<RuntimeStoryPatch>,
battle: Option<RuntimeBattlePresentation>,
toast: Option<String>,
}
struct GeneratedStoryPayload {
story_text: String,
history_result_text: String,
presentation_options: Vec<RuntimeStoryOptionView>,
saved_current_story: Value,
}
struct CurrentEncounterNpcQuestContext {
npc_id: String,
npc_name: String,
}
struct PendingQuestOfferContext {
dialogue: Vec<Value>,
turn_count: i32,
custom_input_placeholder: String,
quest: Value,
quest_id: String,
intro_text: Option<String>,
}
pub async fn resolve_runtime_story_state(
State(state): State<AppState>,
Extension(request_context): Extension<RequestContext>,
@@ -482,19 +463,6 @@ fn validate_client_version(
))
}
struct RuntimeStoryActionResponseParts {
requested_session_id: String,
server_version: u32,
snapshot: RuntimeStorySnapshotPayload,
action_text: String,
result_text: String,
story_text: String,
options: Vec<RuntimeStoryOptionView>,
patches: Vec<RuntimeStoryPatch>,
toast: Option<String>,
battle: Option<RuntimeBattlePresentation>,
}
fn resolve_runtime_story_choice_action(
game_state: &mut Value,
current_story: Option<&Value>,
@@ -650,49 +618,6 @@ fn resolve_continue_adventure_action(
})
}
fn simple_story_resolution(
game_state: &Value,
action_text: String,
result_text: &str,
) -> StoryResolution {
StoryResolution {
action_text,
result_text: result_text.to_string(),
story_text: None,
presentation_options: None,
saved_current_story: None,
patches: vec![build_status_patch(game_state)],
battle: None,
toast: None,
}
}
fn resolve_action_text(default_text: &str, request: &RuntimeStoryActionRequest) -> String {
request
.action
.payload
.as_ref()
.and_then(|payload| read_optional_string_field(payload, "optionText"))
.unwrap_or_else(|| default_text.to_string())
}
fn build_status_patch(game_state: &Value) -> RuntimeStoryPatch {
RuntimeStoryPatch::StatusChanged {
in_battle: read_bool_field(game_state, "inBattle").unwrap_or(false),
npc_interaction_active: read_bool_field(game_state, "npcInteractionActive")
.unwrap_or(false),
current_npc_battle_mode: read_optional_string_field(game_state, "currentNpcBattleMode"),
current_npc_battle_outcome: read_optional_string_field(
game_state,
"currentNpcBattleOutcome",
),
}
}
fn current_world_type(game_state: &Value) -> Option<String> {
read_optional_string_field(game_state, "worldType")
}
fn map_runtime_story_client_error(error: SpacetimeClientError) -> AppError {
let (status, provider) = match error {
SpacetimeClientError::Runtime(_) => (StatusCode::BAD_REQUEST, "runtime-story"),