diff --git a/Cargo.lock b/Cargo.lock
index e797640..e6af4ee 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -611,6 +611,7 @@ dependencies = [
  "glam",
  "heapless",
  "identconv",
+ "instructions",
  "log",
  "macros",
  "memmap2",
@@ -1594,6 +1595,16 @@ dependencies = [
  "hashbrown 0.15.0",
 ]
 
+[[package]]
+name = "instructions"
+version = "0.1.0"
+dependencies = [
+ "macros",
+ "num-derive",
+ "num-traits",
+ "truth",
+]
+
 [[package]]
 name = "is-terminal"
 version = "0.4.13"
diff --git a/Cargo.toml b/Cargo.toml
index 345b03c..5f83106 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,3 +1,9 @@
+[workspace]
+resolver = "2"
+members = ["instructions", "macros"]
+[workspace.dependencies]
+truth = { git = "https://github.com/ExpHP/truth" }
+
 [package]
 name = "chireiden-thing"
 version = "0.1.0"
@@ -13,7 +19,7 @@ ndarray = "0.16.1"
 num-derive = "0.4.2"
 num-traits = "0.2.19"
 rand = "0.8.5"
-truth = { git = "https://github.com/ExpHP/truth" }
+truth = { workspace = true }
 anyhow = { version = "1.0", features = ["backtrace"] }
 glam = { version = "0.29", features = ["bytemuck"] }
 bytemuck = { version = "1.18", features = ["derive"] }
@@ -32,6 +38,7 @@ atomic_refcell = "0.1.13"
 pin-project = "1.1.7"
 heapless = "0.8.0"
 bitfield-struct = "0.9.2"
+instructions = { version = "0.1.0", path = "instructions" }
 
 [dev-dependencies]
 csv = "1.3.0"
diff --git a/flake.nix b/flake.nix
index b6a9954..957d4be 100644
--- a/flake.nix
+++ b/flake.nix
@@ -40,6 +40,12 @@
               alsa-lib
               (pkgs.callPackage ./thtk.nix {})
               (pkgs.callPackage ./truth.nix {})
+
+                  xorg.libX11
+    xorg.libXcursor
+    xorg.libxcb
+    xorg.libXi
+    libxkbcommon
             ];
             LD_LIBRARY_PATH = libPath;
           };
diff --git a/instructions/Cargo.toml b/instructions/Cargo.toml
new file mode 100644
index 0000000..51449cb
--- /dev/null
+++ b/instructions/Cargo.toml
@@ -0,0 +1,10 @@
+[package]
+name = "instructions"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+macros = { version = "0.1.0", path = "../macros" }
+num-derive = "0.4.2"
+num-traits = "0.2.19"
+truth = { workspace = true }
diff --git a/src/game/anm/vm/opcodes.rs b/instructions/src/anm.rs
similarity index 91%
rename from src/game/anm/vm/opcodes.rs
rename to instructions/src/anm.rs
index 1d1cf0c..188b450 100644
--- a/src/game/anm/vm/opcodes.rs
+++ b/instructions/src/anm.rs
@@ -2,9 +2,8 @@ use macros::decode_args;
 use num_derive::FromPrimitive;
 use num_traits::FromPrimitive;
 use truth::llir::RawInstr;
-use wgpu::naga::FastHashMap;
 
