Give AVM1 the notion of a stack frame.
This necessitates the use of a copy of SWF data into the GC arena, along with unsafe (and possibly unsound) pointer manipulation to get the action reader to hold a GC pointer.
This commit is contained in:
parent
693e791d75
commit
617b493733
226
core/src/avm1.rs
226
core/src/avm1.rs
|
@ -3,8 +3,9 @@ use crate::avm1::object::Object;
|
|||
use crate::backend::navigator::NavigationMethod;
|
||||
|
||||
use crate::prelude::*;
|
||||
use gc_arena::{GcCell, MutationContext};
|
||||
use gc_arena::{Gc, GcCell, MutationContext};
|
||||
use rand::{rngs::SmallRng, Rng};
|
||||
use std::slice;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use swf::avm1::read::Reader;
|
||||
|
@ -51,33 +52,115 @@ pub struct ActionContext<'a, 'gc, 'gc_context> {
|
|||
pub navigator: &'a mut dyn crate::backend::navigator::NavigatorBackend,
|
||||
}
|
||||
|
||||
pub struct Avm1<'gc> {
|
||||
/// Represents a single activation of a given AVM1 function or keyframe.
|
||||
pub struct Avm1StackFrame<'gc> {
|
||||
/// Represents the SWF version of a given function.
|
||||
///
|
||||
/// Certain AVM1 operations change behavior based on the version of the SWF
|
||||
/// file they were defined in. For example, case sensitivity changes based
|
||||
/// on the SWF version.
|
||||
swf_version: u8,
|
||||
|
||||
/// Action data being executed by the reader below.
|
||||
data: Gc<'gc, Vec<u8>>,
|
||||
|
||||
/// Represents the current location of the instruction stream this stack
|
||||
/// frame is executing.
|
||||
pc: Reader<'gc>,
|
||||
|
||||
/// The opcode stack values for the current stack frame.
|
||||
stack: Vec<Value<'gc>>,
|
||||
constant_pool: Vec<String>,
|
||||
|
||||
/// All defined local variables in this stack frame.
|
||||
locals: HashMap<String, Value<'gc>>,
|
||||
}
|
||||
|
||||
unsafe impl<'gc> gc_arena::Collect for Avm1StackFrame<'gc> {
|
||||
#[inline]
|
||||
fn trace(&self, cc: gc_arena::CollectionContext) {
|
||||
self.data.trace(cc);
|
||||
self.stack.trace(cc);
|
||||
self.locals.trace(cc);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'gc> Avm1StackFrame<'gc> {
|
||||
pub fn from_action(swf_version: u8, code: &[u8], mc: MutationContext<'gc, '_>) -> Avm1StackFrame<'gc> {
|
||||
let data = Gc::allocate(mc, code.to_vec());
|
||||
|
||||
//TODO: Figure out how to get Reader to hold a GC pointer directly.
|
||||
//If gc_arena moves data around this is completely unsound.
|
||||
let evil_slice = unsafe {
|
||||
slice::from_raw_parts(data.as_ptr(), data.len())
|
||||
};
|
||||
|
||||
Avm1StackFrame {
|
||||
swf_version: swf_version,
|
||||
data: data,
|
||||
pc: Reader::new(evil_slice, swf_version),
|
||||
stack: Vec::new(),
|
||||
locals: HashMap::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the SWF version of the action or function being executed.
|
||||
pub fn swf_version(&self) -> u8 {
|
||||
self.swf_version
|
||||
}
|
||||
|
||||
/// Returns the current ActionScript reader.
|
||||
pub fn pc(&self) -> &Reader<'gc> {
|
||||
&self.pc
|
||||
}
|
||||
|
||||
/// Returns the current ActionScript reader for mutation.
|
||||
pub fn pc_mut(&mut self) -> &mut Reader<'gc> {
|
||||
&mut self.pc
|
||||
}
|
||||
|
||||
/// Returns the value stack.
|
||||
pub fn stack(&self) -> &Vec<Value<'gc>> {
|
||||
&self.stack
|
||||
}
|
||||
|
||||
/// Returns the value stack for mutation.
|
||||
pub fn stack_mut(&mut self) -> &mut Vec<Value<'gc>> {
|
||||
&mut self.stack
|
||||
}
|
||||
|
||||
/// Returns AVM locals.
|
||||
pub fn locals(&self) -> &HashMap<String, Value<'gc>> {
|
||||
&self.locals
|
||||
}
|
||||
|
||||
/// Returns AVM locals for mutation.
|
||||
pub fn locals_mut(&mut self) -> &mut HashMap<String, Value<'gc>> {
|
||||
&mut self.locals
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Avm1<'gc> {
|
||||
constant_pool: Vec<String>,
|
||||
globals: GcCell<'gc, Object<'gc>>,
|
||||
stack_frames: Vec<Avm1StackFrame<'gc>>
|
||||
}
|
||||
|
||||
unsafe impl<'gc> gc_arena::Collect for Avm1<'gc> {
|
||||
#[inline]
|
||||
fn trace(&self, cc: gc_arena::CollectionContext) {
|
||||
self.stack.trace(cc);
|
||||
self.locals.trace(cc);
|
||||
self.globals.trace(cc);
|
||||
self.stack_frames.trace(cc);
|
||||
}
|
||||
}
|
||||
|
||||
type Error = Box<dyn std::error::Error>;
|
||||
|
||||
impl<'gc> Avm1<'gc> {
|
||||
pub fn new(gc_context: MutationContext<'gc, '_>, swf_version: u8) -> Self {
|
||||
pub fn new(gc_context: MutationContext<'gc, '_>) -> Self {
|
||||
Self {
|
||||
swf_version,
|
||||
stack: vec![],
|
||||
constant_pool: vec![],
|
||||
locals: HashMap::new(),
|
||||
globals: GcCell::allocate(gc_context, create_globals(gc_context)),
|
||||
stack_frames: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -88,24 +171,63 @@ impl<'gc> Avm1<'gc> {
|
|||
pub fn locals_into_form_values(&self) -> HashMap<String, String> {
|
||||
let mut form_values = HashMap::new();
|
||||
|
||||
for (k, v) in self.locals.iter() {
|
||||
for (k, v) in self.current_stack_frame().unwrap().locals().iter() {
|
||||
form_values.insert(k.clone(), v.clone().into_string());
|
||||
}
|
||||
|
||||
form_values
|
||||
}
|
||||
|
||||
pub fn do_action(
|
||||
&mut self,
|
||||
context: &mut ActionContext<'_, 'gc, '_>,
|
||||
code: &[u8],
|
||||
) -> Result<(), Error> {
|
||||
let mut pc_stack = vec![Reader::new(code, self.swf_version)];
|
||||
/// Add a stack frame to the current AVM stack.
|
||||
///
|
||||
/// The code pointer given will be copied into the GC arena.
|
||||
pub fn insert_stack_frame(&mut self, swf_version: u8, code: &[u8], mc: MutationContext<'gc, '_>) {
|
||||
self.stack_frames.push(Avm1StackFrame::from_action(swf_version, code, mc));
|
||||
}
|
||||
|
||||
while pc_stack.len() > 0 {
|
||||
let mut reader = pc_stack.last_mut().unwrap();
|
||||
/// Retrieve the current AVM execution frame.
|
||||
///
|
||||
/// Yields None if there is no stack frame.
|
||||
pub fn current_stack_frame(&self) -> Option<&Avm1StackFrame<'gc>> {
|
||||
self.stack_frames.last()
|
||||
}
|
||||
|
||||
if let Some(action) = reader.read_action()? {
|
||||
/// Retrieve the current AVM execution frame for mutation.
|
||||
///
|
||||
/// Yields None if there is no stack frame.
|
||||
pub fn current_stack_frame_mut(&mut self) -> Option<&mut Avm1StackFrame<'gc>> {
|
||||
self.stack_frames.last_mut()
|
||||
}
|
||||
|
||||
/// Get the currently executing SWF version, if there is one.
|
||||
fn current_swf_version(&self) -> Option<u8> {
|
||||
self.current_stack_frame().map(|sf| sf.swf_version())
|
||||
}
|
||||
|
||||
/// Get the current stack frame's action reader (PC), if there is one.
|
||||
fn current_reader_mut(&mut self) -> Option<&mut Reader<'gc>> {
|
||||
self.current_stack_frame_mut().map(|sf| sf.pc_mut())
|
||||
}
|
||||
|
||||
/// Get the current locals, if there are any.
|
||||
fn current_locals(&self) -> Option<&HashMap<String, Value<'gc>>> {
|
||||
self.current_stack_frame().map(|sf| sf.locals())
|
||||
}
|
||||
|
||||
/// Get the current locals, for mutation, if there are any.
|
||||
fn current_locals_mut(&mut self) -> Option<&mut HashMap<String, Value<'gc>>> {
|
||||
self.current_stack_frame_mut().map(|sf| sf.locals_mut())
|
||||
}
|
||||
|
||||
/// Destroy the current stack frame (if there is one).
|
||||
fn retire_stack_frame(&mut self) {
|
||||
self.stack_frames.pop();
|
||||
}
|
||||
|
||||
/// Execute the AVM stack until it is exhausted.
|
||||
pub fn do_action(&mut self, context: &mut ActionContext<'_, 'gc, '_>) -> Result<(), Error> {
|
||||
while self.stack_frames.len() > 0 {
|
||||
if let Some(action) = self.current_reader_mut().unwrap().read_action()? {
|
||||
use swf::avm1::types::Action;
|
||||
let result = match action {
|
||||
Action::Add => self.action_add(context),
|
||||
|
@ -159,11 +281,11 @@ impl<'gc> Avm1<'gc> {
|
|||
scene_offset,
|
||||
} => self.action_goto_frame_2(context, set_playing, scene_offset),
|
||||
Action::GotoLabel(label) => self.action_goto_label(context, &label),
|
||||
Action::If { offset } => self.action_if(context, offset, &mut reader),
|
||||
Action::If { offset } => self.action_if(context, offset),
|
||||
Action::Increment => self.action_increment(context),
|
||||
Action::InitArray => self.action_init_array(context),
|
||||
Action::InitObject => self.action_init_object(context),
|
||||
Action::Jump { offset } => self.action_jump(context, offset, &mut reader),
|
||||
Action::Jump { offset } => self.action_jump(context, offset),
|
||||
Action::Less => self.action_less(context),
|
||||
Action::Less2 => self.action_less_2(context),
|
||||
Action::MBAsciiToChar => self.action_mb_ascii_to_char(context),
|
||||
|
@ -211,10 +333,10 @@ impl<'gc> Avm1<'gc> {
|
|||
Action::WaitForFrame {
|
||||
frame,
|
||||
num_actions_to_skip,
|
||||
} => self.action_wait_for_frame(context, frame, num_actions_to_skip, &mut reader),
|
||||
} => self.action_wait_for_frame(context, frame, num_actions_to_skip),
|
||||
Action::WaitForFrame2 {
|
||||
num_actions_to_skip,
|
||||
} => self.action_wait_for_frame_2(context, num_actions_to_skip, &mut reader),
|
||||
} => self.action_wait_for_frame_2(context, num_actions_to_skip),
|
||||
Action::With { .. } => self.action_with(context),
|
||||
_ => self.unknown_op(context, action),
|
||||
};
|
||||
|
@ -224,7 +346,7 @@ impl<'gc> Avm1<'gc> {
|
|||
}
|
||||
} else {
|
||||
//Out of code. Return to the parent function.
|
||||
pc_stack.pop();
|
||||
self.retire_stack_frame();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -283,11 +405,11 @@ impl<'gc> Avm1<'gc> {
|
|||
}
|
||||
|
||||
fn push(&mut self, value: impl Into<Value<'gc>>) {
|
||||
self.stack.push(value.into());
|
||||
self.current_stack_frame_mut().unwrap().stack_mut().push(value.into());
|
||||
}
|
||||
|
||||
fn pop(&mut self) -> Result<Value<'gc>, Error> {
|
||||
self.stack.pop().ok_or_else(|| "Stack underflow".into())
|
||||
self.current_stack_frame_mut().unwrap().stack_mut().pop().ok_or_else(|| "Stack underflow".into())
|
||||
}
|
||||
|
||||
fn unknown_op(
|
||||
|
@ -329,7 +451,7 @@ impl<'gc> Avm1<'gc> {
|
|||
let a = self.pop()?;
|
||||
let b = self.pop()?;
|
||||
let result = b.into_number_v1() != 0.0 && a.into_number_v1() != 0.0;
|
||||
self.push(Value::from_bool_v1(result, self.swf_version));
|
||||
self.push(Value::from_bool_v1(result, self.current_swf_version().unwrap()));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -418,7 +540,7 @@ impl<'gc> Avm1<'gc> {
|
|||
self.pop()?;
|
||||
}
|
||||
|
||||
self.stack.push(Value::Undefined);
|
||||
self.current_stack_frame_mut().unwrap().stack_mut().push(Value::Undefined);
|
||||
// TODO(Herschel)
|
||||
Err("Unimplemented action: CallFunction".into())
|
||||
}
|
||||
|
@ -439,13 +561,13 @@ impl<'gc> Avm1<'gc> {
|
|||
Value::Undefined | Value::Null => {
|
||||
let this = context.active_clip.read().object().as_object()?.to_owned();
|
||||
let return_value = object.call(self, context, this, &args)?;
|
||||
self.stack.push(return_value);
|
||||
self.current_stack_frame_mut().unwrap().stack_mut().push(return_value);
|
||||
}
|
||||
Value::String(name) => {
|
||||
if name.is_empty() {
|
||||
let return_value =
|
||||
object.call(self, context, object.as_object()?.to_owned(), &args)?;
|
||||
self.stack.push(return_value);
|
||||
self.current_stack_frame_mut().unwrap().stack_mut().push(return_value);
|
||||
} else {
|
||||
let callable = object.as_object()?.read().get(
|
||||
&name,
|
||||
|
@ -461,7 +583,7 @@ impl<'gc> Avm1<'gc> {
|
|||
let return_value =
|
||||
callable.call(self, context, object.as_object()?.to_owned(), &args)?;
|
||||
|
||||
self.stack.push(return_value);
|
||||
self.current_stack_frame_mut().unwrap().stack_mut().push(return_value);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
|
@ -505,13 +627,13 @@ impl<'gc> Avm1<'gc> {
|
|||
fn action_define_local(&mut self, _context: &mut ActionContext) -> Result<(), Error> {
|
||||
let value = self.pop()?;
|
||||
let name = self.pop()?;
|
||||
self.locals.insert(name.as_string()?.clone(), value);
|
||||
self.current_locals_mut().unwrap().insert(name.as_string()?.clone(), value);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn action_define_local_2(&mut self, _context: &mut ActionContext) -> Result<(), Error> {
|
||||
let name = self.pop()?;
|
||||
self.locals
|
||||
self.current_locals_mut().unwrap()
|
||||
.insert(name.as_string()?.clone(), Value::Undefined);
|
||||
Ok(())
|
||||
}
|
||||
|
@ -561,7 +683,7 @@ impl<'gc> Avm1<'gc> {
|
|||
let a = self.pop()?;
|
||||
let b = self.pop()?;
|
||||
let result = b.into_number_v1() == a.into_number_v1();
|
||||
self.push(Value::from_bool_v1(result, self.swf_version));
|
||||
self.push(Value::from_bool_v1(result, self.current_swf_version().unwrap()));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -641,7 +763,7 @@ impl<'gc> Avm1<'gc> {
|
|||
}
|
||||
|
||||
fn action_get_time(&mut self, context: &mut ActionContext) -> Result<(), Error> {
|
||||
self.stack.push(Value::Number(context.global_time as f64));
|
||||
self.current_stack_frame_mut().unwrap().stack_mut().push(Value::Number(context.global_time as f64));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -663,7 +785,7 @@ impl<'gc> Avm1<'gc> {
|
|||
return Ok(());
|
||||
}
|
||||
|
||||
let mut result = self.locals.get(path).map(|v| v.to_owned());
|
||||
let mut result = self.current_locals().unwrap().get(path).map(|v| v.to_owned());
|
||||
|
||||
if result.is_none() {
|
||||
if let Some((node, var_name)) =
|
||||
|
@ -821,12 +943,11 @@ impl<'gc> Avm1<'gc> {
|
|||
fn action_if(
|
||||
&mut self,
|
||||
_context: &mut ActionContext,
|
||||
jump_offset: i16,
|
||||
reader: &mut Reader<'_>,
|
||||
jump_offset: i16
|
||||
) -> Result<(), Error> {
|
||||
let val = self.pop()?;
|
||||
if val.as_bool() {
|
||||
reader.seek(jump_offset.into());
|
||||
self.current_reader_mut().unwrap().seek(jump_offset.into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
@ -861,11 +982,10 @@ impl<'gc> Avm1<'gc> {
|
|||
fn action_jump(
|
||||
&mut self,
|
||||
_context: &mut ActionContext,
|
||||
jump_offset: i16,
|
||||
reader: &mut Reader<'_>,
|
||||
jump_offset: i16
|
||||
) -> Result<(), Error> {
|
||||
// TODO(Herschel): Handle out-of-bounds.
|
||||
reader.seek(jump_offset.into());
|
||||
self.current_reader_mut().unwrap().seek(jump_offset.into());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -874,7 +994,7 @@ impl<'gc> Avm1<'gc> {
|
|||
let a = self.pop()?;
|
||||
let b = self.pop()?;
|
||||
let result = b.into_number_v1() < a.into_number_v1();
|
||||
self.push(Value::from_bool_v1(result, self.swf_version));
|
||||
self.push(Value::from_bool_v1(result, self.current_swf_version().unwrap()));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -945,7 +1065,7 @@ impl<'gc> Avm1<'gc> {
|
|||
// AS1 logical not
|
||||
let val = self.pop()?;
|
||||
let result = val.into_number_v1() == 0.0;
|
||||
self.push(Value::from_bool_v1(result, self.swf_version));
|
||||
self.push(Value::from_bool_v1(result, self.current_swf_version().unwrap()));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -988,7 +1108,7 @@ impl<'gc> Avm1<'gc> {
|
|||
let a = self.pop()?;
|
||||
let b = self.pop()?;
|
||||
let result = b.into_number_v1() != 0.0 || a.into_number_v1() != 0.0;
|
||||
self.push(Value::from_bool_v1(result, self.swf_version));
|
||||
self.push(Value::from_bool_v1(result, self.current_swf_version().unwrap()));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -1063,7 +1183,7 @@ impl<'gc> Avm1<'gc> {
|
|||
}
|
||||
|
||||
fn action_push_duplicate(&mut self, _context: &mut ActionContext) -> Result<(), Error> {
|
||||
let val = self.stack.last().ok_or("Stack underflow")?.clone();
|
||||
let val = self.current_stack_frame().unwrap().stack().last().ok_or("Stack underflow")?.clone();
|
||||
self.push(val);
|
||||
Ok(())
|
||||
}
|
||||
|
@ -1242,7 +1362,7 @@ impl<'gc> Avm1<'gc> {
|
|||
_register: u8,
|
||||
) -> Result<(), Error> {
|
||||
// Does NOT pop the value from the stack.
|
||||
let _val = self.stack.last().ok_or("Stack underflow")?;
|
||||
let _val = self.current_stack_frame().unwrap().stack().last().ok_or("Stack underflow")?;
|
||||
Err("Unimplemented action: StoreRegister".into())
|
||||
}
|
||||
|
||||
|
@ -1261,7 +1381,7 @@ impl<'gc> Avm1<'gc> {
|
|||
let a = self.pop()?;
|
||||
let b = self.pop()?;
|
||||
let result = b.into_string() == a.into_string();
|
||||
self.push(Value::from_bool_v1(result, self.swf_version));
|
||||
self.push(Value::from_bool_v1(result, self.current_swf_version().unwrap()));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -1298,7 +1418,7 @@ impl<'gc> Avm1<'gc> {
|
|||
let b = self.pop()?;
|
||||
// This is specifically a non-UTF8 aware comparison.
|
||||
let result = b.into_string().bytes().lt(a.into_string().bytes());
|
||||
self.push(Value::from_bool_v1(result, self.swf_version));
|
||||
self.push(Value::from_bool_v1(result, self.current_swf_version().unwrap()));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -1358,14 +1478,14 @@ impl<'gc> Avm1<'gc> {
|
|||
&mut self,
|
||||
_context: &mut ActionContext,
|
||||
_frame: u16,
|
||||
num_actions_to_skip: u8,
|
||||
reader: &mut Reader<'_>,
|
||||
num_actions_to_skip: u8
|
||||
) -> Result<(), Error> {
|
||||
// TODO(Herschel): Always true for now.
|
||||
let loaded = true;
|
||||
if !loaded {
|
||||
// Note that the offset is given in # of actions, NOT in bytes.
|
||||
// Read the actions and toss them away.
|
||||
let reader = self.current_reader_mut().unwrap();
|
||||
for _ in 0..num_actions_to_skip {
|
||||
reader.read_action()?;
|
||||
}
|
||||
|
@ -1376,8 +1496,7 @@ impl<'gc> Avm1<'gc> {
|
|||
fn action_wait_for_frame_2(
|
||||
&mut self,
|
||||
_context: &mut ActionContext,
|
||||
num_actions_to_skip: u8,
|
||||
reader: &mut Reader<'_>,
|
||||
num_actions_to_skip: u8
|
||||
) -> Result<(), Error> {
|
||||
// TODO(Herschel): Always true for now.
|
||||
let _frame_num = self.pop()?.as_f64()? as u16;
|
||||
|
@ -1385,6 +1504,7 @@ impl<'gc> Avm1<'gc> {
|
|||
if !loaded {
|
||||
// Note that the offset is given in # of actions, NOT in bytes.
|
||||
// Read the actions and toss them away.
|
||||
let reader = self.current_reader_mut().unwrap();
|
||||
for _ in 0..num_actions_to_skip {
|
||||
reader.read_action()?;
|
||||
}
|
||||
|
|
|
@ -113,7 +113,7 @@ mod tests {
|
|||
F: for<'a, 'gc> FnOnce(&mut Avm1<'gc>, &mut ActionContext<'a, 'gc, '_>) -> R,
|
||||
{
|
||||
rootless_arena(|gc_context| {
|
||||
let mut avm = Avm1::new(gc_context, swf_version);
|
||||
let mut avm = Avm1::new(gc_context);
|
||||
let movie_clip: Box<dyn DisplayObject> = Box::new(MovieClip::new(gc_context));
|
||||
let root = GcCell::allocate(gc_context, movie_clip);
|
||||
let mut context = ActionContext {
|
||||
|
|
|
@ -110,7 +110,7 @@ impl<Audio: AudioBackend, Renderer: RenderBackend, Navigator: NavigatorBackend>
|
|||
)),
|
||||
),
|
||||
mouse_hover_node: GcCell::allocate(gc_context, None),
|
||||
avm: GcCell::allocate(gc_context, Avm1::new(gc_context, header.version)),
|
||||
avm: GcCell::allocate(gc_context, Avm1::new(gc_context)),
|
||||
}),
|
||||
|
||||
frame_rate: header.frame_rate.into(),
|
||||
|
@ -515,9 +515,8 @@ impl<Audio: AudioBackend, Renderer: RenderBackend, Navigator: NavigatorBackend>
|
|||
action_context.start_clip = active_clip;
|
||||
action_context.active_clip = active_clip;
|
||||
action_context.target_clip = Some(active_clip);
|
||||
let _ = update_context
|
||||
.avm
|
||||
.do_action(&mut action_context, action.as_ref());
|
||||
update_context.avm.insert_stack_frame(update_context.swf_version, action.as_ref(), action_context.gc_context);
|
||||
let _ = update_context.avm.do_action(&mut action_context);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue