Skip to content

Commit

Permalink
Match Ink reference v1.2.0
Browse files Browse the repository at this point in the history
  • Loading branch information
bladecoder committed Jul 17, 2024
1 parent 0c1abc5 commit b2939a1
Show file tree
Hide file tree
Showing 14 changed files with 152 additions and 32 deletions.
6 changes: 3 additions & 3 deletions lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
4 changes: 3 additions & 1 deletion lib/src/callstack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()));
}
}
Expand Down Expand Up @@ -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<Rc<Value>> {
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;
}
Expand Down
4 changes: 3 additions & 1 deletion lib/src/choice.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Option<Thread>>,
Expand Down Expand Up @@ -54,12 +55,13 @@ impl Choice {
text: &str,
index: usize,
original_thread_index: usize,
choice_tags: Vec<String>,
) -> 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(),
Expand Down
25 changes: 18 additions & 7 deletions lib/src/container.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<Container>() {
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::<Container>();
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)
Expand Down
16 changes: 16 additions & 0 deletions lib/src/json_read.rs
Original file line number Diff line number Diff line change
Expand Up @@ -348,16 +348,32 @@ fn jobject_to_choice(obj: &Map<String, serde_json::Value>) -> Result<Rc<dyn RTOb
let source_path = obj.get("originalChoicePath").unwrap().as_str().unwrap();
let original_thread_index = obj.get("originalThreadIndex").unwrap().as_i64().unwrap() as usize;
let path_string_on_choice = obj.get("targetPath").unwrap().as_str().unwrap();
let choice_tags = jarray_to_tags(obj);

Ok(Rc::new(Choice::new_from_json(
path_string_on_choice,
source_path.to_string(),
text,
index,
original_thread_index,
choice_tags,
)))
}

fn jarray_to_tags(obj: &Map<String, serde_json::Value>) -> Vec<String> {
let mut tags: Vec<String> = 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<ListDefinitionsOrigin, StoryError> {
Expand Down
11 changes: 11 additions & 0 deletions lib/src/json_write.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<serde_json::Value> = 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<dyn RTObject>],
) -> Result<serde_json::Value, StoryError> {
Expand Down
4 changes: 4 additions & 0 deletions lib/src/list_definitions_origin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ impl ListDefinitionsOrigin {
}

pub fn find_single_item_list_with_name(&self, name: &str) -> Option<&Rc<Value>> {
if name.trim().is_empty() {
return None;
}

self.all_unambiguous_list_value_cache.get(name)
}
}
2 changes: 1 addition & 1 deletion lib/src/native_function_call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ impl NativeFunctionCall {

for p in &params {
if p.as_ref().as_any().is::<Void>() {
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() {
Expand Down
1 change: 1 addition & 0 deletions lib/src/object.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use crate::{
search_result::SearchResult,
};

#[derive(Clone)]
pub struct Object {
parent: RefCell<Weak<Container>>,
path: RefCell<Option<Path>>,
Expand Down
29 changes: 29 additions & 0 deletions lib/src/story/external_functions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(());
Expand Down
25 changes: 14 additions & 11 deletions lib/src/story/progress.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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));
}
}
Expand Down
2 changes: 1 addition & 1 deletion lib/src/story/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
36 changes: 34 additions & 2 deletions lib/src/story_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> {
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()
Expand Down Expand Up @@ -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(),
Expand All @@ -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
Expand Down
19 changes: 14 additions & 5 deletions lib/src/variables_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,27 +44,36 @@ 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<String, ValueType> {
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.
if let Some(changed_variables_for_batch_obs) = self.changed_variables_for_batch_obs.take() {
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) {
Expand Down

0 comments on commit b2939a1

Please sign in to comment.