-use crate::game::param::Param;
+use crate::Param;
 
 #[derive(Debug, FromPrimitive)]
 enum Opcode {
@@ -93,21 +92,21 @@ enum Opcode {
   Color2Time = 78,
   Alpha2Time = 79,
   ColorMode = 80,
-  CaseReturn = 81,
+  ReturnFromInterrupt = 81,
   RotateAuto = 82,
   Ins83CopyPos = 83,
   TexCircle = 84,
   UnknownBitflag = 85,
   SlowdownImmune = 86,
   RandMode = 87,
-  ScriptNew = 88,
+  NewChildBack = 88,
   ResampleMode = 89,
-  ScriptNewUI = 90,
-  ScriptNewFront = 91,
-  ScriptNewUIFront = 92,
+  NewChildUIBack = 90,
+  NewChildFront = 91,
+  NewChildUIFront = 92,
   ScrollXTime = 93,
   ScrollYTime = 94,
-  ScriptNewRoot = 95,
+  NewRootBack = 95,
   ScriptNewPos = 96,
   ScriptNewRootPos = 97,
   MoveBezier = 100,
@@ -163,6 +162,8 @@ pub enum SpriteType {
   Rotate3D = 2,
   RotateZ3D = 3,
   RotateBillboard = 4,
+  Unknown3DA = 6,
+  Unknown3DB = 8,
 }
 
 #[derive(Debug, Clone, Copy)]
@@ -312,38 +313,43 @@ pub enum Op {
   Visible {
     value: i8,
   },
-  ZWriteDisable {
-    value: i32,
-  },
+  ZWriteDisable(bool),
   StdRelatedBitflag {
     enable: i32,
   },
   Wait {
     time: i32,
   },
-  CaseReturn,
+  ReturnFromInterrupt,
   UnknownBitflag {
     enable: i32,
   },
   RandMode {
     mode: i32,
   },
-  ScriptNew {
+  NewChildBack {
     script: i32,
   },
   ResampleMode {
     mode: i32,
   },
-  ScriptNewUI {
+  NewChildUIBack {
     script: i32,
   },
-  ScriptNewFront {
+  NewChildFront {
     script: i32,
   },
-  ScriptNewUIFront {
+  NewChildUIFront {
     script: i32,
   },
   ScrollXTime,
+  NewRootBack {
+    script: i32,
+  },
+  SpriteRand {
+    sprite: i32,
+    end: Param<i32>,
+  },
 }
 
 #[derive(Debug, Clone, Copy)]
@@ -353,7 +359,7 @@ pub struct Instruction {
 }
 
 impl Instruction {
-  pub fn from_raw(inst: RawInstr, instruction_offsets: &FastHashMap<u32, usize>) -> Self {
+  pub fn from_raw(inst: RawInstr, instruction_offsets: &Vec<(u32, usize)>) -> Self {
     let param_int = |value: i32, index: u16| {
       if inst.param_mask & (1 << index) == 0 {
         Param::Value(value)
@@ -385,7 +391,8 @@ impl Instruction {
       );
       value as i32
     };
-    let offset_to_index = |value: i32| instruction_offsets[&(value as u32)];
+    let offset_to_index =
+      |value: i32| instruction_offsets.binary_search_by_key(&(value as u32), |a| a.0).expect("failed to find entry");
 
     let args = &inst.args_blob;
     let opcode_raw = inst.opcode;
@@ -694,6 +701,18 @@ impl Instruction {
         Op::Layer { layer }
       }
       Opcode::StopHide => Op::StopHide,
+      Opcode::ScrollX => {
+        let value = decode_args!(args, "f");
+        Op::ScrollX { value }
+      }
+      Opcode::ScrollY => {
+        let value = decode_args!(args, "f");
+        Op::ScrollY { value }
+      }
+      Opcode::ZWriteDisable => {
+        let value = decode_args!(args, "S");
+        Op::ZWriteDisable(value != 0)
+      }
       Opcode::UnknownBitflag => {
         let enable = decode_args!(args, "S");
 
@@ -703,31 +722,40 @@ impl Instruction {
         let time = decode_args!(args, "S");
         Op::Wait { time }
       }
-      Opcode::CaseReturn => Op::CaseReturn,
+      Opcode::ReturnFromInterrupt => Op::ReturnFromInterrupt,
       Opcode::RandMode => {
         let mode = decode_args!(args, "S");
 
         Op::RandMode { mode }
       }
-      Opcode::ScriptNew => {
+      Opcode::NewChildBack => {
         let script = decode_args!(args, "S");
-        Op::ScriptNew { script }
+        Op::NewChildBack { script }
       }
       Opcode::ResampleMode => {
         let mode = decode_args!(args, "S");
         Op::ResampleMode { mode }
       }
-      Opcode::ScriptNewUI => {
+      Opcode::NewChildUIBack => {
         let script = decode_args!(args, "S");
-        Op::ScriptNewUI { script }
+        Op::NewChildUIBack { script }
       }
-      Opcode::ScriptNewFront => {
+      Opcode::NewChildFront => {
         let script = decode_args!(args, "S");
-        Op::ScriptNewFront { script }
+        Op::NewChildFront { script }
       }
-      Opcode::ScriptNewUIFront => {
+      Opcode::NewChildUIFront => {
         let script = decode_args!(args, "S");
-        Op::ScriptNewUIFront { script }
+        Op::NewChildUIFront { script }
+      }
+      Opcode::NewRootBack => {
+        let script = decode_args!(args, "S");
+        Op::NewRootBack { script }
+      }
+      Opcode::SpriteRand => {
+        let (sprite, end) = decode_args!(args, "SS");
+        let end = param_int(end, 1);
+        Op::SpriteRand { sprite, end }
       }
       opcode => panic!("unsupported instruction {opcode:?} ({opcode_raw})"),
     };
diff --git a/instructions/src/ecl.rs b/instructions/src/ecl.rs
new file mode 100644
index 0000000..71b0a28
--- /dev/null
+++ b/instructions/src/ecl.rs
@@ -0,0 +1,1087 @@
+use macros::{decode_args, spawner_param_decode_args};
+use num_derive::FromPrimitive;
+use num_traits::FromPrimitive;
+use truth::llir::RawInstr;
+
+use crate::Param;
+
+#[derive(Debug, FromPrimitive, Clone, Copy)]
+pub enum Opcode {
+  Nop = 0,
+
+  // control flow
+  Delete = 1,
+  Ret = 10,
+  Call = 11,
+  Jump = 12,
+  JumpEq = 13,
+  JumpNeq = 14,
+  CallAsync = 15,
+  CallAsyncById = 16,
+  KillAsync = 17,
+  KillAllAsync = 21,
+
+  // stack math
+  StackAlloc = 40,
+  PushInt = 42,
+  PopInt = 43,
+  PushFloat = 44,
+  PopFloat = 45,
+  AddInt = 50,
+  AddFloat = 51,
+  SubInt = 52,
+  SubFloat = 53,
+  MulInt = 54,
+  MulFloat = 55,
+  DivInt = 56,
+  DivFloat = 57,
+  ModInt = 58,
+  EqualInt = 59,
+  EqualFloat = 60,
+  NotEqualInt = 61,
+  NotEqualFloat = 62,
+  LessInt = 63,
+  LessFloat = 64,
+  LessEqualInt = 65,
+  LessEqualFloat = 66,
+  GreaterInt = 67,
+  GreaterFloat = 68,
+  GreaterEqualInt = 69,
+  GreaterEqualFloat = 70,
+  NotInt = 71,
+  NotFloat = 72,
+  LogicalOr = 73,
+  LogicalAnd = 74,
+  BitwiseXor = 75,
+  BitwiseOr = 76,
+  BitwiseAnd = 77,
+  DecrementInt = 78,
+  Sin = 79,
+  Cos = 80,
+  CirclePos = 81,
+  NormalizeRad = 82,
+  Wait = 83,
+  NegateInt = 84,
+  NegateFloat = 85,
+  SquareSum = 86,
+  GetAngle = 87,
+  SquareRoot = 88,
+  Linear = 89,
+  PointRotate = 90,
+  FloatTime = 91,
+  Math92 = 92,
+  Math93 = 93,
+
+  EnemyCreateRel = 256,
+  EnemyCreateAbs = 257,
+  EnemySelectAnm = 258,
+  EnemySetAnmScript = 259,
+  EnemyCreateMirroredRel = 260,
+  EnemyCreateMirroredAbs = 261,
+  EnemySetMainAnmScript = 262,
+  EnemyPlayAnmRel = 263,
+  EnemyPlayAnmAbs = 264,
+  EnemyCreateFillerRel = 265,
+  EnemyCreateFillerAbs = 266,
+  EnemyCreateFillerMirroredRel = 267,
+  EnemyCreateFillerMirroredAbs = 268,
+  EnemyPlaySelected = 269,
+  EnemySwitchAnm = 275,
+  EnemyResetAnm = 276,
+  EnemyInst277 = 277,
+  EnemyInst278 = 278,
+
+  MovePosAbs = 280,
+  MovePosAbsTime = 281,
+  MovePosRel = 282,
+  MovePosRelTime = 283,
+  MoveVelAbs = 284,
+  MoveVelAbsTime = 285,
+  MoveVelRel = 286,
+  MoveVelRelTime = 287,
+  MoveCircle = 288,
+  MoveCircleTime = 289,
+  MoveCircleRel = 290,
+  MoveCircleRelTime = 291,
+  MoveRandAbs = 292,
+  MoveRandRel = 293,
+  MoveAdd = 294,
+  MoveAddRel = 295,
+  MoveEllipse = 300,
+  MoveEllipseRel = 301,
+  MoveBezier = 305,
+  MoveReset = 307,
+
+  SetHurtbox = 320,
+  SetHitbox = 321,
+  FlagSet = 322,
+  FlagClear = 323,
+  SetMoveBounds = 324,
+  ResetMoveBounds = 325,
+  ClearExtraDrops = 326,
+  AddExtraDrops = 327,
+  SetItemDropArea = 328,
+  DropItems = 329,
+  SetItemReward = 330,
+  SetHealth = 331,
+  SetBoss = 332,
+  ResetBossTimer = 333,
+  SetInterrupt = 334,
+  SetInvulnerable = 335,
+  PlaySound = 336,
+  ShakeScreen = 337,
+  StartDialogue = 338,
+  WaitForDialogue = 339,
+  WaitForDeath = 340,
+  SetTimeoutInterrupt = 341,
+  StartSpell = 342,
+  EndSpell = 343,
+  SetChapter = 344,
+  KillAllEnemies = 345,
+  ProtectPlayer = 346,
+  LifeMarker = 347,
+  SetByDifficultyInt = 355,
+  SetByDifficultyFloat = 356,
+  StartSpellDifficulty = 357,
+  StartSpellDifficultyM1 = 358,
+  StartSpellDifficultyM2 = 359,
+  SetBossLifeCount = 360,
+
+  SpawnerReset = 400,
+  SpawnerShoot = 401,
+  SpawnerSetSprite = 402,
+  SpawnerSetOffset = 403,
+  SpawnerSetAngle = 404,
+  SpawnerSetSpeed = 405,
+  SpawnerSetCount = 406,
+  SpawnerSetAimMode = 407,
+  SpawnerSetSounds = 408,
+  SpawnerSetEffects = 409,
+  SpawnerCancel = 410,
+  SpawnerCopy = 411,
+  LaserLineCreate = 412,
+  LaserInfCreate = 413,
+  LaserOffset = 414,
+  LaserTarget = 415,
+  LaserSpeed = 416,
+  LaserWidth = 417,
+  LaserAngle = 418,
+  LaserRotate = 419,
+  BulletCancelRadius = 420,
+  BulletClearRadius = 421,
+  SpawnerSpeedRank3 = 422,
+  SpawnerSpeedRank5 = 423,
+  SpawnerSpeedRankLerp = 424,
+  SpawnerCountRank3 = 425,
+  SpawnerCountRank5 = 426,
+  SpawnerCountRankLerp = 427,
+  LaserLineCreateNoBlend = 428,
+  LaserInfCreateNoBlend = 429,
+  SetFloatAngleToPlayerFromPoint = 430, // cmon can we think of a better name than that???
+  LaserLineExCreate = 431,
+  LaserInfExCreate = 432,
+  LaserLineExCreateNoBlend = 433,
+  LaserInfExCreateNoBlend = 434,
+  SpawnerSpeedDifficulty = 435,
+  SpawnerCountDifficulty = 436,
+}
+
+#[derive(Debug)]
+pub enum PrimOp {
+  Add,
+  Sub,
+  Mul,
+  Div,
+  Equal,
+  NotEqual,
+  Less,
+  LessEqual,
+  Greater,
+  GreaterEqual,
+  Not,
+  Negate,
+}
+
+#[derive(Debug)]
+pub enum Op {
+  Nop,
+  Delete,
+  Return,
+  Call(String, Vec<u8>),
+  Jump {
+    index: usize,
+    time: i32,
+  },
+  JumpEqual {
+    index: usize,
+    time: i32,
+  },
+  JumpNotEqual {
+    index: usize,
+    time: i32,
+  },
+  CallAsync(String, Vec<u8>),
+  CallAsyncById(String, i32, Vec<u8>),
+  KillAsync(i32),
+  KillAllAsync,
+  StackAlloc(usize),
+  PushInt(Param<i32>),
+  PopInt(i32),
+  PushFloat(Param<f32>),
+  PopFloat(i32),
+  IntOp(PrimOp),
+  ModuloInt,
+  FloatOp(PrimOp),
+  Or,
+  And,
+  BitwiseXor,
+  BitwiseOr,
+  BitwiseAnd,
+  DecrementInt(i32),
+  Sin,
+  Cos,
+  CirclePos {
+    x: Param<f32>,
+    y: Param<f32>,
+    angle: Param<f32>,
+    radius: Param<f32>,
+  },
+  NormalizeRad(i32),
+  Wait(Param<i32>),
+
+  EnemyCreate {
+    relative: bool,
+    mirrored: bool,
+    filler: bool,
+    subroutine: String,
+    x: Param<f32>,
+    y: Param<f32>,
+    health: i32,
+    score: i32,
+    item: i32,
+  },
+  EnemySelectAnm {
+    source: i32,
+  },
+  EnemySetAnmScript {
+    slot: Param<i32>,
+    script: Param<i32>,
+  },
+  EnemySetMainAnmScript {
+    slot: Param<i32>,
+    script: Param<i32>,
+  },
+  EnemyPlayAnm {
+    relative: bool,
+    slot: Param<i32>,
+    script: Param<i32>,
+  },
+  EnemyPlaySelected {
+    slot: Param<i32>,
+  },
+
+  MovePos {
+    relative: bool,
+    x: Param<f32>,
+    y: Param<f32>,
+  },
+  MovePosTime {
+    relative: bool,
+    time: Param<i32>,
+    mode: Param<i32>,
+    x: Param<f32>,
+    y: Param<f32>,
+  },
+
+  MoveVel {
+    relative: bool,
+    x: Param<f32>,
+    y: Param<f32>,
+  },
+  MoveVelTime {
+    relative: bool,
+    time: Param<i32>,
+    mode: Param<i32>,
+    x: Param<f32>,
+    y: Param<f32>,
+  },
+  MoveRand {
+    relative: bool,
+    time: Param<i32>,
+    mode: Param<i32>,
+    speed: Param<f32>,
+  },
+  MoveOrbit {
+    relative: bool,
+    initial_angle: Param<f32>,
+    speed: Param<f32>,
+    radius: Param<f32>,
+    radius_growth: Param<f32>,
+  },
+  MoveOrbitTime {
+    relative: bool,
+    time: Param<i32>,
+    mode: Param<i32>,
+    radius: Param<f32>,
+    radius_growth: Param<f32>,
+  },
+  SetHurtbox {
+    x: f32,
+    y: f32,
+  },
+  SetHitbox {
+    x: f32,
+    y: f32,
+  },
+  SetFlag(i32),
+  ClearFlag(i32),
+  SetMoveBounds {
+    x: f32,
+    y: f32,
+    width: f32,
+    height: f32,
+  },
+  ResetMoveBounds,
+  ClearExtraDrops,
+  AddExtraDrops {
+    drop: i32,
+    item: i32,
+  },
+  SetItemDropArea {
+    width: i32,
+    height: i32,
+  },
+  DropItems,
+  SetItemReward(i32),
+  SetHealth(Param<i32>),
+  SetBoss(i32),
+  ResetBossTimer,
+  SetInterrupt {
+    slot: i32,
+    hp: i32,
+    time: i32,
+    subroutine: String,
+  },
+  SetInvulnerable(Param<i32>),
+  PlaySound(i32),
+  ShakeScreen {
+    time: Param<i32>,
+    // are these name correct?
+    min_intensity: Param<i32>,
+    max_intensity: Param<i32>,
+  },
+  StartDialogue(i32),
+  WaitForDialogue,
+  WaitForDeath,
+  SetTimeoutInterrupt {
+    slot: i32,
+    subroutine: String,
+  },
+  StartSpell {
+    id: i32,
+    time: i32,
+    bonus: i32,
+    name: String,
+  },
+  EndSpell,
+  SetChapter(i32),
+  LifeMarker {
+    slot: i32,
+    hp: i32,
+    color: i32,
+  },
+  SetByDifficultyInt {
+    var: i32,
+    easy: i32,
+    normal: i32,
+    hard: i32,
+    lunatic: i32,
+  },
+  SetByDifficultyFloat {
+    var: i32,
+    easy: f32,
+    normal: f32,
+    hard: f32,
+    lunatic: f32,
+  },
+  StartSpellDifficulty {
+    id: i32,
+    time: i32,
+    bonus: i32,
+    name: String,
+  },
+  StartSpellDifficultyM1 {
+    id: i32,
+    time: i32,
+    bonus: i32,
+    name: String,
+  },
+  StartSpellDifficultyM2 {
+    id: i32,
+    time: i32,
+    bonus: i32,
+    name: String,
+  },
+  SetBossLifeCount(Param<i32>),
+
+  SpawnerReset {
+    spawner: Param<i32>,
+  },
+  SpawnerShoot {
+    spawner: Param<i32>,
+  },
+  SpawnerSetSprite {
+    spawner: Param<i32>,
+    sprite: Param<i32>,
+    color: Param<i32>,
+  },
+  SpawnerSetOffset {
+    spawner: Param<i32>,
+    x: Param<f32>,
+    y: Param<f32>,
+  },
+  SpawnerSetAngle {
+    spawner: Param<i32>,
+    angle: [Param<f32>; 2],
+  },
+  SpawnerSetSpeed {
+    spawner: Param<i32>,
+    speed: [Param<f32>; 2],
+  },
+  SpawnerSetCount {
+    spawner: Param<i32>,
+    count: [Param<i32>; 2],
+  },
+  SpawnerSetAimMode {
+    spawner: Param<i32>,
+    mode: Param<i32>,
+  },
+  SpawnerSetSounds {
+    spawner: Param<i32>,
+    fire_sound: Param<i32>,
+    transform_sound: Param<i32>,
+  },
+  SpawnerSetEffects {
+    spawner: Param<i32>,
+    effect_index: Param<i32>,
+    is_async: Param<i32>,
+    id: Param<i32>,
+    int_1: Param<i32>,
+    int_2: Param<i32>,
+    float_1: Param<f32>,
+    float_2: Param<f32>,
+  },
+  SpawnerCancel,
+  SpawnerCopy {
+    dest_spawner: Param<i32>,
+    src_spawner: Param<i32>,
+  },
+  LaserCreate {
+    opcode: Opcode,
+    arguments: Vec<u8>,
+  },
+  LaserOffset,
+  LaserTarget,
+  LaserSpeed,
+  LaserWidth,
+  LaserAngle,
+  LaserRotate,
+  BulletCancelRadius(f32),
+  BulletClearRadius(f32),
+  SpawnerSpeedRank3 {
+    spawner: Param<i32>,
+    speeds: [[Param<f32>; 2]; 3],
+  },
+  SpawnerSpeedRank5 {
+    spawner: Param<i32>,
+    speeds: [[Param<f32>; 2]; 5],
+  },
+  SpawnerSpeedRankLerp,
+  SpawnerCountRank3{
+    spawner: Param<i32>,
+    counts: [[Param<i32>; 2]; 3],
+  },
+  SpawnerCountRank5{
+    spawner: Param<i32>,
+    counts: [[Param<i32>; 2]; 5],
+  },
+  SpawnerCountRankLerp,
+}
+
+#[derive(Debug)]
+pub struct Instruction {
+  pub time: i32,
+  pub difficulty: u8,
+  pub op: Op,
+}
+
+impl Instruction {
+  pub fn from_raw(inst: RawInstr, offset: u32, instruction_offsets: &Vec<(u32, usize)>) -> Self {
+    let param_int = |value: i32, index: u16| {
+      if inst.param_mask & (1 << index) == 0 {
+        Param::Value(value)
+      } else {
+        Param::Variable(value)
+      }
+    };
+    let param_float = |value: f32, index: u16| {
+      if inst.param_mask & (1 << index) == 0 {
+        Param::Value(value)
+      } else {
+        Param::Variable(value as i32)
+      }
+    };
+    let var_int = |value: i32, index: u16| {
+      assert!(inst.param_mask & (1 << index) != 0, "param must be a var");
+      // assert!(value <= -9932 && value >= -10000, "value must be a var");
+      value
+    };
+    let var_float = |value: f32, index: u16| {
+      assert!(inst.param_mask & (1 << index) != 0, "param must be a var");
+      // assert!(
+      //   value.trunc() == value && value <= -9932. && value >= -10000.,
+      //   "value must be a var"
+      // );
+      value as i32
+    };
+    let offset_to_index = |value: i32| {
+      let final_offset = offset.checked_add_signed(value).expect("add overflowed");
+      instruction_offsets
+        [instruction_offsets.binary_search_by_key(&final_offset, |entry| entry.0).expect("failed to locate offset")]
+      .1
+    };
+
+    let args = inst.args_blob;
+    let opcode_raw = inst.opcode;
+    let opcode = Opcode::from_u16(opcode_raw).unwrap_or_else(|| todo!("failed to convert opcode {opcode_raw}"));
+    let op = match opcode {
+      Opcode::Nop => Op::Nop,
+      Opcode::Delete => Op::Delete,
+      Opcode::Ret => Op::Return,
+      Opcode::Call => {
+        let (name, args) = decode_args!(args, "P(bs=4)v");
+        Op::Call(name, args)
+      }
+      Opcode::Jump => {
+        let (offset, time) = decode_args!(args, "ot");
+
+        Op::Jump {
+          index: offset_to_index(offset),
+          time,
+        }
+      }
+      Opcode::JumpEq => {
+        let (offset, time) = decode_args!(args, "ot");
+
+        Op::JumpEqual {
+          index: offset_to_index(offset),
+          time,
+        }
+      }
+      Opcode::JumpNeq => {
+        let (offset, time) = decode_args!(args, "ot");
+
+        Op::JumpNotEqual {
+          index: offset_to_index(offset),
+          time,
+        }
+      }
+      Opcode::CallAsync => {
+        let (name, args) = decode_args!(args, "P(bs=4)v");
+        Op::CallAsync(name, args)
+      }
+      Opcode::CallAsyncById => {
+        let (name, id, args) = decode_args!(args, "P(bs=4)Sv");
+        Op::CallAsyncById(name, id, args)
+      }
+      Opcode::KillAsync => {
+        let id = decode_args!(args, "S");
+        Op::KillAsync(id)
+      }
+      Opcode::KillAllAsync => Op::KillAllAsync,
+      Opcode::StackAlloc => Op::StackAlloc(decode_args!(args, "S") as usize),
+      Opcode::PushInt => {
+        let value = decode_args!(args, "S");
+        Op::PushInt(param_int(value, 0))
+      }
+      Opcode::PopInt => {
+        let value = decode_args!(args, "S");
+        Op::PopInt(var_int(value, 0))
+      }
+      Opcode::PushFloat => {
+        let float = decode_args!(args, "f");
+        Op::PushFloat(param_float(float, 0))
+      }
+      Opcode::PopFloat => {
+        let var = decode_args!(args, "f");
+        Op::PopFloat(var_float(var, 0))
+      }
+      Opcode::AddInt => Op::IntOp(PrimOp::Add),
+      Opcode::AddFloat => Op::FloatOp(PrimOp::Add),
+      Opcode::SubInt => Op::IntOp(PrimOp::Sub),
+      Opcode::SubFloat => Op::FloatOp(PrimOp::Sub),
+      Opcode::MulInt => Op::IntOp(PrimOp::Mul),
+      Opcode::MulFloat => Op::FloatOp(PrimOp::Mul),
+      Opcode::DivInt => Op::IntOp(PrimOp::Div),
+      Opcode::DivFloat => Op::FloatOp(PrimOp::Div),
+      Opcode::ModInt => Op::ModuloInt,
+      Opcode::EqualInt => Op::IntOp(PrimOp::Equal),
+      Opcode::EqualFloat => Op::FloatOp(PrimOp::Equal),
+      Opcode::NotEqualInt => Op::IntOp(PrimOp::NotEqual),
+      Opcode::NotEqualFloat => Op::FloatOp(PrimOp::NotEqual),
+      Opcode::LessInt => Op::IntOp(PrimOp::Less),
+      Opcode::LessFloat => Op::FloatOp(PrimOp::Less),
+      Opcode::LessEqualInt => Op::IntOp(PrimOp::LessEqual),
+      Opcode::LessEqualFloat => Op::FloatOp(PrimOp::LessEqual),
+      Opcode::GreaterInt => Op::IntOp(PrimOp::Greater),
+      Opcode::GreaterFloat => Op::FloatOp(PrimOp::Greater),
+      Opcode::GreaterEqualInt => Op::IntOp(PrimOp::GreaterEqual),
+      Opcode::GreaterEqualFloat => Op::FloatOp(PrimOp::GreaterEqual),
+      Opcode::NotInt => Op::IntOp(PrimOp::Not),
+      Opcode::NotFloat => Op::FloatOp(PrimOp::Not),
+      Opcode::LogicalOr => Op::Or,
+      Opcode::LogicalAnd => Op::And,
+      Opcode::BitwiseXor => Op::BitwiseXor,
+      Opcode::BitwiseOr => Op::BitwiseOr,
+      Opcode::BitwiseAnd => Op::BitwiseAnd,
+      Opcode::DecrementInt => {
+        let by = decode_args!(args, "S");
+        Op::DecrementInt(var_int(by, 0))
+      }
+      Opcode::Sin => Op::Sin,
+      Opcode::Cos => Op::Cos,
+      Opcode::CirclePos => {
+        let (x, y, angle, radius) = decode_args!(args, "ffff");
+        let (x, y, angle, radius) = (
+          param_float(x, 0),
+          param_float(y, 1),
+          param_float(angle, 2),
+          param_float(radius, 3),
+        );
+        Op::CirclePos { x, y, angle, radius }
+      }
+      Opcode::NormalizeRad => {
+        let var = decode_args!(args, "S");
+        Op::NormalizeRad(var_int(var, 0))
+      }
+      Opcode::Wait => {
+        let time = decode_args!(args, "S");
+        Op::Wait(param_int(time, 0))
+      }
+      Opcode::NegateInt => Op::IntOp(PrimOp::Negate),
+      Opcode::NegateFloat => Op::FloatOp(PrimOp::Negate),
+      Opcode::EnemyCreateRel
+      | Opcode::EnemyCreateAbs
+      | Opcode::EnemyCreateMirroredRel
+      | Opcode::EnemyCreateMirroredAbs
+      | Opcode::EnemyCreateFillerRel
+      | Opcode::EnemyCreateFillerAbs
+      | Opcode::EnemyCreateFillerMirroredRel
+      | Opcode::EnemyCreateFillerMirroredAbs => {
+        let (subroutine, x, y, health, score, item) = decode_args!(args, "P(bs=4)ffSSS");
+        let (x, y) = (param_float(x, 0), param_float(y, 1));
+        Op::EnemyCreate {
+          relative: matches!(
+            opcode,
+            Opcode::EnemyCreateRel
+              | Opcode::EnemyCreateMirroredRel
+              | Opcode::EnemyCreateFillerRel
+              | Opcode::EnemyCreateFillerMirroredRel
+          ),
+          mirrored: matches!(
+            opcode,
+            Opcode::EnemyCreateMirroredRel
+              | Opcode::EnemyCreateMirroredAbs
+              | Opcode::EnemyCreateFillerMirroredRel
+              | Opcode::EnemyCreateFillerMirroredAbs
+          ),
+          filler: matches!(
+            opcode,
+            Opcode::EnemyCreateFillerRel
+              | Opcode::EnemyCreateFillerAbs
+              | Opcode::EnemyCreateFillerMirroredRel
+              | Opcode::EnemyCreateFillerMirroredAbs
+          ),
+          subroutine,
+          x,
+          y,
+          health,
+          score,
+          item,
+        }
+      }
+      Opcode::EnemySelectAnm => {
+        let source = decode_args!(args, "S");
+        Op::EnemySelectAnm { source }
+      }
+      Opcode::EnemySetAnmScript => {
+        let (slot, script) = decode_args!(args, "SS");
+        let (slot, script) = (param_int(slot, 0), param_int(script, 1));
+        Op::EnemySetAnmScript { slot, script }
+      }
+      Opcode::EnemySetMainAnmScript => {
+        let (slot, script) = decode_args!(args, "SS");
+        let (slot, script) = (param_int(slot, 0), param_int(script, 1));
+        Op::EnemySetMainAnmScript { slot, script }
+      }
+      Opcode::EnemyPlayAnmRel | Opcode::EnemyPlayAnmAbs => {
+        let (slot, script) = decode_args!(args, "SS");
+        let (slot, script) = (param_int(slot, 0), param_int(script, 1));
+        Op::EnemyPlayAnm {
+          relative: matches!(opcode, Opcode::EnemyPlayAnmRel),
+          slot,
+          script,
+        }
+      }
+      Opcode::EnemyPlaySelected => {
+        let slot = decode_args!(args, "S");
+        Op::EnemyPlaySelected {
+          slot: param_int(slot, 0),
+        }
+      }
+      Opcode::MovePosAbs | Opcode::MovePosRel => {
+        let (x, y) = decode_args!(args, "ff");
+        let (x, y) = (param_float(x, 0), param_float(y, 1));
+        Op::MovePos {
+          relative: matches!(opcode, Opcode::MovePosRel),
+          x,
+          y,
+        }
+      }
+      Opcode::MovePosAbsTime | Opcode::MovePosRelTime => {
+        let (time, mode, x, y) = decode_args!(args, "SSff");
+        let (time, mode, x, y) = (
+          param_int(time, 0),
+          param_int(mode, 1),
+          param_float(x, 2),
+          param_float(y, 3),
+        );
+        Op::MovePosTime {
+          relative: matches!(opcode, Opcode::MovePosRelTime),
+          time,
+          mode,
+          x,
+          y,
+        }
+      }
+      Opcode::MoveVelAbs | Opcode::MoveVelRel => {
+        let (x, y) = decode_args!(args, "ff");
+        let (x, y) = (param_float(x, 0), param_float(y, 1));
+        Op::MoveVel {
+          relative: matches!(opcode, Opcode::MoveVelRel),
+          x,
+          y,
+        }
+      }
+      Opcode::MoveVelAbsTime | Opcode::MoveVelRelTime => {
+        let (time, mode, x, y) = decode_args!(args, "SSff");
+        let (time, mode, x, y) = (
+          param_int(time, 0),
+          param_int(mode, 1),
+          param_float(x, 2),
+          param_float(y, 3),
+        );
+        Op::MoveVelTime {
+          relative: matches!(opcode, Opcode::MoveVelRelTime),
+          time,
+          mode,
+          x,
+          y,
+        }
+      }
+      Opcode::MoveRandAbs | Opcode::MoveRandRel => {
+        let (time, mode, speed) = decode_args!(args, "SSf");
+        let (time, mode, speed) = (param_int(time, 0), param_int(mode, 1), param_float(speed, 2));
+        Op::MoveRand {
+          relative: matches!(opcode, Opcode::MoveRandRel),
+          time,
+          mode,
+          speed,
+        }
+      }
+      Opcode::SetHurtbox => {
+        let (x, y) = decode_args!(args, "ff");
+        Op::SetHurtbox { x, y }
+      }
+      Opcode::SetHitbox => {
+        let (x, y) = decode_args!(args, "ff");
+        Op::SetHitbox { x, y }
+      }
+      Opcode::FlagSet => {
+        let flag = decode_args!(args, "S");
+        Op::SetFlag(flag)
+      }
+      Opcode::FlagClear => {
+        let flag = decode_args!(args, "S");
+        Op::ClearFlag(flag)
+      }
+      Opcode::SetMoveBounds => {
+        let (x, y, width, height) = decode_args!(args, "ffff");
+        Op::SetMoveBounds { x, y, width, height }
+      }
+      Opcode::ResetMoveBounds => Op::ResetMoveBounds,
+      Opcode::ClearExtraDrops => Op::ClearExtraDrops,
+      Opcode::AddExtraDrops => {
+        let (drop, item) = decode_args!(args, "SS");
+        Op::AddExtraDrops { drop, item }
+      }
+      Opcode::SetItemDropArea => {
+        let (width, height) = decode_args!(args, "SS");
+        Op::SetItemDropArea { width, height }
+      }
+      Opcode::DropItems => Op::DropItems,
+      Opcode::SetItemReward => {
+        let reward = decode_args!(args, "S");
+        Op::SetItemReward(reward)
+      }
+      Opcode::SetHealth => {
+        let health = decode_args!(args, "S");
+        Op::SetHealth(param_int(health, 0))
+      }
+      Opcode::SetBoss => {
+        let boss = decode_args!(args, "S");
+        Op::SetBoss(boss)
+      }
+      Opcode::ResetBossTimer => Op::ResetBossTimer,
+      Opcode::SetInterrupt => {
+        let (slot, hp, time, subroutine) = decode_args!(args, "SSSP(bs=4)");
+        Op::SetInterrupt {
+          slot,
+          hp,
+          time,
+          subroutine,
+        }
+      }
+      Opcode::SetInvulnerable => {
+        let timer = decode_args!(args, "S");
+        Op::SetInvulnerable(param_int(timer, 0))
+      }
+      Opcode::PlaySound => {
+        let sound = decode_args!(args, "S");
+        Op::PlaySound(sound)
+      }
+      Opcode::ShakeScreen => {
+        let (time, min_intensity, max_intensity) = decode_args!(args, "SSS");
+        let (time, min_intensity, max_intensity) = (
+          param_int(time, 0),
+          param_int(min_intensity, 1),
+          param_int(max_intensity, 2),
+        );
+        Op::ShakeScreen {
+          time,
+          min_intensity,
+          max_intensity,
+        }
+      }
+      Opcode::StartDialogue => {
+        let id = decode_args!(args, "S");
+        Op::StartDialogue(id)
+      }
+      Opcode::WaitForDialogue => Op::WaitForDialogue,
+      Opcode::WaitForDeath => Op::WaitForDeath,
+      Opcode::SetTimeoutInterrupt => {
+        let (slot, subroutine) = decode_args!(args, "SP(bs=4)");
+
+        Op::SetTimeoutInterrupt { slot, subroutine }
+      }
+      Opcode::StartSpell => {
+        let (id, time, bonus, name) = decode_args!(args, "SSSp(bs=4;mask=0x77,7,16)");
+        Op::StartSpell { id, time, bonus, name }
+      }
+      Opcode::EndSpell => Op::EndSpell,
+      Opcode::SetChapter => {
+        let chapter = decode_args!(args, "S");
+        Op::SetChapter(chapter)
+      }
+      Opcode::LifeMarker => {
+        let (slot, hp, color) = decode_args!(args, "SSS");
+        Op::LifeMarker { slot, hp, color }
+      }
+      Opcode::SetByDifficultyInt => {
+        let (var, easy, normal, hard, lunatic) = decode_args!(args, "SSSSS");
+        Op::SetByDifficultyInt {
+          var: var_int(var, 0),
+          easy,
+          normal,
+          hard,
+          lunatic,
+        }
+      }
+      Opcode::SetByDifficultyFloat => {
+        let (var, easy, normal, hard, lunatic) = decode_args!(args, "fffff");
+        Op::SetByDifficultyFloat {
+          var: var_float(var, 0),
+          easy,
+          normal,
+          hard,
+          lunatic,
+        }
+      }
+      Opcode::StartSpellDifficulty => {
+        let (id, time, bonus, name) = decode_args!(args, "SSSp(bs=4;mask=0x77,7,16)");
+        Op::StartSpellDifficulty { id, time, bonus, name }
+      }
+      Opcode::StartSpellDifficultyM1 => {
+        let (id, time, bonus, name) = decode_args!(args, "SSSp(bs=4;mask=0x77,7,16)");
+        Op::StartSpellDifficultyM1 { id, time, bonus, name }
+      }
+      Opcode::StartSpellDifficultyM2 => {
+        let (id, time, bonus, name) = decode_args!(args, "SSSp(bs=4;mask=0x77,7,16)");
+        Op::StartSpellDifficultyM2 { id, time, bonus, name }
+      }
+      Opcode::SetBossLifeCount => {
+        let count = decode_args!(args, "S");
+        Op::SetBossLifeCount(param_int(count, 0))
+      }
+      Opcode::SpawnerReset => {
+        let spawner = decode_args!(args, "S");
+        Op::SpawnerReset {
+          spawner: param_int(spawner, 0),
+        }
+      }
+      Opcode::SpawnerShoot => {
+        let spawner = decode_args!(args, "S");
+        Op::SpawnerShoot {
+          spawner: param_int(spawner, 0),
+        }
+      }
+      Opcode::SpawnerSetSprite => {
+        let (spawner, sprite, color) = decode_args!(args, "SSS");
+        let (spawner, sprite, color) = (param_int(spawner, 0), param_int(sprite, 1), param_int(color, 2));
+        Op::SpawnerSetSprite { spawner, sprite, color }
+      }
+      Opcode::SpawnerSetOffset => {
+        let (spawner, x, y) = decode_args!(args, "Sff");
+        let (spawner, x, y) = (param_int(spawner, 0), param_float(x, 1), param_float(y, 2));
+        Op::SpawnerSetOffset { spawner, x, y }
+      }
+      Opcode::SpawnerSetAngle => {
+        let (spawner, angle_1, angle_2) = decode_args!(args, "Sff");
+        let (spawner, angle_1, angle_2) = (param_int(spawner, 0), param_float(angle_1, 1), param_float(angle_2, 2));
+        Op::SpawnerSetAngle {
+          spawner,
+          angle: [angle_1, angle_2],
+        }
+      }
+      Opcode::SpawnerSetSpeed => {
+        let (spawner, speed_1, speed_2) = decode_args!(args, "Sff");
+        let (spawner, speed_1, speed_2) = (param_int(spawner, 0), param_float(speed_1, 1), param_float(speed_2, 2));
+        Op::SpawnerSetSpeed {
+          spawner,
+          speed: [speed_1, speed_2],
+        }
+      }
+      Opcode::SpawnerSetCount => {
+        let (spawner, count_1, count_2) = decode_args!(args, "SSS");
+        let (spawner, count_1, count_2) = (param_int(spawner, 0), param_int(count_1, 1), param_int(count_2, 2));
+        Op::SpawnerSetCount {
+          spawner,
+          count: [count_1, count_2],
+        }
+      }
+      Opcode::SpawnerSetAimMode => {
+        let (spawner, mode) = decode_args!(args, "SS");
+        let (spawner, mode) = (param_int(spawner, 0), param_int(mode, 1));
+        Op::SpawnerSetAimMode { spawner, mode }
+      }
+      Opcode::SpawnerSetSounds => {
+        let (spawner, fire_sound, transform_sound) = decode_args!(args, "SSS");
+        let (spawner, fire_sound, transform_sound) = (
+          param_int(spawner, 0),
+          param_int(fire_sound, 1),
+          param_int(transform_sound, 2),
+        );
+        Op::SpawnerSetSounds {
+          spawner,
+          fire_sound,
+          transform_sound,
+        }
+      }
+      Opcode::SpawnerSetEffects => {
+        let (spawner, effect_index, is_async, id, int_1, int_2, float_1, float_2) = decode_args!(args, "SSSSSSff");
+        let (spawner, effect_index, is_async, id, int_1, int_2, float_1, float_2) = (
+          param_int(spawner, 0),
+          param_int(effect_index, 1),
+          param_int(is_async, 2),
+          param_int(id, 3),
+          param_int(int_1, 4),
+          param_int(int_2, 5),
+          param_float(float_1, 6),
+          param_float(float_2, 7),
+        );
+        Op::SpawnerSetEffects {
+          spawner,
+          effect_index,
+          is_async,
+          id,
+          int_1,
+          int_2,
+          float_1,
+          float_2,
+        }
+      }
+      Opcode::SpawnerCancel => Op::SpawnerCancel,
+      Opcode::SpawnerCopy => {
+        let (dest_spawner, src_spawner) = decode_args!(args, "SS");
+        let (dest_spawner, src_spawner) = (param_int(dest_spawner, 0), param_int(src_spawner, 1));
+        Op::SpawnerCopy {
+          dest_spawner,
+          src_spawner,
+        }
+      }
+      Opcode::LaserLineCreate
+      | Opcode::LaserLineCreateNoBlend
+      | Opcode::LaserLineExCreate
+      | Opcode::LaserLineExCreateNoBlend
+      | Opcode::LaserInfCreate
+      | Opcode::LaserInfCreateNoBlend
+      | Opcode::LaserInfExCreate
+      | Opcode::LaserInfExCreateNoBlend => Op::LaserCreate {
+        opcode,
+        arguments: args,
+      },
+      Opcode::BulletCancelRadius => {
+        let radius = decode_args!(args, "f");
+        Op::BulletCancelRadius(radius)
+      }
+      Opcode::BulletClearRadius => {
+        let radius = decode_args!(args, "f");
+        Op::BulletClearRadius(radius)
+      }
+      Opcode::SpawnerSpeedRank3 => {
+        let (spawner, speeds) = spawner_param_decode_args!(args, "S", [[f32; 2]; 3]);
+        let spawner = param_int(spawner, 0);
+        Op::SpawnerSpeedRank3 { spawner, speeds }
+      }
+      Opcode::SpawnerSpeedRank5 => {
+        let (spawner, speeds) = spawner_param_decode_args!(args, "S", [[f32; 2]; 5]);
+        let spawner = param_int(spawner, 0);
+        Op::SpawnerSpeedRank5 { spawner, speeds }
+      }
+      // Opcode::SpawnerSpeedRankLerp => {
+      //   let (spawner, speeds) = spawner_param_decode_args!(args, "S", [[f32; 2]; 5]);
+      //   let spawner = param_int(spawner, 0);
+      //   Op::SpawnerSpeedRankLerp { spawner, speeds }
+      // }
+      Opcode::SpawnerCountRank3 => {
+        let (spawner, counts) = spawner_param_decode_args!(args, "S", [[i32; 2]; 3]);
+        let spawner = param_int(spawner, 0);
+        Op::SpawnerCountRank3 { spawner, counts }
+      }
+      Opcode::SpawnerCountRank5 => {
+        let (spawner, counts) = spawner_param_decode_args!(args, "S", [[i32; 2]; 5]);
+        let spawner = param_int(spawner, 0);
+        Op::SpawnerCountRank5 { spawner, counts }
+      }
+      _ => {
+        unimplemented!("opcode {opcode:?} ({opcode_raw}) not implemented");
+      }
+    };
+
+    Self {
+      time: inst.time,
+      difficulty: inst.difficulty,
+      op,
+    }
+  }
+}
diff --git a/instructions/src/lib.rs b/instructions/src/lib.rs
new file mode 100644
index 0000000..933622a
--- /dev/null
+++ b/instructions/src/lib.rs
@@ -0,0 +1,5 @@
+pub mod anm;
+mod param;
+pub mod ecl;
+pub mod std;
+pub use param::*;
diff --git a/src/game/param.rs b/instructions/src/param.rs
similarity index 100%
rename from src/game/param.rs
rename to instructions/src/param.rs
diff --git a/instructions/src/std.rs b/instructions/src/std.rs
new file mode 100644
index 0000000..d17aa2b
--- /dev/null
+++ b/instructions/src/std.rs
@@ -0,0 +1,149 @@
+use macros::decode_args;
+use num_derive::FromPrimitive;
+use num_traits::FromPrimitive;
+use truth::llir::RawInstr;
+
+#[derive(Debug, FromPrimitive)]
+enum Opcode {
+  Halt = 0,
+  Jump = 1,
+  Position = 2,
+  PositionInterp = 3,
+  Facing = 4,
+  FacingInterp = 5,
+  Rotation = 6,
+  FieldOfView = 7,
+  Fog = 8,
+  FogInterp = 9,
+  PositionInterpBezier = 10,
+  FacingInterpBezier = 11,
+  ShakingMode = 12,
+  BackgroundFillColor = 13,
+  BackgroundSprite = 14,
+}
+
+#[derive(Debug)]
+pub enum Op {
+  Halt,
+  Jump {
+    index: usize,
+    time: i32,
+  },
+  Position {
+    x: f32,
+    y: f32,
+    z: f32,
+  },
+  PositionInterp {
+    time: i32,
+    mode: i32,
+    x: f32,
+    y: f32,
+    z: f32,
+  },
+  Facing {
+    x: f32,
+    y: f32,
+    z: f32,
+  },
+  FacingInterp {
+    time: i32,
+    mode: i32,
+    x: f32,
+    y: f32,
+    z: f32,
+  },
+  Rotation {
+    x: f32,
+    y: f32,
+    z: f32,
+  },
+  FieldOfView(f32),
+  Fog {
+    color: i32,
+    start: f32,
+    end: f32,
+  },
+  FogInterp {
+    time: i32,
+    mode: i32,
+    color: i32,
+    start: f32,
+    end: f32,
+  },
+  ShakingMode {
+    mode: i32,
+  },
+}
+
+pub struct Instruction {
+  pub time: i32,
+  pub op: Op,
+}
+
+impl Instruction {
+  pub fn from_raw(inst: RawInstr, instruction_offsets: &Vec<(u32, usize)>) -> Self {
+    let offset_to_index =
+      |value: i32| instruction_offsets.binary_search_by_key(&(value as u32), |a| a.0).expect("failed to find entry");
+
+    let args = &inst.args_blob;
+    let opcode_raw = inst.opcode;
+    let opcode = Opcode::from_u16(opcode_raw).expect("failed to convert opcode");
+
+    let op = match opcode {
+      Opcode::Halt => Op::Halt,
+      Opcode::Jump => {
+        let (offset, time) = decode_args!(args, "ot");
+        Op::Jump {
+          index: offset_to_index(offset),
+          time,
+        }
+      }
+      Opcode::Position => {
+        let (x, y, z) = decode_args!(args, "fff");
+        Op::Position { x, y, z }
+      }
+      Opcode::PositionInterp => {
+        let (time, mode, x, y, z) = decode_args!(args, "SUfff");
+        Op::PositionInterp { time, mode, x, y, z }
+      }
+      Opcode::Facing => {
+        let (x, y, z) = decode_args!(args, "fff");
+        Op::Facing { x, y, z }
+      }
+      Opcode::FacingInterp => {
+        let (time, mode, x, y, z) = decode_args!(args, "SUfff");
+        Op::FacingInterp { time, mode, x, y, z }
+      }
+      Opcode::Rotation => {
+        let (x, y, z) = decode_args!(args, "fff");
+        Op::Rotation { x, y, z }
+      }
+      Opcode::FieldOfView => {
+        let fov = decode_args!(args, "f");
+        Op::FieldOfView(fov)
+      }
+      Opcode::Fog => {
+        let (color, start, end) = decode_args!(args, "Sff");
+        Op::Fog { color, start, end }
+      }
+      Opcode::FogInterp => {
+        let (time, mode, color, start, end) = decode_args!(args, "SUSff");
+        Op::FogInterp {
+          time,
+          mode,
+          color,
+          start,
+          end,
+        }
+      }
+      Opcode::PositionInterpBezier => todo!(),
+      Opcode::FacingInterpBezier => todo!(),
+      Opcode::ShakingMode => todo!(),
+      Opcode::BackgroundFillColor => todo!(),
+      Opcode::BackgroundSprite => todo!(),
+    };
+
+    Instruction { time: inst.time, op }
+  }
+}
diff --git a/macros/src/lib.rs b/macros/src/lib.rs
index 6c59bf8..f72f254 100644
--- a/macros/src/lib.rs
+++ b/macros/src/lib.rs
@@ -1,20 +1,21 @@
-use proc_macro2::TokenStream;
+use proc_macro2::{Span, TokenStream};
 use quote::quote;
 use syn::{
-  parse::{ParseStream, Parser}, Expr, LitStr, Token
+  parse::{ParseStream, Parser},
+  spanned::Spanned,
+  Expr, ExprLit, Ident, Lit, LitStr, Token, Type, TypeArray,
 };
 use truth::{
-  context::RootEmitter, llir::{ArgEncoding, InstrAbi, StringArgSize}, pos::SourceStr
+  context::RootEmitter,
+  llir::{ArgEncoding, InstrAbi, StringArgSize},
+  pos::SourceStr,
 };
 
 #[proc_macro]
 pub fn decode_args(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
-  match blob_impl.parse(input.clone()) {
+  match blob_impl.parse(input) {
     Ok(data) => data.into(),
-    Err(error) => {
-      // error.span().unwrap().error(error.to_string());
-      error.into_compile_error().into()
-    }
+    Err(error) => error.into_compile_error().into(),
   }
 }
 
@@ -22,11 +23,14 @@ fn blob_impl(input: ParseStream) -> syn::Result<TokenStream> {
   let reader: Expr = input.parse()?;
   let _: Token![,] = input.parse()?;
   let sig_str: LitStr = input.parse()?;
-  let sig = InstrAbi::parse(
-    SourceStr::from_full_source(None, sig_str.value().as_str()),
-    &RootEmitter::new_stderr(),
-  )
-  .unwrap();
+  let sig_str_span = sig_str.span();
+  let sig_str = sig_str.value();
+  let (sig_str, variadic) = if sig_str.ends_with('v') {
+    (&sig_str.as_str()[..sig_str.len() - 1], true)
+  } else {
+    (sig_str.as_str(), false)
+  };
+  let sig = InstrAbi::parse(SourceStr::from_full_source(None, sig_str), &RootEmitter::new_stderr()).unwrap();
   let encodings: Vec<_> = sig
     .arg_encodings()
     .map(|enc| {
@@ -43,22 +47,127 @@ fn blob_impl(input: ParseStream) -> syn::Result<TokenStream> {
         }
         ArgEncoding::JumpTime => quote!(cursor.read_i32().unwrap()),
         ArgEncoding::JumpOffset => quote!(cursor.read_i32().unwrap()),
+        ArgEncoding::Color => quote!(cursor.read_u32().unwrap()),
         ArgEncoding::String {
           size: StringArgSize::Fixed { len, .. },
           ..
-        } => quote!(cursor.read_cstring_exact(#len)),
-        _ => return Err(syn::Error::new(sig_str.span(), "failed...")),
+        } => quote!(cursor.read_cstring_exact(#len).unwrap().decode(truth::io::DEFAULT_ENCODING).unwrap()),
+        ArgEncoding::String {
+          size: StringArgSize::Pascal { block_size },
+          ..
+        } => quote!(cursor.read_cstring_blockwise(#block_size).unwrap().decode(truth::io::DEFAULT_ENCODING).unwrap()),
+        _ => return Err(syn::Error::new(sig_str_span, "failed...")),
       })
     })
     .collect::<syn::Result<_>>()?;
 
+  let variadic = variadic.then(|| {
+    quote! {
+      , {
+        let file_size = cursor.file_size().unwrap();
+        let offset = (file_size - cursor.pos().unwrap()) as usize;
+        cursor.read_byte_vec(offset).unwrap()
+      }
+    }
+  });
+
   Ok(quote! {
     {
       use truth::io::BinRead;
       let mut cursor = ::std::io::Cursor::new(#reader);
       (
         #(#encodings),*
+        #variadic
       )
     }
   })
 }
+
+//spawner_rank_decode_args!(args, "S", [[f32; 2]; 3])
+
+#[proc_macro]
+pub fn spawner_param_decode_args(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
+  match spawner_param_decode_args_impl.parse(input) {
+    Ok(data) => data.into(),
+    Err(error) => error.into_compile_error().into(),
+  }
+}
+
+fn spawner_param_decode_args_impl(input: ParseStream) -> syn::Result<TokenStream> {
+  let reader: Expr = input.parse()?;
+  let _: Token![,] = input.parse()?;
+  let sig_str: LitStr = input.parse()?;
+  let _: Token![,] = input.parse()?;
+  let array = input.parse::<TypeArray>()?;
+
+  let Expr::Lit(ExprLit { lit: Lit::Int(int), .. }) = array.len else {
+    return Err(syn::Error::new(array.len.span(), "length must be a constant number"));
+  };
+  let rank_count = int.base10_parse::<usize>()?;
+  let Type::Array(TypeArray {
+    elem,
+    len: Expr::Lit(ExprLit { lit: Lit::Int(int), .. }),
+    ..
+  }) = array.elem.as_ref()
+  else {
+    return Err(syn::Error::new(
+      array.elem.span(),
+      "element was not an array with constant number ",
+    ));
+  };
+  let entry_count = int.base10_parse::<usize>()?;
+  let Type::Path(path) = elem.as_ref() else {
+    return Err(syn::Error::new(elem.span(), "expected path as element"));
+  };
+  let ident = path.path.get_ident().ok_or(syn::Error::new(path.span(), "path was not a single ident"))?;
+  let type_id = ident.to_string();
+  enum ParamType {
+    Float,
+    Int,
+  }
+  let param_ty = match type_id.as_str() {
+    "f32" => ParamType::Float,
+    "i32" => ParamType::Int,
+    _ => return Err(syn::Error::new(ident.span(), "expected i32 or f32")),
+  };
+
+  let total = entry_count * rank_count;
+  let mut arguments = sig_str.value();
+
+  arguments.extend(std::iter::repeat_n(
+    match param_ty {
+      ParamType::Int => 'S',
+      ParamType::Float => 'f',
+    },
+    total,
+  ));
+
+  let rank_list: Vec<Vec<Ident>> = (0..rank_count)
+    .map(|rank| {
+      (0..entry_count).map(move |entry| Ident::new(&format!("__a_{rank}_{entry}"), Span::call_site())).collect()
+    })
+    .collect();
+
+  let tuple_pat = rank_list.iter().flatten();
+  let rank_array = rank_list.iter().enumerate().map(|(rank_index, list)| {
+    let list = list.iter().enumerate().map(|(entry_index, entry)| {
+      let param_index = rank_index * entry_count + entry_index + 1;
+      let param_index = param_index as u16;
+      let kind = match param_ty {
+        ParamType::Float => quote!(param_float),
+        ParamType::Int => quote!(param_int),
+      };
+      quote!(#kind(#entry, #param_index))
+    });
+    quote! {
+      [#(#list),*]
+    }
+  });
+
+  Ok(quote! {
+    {
+      let (id, #(#tuple_pat),*) = decode_args!(#reader, #arguments);
+      (id, [#(#rank_array),*])
+    }
+  })
+}
diff --git a/src/engine.rs b/src/engine.rs
index 96b9c1b..b2a538c 100644
--- a/src/engine.rs
+++ b/src/engine.rs
@@ -1,5 +1,5 @@
 use std::{
-  sync::{Arc, LazyLock, Mutex}, time::{Duration, Instant}
+  cell::Cell, ops::ControlFlow, sync::{Arc, LazyLock}, time::{Duration, Instant}
 };
 
 use wgpu::{
@@ -23,6 +23,7 @@ pub struct Engine<'a> {
   last_frame_instant: Instant,
   last_second: Instant,
   frames: u32,
+  should_exit: Cell<bool>,
 
   state: GameRunner,
   focused: bool,
@@ -48,6 +49,7 @@ impl KeyState {
 #[derive(Default)]
 pub struct Keys {
   values: FastHashMap<KeyCode, KeyState>,
+  down_count: u32,
 }
 
 impl Keys {
@@ -55,8 +57,10 @@ impl Keys {
     let key_state = self.values.entry(key).or_default();
     if key_state.is_down() != state.is_pressed() {
       *key_state = if state.is_pressed() {
+        self.down_count += 1;
         KeyState::Pressed
       } else {
+        self.down_count -= 1;
         KeyState::Released
       }
     }
@@ -85,6 +89,10 @@ impl Keys {
   pub fn was_key_pressed(&self, key: KeyCode) -> bool {
     matches!(self.values.get(&key), Some(KeyState::Pressed))
   }
+
+  pub fn is_any_key_down(&self) -> bool {
+    self.down_count > 0
+  }
 }
 
 impl<'a> Engine<'a> {
@@ -127,7 +135,7 @@ impl<'a> Engine<'a> {
     let caps = surface.get_capabilities(&adapter);
     let format =
       caps.formats.iter().find(|f| matches!(f, TextureFormat::Bgra8Unorm)).cloned().unwrap_or(caps.formats[0]);
-    let config = (SurfaceConfiguration {
+    let config = SurfaceConfiguration {
       usage: TextureUsages::RENDER_ATTACHMENT,
       format,
       width: size.width,
@@ -136,17 +144,19 @@ impl<'a> Engine<'a> {
       desired_maximum_frame_latency: 2,
       alpha_mode: caps.alpha_modes[0],
       view_formats: vec![],
-    });
+    };
 
     surface.configure(&device, &config);
 
     let keys = Keys::default();
+    let should_exit = Cell::new(false);
     let state = GameRunner::new(&UpdateContext {
       device: &device,
       queue: &queue,
       window: &window,
       keys: &keys,
       config: &config,
+      should_exit: &should_exit,
     });
 
     let now = Instant::now();
@@ -163,6 +173,7 @@ impl<'a> Engine<'a> {
       frames: 0,
       keys: Keys::default(),
       focused: false,
+      should_exit,
 
       state,
     }
@@ -206,19 +217,20 @@ impl<'a> Engine<'a> {
           keys: &self.keys,
           window: &self.window,
           config: &self.config,
+          should_exit: &self.should_exit
         },
         new_size,
       );
     }
   }
 
-  pub fn update(&mut self) {
-    // fixed step
+  pub fn update(&mut self) -> ControlFlow<()> {
+    // target game at fixed step 60fps always,
     const TARGET_FRAMERATE: LazyLock<Duration> = LazyLock::new(|| Duration::from_secs_f64(1. / 60.));
     let now = Instant::now();
     let since_last_frame = now - self.last_frame_instant;
     if since_last_frame < *TARGET_FRAMERATE {
-      return;
+      return ControlFlow::Continue(());
     }
     self.last_frame_instant = now;
     self.frames += 1;
@@ -229,6 +241,7 @@ impl<'a> Engine<'a> {
       keys: &self.keys,
       window: &self.window,
       config: &self.config,
+      should_exit: &self.should_exit
     });
     self.keys.tick_keys();
 
@@ -236,6 +249,11 @@ impl<'a> Engine<'a> {
       self.last_second = now;
       self.frames = 0;
     }
+
+    match self.should_exit.get() {
+      true => ControlFlow::Break(()),
+      false => ControlFlow::Continue(())
+    }
   }
 
   pub fn render(&self) -> Result<(), wgpu::SurfaceError> {
@@ -258,6 +276,7 @@ pub struct UpdateContext<'a> {
   pub keys: &'a Keys,
   pub window: &'a Arc<Window>,
   pub config: &'a SurfaceConfiguration,
+  pub should_exit: &'a Cell<bool>,
 }
 
 pub trait EngineState {
diff --git a/src/game/anm/loaded_file.rs b/src/game/anm/loaded_file.rs
index 9e7b25a..fc860ff 100644
--- a/src/game/anm/loaded_file.rs
+++ b/src/game/anm/loaded_file.rs
@@ -1,8 +1,9 @@
-use std::{io::Cursor, ops::Add, path::Path, sync::Arc};
+use std::{io::Cursor, ops::Add, sync::Arc};
 
 use async_std::fs;
 use bytemuck::{Pod, Zeroable};
 use glam::Vec2;
+use instructions::anm::{Instruction, Op};
 use nonoverlapping_interval_tree::NonOverlappingIntervalTree;
 use truth::{context::RootEmitter, io::BinReader, AnmFile, Game as TruthGame};
 use wgpu::{
@@ -14,10 +15,7 @@ use winit::dpi::PhysicalSize;
 
 use crate::utils::game::Game;
 
-use super::{
-  image::{produce_image_from_entry, Image},
-  vm::opcodes::{Instruction, Op},
-};
+use super::image::{produce_image_from_entry, Image};
 
 pub struct LoadedEntry {
   _texture: Texture,
@@ -169,10 +167,10 @@ pub struct LoadedScript {
 
 impl LoadedScript {
   fn load(script: &truth::anm::Script) -> Arc<LoadedScript> {
-    let mut offset_instructions: FastHashMap<_, _> = Default::default();
+    let mut offset_instructions: Vec<(_, _)> = Default::default();
     let mut current_offset = 0;
     for (index, inst) in script.instrs.iter().enumerate() {
-      offset_instructions.insert(current_offset, index);
+      offset_instructions.push((current_offset, index));
       current_offset += 8 + inst.args_blob.len() as u32
     }
 
@@ -204,11 +202,12 @@ pub struct LoadedFile {
 }
 
 impl LoadedFile {
-  pub async fn load(device: &Device, queue: &Queue, size: PhysicalSize<u32>, game: Game, file_name: &str) -> Self {
-    let file_data = fs::read(game.asset_path(file_name)).await.expect("failed to load anm file");
+  pub async fn load(device: Arc<Device>, queue: Arc<Queue>, size: PhysicalSize<u32>, game: Game, file_name: impl Into<String> + Send) -> Self {
+    let file_name = file_name.into();
+    let file_data = fs::read(game.asset_path(&file_name)).await.expect("failed to load anm file");
 
     let file = AnmFile::read_from_stream(
-      &mut BinReader::from_reader(&RootEmitter::new_stderr(), file_name, Cursor::new(file_data)),
+      &mut BinReader::from_reader(&RootEmitter::new_stderr(), &file_name, Cursor::new(file_data)),
       match game {
         Game::Mof => TruthGame::Th10,
         Game::Sa => TruthGame::Th11,
@@ -220,7 +219,7 @@ impl LoadedFile {
     let scripts =
       file.entries.iter().flat_map(|entry| &entry.scripts).map(|(_, script)| LoadedScript::load(script)).collect();
 
-    let entries = file.entries.iter().map(|entry| LoadedEntry::load(device, queue, size, entry)).collect::<Vec<_>>();
+    let entries = file.entries.iter().map(|entry| LoadedEntry::load(&device, &queue, size, entry)).collect::<Vec<_>>();
 
     let mut sprite_entries = NonOverlappingIntervalTree::new();
 
diff --git a/src/game/anm/manager/loading.rs b/src/game/anm/manager/loading.rs
index a8f243f..d5ecf3f 100644
--- a/src/game/anm/manager/loading.rs
+++ b/src/game/anm/manager/loading.rs
@@ -13,8 +13,8 @@ use super::Manager;
 impl Manager {
   pub fn start_load_anm(
     &mut self,
-    device: Arc<Device>,
-    queue: Arc<Queue>,
+    device: &Arc<Device>,
+    queue: &Arc<Queue>,
     size: PhysicalSize<u32>,
     game: Game,
     file_name: impl Into<String>,
@@ -25,8 +25,9 @@ impl Manager {
     }
 
     let sender = self.anm_sender.clone();
+    let (device, queue) = (device.clone(), queue.clone());
     Soon::new(async move {
-      let file = Arc::new(LoadedFile::load(&device, &queue, size, game, &file_name).await);
+      let file = Arc::new(LoadedFile::load(device, queue, size, game, &file_name).await);
 
       sender.send((file_name, file.clone())).unwrap();
 
@@ -42,8 +43,8 @@ impl Manager {
 
   pub fn load_anm(
     &mut self,
-    device: &Device,
-    queue: &Queue,
+    device: &Arc<Device>,
+    queue: &Arc<Queue>,
     window: &Window,
     game: Game,
     file_name: impl Into<String>,
@@ -54,8 +55,8 @@ impl Manager {
     }
 
     let loaded_anm = Arc::new(async_std::task::block_on(LoadedFile::load(
-      device,
-      queue,
+      device.clone(),
+      queue.clone(),
       window.inner_size(),
       game,
       &file_name,
diff --git a/src/game/anm/manager/mod.rs b/src/game/anm/manager/mod.rs
index 530e80b..605b89d 100644
--- a/src/game/anm/manager/mod.rs
+++ b/src/game/anm/manager/mod.rs
@@ -3,7 +3,8 @@ use bytemuck::{Pod, Zeroable};
 use crossbeam::channel::{Receiver, Sender};
 use glam::Mat4;
 use std::{
-  collections::VecDeque, sync::{Arc, Weak}
+  collections::VecDeque,
+  sync::{Arc, Weak},
 };
 use wgpu::{naga::FastHashMap, BindGroup, Buffer, PipelineLayout, RenderPipeline, ShaderModule, Texture};
 
@@ -27,14 +28,14 @@ pub struct Manager {
   anm_files: FastHashMap<String, Arc<LoadedFile>>,
   anm_sender: Sender<(String, Arc<LoadedFile>)>,
   anm_receiver: Receiver<(String, Arc<LoadedFile>)>,
-  
+
   world_backbuffer_anm: WeakVm,
   ui_vms: VecDeque<WeakVm>,
   world_vms: VecDeque<WeakVm>,
 
   depth_texture: Texture,
   ui_uniform: Uniform,
-  _world_uniform: Uniform,
+  world_uniform: Uniform,
   uniform_buffer: Buffer,
   render_bind_group: BindGroup,
   render_pipeline_layout: PipelineLayout,
@@ -62,8 +63,6 @@ impl Manager {
       vm.borrow_mut().interrupt(interrupt);
     }
 
-    self.update_single(&vm);
-
     let mut context = ManagerUpdate::new();
     vm.borrow_mut().tick(&mut context);
 
@@ -83,12 +82,17 @@ impl Manager {
     vm
   }
 
-  fn update_single(&mut self, vm: &Vm) {
+  pub fn update_single(&mut self, vm: &Vm) {
     let mut context = ManagerUpdate::new();
     vm.borrow_mut().tick(&mut context);
     context.apply_lists(&mut self.ui_vms, &mut self.world_vms);
   }
 
+  pub fn interrupt_immediately(&mut self, vm: &Vm, interrupt: u32) {
+    vm.borrow_mut().interrupt(interrupt);
+    self.update_single(vm);
+  }
+
   fn update_list(list: &mut VecDeque<WeakVm>, context: &mut ManagerUpdate) {
     list.retain(|value| {
       if let Some(rc) = value.upgrade() {
diff --git a/src/game/anm/manager/rendering.rs b/src/game/anm/manager/rendering.rs
index c0611c9..53469b2 100644
--- a/src/game/anm/manager/rendering.rs
+++ b/src/game/anm/manager/rendering.rs
@@ -4,12 +4,20 @@ use bytemuck::{bytes_of, Pod, Zeroable};
 use glam::{Mat4, Vec2, Vec3, Vec4};
 use num_traits::FloatConst;
 use wgpu::{
-  include_wgsl, naga::FastHashMap, util::{BufferInitDescriptor, DeviceExt}, BindGroupDescriptor, BindGroupEntry, BindGroupLayoutDescriptor, BindGroupLayoutEntry, BindingResource, BlendState, BufferDescriptor, BufferUsages, Color, ColorTargetState, ColorWrites, DepthStencilState, Device, Extent3d, FragmentState, LoadOp, Operations, PipelineLayoutDescriptor, PrimitiveState, RenderPass, RenderPassColorAttachment, RenderPassDepthStencilAttachment, RenderPassDescriptor, RenderPipeline, RenderPipelineDescriptor, ShaderStages, StoreOp, TextureDescriptor, TextureFormat, TextureUsages, VertexAttribute, VertexBufferLayout, VertexState
+  include_wgsl,
+  naga::FastHashMap,
+  util::{BufferInitDescriptor, DeviceExt},
+  BindGroupDescriptor, BindGroupEntry, BindGroupLayoutDescriptor, BindGroupLayoutEntry, BindingResource, BlendState,
+  BufferDescriptor, BufferUsages, Color, ColorTargetState, ColorWrites, DepthStencilState, Device, Extent3d,
+  FragmentState, LoadOp, Operations, PipelineLayoutDescriptor, PrimitiveState, RenderPass, RenderPassColorAttachment,
+  RenderPassDepthStencilAttachment, RenderPassDescriptor, RenderPipeline, RenderPipelineDescriptor, ShaderStages,
+  StoreOp, TextureDescriptor, TextureFormat, TextureUsages, VertexAttribute, VertexBufferLayout, VertexState,
 };
 use winit::dpi::PhysicalSize;
 
 use crate::{
-  engine::{Engine, UpdateContext}, game::anm::{loaded_file::SpriteUvs, vm::RenderingState, AnmVm}
+  engine::{Engine, UpdateContext},
+  game::anm::{loaded_file::SpriteUvs, vm::RenderingState, AnmVm},
 };
 
 use super::{Manager, Uniform, WeakVm};
@@ -203,7 +211,7 @@ impl Manager {
 
       depth_texture,
       ui_uniform,
-      _world_uniform: world_uniform,
+      world_uniform,
       uniform_buffer,
       render_bind_group,
       render_pipeline_layout: pipeline_layout,
@@ -292,13 +300,6 @@ impl Manager {
     render_pipeline
   }
 
-  pub fn render_vms(&self, engine: &Engine, pass: &mut RenderPass) {
-    self.render_layer(engine, pass, &self.ui_vms, 21);
-    self.render_layer(engine, pass, &self.ui_vms, 22);
-    self.render_layer(engine, pass, &self.ui_vms, 23);
-    self.render_layer(engine, pass, &self.ui_vms, 29);
-  }
-
   fn render_layer(&self, engine: &Engine, pass: &mut RenderPass, list: &VecDeque<WeakVm>, layer: u32) {
     for vm in list {
       if let Some(vm) = vm.upgrade() {
@@ -369,7 +370,7 @@ impl Manager {
   fn render_ui(&self, engine: &Engine, encoder: &mut wgpu::CommandEncoder, surface: &wgpu::Texture) {
     engine.queue.write_buffer(&self.uniform_buffer, 0, bytes_of(&self.ui_uniform));
     let mut pass = encoder.begin_render_pass(&RenderPassDescriptor {
-      label: Some("anm"),
+      label: Some("ui anms"),
       color_attachments: &[Some(RenderPassColorAttachment {
         view: &surface.create_view(&Default::default()),
         resolve_target: None,
@@ -398,7 +399,50 @@ impl Manager {
       ..Default::default()
     });
 
-    self.render_vms(engine, &mut pass);
+    self.render_layer(engine, &mut pass, &self.ui_vms, 21);
+    self.render_layer(engine, &mut pass, &self.ui_vms, 22);
+    self.render_layer(engine, &mut pass, &self.ui_vms, 23);
+    self.render_layer(engine, &mut pass, &self.ui_vms, 29);
+  }
+
+  fn render_world(&self, engine: &Engine, encoder: &mut wgpu::CommandEncoder, surface: &wgpu::Texture) {
+    engine.queue.write_buffer(&self.uniform_buffer, 0, bytes_of(&self.world_uniform));
+    let mut pass = encoder.begin_render_pass(&RenderPassDescriptor {
+      label: Some("world anms"),
+      color_attachments: &[Some(RenderPassColorAttachment {
+        view: &surface.create_view(&Default::default()),
+        resolve_target: None,
+        ops: Operations {
+          // load: LoadOp::Clear(Color {
+          //   // r: 100. / 255.,
+          //   // g: 149. / 255.,
+          //   // b: 237. / 255.,
+          //   r: 1.0,
+          //   g: 1.0,
+          //   b: 1.0,
+          //   a: 1.0,
+          // }),
+          load: LoadOp::Load,
+          ..Default::default()
+        },
+      })],
+      depth_stencil_attachment: Some(RenderPassDepthStencilAttachment {
+        view: &self.depth_texture.create_view(&Default::default()),
+        depth_ops: Some(Operations {
+          load: LoadOp::Clear(1.0),
+          store: StoreOp::Store,
+        }),
+        stencil_ops: None,
+      }),
+      ..Default::default()
+    });
+
+    self.render_layer(engine, &mut pass, &self.world_vms, 0);
+    self.render_layer(engine, &mut pass, &self.world_vms, 1);
+    self.render_layer(engine, &mut pass, &self.world_vms, 2);
+    self.render_layer(engine, &mut pass, &self.world_vms, 3);
+    self.render_layer(engine, &mut pass, &self.world_vms, 8);
+
   }
 
   fn clear(&self, encoder: &mut wgpu::CommandEncoder, surface: &wgpu::Texture) {
@@ -434,6 +478,7 @@ impl Manager {
 
   pub fn render(&self, engine: &Engine, surface: &wgpu::Texture, encoder: &mut wgpu::CommandEncoder) {
     self.clear(encoder, surface);
+    self.render_world(engine, encoder, surface);
     self.render_ui(engine, encoder, surface);
 
     if let Some(backbuffer_anm) = self.world_backbuffer_anm.upgrade() {
diff --git a/src/game/anm/vm/execute.rs b/src/game/anm/vm/execute.rs
index 091f49c..553cb77 100644
--- a/src/game/anm/vm/execute.rs
+++ b/src/game/anm/vm/execute.rs
@@ -1,14 +1,17 @@
 use glam::{Vec2, Vec3};
+use instructions::{
+  anm::{Instruction, Op, PrimOp, PrimSetOp},
+  Param,
+};
 use num_traits::{FloatConst, FromPrimitive};
 use rand::Rng;
 
 use crate::{
-  game::{anm::{manager::ManagerUpdate, VmLocation}, param::Param}, interp::Mode
+  game::anm::{manager::ManagerUpdate, VmLocation},
+  interp::Mode,
 };
 
-use super::{
-  opcodes::{Instruction, Op, PrimOp, PrimSetOp}, AnmVm
-};
+use super::AnmVm;
 
 impl AnmVm {
   fn next_instruction(&mut self) -> Option<Instruction> {
@@ -104,7 +107,9 @@ impl AnmVm {
   }
 
   pub fn interrupt(&mut self, interrupt_id: u32) {
-    self.pending_interrupt = Some(interrupt_id);
+    if self.script.interrupts.contains_key(&interrupt_id) {
+      self.pending_interrupt = Some(interrupt_id);
+    }
 
     for ele in self.children.iter() {
       ele.borrow_mut().interrupt(interrupt_id);
@@ -115,7 +120,7 @@ impl AnmVm {
     if let Some((int_pc, int_time)) =
       self.pending_interrupt.take().and_then(|int| self.script.interrupts.get(&int).cloned())
     {
-      self.interrupt_return = Some((self.pc, self.time));
+      self.interrupt_return.replace((self.pc, self.time));
       self.pc = int_pc;
       self.time = Default::default();
       self.time.set_time(int_time)
@@ -124,6 +129,7 @@ impl AnmVm {
 
   fn return_from_interrupt(&mut self) {
     if let Some((pc, time)) = self.interrupt_return.take() {
+      println!("returned");
       self.pc = pc;
       self.time.set_time(time.time());
     }
@@ -313,26 +319,27 @@ impl AnmVm {
           self.sprite_mode = mode;
         }
         Op::Layer { layer } => self.layer = layer as u32,
+        Op::ZWriteDisable(value) => self.zwrite_disable = value,
         Op::Wait { time } => self.time.wait(time as u32),
-        Op::CaseReturn => {
+        Op::ReturnFromInterrupt => {
           self.return_from_interrupt();
         }
         Op::UnknownBitflag { .. } => {}
         Op::RandMode { mode } => self.random_mode = mode,
-        Op::ScriptNew { script } => {
+        Op::NewChildBack { script } => {
           self.children.push(manager.new_vm(self.file.clone(), script as usize, VmLocation::new_child(self.layer)));
         }
-        Op::ScriptNewUI { script } => {
+        Op::NewChildUIBack { script } => {
           self.children.push(manager.new_vm(self.file.clone(), script as usize, VmLocation::new_child_ui(self.layer)));
         }
-        Op::ScriptNewFront { script } => {
+        Op::NewChildFront { script } => {
           self.children.push(manager.new_vm(
             self.file.clone(),
             script as usize,
             VmLocation::new_child_front(self.layer),
           ));
         }
-        Op::ScriptNewUIFront { script } => {
+        Op::NewChildUIFront { script } => {
           self.children.push(manager.new_vm(
             self.file.clone(),
             script as usize,
diff --git a/src/game/anm/vm/mod.rs b/src/game/anm/vm/mod.rs
index 79bc84e..501d3e3 100644
--- a/src/game/anm/vm/mod.rs
+++ b/src/game/anm/vm/mod.rs
@@ -2,17 +2,21 @@ use std::sync::Arc;
 
 use atomic_refcell::AtomicRefCell;
 use glam::{Quat, Vec2, Vec3};
-use opcodes::SpriteType;
+use instructions::anm::SpriteType;
 use wgpu::{BlendComponent, BlendFactor, BlendOperation, BlendState, Buffer, RenderPipeline};
 
-use crate::{game::timer::Timer, interp::{FloatInterpolator, Vec2Interpolator, Vec3Interpolator}};
+use crate::{
+  game::timer::Timer,
+  interp::{FloatInterpolator, Vec2Interpolator, Vec3Interpolator},
+};
 
 use super::{
-  loaded_file::{LoadedEntry, LoadedFile, LoadedScript, LoadedSprite}, manager::ManagerUpdate, Vm
+  loaded_file::{LoadedEntry, LoadedFile, LoadedScript, LoadedSprite},
+  manager::ManagerUpdate,
+  Vm,
 };
 
 pub mod execute;
-pub(super) mod opcodes;
 
 pub(super) struct RenderingState {
   pub instance_buffer: Buffer,
@@ -48,6 +52,7 @@ pub struct AnmVm {
   pub(super) visible: bool,
   pub(super) sprite: Option<u32>,
   pub(super) layer: u32,
+  pub(super) zwrite_disable: bool,
   random_mode: i32,
   sprite_mode: SpriteType,
   pub(super) blend_state: BlendState,
@@ -87,6 +92,7 @@ impl AnmVm {
       visible: true,
       sprite: None,
       layer: default_layer,
+      zwrite_disable: false,
       sprite_mode: SpriteType::NoRotate,
       blend_state: BlendState::REPLACE,
       random_mode: 0,
@@ -123,6 +129,14 @@ impl AnmVm {
     self.debug = true;
   }
 
+  pub fn child(&self, index: usize) -> Option<&Vm> {
+    self.children.get(index)
+  }
+
+  pub fn file(&self) -> &Arc<LoadedFile> {
+    &self.file
+  }
+
   pub fn scale(&self) -> Vec2 {
     self.scale_interpolator.current()
   }
@@ -210,7 +224,7 @@ impl AnmVm {
         // additive blending
         color: BlendComponent {
           src_factor: BlendFactor::SrcAlpha,
-          dst_factor: BlendFactor::OneMinusSrcAlpha,
+          dst_factor: BlendFactor::One,
           operation: BlendOperation::Add,
         },
         alpha: BlendComponent::OVER,
diff --git a/src/game/enemy/manager.rs b/src/game/enemy/manager.rs
deleted file mode 100644
index d022deb..0000000
--- a/src/game/enemy/manager.rs
+++ /dev/null
@@ -1,4 +0,0 @@
-pub struct Manager {
-  
-  
-}
diff --git a/src/game/enemy/mod.rs b/src/game/enemy/mod.rs
deleted file mode 100644
index 7944012..0000000
--- a/src/game/enemy/mod.rs
+++ /dev/null
@@ -1,3 +0,0 @@
-pub mod loaded_file;
-pub mod manager;
-pub mod vm;
diff --git a/src/game/enemy/vm/opcodes.rs b/src/game/enemy/vm/opcodes.rs
deleted file mode 100644
index 5a0d1e1..0000000
--- a/src/game/enemy/vm/opcodes.rs
+++ /dev/null
@@ -1,343 +0,0 @@
-use macros::decode_args;
-use num_derive::FromPrimitive;
-use num_traits::FromPrimitive;
-use truth::llir::RawInstr;
-use wgpu::naga::FastHashMap;
-
-use crate::game::param::Param;
-
-#[derive(Debug, FromPrimitive, Clone, Copy)]
-enum Opcode {
-  Nop = 0,
-
-  // control flow
-  Delete = 1,
-  Ret = 10,
-  Call = 11,
-  Jump = 12,
-  JumpEq = 13,
-  JumpNeq = 14,
-  CallAsync = 15,
-  CallAsyncById = 16,
-  KillAsync = 17,
-  KillAllAsync = 21,
-
-  // stack math
-  StackAlloc = 40,
-  PushInt = 42,
-  SetInt = 43,
-  PushFloat = 44,
-  SetFloat = 45,
-  AddInt = 50,
-  AddFloat = 51,
-  SubInt = 52,
-  SubFloat = 53,
-  MulInt = 54,
-  MulFloat = 55,
-  DivInt = 56,
-  DivFloat = 57,
-  ModInt = 58,
-  EqualInt = 59,
-  EqualFloat = 60,
-  NotEqualInt = 61,
-  NotEqualFloat = 62,
-  LessInt = 63,
-  LessFloat = 64,
-  LessEqualInt = 65,
-  LessEqualFloat = 66,
-  GreaterInt = 67,
-  GreaterFloat = 68,
-  GreaterEqualInt = 69,
-  GreaterEqualFloat = 70,
-  NotInt = 71,
-  NotFloat = 72,
-  LogicalOr = 73,
-  LogicalAnd = 74,
-  BitwiseXor = 75,
-  BitwiseOr = 76,
-  BitwiseAnd = 77,
-  DecrementInt = 78,
-  Sin = 79,
-  Cos = 80,
-  CirclePos = 81,
-  ValidRad = 82,
-  Wait = 83,
-  NegateInt = 84,
-  NegateFloat = 85,
-  SquareSum = 86,
-  GetAngle = 87,
-  SquareRoot = 88,
-  Linear = 89,
-  PointRotate = 90,
-  FloatTime = 91,
-  Math92 = 92,
-  Math93 = 93,
-
-  EnemyCreate = 256,
-  EnemyCreateAbsolute = 257,
-  EnemySelectAnm = 258,
-  EnemySetAnmSprite = 259,
-  EnemyCreateMirrored = 260,
-  EnemyCreateMirroredAbsolute = 261,
-  EnemySetMainAnm = 262,
-  EnemyPlayAnm = 263,
-  EnemyPlayAnmAbsolute = 264,
-  EnemyCreateFiller = 265,
-  EnemyCreateFillerAbsolute = 266,
-  EnemyCreateFillerMirrored = 267,
-  EnemyCreateFillerMirroredAbsolute = 268,
-  EnemyPlaySelected = 269,
-  EnemySwitchAnm = 275,
-  EnemyResetAnm = 276,
-  EnemyInst277 = 277,
-  EnemyInst278 = 278,
-
-  MovePos = 280,
-  MovePosTime = 281,
-  MovePosRel = 282,
-  MovePosRelTime = 283,
-  MoveVel = 284,
-  MoveVelTime = 285,
-  MoveVelRel = 286,
-  MoveVelRelTime = 287,
-  MoveCircle = 288,
-  MoveCircleTime = 289,
-  MoveCircleRel = 290,
-  MoveCircleRelTime = 291,
-  MoveRand = 292,
-  MoveRandRel = 293,
-  MoveAdd = 294,
-  MoveAddRel = 295,
-  MoveEllipse = 300,
-  MoveEllipseRel = 301,
-  MoveBezier = 305,
-  MoveReset = 307,
-
-  SetHurtbox = 320,
-  SetHitbox = 321,
-  FlagSet = 322,
-  FlagClear = 323,
-  Movelimit = 324,
-  MoveLimitReset = 325,
-  ClearExtraDrops = 326,
-  AddExtraDrops = 327,
-  SetDropArea = 328,
-  DropItems = 329,
-  SetMainDrop = 330,
-  SetHealth = 331,
-  SetBoss = 332,
-  TimerReset = 333,
-  SetInterrupt = 334,
-  SetInvulnerable = 335,
-  PlaySound = 336,
-  ShakeScreen = 337,
-  StartDialogue = 338,
-  WaitForDialogue = 339,
-  WaitForDeath = 340,
-  SetTimeout = 341,
-  SpellById = 342,
-  EndSpell = 343,
-  SetChapter = 344,
-  KillAllEnemies = 345,
-  ProtectPlayer = 346,
-  LifeMarker = 347,
-  SetByDifficultyInt = 355,
-  SetByDifficultyFloat = 356,
-  SpellDifficulty = 357,
-  SpellDifficultyM1 = 358,
-  SpellDifficultyM2 = 359,
-
-  SpawnerReset = 400,
-  SpawnerEnable = 401,
-  SpawnerSetSprite = 402,
-  SpawnerSetOffset = 403,
-  SpawnerSetAngle = 404,
-  SpawnerSetSpeed = 405,
-  SpawnerSetCount = 406,
-  SpawnerSetAimMode = 407,
-  SpawnerSetSounds,
-  SpawnerSet,
-  SpawnerSetTransformation,
-  SpawnerSetTransformation2,
-  SpawnerAddTransformation,
-  SpawnerAddTransformation2,
-  ClearAllBullets,
-  SpawnerCopy,
-  ShootLaserAimed,
-  CancelBullets,
-}
-
-pub enum PrimOp {
-  Add,
-  Sub,
-  Mul,
-  Div,
-  Equal,
-  NotEqual,
-  LessThan,
-  LessEqual,
-  Greater,
-  GreaterEqual,
-  Not,
-  Negate,
-}
-
-pub enum Op {
-  Nop,
-  Delete,
-  Return,
-  Jump {
-    index: usize,
-    time: i32,
-  },
-  JumpEqual {
-    index: usize,
-    time: i32,
-  },
-  JumpNotEqual {
-    index: usize,
-    time: i32,
-  },
-  CallAsync(String),
-  CallAsyncId(String, u32),
-  StackAlloc(usize),
-  PushInt(Param<i32>),
-  SetInt(i32),
-  PushFloat(Param<f32>),
-  SetFloat(i32),
-  IntOp(PrimOp),
-  ModuloInt,
-  FloatOp(PrimOp),
-  Or,
-  And,
-  BitwiseXor,
-  BitwiseOr,
-  BitwiseAnd,
-  DecrementInt(i32),
-  Sin,
-  Cos,
-  CirclePos {
-    x: f32,
-    y: f32,
-    angle: Param<f32>,
-    radius: Param<f32>,
-  },
-
-  SetFlag(i32),
-  ClearFlag(i32),
-
-  ClearAllBullets,
-  CancelBullets(i32),
-}
-
-pub struct Instruction {
-  time: i32,
-  difficulty: u8,
-  op: Op,
-}
-
-impl Instruction {
-  pub fn from_raw(inst: RawInstr, instruction_offsets: &FastHashMap<u32, usize>) -> Self {
-    let param_int = |value: i32, index: u16| {
-      if inst.param_mask & (1 << index) == 0 {
-        Param::Value(value)
-      } else if value < -9932 || value > 10000 {
-        Param::Value(value)
-      } else {
-        Param::Variable(value)
-      }
-    };
-    let param_float = |value: f32, index: u16| {
-      if inst.param_mask & (1 << index) == 0 {
-        Param::Value(value)
-      } else if value != value.trunc() && value < -9932. || value > 10000. {
-        Param::Value(value)
-      } else {
-        Param::Variable(value as i32)
-      }
-    };
-    let var_int = |value: i32, index: u16| {
-      assert!(inst.param_mask & (1 << index) != 0, "param must be a var");
-      assert!(value >= -9932 && value <= 10000, "value must be a var");
-      value
-    };
-    let var_float = |value: f32, index: u16| {
-      assert!(inst.param_mask & (1 << index) != 0, "param must be a var");
-      assert!(
-        value.trunc() == value && value >= -9932. && value <= 10000.,
-        "value must be a var"
-      );
-      value as i32
-    };
-    let offset_to_index = |value: i32| instruction_offsets[&(value as u32)];
-
-    let args = &inst.args_blob;
-    let opcode_raw = inst.opcode;
-    let opcode = Opcode::from_u16(opcode_raw).unwrap_or_else(|| todo!("failed to convert opcode {opcode_raw}"));
-    let op = match opcode {
-      Opcode::Nop => Op::Nop,
-      Opcode::Delete => todo!(),
-      Opcode::Ret => todo!(),
-      Opcode::Call => todo!(),
-      Opcode::Jump => {
-        let (offset, time) = decode_args!(args, "ot");
-
-        Op::Jump {
-          index: offset_to_index(offset),
-          time,
-        }
-      }
-      Opcode::JumpEq => {
-        let (offset, time) = decode_args!(args, "ot");
-
-        Op::JumpEqual {
-          index: offset_to_index(offset),
-          time,
-        }
-      }
-      Opcode::JumpNeq => {
-        let (offset, time) = decode_args!(args, "ot");
-
-        Op::JumpNotEqual {
-          index: offset_to_index(offset),
-          time,
-        }
-      }
-      Opcode::CallAsync => todo!(),
-      Opcode::CallAsyncById => todo!(),
-      Opcode::KillAsync => todo!(),
-      Opcode::KillAllAsync => todo!(),
-      Opcode::StackAlloc => Op::StackAlloc(decode_args!(args, "S") as usize),
-      Opcode::PushInt => todo!(),
-      Opcode::SetInt => todo!(),
-      Opcode::PushFloat => {
-        let float = decode_args!(args, "f");
-        Op::PushFloat(param_float(float, 0))
-      }
-      Opcode::SetFloat => {
-        let var = decode_args!(args, "f");
-        Op::SetFloat(var_float(var, 0))
-      }
-      Opcode::FlagSet => {
-        let flag = decode_args!(args, "S");
-        Op::SetFlag(flag)
-      }
-      Opcode::FlagClear => {
-        let flag = decode_args!(args, "S");
-        Op::ClearFlag(flag)
-      }
-      Opcode::CancelBullets => {
-        todo!()
-      }
-      _ => {
-        unimplemented!("opcode {opcode:?} not implemented");
-      }
-    };
-
-    Self {
-      time: inst.time,
-      difficulty: inst.difficulty,
-      op,
-    }
-  }
-}
diff --git a/src/game/mod.rs b/src/game/mod.rs
index 5bb72e6..759538c 100644
--- a/src/game/mod.rs
+++ b/src/game/mod.rs
@@ -1,4 +1,4 @@
-use std::{path::Path, sync::Arc};
+use std::sync::Arc;
 
 use anm::LoadedFile;
 use states::GameStateMachine;
@@ -9,8 +9,7 @@ use crate::{
 };
 
 mod anm;
-mod enemy;
-mod param;
+mod stage;
 mod snd;
 mod states;
 mod timer;
@@ -30,8 +29,8 @@ struct GameContext<'a> {
 impl GameContext<'_> {
   pub fn start_load_anm(&mut self, game: Game, file_name: impl Into<String>) -> Soon<Arc<LoadedFile>> {
     self.anm_manager.start_load_anm(
-      self.engine.device.clone(),
-      self.engine.queue.clone(),
+      self.engine.device,
+      self.engine.queue,
       self.engine.window.inner_size(),
       game,
       file_name,
@@ -40,9 +39,9 @@ impl GameContext<'_> {
 
   pub fn load_anm(&mut self, game: Game, file_name: impl Into<String>) -> Arc<LoadedFile> {
     self.anm_manager.load_anm(
-      &self.engine.device,
-      &self.engine.queue,
-      &self.engine.window,
+      self.engine.device,
+      self.engine.queue,
+      self.engine.window,
       game,
       file_name,
     )
diff --git a/src/game/snd/bgm/format.rs b/src/game/snd/bgm/format.rs
index 7e71476..e4e652d 100644
--- a/src/game/snd/bgm/format.rs
+++ b/src/game/snd/bgm/format.rs
@@ -52,7 +52,7 @@ impl BgmFormat {
       }
 
       let track_name = track_name.to_str().unwrap().to_owned();
-      println!("track: {:?}", track_name);
+      println!("track {track_name}");
       let track = Track::ref_from_bytes(&bytes[(start + TRACK_NAME_SIZE)..(start + TRACK_SIZE)]).unwrap();
       tracks.insert(
         track_name,
@@ -70,7 +70,9 @@ impl BgmFormat {
   }
 
   pub fn get_track_buffers(&mut self, file_name: &str) -> (Range<usize>, Range<usize>) {
-    let track = self.tracks.get(file_name).expect("failed to find bgm file");
+    let Some(track) = self.tracks.get(file_name) else {
+      panic!("failed to find bgm file: {file_name:?}")
+    };
 
     let intro_start = track.track_offset as usize;
     let track_start = intro_start + track.intro_size as usize;
diff --git a/src/game/snd/bgm/mod.rs b/src/game/snd/bgm/mod.rs
index 25229d3..b28a4da 100644
--- a/src/game/snd/bgm/mod.rs
+++ b/src/game/snd/bgm/mod.rs
@@ -1,4 +1,4 @@
-use std::{rc::Rc, sync::Arc};
+use std::sync::Arc;
 
 use async_std::fs::read;
 use format::BgmFormat;
diff --git a/src/game/stage/enemy/execute.rs b/src/game/stage/enemy/execute.rs
new file mode 100644
index 0000000..0b676b9
--- /dev/null
+++ b/src/game/stage/enemy/execute.rs
@@ -0,0 +1,6 @@
+use super::Vm;
+
+impl Vm {
+  pub fn execute(&mut self) {
+  }
+}
diff --git a/src/game/enemy/vm/flags.rs b/src/game/stage/enemy/flags.rs
similarity index 70%
rename from src/game/enemy/vm/flags.rs
rename to src/game/stage/enemy/flags.rs
index d540476..70b94db 100644
--- a/src/game/enemy/vm/flags.rs
+++ b/src/game/stage/enemy/flags.rs
@@ -7,11 +7,12 @@ pub struct Flags {
   pub disable_offscreen_horizontal: bool,
   pub disable_offscreen_vertical: bool,
   pub invincible: bool,
-  pub intangible: bool,
+  pub intangible: bool, // override: disable_hurtbox | disable_hitbox | invincible | no_global_delete
   pub no_global_delete: bool,
   pub always_global_delete: bool,
   pub graze: bool,
   pub only_delete_on_dialog: bool,
-  #[bits(22)]
+  pub only_delete_on_clear: bool,
+  #[bits(21)]
   _a: u32,
 }
diff --git a/src/game/enemy/loaded_file.rs b/src/game/stage/enemy/loaded_file.rs
similarity index 61%
rename from src/game/enemy/loaded_file.rs
rename to src/game/stage/enemy/loaded_file.rs
index ccfa3a5..e524829 100644
--- a/src/game/enemy/loaded_file.rs
+++ b/src/game/stage/enemy/loaded_file.rs
@@ -1,4 +1,4 @@
-use std::{io::Cursor, path::Path, sync::Arc};
+use std::{io::Cursor, sync::Arc};
 
 use async_std::fs;
 use futures::future::{join3, join_all};
@@ -8,7 +8,7 @@ use winit::dpi::PhysicalSize;
 
 use crate::{game::anm, utils::game::Game};
 
-use super::vm::opcodes::Instruction;
+use instructions::ecl::Instruction;
 
 pub struct LoadedFile {
   anm_files: Vec<anm::LoadedFile>,
@@ -35,30 +35,41 @@ impl LoadedFile {
     )
     .unwrap();
 
-    let anm_files = join_all(file.anim_list.into_iter().map(|sp| sp.value).map(|anm| {
-      let (device, queue) = (device.clone(), queue.clone());
-      async move { anm::LoadedFile::load(&device, &queue, size, game, &anm).await }
-    }));
-    let ecl_files = join_all(file.ecli_list.into_iter().map(|sp| sp.value).map(|ecl| {
-      let (device, queue) = (device.clone(), queue.clone());
-      async move { LoadedFile::load(device, queue, size, game, ecl).await }
-    }));
+    let anm_files = join_all(
+      file
+        .anim_list
+        .into_iter()
+        .map(|sp| sp.value)
+        .map(|anm| async_std::task::spawn(anm::LoadedFile::load(device.clone(), queue.clone(), size, game, anm))),
+    );
+    let ecl_files = join_all(
+      file
+        .ecli_list
+        .into_iter()
+        .map(|sp| sp.value)
+        .map(|ecl| LoadedFile::load(device.clone(), queue.clone(), size, game, ecl)),
+    );
 
     let subs = file.subs.into_iter().map(|(k, v)| (k.value, v)).collect::<Vec<_>>();
     let subs = async_std::task::spawn_blocking(|| {
       subs
         .into_iter()
         .map(|(sub, instructions)| {
-          let mut offset_instructions: FastHashMap<_, _> = Default::default();
+          let mut offset_instructions: Vec<(_, _)> = Default::default();
           let mut current_offset = 0;
           for (index, inst) in instructions.iter().enumerate() {
-            offset_instructions.insert(current_offset, index);
-            current_offset += 8 + inst.args_blob.len() as u32
+            offset_instructions.push((current_offset, index));
+            current_offset += 16 + inst.args_blob.len() as u32
           }
 
+          println!("hitting {sub}");
           (
             sub,
-            instructions.into_iter().map(|inst| Instruction::from_raw(inst, &offset_instructions)).collect::<Vec<_>>(),
+            instructions
+              .into_iter()
+              .zip(offset_instructions.iter())
+              .map(|(inst, (offset, _))| Instruction::from_raw(inst, *offset, &offset_instructions))
+              .collect::<Vec<_>>(),
           )
         })
         .collect::<FastHashMap<_, _>>()
diff --git a/src/game/enemy/vm/mod.rs b/src/game/stage/enemy/mod.rs
similarity index 86%
rename from src/game/enemy/vm/mod.rs
rename to src/game/stage/enemy/mod.rs
index 8ddf7be..c40ce49 100644
--- a/src/game/enemy/vm/mod.rs
+++ b/src/game/stage/enemy/mod.rs
@@ -1,7 +1,8 @@
 use crate::game::timer::Timer;
 
+pub mod loaded_file;
 mod flags;
-pub(super) mod opcodes;
+mod execute;
 
 struct StackFrame {
   pc: usize,
@@ -17,4 +18,6 @@ enum Value {
 pub struct Vm {
   call_stack: heapless::Vec<StackFrame, 8>,
   current_frame: StackFrame,
+
+  
 }
diff --git a/src/game/stage/mod.rs b/src/game/stage/mod.rs
new file mode 100644
index 0000000..3125d7e
--- /dev/null
+++ b/src/game/stage/mod.rs
@@ -0,0 +1,71 @@
+use std::{cell::RefCell, rc::Rc};
+
+use vm::{LoadedFile, StageVm};
+
+use crate::utils::game::Game;
+
+use super::GameContext;
+
+mod enemy;
+mod vm;
+
+#[derive(Debug, Clone, Copy)]
+pub enum StageNumber {
+  Stage1 = 1,
+  Stage2 = 2,
+  Stage3 = 3,
+  Stage4 = 4,
+  Stage5 = 5,
+  Stage6 = 6,
+  Extra = 7,
+}
+
+impl StageNumber {
+  pub fn next(&self) -> Option<Self> {
+    match self {
+      Self::Stage1 => Some(Self::Stage2),
+      Self::Stage2 => Some(Self::Stage3),
+      Self::Stage3 => Some(Self::Stage4),
+      Self::Stage4 => Some(Self::Stage5),
+      Self::Stage5 => Some(Self::Stage6),
+      Self::Stage6 => None,
+      Self::Extra => None,
+    }
+  }
+
+  pub fn stage_music(&self, game: Game, boss: bool) -> &'static str {
+    match (self, game, boss) {
+      (Self::Stage1, Game::Mof, false) => "th10_00.wav",
+      (Self::Stage1, Game::Mof, true) => "th10_01.wav",
+      _ => todo!()
+    }
+  }
+}
+
+pub struct Stage {
+  vm: StageVm,
+  enemies: Vec<Rc<RefCell<enemy::Vm>>>,
+  game: Game,
+  stage_number: StageNumber,
+}
+
+impl Stage {
+  pub async fn new(context: &mut GameContext<'_>, game: Game, stage_number: StageNumber) -> Self {
+    let file = LoadedFile::load(context, game, format!("stage0{}.std", stage_number as u8)).await;
+
+    Self {
+      vm: StageVm::new(file),
+      enemies: vec![],
+      game,
+      stage_number,
+    }
+  }
+
+  pub fn started(&mut self, context: &mut GameContext) {
+    context.sound_manager.get_bgm_manager(self.game).play_track(self.stage_number.stage_music(self.game, false));
+  }
+
+  pub fn update(&mut self, context: &mut GameContext) {
+    self.vm.update(context);
+  }
+}
diff --git a/src/game/stage/vm/loaded_file.rs b/src/game/stage/vm/loaded_file.rs
new file mode 100644
index 0000000..173a54c
--- /dev/null
+++ b/src/game/stage/vm/loaded_file.rs
@@ -0,0 +1,58 @@
+use std::io::Cursor;
+
+use async_std::fs;
+use instructions::std::Instruction;
+use truth::{context::RootEmitter, io::BinReader, std::StdExtra, Game as TruthGame, StdFile};
+use wgpu::naga::FastHashMap;
+
+use crate::{
+  game::{
+    anm::{self, Vm, VmLocation},
+    GameContext,
+  },
+  utils::game::Game,
+};
+
+pub struct LoadedFile {
+  pub layers: FastHashMap<u16, Vec<Vm>>,
+  pub instructions: Vec<instructions::std::Instruction>,
+}
+
+impl LoadedFile {
+  pub async fn load(context: &mut GameContext<'_>, game: Game, file_name: impl Into<String> + Send) -> LoadedFile {
+    let file_name = file_name.into();
+    let file_data = fs::read(game.asset_path(&file_name)).await.expect("failed to load anm file");
+    let file = StdFile::read_from_stream(
+      &mut BinReader::from_reader(&RootEmitter::new_stderr(), &file_name, Cursor::new(file_data)),
+      match game {
+        Game::Mof => TruthGame::Th10,
+        Game::Sa => TruthGame::Th11,
+      },
+    )
+    .unwrap();
+    let StdExtra::Th10 { anm_path } = file.extra else {
+      unreachable!()
+    };
+    let anm_file = context.start_load_anm(game, anm_path.value).await;
+
+    let mut layers: FastHashMap<u16, Vec<Vm>> = FastHashMap::default();
+
+    for instance in file.instances {
+      let object = file.objects.get(&instance.object).expect("failed to get object");
+      for quad in &object.quads {
+        let vm = context.anm_manager.new_vm(anm_file.clone(), None, quad.anm_script.into(), VmLocation::new());
+        layers.entry(object.layer).or_default().push(vm);
+      }
+    }
+    let mut offset_instructions: Vec<(_, _)> = Default::default();
+    let mut current_offset = 0;
+    for (index, inst) in file.script.iter().enumerate() {
+      offset_instructions.push((current_offset, index));
+      current_offset += 8 + inst.args_blob.len() as u32
+    }
+
+    let instructions =
+      file.script.into_iter().map(|inst| Instruction::from_raw(inst, &offset_instructions)).collect::<Vec<_>>();
+    LoadedFile { layers, instructions }
+  }
+}
diff --git a/src/game/stage/vm/mod.rs b/src/game/stage/vm/mod.rs
new file mode 100644
index 0000000..3050dd3
--- /dev/null
+++ b/src/game/stage/vm/mod.rs
@@ -0,0 +1,27 @@
+pub use loaded_file::LoadedFile;
+
+use crate::game::{timer::Timer, GameContext};
+
+mod loaded_file;
+
+pub struct StageVm {
+  pub file: LoadedFile,
+  pc: usize,
+  timer: Timer,
+}
+
+impl StageVm {
+  pub fn new(file: LoadedFile) -> Self {
+    Self {
+      file,
+      pc: 0,
+      timer: Timer::default()
+    }
+  }
+
+  pub fn update(&mut self, context: &mut GameContext) {
+    self.timer.tick();
+
+    
+  }
+}
diff --git a/src/game/states/gameplay/mod.rs b/src/game/states/gameplay/mod.rs
index 6b7081c..837d387 100644
--- a/src/game/states/gameplay/mod.rs
+++ b/src/game/states/gameplay/mod.rs
@@ -1,24 +1,47 @@
-use async_std::task::{block_on, spawn};
+use async_std::task::block_on;
 use sfsm::State;
 
 use crate::{
-  game::{enemy::loaded_file::LoadedFile, GameContext},
+  game::{
+    stage::{Stage, StageNumber},
+    GameContext,
+  },
   utils::game::Game,
 };
 
-pub struct Gameplay {}
+use super::{loading::Loading, title_mof::TitleScreenMof, UPDATE_CONTEXT};
+
+pub struct Gameplay {
+  stage: Stage,
+}
 
 impl Gameplay {
-  pub fn new(context: &GameContext) -> Self {
-    block_on(LoadedFile::load(
-      context.engine.device.clone(),
-      context.engine.queue.clone(),
-      context.engine.window.inner_size(),
-      Game::Sa,
-      "default.ecl",
-    ));
-    Self {}
+  pub fn new_blocking(context: &mut GameContext<'_>, game: Game, stage_number: StageNumber) -> Self {
+    block_on(Self::new(context, game, stage_number))
+  }
+  pub async fn new(context: &mut GameContext<'_>, game: Game, stage_number: StageNumber) -> Self {
+    let stage = Stage::new(context, game, stage_number).await;
+    Self { stage }
   }
 }
 
-impl State for Gameplay {}
+impl State for Gameplay {
+  fn entry(&mut self) {
+    UPDATE_CONTEXT.with(|context| self.stage.started(context));
+  }
+  fn execute(&mut self) {
+    UPDATE_CONTEXT.with(|context| self.stage.update(context));
+  }
+}
+
+impl From<Loading> for Gameplay {
+  fn from(_: Loading) -> Self {
+    UPDATE_CONTEXT.with(|context| Gameplay::new_blocking(context, Game::Mof, StageNumber::Stage1))
+  }
+}
+
+impl From<TitleScreenMof> for Gameplay {
+  fn from(_: TitleScreenMof) -> Self {
+    UPDATE_CONTEXT.with(|context| Gameplay::new_blocking(context, Game::Mof, StageNumber::Stage1))
+  }
+}
diff --git a/src/game/states/loading.rs b/src/game/states/loading.rs
index 3e99e34..b72628a 100644
--- a/src/game/states/loading.rs
+++ b/src/game/states/loading.rs
@@ -11,11 +11,12 @@ use crate::{
   utils::{game::Game, soon::Soon},
 };
 
-use super::{title::TitleScreen, UPDATE_CONTEXT};
+use super::{gameplay::Gameplay, title_mof::TitleScreenMof, title_sa::TitleScreenSa, UPDATE_CONTEXT};
 
 pub struct Loading {
   _sig: Vm,
   _ascii_loading: Vm,
+  to_gameplay: bool,
 
   title_anm: RefCell<Soon<Arc<LoadedFile>>>,
 }
@@ -31,27 +32,35 @@ impl Loading {
     Loading {
       _sig: sig,
       _ascii_loading: ascii_loading,
+      to_gameplay: true,
 
-      title_anm: context.start_load_anm(Game::Sa, "title.anm").into(),
+      title_anm: context.start_load_anm(Game::Mof, "title.anm").into(),
     }
   }
 }
 
-impl State for Loading {
-  fn execute(&mut self) {
-    // if UPDATE_CONTEXT.with(|context| context.sound_manager.get_bgm_manager().is_some()) {
-    //   log::info!("a");
-    // }
-    // if self.title_anm.borrow_mut().is_done() {
-    //   log::info!("b")
-    // }
-  }
-}
+sfsm::derive_state!(Loading);
 
-impl Transition<TitleScreen> for Loading {
+impl Transition<TitleScreenSa> for Loading {
   fn guard(&self) -> TransitGuard {
     UPDATE_CONTEXT
       .with(|context| context.sound_manager.is_bgm_manager_loaded() && self.title_anm.borrow_mut().is_done())
       .into()
   }
 }
+
+impl Transition<TitleScreenMof> for Loading {
+  fn guard(&self) -> TransitGuard {
+    UPDATE_CONTEXT
+      .with(|context|!self.to_gameplay && context.sound_manager.is_bgm_manager_loaded() && self.title_anm.borrow_mut().is_done())
+      .into()
+  }
+}
+
+impl Transition<Gameplay> for Loading {
+  fn guard(&self) -> TransitGuard {
+    UPDATE_CONTEXT
+      .with(|context| self.to_gameplay && context.sound_manager.is_bgm_manager_loaded() && self.title_anm.borrow_mut().is_done())
+      .into()
+  }
+}
diff --git a/src/game/states/mod.rs b/src/game/states/mod.rs
index 143f30d..1499f97 100644
--- a/src/game/states/mod.rs
+++ b/src/game/states/mod.rs
@@ -1,21 +1,24 @@
 mod gameplay;
 mod loading;
-mod title;
-
-use gameplay::Gameplay;
-use sfsm::*;
+mod title_mof;
+mod title_sa;
 
 use crate::utils::context::ContextMut;
+use gameplay::Gameplay;
 use loading::Loading;
-use title::TitleScreen;
+use sfsm::*;
+use title_mof::TitleScreenMof;
+use title_sa::TitleScreenSa;
 
 use super::GameContext;
 
 pub(super) static UPDATE_CONTEXT: ContextMut<GameContext, GameStateMachine> = ContextMut::new();
 
-add_state_machine!(Machine, Loading, {Loading, TitleScreen, Gameplay}, {
-  Loading => TitleScreen,
-  TitleScreen => TitleScreen
+// add_state_machine!(Machine, Gameplay, {Loading, TitleScreen, Gameplay}, {
+add_state_machine!(Machine, Loading, {Loading, TitleScreenSa, TitleScreenMof, Gameplay}, {
+  Loading => TitleScreenMof,
+  Loading => Gameplay,
+  TitleScreenMof => Gameplay,
 });
 
 pub(super) struct GameStateMachine(Machine);
diff --git a/src/game/states/title_mof/main_menu.rs b/src/game/states/title_mof/main_menu.rs
new file mode 100644
index 0000000..5605d1c
--- /dev/null
+++ b/src/game/states/title_mof/main_menu.rs
@@ -0,0 +1,218 @@
+use sfsm::*;
+use winit::keyboard::KeyCode;
+
+use crate::{
+  game::{
+    anm::{Vm, VmLocation},
+    states::UPDATE_CONTEXT,
+  },
+  utils::game::Game,
+};
+
+pub struct MainMenu(MainMenuMachine);
+impl MainMenu {
+  pub fn new() -> MainMenu {
+    UPDATE_CONTEXT.with(|context| {
+      let title = context.load_anm(Game::Mof, "title.anm");
+      let title_ver = context.load_anm(Game::Mof, "title_v.anm");
+
+      let mut main_menu = MainMenu(MainMenuMachine::new());
+      main_menu
+        .0
+        .start(Boot {
+          logo: context.anm_manager.new_vm(title, None, 90, VmLocation::new_ui()),
+          title_ver: context.anm_manager.new_vm(title_ver, None, 0, VmLocation::new_ui()),
+        })
+        .unwrap();
+
+      main_menu
+    })
+  }
+
+  pub fn switch_to_gameplay(&self) -> bool {
+    sfsm::IsState::<StartingGameplay>::is_state(&self.0)
+  }
+}
+impl State for MainMenu {
+  fn execute(&mut self) {
+    self.0.step().unwrap();
+  }
+}
+
+add_state_machine!(MainMenuMachine, Boot, {Boot, Starting, Selecting, StartingGameplay, Exiting}, {
+  Boot => Starting,
+  Starting => Selecting,
+  Selecting => StartingGameplay,
+  Selecting => Exiting,
+});
+struct Boot {
+  logo: Vm,
+  title_ver: Vm,
+}
+impl State for Boot {
+  fn execute(&mut self) {
+    self.exit();
+    UPDATE_CONTEXT.with(|context| {
+      if context.engine.keys.is_any_key_down() {
+        self.logo.borrow_mut().interrupt(6);
+        self.title_ver.borrow_mut().interrupt(1);
+      }
+    });
+  }
+}
+
+impl Transition<Starting> for Boot {
+  fn guard(&self) -> TransitGuard {
+    self.title_ver.borrow_mut().deleted().into()
+  }
+}
+impl From<Boot> for Starting {
+  fn from(value: Boot) -> Self {
+    Starting {
+      logo: value.logo,
+      time_in_state: 0,
+    }
+  }
+}
+
+struct Starting {
+  logo: Vm,
+  time_in_state: u32,
+}
+impl State for Starting {
+  fn execute(&mut self) {
+    self.time_in_state += 1;
+  }
+}
+
+impl Transition<Selecting> for Starting {
+  fn guard(&self) -> TransitGuard {
+    (self.time_in_state >= 30).into()
+  }
+}
+impl From<Starting> for Selecting {
+  fn from(val: Starting) -> Self {
+    UPDATE_CONTEXT.with(|context| {
+      let file = val.logo.borrow().file().clone();
+      Selecting {
+        logo: val.logo,
+        main_ui: context.anm_manager.new_vm(file, None, 0, VmLocation::new_child_ui(0)),
+        selected: 0,
+        selection_changed: false,
+        input_locked: false,
+      }
+    })
+  }
+}
+
+struct Selecting {
+  logo: Vm,
+  main_ui: Vm,
+  selected: u32,
+  selection_changed: bool,
+  input_locked: bool,
+}
+impl Selecting {
+  fn selected(&self, desired: u32) -> TransitGuard {
+    (self.selected == desired && self.input_locked).into()
+  }
+}
+impl State for Selecting {
+  fn execute(&mut self) {
+    UPDATE_CONTEXT.with(|context| {
+      if self.input_locked {
+        return;
+      }
+      if context.engine.keys.was_key_pressed(KeyCode::ArrowUp) {
+        if let Some(prev) = self.selected.checked_sub(1) {
+          self.selected = prev;
+        } else {
+          self.selected = 7;
+        }
+        if self.selected == 1 {
+          self.selected = 0;
+        }
+
+        self.selection_changed = false;
+      }
+
+      if context.engine.keys.was_key_pressed(KeyCode::ArrowDown) {
+        if self.selected >= 7 {
+          self.selected = 0;
+        } else {
+          self.selected += 1;
+        }
+        if self.selected == 1 {
+          self.selected = 2;
+        }
+
+        self.selection_changed = false;
+      }
+
+      if context.engine.keys.was_key_pressed(KeyCode::KeyZ) {
+        self.main_ui.borrow_mut().interrupt(3);
+        self.input_locked = true;
+      }
+
+      if !self.selection_changed {
+        self.selection_changed = true;
+        let main_ui = self.main_ui.borrow_mut();
+        for i in 0..8u32 {
+          let child = main_ui.child(i as usize).unwrap();
+          let child_bg = main_ui.child(8 + i as usize).unwrap();
+          if i == self.selected {
+            child.borrow_mut().interrupt(i + 7);
+            child_bg.borrow_mut().interrupt(i + 7);
+          } else {
+            context.anm_manager.interrupt_immediately(&child, 3);
+            context.anm_manager.interrupt_immediately(&child_bg, 3);
+          }
+        }
+      }
+    });
+  }
+}
+
+#[derive(Default)]
+struct StartingGameplay;
+derive_state!(StartingGameplay);
+derive_transition_into_default!(Selecting, StartingGameplay);
+impl Transition<StartingGameplay> for Selecting {
+  fn guard(&self) -> TransitGuard {
+    self.selected(0)
+  }
+
+  fn action(&mut self) {
+    println!("agmlng")
+  }
+}
+
+impl Transition<Exiting> for Selecting {
+  fn guard(&self) -> TransitGuard {
+    self.selected(7)
+  }
+}
+impl From<Selecting> for Exiting {
+  fn from(value: Selecting) -> Self {
+    Exiting {
+      _logo: value.logo,
+      _main_ui: value.main_ui,
+      timer: 0,
+    }
+  }
+}
+
+struct Exiting {
+  _logo: Vm,
+  _main_ui: Vm,
+  timer: u32,
+}
+
+impl State for Exiting {
+  fn execute(&mut self) {
+    self.timer += 1;
+    if self.timer >= 20 {
+      UPDATE_CONTEXT.with(|context| context.engine.should_exit.set(true));
+    }
+  }
+}
diff --git a/src/game/states/title_mof/mod.rs b/src/game/states/title_mof/mod.rs
new file mode 100644
index 0000000..95b831d
--- /dev/null
+++ b/src/game/states/title_mof/mod.rs
@@ -0,0 +1,72 @@
+mod main_menu;
+
+use std::sync::Arc;
+
+use main_menu::MainMenu;
+use sfsm::*;
+use winit::keyboard::KeyCode;
+
+use crate::{
+  game::{
+    anm::{LoadedFile, Vm, VmLocation},
+    GameContext,
+  },
+  utils::game::Game,
+};
+
+use super::{gameplay::Gameplay, loading::Loading, UPDATE_CONTEXT};
+
+pub struct TitleScreenMof {
+  _splash: Vm,
+  _title_anm: Arc<LoadedFile>,
+  machine: TitleScreenMachine,
+}
+
+add_state_machine!(TitleScreenMachine, MainMenu, { MainMenu }, {});
+
+impl TitleScreenMof {
+  fn new(context: &mut GameContext, _: bool) -> Self {
+    let title = context.load_anm(Game::Mof, "title.anm");
+
+    // 79, 83
+    let splash = context.anm_manager.new_vm(title.clone(), None, 88, VmLocation::new_ui());
+
+    Self {
+      _splash: splash,
+      _title_anm: title,
+      machine: TitleScreenMachine::new(),
+    }
+  }
+}
+
+impl State for TitleScreenMof {
+  fn entry(&mut self) {
+    self.machine.start(MainMenu::new()).unwrap();
+  }
+  fn execute(&mut self) {
+    self.machine.step().unwrap();
+  }
+}
+
+impl From<Loading> for TitleScreenMof {
+  fn from(_: Loading) -> Self {
+    UPDATE_CONTEXT.with(|context| {
+      context.sound_manager.get_bgm_manager(Game::Mof).play_track("th10_02.wav");
+      // context.sound_manager.get_bgm_manager(Game::Mof).play_track("th11_00.wav");
+      TitleScreenMof::new(context, true)
+    })
+  }
+}
+
+impl Transition<Gameplay> for TitleScreenMof {
+  fn guard(&self) -> TransitGuard {
+    match &self.machine.states {
+      TitleScreenMachineStates::MainMenuState(Some(main_menu)) => main_menu.switch_to_gameplay().into(),
+      _ => TransitGuard::Remain
+    }
+  }
+
+  fn action(&mut self) {
+    println!("hsttkasdgn")
+  }
+}
diff --git a/src/game/states/title.rs b/src/game/states/title_sa.rs
similarity index 57%
rename from src/game/states/title.rs
rename to src/game/states/title_sa.rs
index f2b8d17..d13749d 100644
--- a/src/game/states/title.rs
+++ b/src/game/states/title_sa.rs
@@ -11,20 +11,18 @@ use crate::{
 
 use super::{loading::Loading, UPDATE_CONTEXT};
 
-pub struct TitleScreen {
+pub struct TitleScreenSa {
   _logo: Vm,
   _splash: Vm,
 }
 
-impl TitleScreen {
+impl TitleScreenSa {
   fn new(context: &mut GameContext, _: bool) -> Self {
-    let title = context.load_anm(Game::Mof, "title.anm");
+    let title = context.load_anm(Game::Sa, "title.anm");
 
     // 79, 83
-    // let splash = context.anm_manager.new_vm(title.clone(), Some(8), 79, VmLocation::new_ui());
-    // let logo = context.anm_manager.new_vm(title.clone(), Some(8), 83, VmLocation::new_ui());
-    let splash = context.anm_manager.new_vm(title.clone(), Some(8), 88, VmLocation::new_ui());
-    let logo = context.anm_manager.new_vm(title.clone(), Some(8), 90, VmLocation::new_ui());
+    let splash = context.anm_manager.new_vm(title.clone(), Some(8), 79, VmLocation::new_ui());
+    let logo = context.anm_manager.new_vm(title.clone(), Some(8), 83, VmLocation::new_ui());
 
     Self {
       _logo: logo,
@@ -33,7 +31,7 @@ impl TitleScreen {
   }
 }
 
-impl State for TitleScreen {
+impl State for TitleScreenSa {
   fn execute(&mut self) {
     UPDATE_CONTEXT.with(|context| {
       if context.engine.keys.was_key_pressed(KeyCode::Space) {
@@ -43,19 +41,19 @@ impl State for TitleScreen {
   }
 }
 
-impl From<Loading> for TitleScreen {
+impl From<Loading> for TitleScreenSa {
   fn from(_: Loading) -> Self {
     UPDATE_CONTEXT.with(|context| {
-      context.sound_manager.get_bgm_manager(Game::Mof).play_track("th10_02.wav");
-      // context.sound_manager.get_bgm_manager_sa().play_track("th11_00.wav");
-      TitleScreen::new(context, true)
+      // context.sound_manager.get_bgm_manager(Game::Mof).play_track("th10_02.wav");
+      context.sound_manager.get_bgm_manager(Game::Sa).play_track("th11_00.wav");
+      TitleScreenSa::new(context, true)
     })
   }
 }
 
-impl Transition<TitleScreen> for TitleScreen {
+impl Transition<TitleScreenSa> for TitleScreenSa {
   fn action(&mut self) {
-    *self = UPDATE_CONTEXT.with(|context| TitleScreen::new(context, false))
+    *self = UPDATE_CONTEXT.with(|context| TitleScreenSa::new(context, false))
   }
 
   fn guard(&self) -> TransitGuard {
diff --git a/src/main.rs b/src/main.rs
index 9abe486..a400b64 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -4,13 +4,19 @@ pub mod interp;
 pub mod utils;
 
 use std::{
-  sync::{Arc, Mutex}, time::Duration
+  ops::ControlFlow,
+  sync::Arc,
 };
 
 use engine::Engine;
 use wgpu::SurfaceError;
 use winit::{
-  application::ApplicationHandler, dpi::PhysicalSize, event::{ElementState, KeyEvent, StartCause, WindowEvent}, event_loop::EventLoop, keyboard::{KeyCode, PhysicalKey}, platform::pump_events::EventLoopExtPumpEvents, window::Window
+  application::ApplicationHandler,
+  dpi::PhysicalSize,
+  event::{ElementState, KeyEvent, StartCause, WindowEvent},
+  event_loop::EventLoop,
+  keyboard::{KeyCode, PhysicalKey},
+  window::Window,
 };
 
 #[derive(Default)]
@@ -73,8 +79,10 @@ impl<'a> ApplicationHandler for App<'a> {
         self.engine.as_mut().unwrap().resize(new_size);
       }
       WindowEvent::RedrawRequested => {
-        let mut engine = self.engine.as_mut().unwrap();
-        engine.update();
+        let engine = self.engine.as_mut().unwrap();
+        if let ControlFlow::Break(()) = engine.update() {
+          return event_loop.exit();
+        }
         match engine.render() {
           Ok(_) => {}
           Err(SurfaceError::Lost) => {
diff --git a/src/utils/context.rs b/src/utils/context.rs
index 8e55e11..289419f 100644
--- a/src/utils/context.rs
+++ b/src/utils/context.rs
@@ -1,4 +1,4 @@
-use std::marker::PhantomData;
+use std::{cell::RefCell, ffi::c_void, marker::PhantomData, thread::LocalKey};
 
 #[doc(hidden)]
 #[derive(Default)]
@@ -12,9 +12,9 @@ impl<T, Key> ContextMut<T, Key> {
     ContextMut(PhantomData)
   }
 
-  fn thread_local() -> &'static std::thread::LocalKey<std::cell::RefCell<Option<*mut std::ffi::c_void>>> {
+  fn thread_local() -> &'static LocalKey<RefCell<Option<*mut c_void>>> {
     std::thread_local! {
-      static CTX: std::cell::RefCell<Option<*mut std::ffi::c_void>> = Default::default();
+      static CTX: RefCell<Option<*mut c_void>> = Default::default();
     }
 
     &CTX
@@ -54,9 +54,9 @@ impl<T, Key> Context<T, Key> {
     Context(PhantomData)
   }
 
-  fn thread_local() -> &'static std::thread::LocalKey<std::cell::RefCell<Option<*const std::ffi::c_void>>> {
+  fn thread_local() -> &'static LocalKey<RefCell<Option<*const c_void>>> {
     std::thread_local! {
-      static CTX: std::cell::RefCell<Option<*const std::ffi::c_void>> = Default::default();
+      static CTX: RefCell<Option<*const c_void>> = Default::default();
     }
 
     &CTX
diff --git a/utils/th10.eclm b/utils/th10.eclm
index 2a86cf6..6adabcb 100644
--- a/utils/th10.eclm
+++ b/utils/th10.eclm
@@ -649,7 +649,7 @@
 !ins_intrinsics
 #10 RetStack() ## Internally calls FrameLeave()
 #11 CallStack()
-12 Jmp()
+#12 Jmp()
 #13 StackJmp(op="==")
 #14 StackJmp(op="!=")
 #15 CallStackAsync()