From b2939a15490cd5a4aec614c9475408f901312c38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Garc=C3=ADa?= Date: Wed, 17 Jul 2024 14:59:49 +0000 Subject: [PATCH] Match Ink reference v1.2.0 --- lib/Cargo.toml | 6 ++--- lib/src/callstack.rs | 4 +++- lib/src/choice.rs | 4 +++- lib/src/container.rs | 25 ++++++++++++++------ lib/src/json_read.rs | 16 +++++++++++++ lib/src/json_write.rs | 11 +++++++++ lib/src/list_definitions_origin.rs | 4 ++++ lib/src/native_function_call.rs | 2 +- lib/src/object.rs | 1 + lib/src/story/external_functions.rs | 29 +++++++++++++++++++++++ lib/src/story/progress.rs | 25 +++++++++++--------- lib/src/story/state.rs | 2 +- lib/src/story_state.rs | 36 +++++++++++++++++++++++++++-- lib/src/variables_state.rs | 19 +++++++++++---- 14 files changed, 152 insertions(+), 32 deletions(-) diff --git a/lib/Cargo.toml b/lib/Cargo.toml index c1741fd..9bd42e8 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -19,11 +19,11 @@ path = "src/lib.rs" [dependencies] serde = { version = "1.0.152", features = ["derive"] } serde_json = "1.0.93" -strum = { version = "0.25.0", features = ["derive"] } +strum = { version = "0.26.3", features = ["derive"] } as-any = "0.3.0" rand = "0.8.5" instant = "0.1.12" [features] -stdweb = [ "instant/stdweb" ] -wasm-bindgen = [ "instant/wasm-bindgen" ] +stdweb = ["instant/stdweb"] +wasm-bindgen = ["instant/wasm-bindgen"] diff --git a/lib/src/callstack.rs b/lib/src/callstack.rs index 81722c2..699b764 100644 --- a/lib/src/callstack.rs +++ b/lib/src/callstack.rs @@ -103,7 +103,7 @@ impl Thread { pointer.index = pointer_index; if thread_pointer_result.approximate { - // TODO + // TODO warning not accessible from here // story_context.warning(format!("When loading state, exact internal story location couldn't be found: '{}', so it was approximated to '{}' to recover. Has the story changed since this save data was created?", current_container_path_str, pointer_container.get_path().to_string())); } } @@ -365,12 +365,14 @@ impl CallStack { 0 } + // Get variable value, dereferencing a variable pointer if necessary pub fn get_temporary_variable_with_name( &self, name: &str, context_index: i32, ) -> Option> { let mut context_index = context_index; + // contextIndex 0 means global, so index is actually 1-based if context_index == -1 { context_index = self.get_current_element_index() + 1; } diff --git a/lib/src/choice.rs b/lib/src/choice.rs index ce3c183..fb55f9d 100644 --- a/lib/src/choice.rs +++ b/lib/src/choice.rs @@ -9,6 +9,7 @@ use crate::{ }; /// Represents a choice generated by a [`Story`](crate::story::Story). +#[derive(Clone)] pub struct Choice { obj: Object, thread_at_generation: RefCell>, @@ -54,12 +55,13 @@ impl Choice { text: &str, index: usize, original_thread_index: usize, + choice_tags: Vec, ) -> Choice { Choice { obj: Object::new(), target_path: Path::new_with_components_string(Some(path_string_on_choice)), is_invisible_default: false, - tags: Vec::new(), + tags: choice_tags, index: RefCell::new(index), original_thread_index: RefCell::new(original_thread_index), text: text.to_string(), diff --git a/lib/src/container.rs b/lib/src/container.rs index 6d3db5d..199294d 100644 --- a/lib/src/container.rs +++ b/lib/src/container.rs @@ -206,13 +206,24 @@ impl Container { break; } - current_obj = found_obj.unwrap().clone(); - current_container = - if let Ok(container) = current_obj.clone().into_any().downcast::() { - Some(container) - } else { - None - }; + let found_obj = found_obj.unwrap(); + + // Are we about to loop into another container? + // Is the object a container as expected? It might + // no longer be if the content has shuffled around, so what + // was originally a container no longer is. + let next_container = found_obj.clone().into_any().downcast::(); + if (i as i32) < (partial_path_length - 1) && next_container.is_err() { + approximate = true; + break; + } + + current_obj = found_obj; + current_container = if let Ok(container) = next_container { + Some(container) + } else { + None + }; } SearchResult::new(current_obj, approximate) diff --git a/lib/src/json_read.rs b/lib/src/json_read.rs index a40e899..1ed4bc5 100644 --- a/lib/src/json_read.rs +++ b/lib/src/json_read.rs @@ -348,6 +348,7 @@ fn jobject_to_choice(obj: &Map) -> Result) -> Result) -> Vec { + let mut tags: Vec = Vec::new(); + + let prop_value = obj.get("tags"); + if let Some(pv) = prop_value { + let tags_array = pv.as_array().unwrap(); + for tag in tags_array { + tags.push(tag.as_str().unwrap().to_string()); + } + } + + tags +} + pub fn jtoken_to_list_definitions( def: &serde_json::Value, ) -> Result { diff --git a/lib/src/json_write.rs b/lib/src/json_write.rs index 8418fea..dbed038 100644 --- a/lib/src/json_write.rs +++ b/lib/src/json_write.rs @@ -271,9 +271,20 @@ pub fn write_choice(choice: &Choice) -> serde_json::Value { json!(choice.target_path.to_string()), ); + jobj.insert("tags".to_owned(), write_choice_tags(choice)); + serde_json::Value::Object(jobj) } +fn write_choice_tags(choice: &Choice) -> serde_json::Value { + let mut tags: Vec = Vec::new(); + for t in &choice.tags { + tags.push(json!(t)); + } + + serde_json::Value::Array(tags) +} + pub(crate) fn write_list_rt_objs( objs: &[Rc], ) -> Result { diff --git a/lib/src/list_definitions_origin.rs b/lib/src/list_definitions_origin.rs index 382b503..d29062d 100644 --- a/lib/src/list_definitions_origin.rs +++ b/lib/src/list_definitions_origin.rs @@ -43,6 +43,10 @@ impl ListDefinitionsOrigin { } pub fn find_single_item_list_with_name(&self, name: &str) -> Option<&Rc> { + if name.trim().is_empty() { + return None; + } + self.all_unambiguous_list_value_cache.get(name) } } diff --git a/lib/src/native_function_call.rs b/lib/src/native_function_call.rs index ee22396..33f3863 100644 --- a/lib/src/native_function_call.rs +++ b/lib/src/native_function_call.rs @@ -218,7 +218,7 @@ impl NativeFunctionCall { for p in ¶ms { if p.as_ref().as_any().is::() { - return Err(StoryError::InvalidStoryState("Attempting to perform operation on a void value. Did you forget to 'return' a value from a function you called here?".to_owned())); + return Err(StoryError::InvalidStoryState(format!("Attempting to perform {} on a void value. Did you forget to 'return' a value from a function you called here?", Self::get_name(self.op)))); } if Value::get_list_value(p.as_ref()).is_some() { diff --git a/lib/src/object.rs b/lib/src/object.rs index 3757720..1d05821 100644 --- a/lib/src/object.rs +++ b/lib/src/object.rs @@ -13,6 +13,7 @@ use crate::{ search_result::SearchResult, }; +#[derive(Clone)] pub struct Object { parent: RefCell>, path: RefCell>, diff --git a/lib/src/story/external_functions.rs b/lib/src/story/external_functions.rs index 832370b..4361144 100644 --- a/lib/src/story/external_functions.rs +++ b/lib/src/story/external_functions.rs @@ -94,6 +94,35 @@ impl Story { // Should this function break glue? Abort run if we've already seen a newline. // Set a bool to tell it to restore the snapshot at the end of this instruction. if let Some(func_def) = self.externals.get(func_name) { + if func_def.lookahead_safe && self.get_state().in_string_evaluation() { + // 16th Jan 2023: Example ink that was failing: + // + // A line above + // ~ temp text = "{theFunc()}" + // {text} + // + // === function theFunc() + // { external(): + // Boom + // } + // + // EXTERNAL external() + // + // What was happening: The external() call would exit out early due to + // _stateSnapshotAtLastNewline having a value, leaving the evaluation stack + // without a return value on it. When the if-statement tried to pop a value, + // the evaluation stack would be empty, and there would be an exception. + // + // The snapshot rewinding code is only designed to work when outside of + // string generation code (there's a check for that in the snapshot rewinding code), + // hence these things are incompatible, you can't have unsafe functions that + // cause snapshot rewinding in the middle of string generation. + // + self.add_error(&format!("External function {} could not be called because 1) it wasn't marked as lookaheadSafe when BindExternalFunction was called and 2) the story is in the middle of string generation, either because choice text is being generated, or because you have ink like \"hello {{func()}}\". You can work around this by generating the result of your function into a temporary variable before the string or choice gets generated: ~ temp x = {}()", func_name, func_name), false); + + return Ok(()); + } + if !func_def.lookahead_safe && self.state_snapshot_at_last_new_line.is_some() { self.saw_lookahead_unsafe_function_after_new_line = true; return Ok(()); diff --git a/lib/src/story/progress.rs b/lib/src/story/progress.rs index 182cdc8..44bd81f 100644 --- a/lib/src/story/progress.rs +++ b/lib/src/story/progress.rs @@ -87,10 +87,10 @@ impl Story { // In this case, we only want to batch observe variable changes // for the outermost call. if self.recursive_continue_count == 1 { - self.state - .variables_state - .start_batch_observing_variable_changes(); + self.state.variables_state.start_variable_observation(); } + } else if self.async_continue_active && !is_async_time_limited { + self.async_continue_active = false; } // Start timing (only when necessary) @@ -128,6 +128,8 @@ impl Story { } } + let mut changed_variables_to_observe = None; + // 4 outcomes: // - got newline (so finished this line of text) // - can't continue (e.g. choices or ending) @@ -182,14 +184,8 @@ impl Story { self.saw_lookahead_unsafe_function_after_new_line = false; if self.recursive_continue_count == 1 { - let changed = self - .state - .variables_state - .stop_batch_observing_variable_changes(); - - for (variable_name, value) in changed { - self.notify_variable_changed(&variable_name, &value); - } + changed_variables_to_observe = + Some(self.state.variables_state.complete_variable_observation()); } self.async_continue_active = false; @@ -263,6 +259,13 @@ impl Story { ); } + // Send out variable observation events at the last second, since it might trigger new ink to be run + if let Some(changed) = changed_variables_to_observe { + for (variable_name, value) in changed { + self.notify_variable_changed(&variable_name, &value); + } + } + return Err(StoryError::InvalidStoryState(sb)); } } diff --git a/lib/src/story/state.rs b/lib/src/story/state.rs index 32b32a0..81cb444 100644 --- a/lib/src/story/state.rs +++ b/lib/src/story/state.rs @@ -91,7 +91,7 @@ impl Story { pub(crate) fn state_snapshot(&mut self) { // tmp_state contains the new state and current state is stored in snapshot - let mut tmp_state = self.state.copy_and_start_patching(); + let mut tmp_state = self.state.copy_and_start_patching(false); std::mem::swap(&mut tmp_state, &mut self.state); self.state_snapshot_at_last_new_line = Some(tmp_state); } diff --git a/lib/src/story_state.rs b/lib/src/story_state.rs index 1c1431d..2f02f0d 100644 --- a/lib/src/story_state.rs +++ b/lib/src/story_state.rs @@ -103,6 +103,22 @@ impl StoryState { !self.current_errors.is_empty() } + /** + * Get the previous state of currentPathString, which can be helpful + * for finding out where the story was before it ended (when the path + * string becomes null) + * + * Marked as dead code by now. + */ + #[allow(dead_code)] + pub fn previous_path_string(&self) -> Option { + let pointer = self.get_previous_pointer(); + match pointer.get_path() { + Some(path) => Some(path.to_string()), + None => None, + } + } + pub fn get_current_pointer(&self) -> Pointer { self.get_callstack() .borrow() @@ -738,7 +754,7 @@ impl StoryState { Some(&self.current_flow.current_choices) } - pub fn copy_and_start_patching(&self) -> StoryState { + pub fn copy_and_start_patching(&self, for_background_save: bool) -> StoryState { let mut copy = StoryState::new( self.main_content_container.clone(), self.list_definitions.clone(), @@ -753,10 +769,26 @@ impl StoryState { copy.current_flow.callstack = Rc::new(RefCell::new( self.current_flow.callstack.as_ref().borrow().clone(), )); - copy.current_flow.current_choices = self.current_flow.current_choices.clone(); copy.current_flow.output_stream = self.current_flow.output_stream.clone(); copy.output_stream_dirty(); + // When background saving we need to make copies of choices since they each have + // a snapshot of the thread at the time of generation since the game could progress + // significantly and threads modified during the save process. + // However, when doing internal saving and restoring of snapshots this isn't an issue, + // and we can simply ref-copy the choices with their existing threads. + if for_background_save { + copy.current_flow.current_choices = + Vec::with_capacity(self.current_flow.current_choices.len()); + + for choice in self.current_flow.current_choices.iter() { + let c = choice.as_ref().clone(); + copy.current_flow.current_choices.push(Rc::new(c)); + } + } else { + copy.current_flow.current_choices = self.current_flow.current_choices.clone(); + } + // The copy of the state has its own copy of the named flows dictionary, // except with the current flow replaced with the copy above // (Assuming we're in multi-flow mode at all. If we're not then diff --git a/lib/src/variables_state.rs b/lib/src/variables_state.rs index 2a54524..1ec088a 100644 --- a/lib/src/variables_state.rs +++ b/lib/src/variables_state.rs @@ -44,15 +44,15 @@ impl VariablesState { } } - pub fn start_batch_observing_variable_changes(&mut self) { + pub fn start_variable_observation(&mut self) { self.batch_observing_variable_changes = true; self.changed_variables_for_batch_obs = Some(HashSet::new()); } - pub fn stop_batch_observing_variable_changes(&mut self) -> Vec<(String, ValueType)> { + pub fn complete_variable_observation(&mut self) -> HashMap { self.batch_observing_variable_changes = false; - let mut changed: Vec<(String, ValueType)> = Vec::with_capacity(0); + let mut changed_vars = HashMap::with_capacity(0); // Finished observing variables in a batch - now send // notifications for changed variables all in one go. @@ -60,11 +60,20 @@ impl VariablesState { for variable_name in changed_variables_for_batch_obs { let current_value = self.global_variables.get(&variable_name).unwrap(); - changed.push((variable_name, current_value.value.clone())); + changed_vars.insert(variable_name, current_value.value.clone()); } } - changed + // Patch may still be active - e.g. if we were in the middle of a background save + if let Some(patch) = &self.patch { + for variable_name in patch.changed_variables.iter() { + if let Some(patched_val) = patch.get_global(variable_name) { + changed_vars.insert(variable_name.to_string(), patched_val.value.clone()); + } + } + } + + changed_vars } pub fn snapshot_default_globals(&mut self) {