Prechádzať zdrojové kódy

Field installer fixes: scale, scripts, backgrounds and animations

Field scripts and data:
- Walkmesh, spawn heights and LINE triggers use the field scale, like the
  rest of the script positions.
- on_update runs once when the script returns, as in the original game.
- Temporary variable bank 5 is reset at the start of every field.
- Messages in windows the field never defines get a default window.
- Models with no Idle animation play their first one by default.
- BATTLE passes its real battle ID.

Scripting and engine:
- Implemented SCRLA, SCRLW, SCRLC, SCRCC, FMOVE, ANIME2, ANIM!1, CANIM1,
  CANM!1 and ANIMW.
- Scripts that wait for a script that is already queued are resumed when
  it ends, instead of waiting forever.
- Background2D can scroll to an entity, and scroll_sync no longer blocks
  when nothing is scrolling.
- Background tiles in scripted areas are shown and hidden by BGON, BGOFF
  and BGCLR (the installer writes area and state; states are bit masks).
- World map Cloud is sized from his unanimated bounds.

Tests cover the new decompiler output and the field script header.
Iñigo Valentin 5 dní pred
rodič
commit
b1af0c86f5
45 zmenil súbory, kde vykonal 1417 pridanie a 199 odobranie
  1. 2 2
      data/data/config/config.cfg
  2. 184 72
      src/core/Background2D.cpp
  3. 139 3
      src/core/Background2D.h
  4. 2 4
      src/core/Enemy.cpp
  5. 3 3
      src/core/Enemy.h
  6. 12 5
      src/core/Entity.cpp
  7. 12 3
      src/core/Entity.h
  8. 37 15
      src/core/EntityManager.cpp
  9. 12 1
      src/core/EntityManager.h
  10. 2 4
      src/core/EntityPoint.cpp
  11. 5 2
      src/core/EntityPoint.h
  12. 30 12
      src/core/ScriptManager.cpp
  13. 10 4
      src/core/ScriptManager.h
  14. 4 1
      src/core/ScriptManagerBinds.h
  15. 5 1
      src/core/XmlBackground2DFile.cpp
  16. 14 2
      src/data/VGearsAFile.cpp
  17. 10 1
      src/data/VGearsAFile.h
  18. 2 3
      src/data/VGearsFLevelFileSerializer.cpp
  19. 55 12
      src/installer/FieldDataInstaller.cpp
  20. 43 0
      src/installer/FieldDataInstaller.h
  21. 2 0
      src/installer/decompiler/ScriptFormatter.cpp
  22. 12 0
      src/installer/decompiler/ScriptFormatter.h
  23. 35 1
      src/installer/decompiler/field/FieldCodeGenerator.cpp
  24. 43 0
      src/installer/decompiler/field/FieldCodeGenerator.h
  25. 2 0
      src/installer/decompiler/field/FieldDecompiler.cpp
  26. 15 0
      src/installer/decompiler/field/FieldDecompiler.h
  27. 9 0
      src/installer/decompiler/field/FieldEngine.cpp
  28. 32 0
      src/installer/decompiler/field/FieldEngine.h
  29. 12 9
      src/installer/decompiler/field/instruction/FieldBackgroundInstruction.cpp
  30. 3 3
      src/installer/decompiler/field/instruction/FieldBackgroundInstruction.h
  31. 42 6
      src/installer/decompiler/field/instruction/FieldCameraInstruction.cpp
  32. 33 0
      src/installer/decompiler/field/instruction/FieldCameraInstruction.h
  33. 4 0
      src/installer/decompiler/field/instruction/FieldControlFlowInstruction.cpp
  34. 119 8
      src/installer/decompiler/field/instruction/FieldModelInstruction.cpp
  35. 78 0
      src/installer/decompiler/field/instruction/FieldModelInstruction.h
  36. 4 1
      src/installer/decompiler/field/instruction/FieldModuleInstruction.cpp
  37. 10 0
      src/installer/decompiler/field/instruction/FieldWindowInstruction.cpp
  38. 2 0
      src/map/VGearsBackground2DFileXMLSerializer.cpp
  39. 10 0
      src/map/VGearsTile.h
  40. 5 1
      src/modules/worldmap/WorldmapModule.cpp
  41. 13 5
      test/core/EntityManager.cpp
  42. 14 5
      test/core/EntityPoint.cpp
  43. 32 5
      test/data/VGearsAFile.cpp
  44. 11 0
      test/installer/FieldDataInstaller.cpp
  45. 292 5
      test/installer/decompiler/field/FieldDecompiler.cpp

+ 2 - 2
data/data/config/config.cfg

@@ -11,8 +11,8 @@ set debug_ui 0
 resolution 1024 768 0
 
 bind F11 "toggle timer_scale_game 0.0 1.0"
-bind NumAdd "increment timer_scale_game 0.0 1.0 0.1"
-bind NumSub "increment timer_scale_game 0.0 1.0 -0.1"
+bind NumAdd "increment timer_scale_game 0.0 4.0 0.5"
+bind NumSub "increment timer_scale_game 0.0 4.0 -0.5"
 
 bind LAlt+F4 "quit"
 bind RAlt+F4 "quit"

+ 184 - 72
src/core/Background2D.cpp

@@ -41,6 +41,8 @@ Background2D::Background2D():
   scroll_type_(Background2D::NONE),
   scroll_seconds_(0),
   scroll_current_seconds_(0),
+  scroll_target_entity_(nullptr),
+  scroll_follow_target_(false),
   position_(Ogre::Vector2::ZERO),
   position_real_(Ogre::Vector2::ZERO),
   offset_(Ogre::Vector2::ZERO),
@@ -187,65 +189,113 @@ void Background2D::calculateScreenScale(){
 
 void Background2D::OnResize(){
     calculateScreenScale();
-    for (unsigned int i = 0; i < tiles_.size(); ++ i){
-        Tile &tile(tiles_[i]);
-        Ogre::Vector2 top_left(static_cast<Ogre::Real>(tile.x), static_cast<Ogre::Real>(- tile.y));
-        Ogre::Vector2 top_right(
-          static_cast<Ogre::Real>(tile.x + tile.width), static_cast<Ogre::Real>(top_left.y)
-        );
-        Ogre::Vector2 bottom_right(
-          static_cast<Ogre::Real>(top_right.x), static_cast<Ogre::Real>(-(tile.y + tile.height))
-        );
-        Ogre::Vector2 bottom_left(
-          static_cast<Ogre::Real>(top_left.x), static_cast<Ogre::Real>(bottom_right.y)
-        );
-        virtualScreenToWorldSpace(top_left);
-        virtualScreenToWorldSpace(top_right);
-        virtualScreenToWorldSpace(bottom_right);
-        virtualScreenToWorldSpace(bottom_left);
-        float new_x1 = top_left.x;
-        float new_y1 = top_left.y;
-        float new_x2 = top_right.x;
-        float new_y2 = top_right.y;
-        float new_x3 = bottom_right.x;
-        float new_y3 = bottom_right.y;
-        float new_x4 = bottom_left.x;
-        float new_y4 = bottom_left.y;
-        Ogre::HardwareVertexBufferSharedPtr vertex_buffer;
-        if (tiles_[i].blending == VGears::B_ALPHA) vertex_buffer = alpha_vertex_buffer_;
-        else if(tiles_[i].blending == VGears::B_ADD) vertex_buffer = add_vertex_buffer_;
-        else if(tiles_[i].blending == VGears::B_SUBTRACT) vertex_buffer = subtract_vertex_buffer_;
-        float* write_iterator
-          = static_cast<float*>(vertex_buffer->lock(Ogre::HardwareBuffer::HBL_NORMAL));
-        write_iterator += tiles_[i].start_vertex_index * TILE_VERTEX_INDEX_SIZE;
-        *write_iterator ++ = new_x1;
-        *write_iterator ++ = new_y1;
-        write_iterator += 7;
-        *write_iterator ++ = new_x2;
-        *write_iterator ++ = new_y2;
-        write_iterator += 7;
-        *write_iterator ++ = new_x3;
-        *write_iterator ++ = new_y3;
-        write_iterator += 7;
-        *write_iterator++ = new_x1;
-        *write_iterator++ = new_y1;
-        write_iterator += 7;
-        *write_iterator ++ = new_x3;
-        *write_iterator ++ = new_y3;
-        write_iterator += 7;
-        *write_iterator ++ = new_x4;
-        *write_iterator ++ = new_y4;
-        vertex_buffer->unlock();
-    }
+    for (unsigned int i = 0; i < tiles_.size(); ++ i) WriteTilePosition(tiles_[i]);
     applyScroll();
 }
 
+void Background2D::WriteTilePosition(const Tile& tile){
+    Ogre::Vector2 top_left(static_cast<Ogre::Real>(tile.x), static_cast<Ogre::Real>(- tile.y));
+    Ogre::Vector2 top_right(
+      static_cast<Ogre::Real>(tile.x + tile.width), static_cast<Ogre::Real>(top_left.y)
+    );
+    Ogre::Vector2 bottom_right(
+      static_cast<Ogre::Real>(top_right.x), static_cast<Ogre::Real>(-(tile.y + tile.height))
+    );
+    Ogre::Vector2 bottom_left(
+      static_cast<Ogre::Real>(top_left.x), static_cast<Ogre::Real>(bottom_right.y)
+    );
+    virtualScreenToWorldSpace(top_left);
+    virtualScreenToWorldSpace(top_right);
+    virtualScreenToWorldSpace(bottom_right);
+    virtualScreenToWorldSpace(bottom_left);
+    // A tile that is not drawn has all its corners in the same point.
+    if (!tile.visible){
+        top_right = top_left;
+        bottom_right = top_left;
+        bottom_left = top_left;
+    }
+    Ogre::HardwareVertexBufferSharedPtr vertex_buffer;
+    if (tile.blending == VGears::B_ALPHA) vertex_buffer = alpha_vertex_buffer_;
+    else if (tile.blending == VGears::B_ADD) vertex_buffer = add_vertex_buffer_;
+    else if (tile.blending == VGears::B_SUBTRACT) vertex_buffer = subtract_vertex_buffer_;
+    else return;
+    float* write_iterator
+      = static_cast<float*>(vertex_buffer->lock(Ogre::HardwareBuffer::HBL_NORMAL));
+    write_iterator += tile.start_vertex_index * TILE_VERTEX_INDEX_SIZE;
+    *write_iterator ++ = top_left.x;
+    *write_iterator ++ = top_left.y;
+    write_iterator += 7;
+    *write_iterator ++ = top_right.x;
+    *write_iterator ++ = top_right.y;
+    write_iterator += 7;
+    *write_iterator ++ = bottom_right.x;
+    *write_iterator ++ = bottom_right.y;
+    write_iterator += 7;
+    *write_iterator ++ = top_left.x;
+    *write_iterator ++ = top_left.y;
+    write_iterator += 7;
+    *write_iterator ++ = bottom_right.x;
+    *write_iterator ++ = bottom_right.y;
+    write_iterator += 7;
+    *write_iterator ++ = bottom_left.x;
+    *write_iterator ++ = bottom_left.y;
+    vertex_buffer->unlock();
+}
+
+bool Background2D::IsAreaStateOn(const int area, const int layers) const{
+    const auto it = area_states_on_.find(area);
+    return it != area_states_on_.end() && (it->second & static_cast<unsigned int>(layers)) != 0;
+}
+
+void Background2D::RefreshArea(const int area){
+    for (Tile& tile : tiles_){
+        if (tile.area != area) continue;
+        const bool visible = IsAreaStateOn(tile.area, tile.state);
+        if (visible == tile.visible) continue;
+        tile.visible = visible;
+        WriteTilePosition(tile);
+    }
+}
+
+void Background2D::ScriptAreaOn(const int area, const int layer){
+    LOG_TRIVIAL(
+      "Background2d area " + std::to_string(area) + " layer " + std::to_string(layer) + " on."
+    );
+    if (layer < 0 || layer > 31){
+        LOG_WARNING("Background2d layer " + std::to_string(layer) + " is out of range.");
+        return;
+    }
+    area_states_on_[area] |= 1u << layer;
+    RefreshArea(area);
+}
+
+void Background2D::ScriptAreaOff(const int area, const int layer){
+    LOG_TRIVIAL(
+      "Background2d area " + std::to_string(area) + " layer " + std::to_string(layer) + " off."
+    );
+    if (layer < 0 || layer > 31){
+        LOG_WARNING("Background2d layer " + std::to_string(layer) + " is out of range.");
+        return;
+    }
+    const auto it = area_states_on_.find(area);
+    if (it != area_states_on_.end()) it->second &= ~(1u << layer);
+    RefreshArea(area);
+}
+
+void Background2D::ScriptAreaClear(const int area){
+    LOG_TRIVIAL("Background2d area " + std::to_string(area) + " cleared.");
+    area_states_on_.erase(area);
+    RefreshArea(area);
+}
+
 void Background2D::Clear(){
     scroll_entity_ = nullptr;
     scroll_position_start_ = Ogre::Vector2::ZERO;
     scroll_position_end_ = Ogre::Vector2::ZERO;
     scroll_seconds_ = 0;
     scroll_current_seconds_ = 0;
+    scroll_target_entity_ = nullptr;
+    scroll_follow_target_ = false;
     position_ = Ogre::Vector2::ZERO;
     position_real_ = Ogre::Vector2::ZERO;
     offset_ = Ogre::Vector2::ZERO;
@@ -258,6 +308,7 @@ void Background2D::Clear(){
             ScriptManager::getSingleton().ContinueScriptExecution(animation_played_[i].sync[j]);
     animation_played_.clear();
     tiles_.clear();
+    area_states_on_.clear();
     DestroyVertexBuffers();
     CreateVertexBuffers();
 }
@@ -266,17 +317,61 @@ void Background2D::ScriptAutoScrollToEntity(Entity* entity){scroll_entity_ = ent
 
 Entity* Background2D::GetAutoScrollEntity() const{return scroll_entity_;}
 
-void Background2D::ScriptScrollToPlayer(const SCROLL_TYPE type, const unsigned int seconds){
+void Background2D::ScriptScrollToPlayer(const SCROLL_TYPE type, const float seconds){
     Entity* player = EntityManager::getSingleton().ScriptGetPlayerEntity();
     if (player == nullptr) return;
-    Ogre::Vector2 position(player->GetPosition().x, player->GetPosition().y);
+    scroll_entity_ = nullptr;
+    StartScroll(GetEntityScroll(player), type, seconds, player, type == Background2D::NONE);
+}
+
+void Background2D::ScriptScrollToEntity(
+  Entity* entity, const SCROLL_TYPE type, const float seconds
+){
+    if (entity == nullptr){
+        LOG_WARNING("Background2d can't scroll to an entity that doesn't exist.");
+        return;
+    }
+    scroll_entity_ = nullptr;
+    StartScroll(GetEntityScroll(entity), type, seconds, entity, true);
+}
+
+void Background2D::UpdateScrollTarget(){
+    if (scroll_target_entity_ != nullptr)
+        scroll_position_end_ = ClampScroll(GetEntityScroll(scroll_target_entity_));
+}
+
+const Ogre::Vector2 Background2D::GetEntityScroll(const Entity* entity) const{
+    CameraManager &camera_manager(CameraManager::getSingleton());
+    const Ogre::Vector3 view = camera_manager.ProjectPointToScreen(entity->GetPosition());
+    const Ogre::Vector2 scroll = GetScreenScroll();
+    const Ogre::Viewport* viewport(camera_manager.getViewport());
+    const float width = static_cast<float>(viewport->getActualWidth());
+    const float height = static_cast<float>(viewport->getActualHeight());
+    return Ogre::Vector2(
+      scroll.x + width / 2 - view.x, scroll.y + height / 2 - view.y
+    ) / screen_scale_;
+}
+
+void Background2D::StartScroll(
+  const Ogre::Vector2& position, const SCROLL_TYPE type, const float seconds,
+  Entity* target_entity, const bool follow_target
+){
+    // Finish the previous scroll first, without following its target.
+    scroll_target_entity_ = nullptr;
+    scroll_follow_target_ = false;
+    UnsetScroll();
+    scroll_target_entity_ = target_entity;
+    scroll_follow_target_ = follow_target;
+    if (type == Background2D::NONE || seconds <= 0){
+        SetScroll(position);
+        UnsetScroll();
+        return;
+    }
     scroll_position_start_ = position_;
-    scroll_position_end_ = position;
+    scroll_position_end_ = ClampScroll(position);
     scroll_type_ = type;
     scroll_seconds_ = seconds;
     scroll_current_seconds_ = 0;
-
-    return;
 }
 
 void Background2D::ScriptScrollToPosition(
@@ -286,20 +381,13 @@ void Background2D::ScriptScrollToPosition(
       "Background2d set scroll to position '"
       + Ogre::StringConverter::toString(Ogre::Vector2(x, y)) + "'."
     );
-    Ogre::Vector2 position = Ogre::Vector2(x, y);
     scroll_entity_ = nullptr;
-    if (type == Background2D::NONE){
-        SetScroll(position);
-        return;
-    }
-    scroll_position_start_ = position_;
-    scroll_position_end_ = position;
-    scroll_type_ = type;
-    scroll_seconds_ = seconds;
-    scroll_current_seconds_ = 0;
+    StartScroll(Ogre::Vector2(x, y), type, seconds, nullptr, false);
 }
 
 int Background2D::ScriptScrollSync(){
+    // Nothing would ever wake the script up if there is no scroll to wait for.
+    if (scroll_type_ == Background2D::NONE) return 1;
     ScriptId script = ScriptManager::getSingleton().GetCurrentScriptId();
     LOG_TRIVIAL(
       "Wait Background2d scroll for function '"
@@ -322,6 +410,9 @@ void Background2D::ScriptOffset(const float x, const float y){
 
 void Background2D::UnsetScroll(){
     scroll_type_ = Background2D::NONE;
+    if (scroll_follow_target_) scroll_entity_ = scroll_target_entity_;
+    scroll_target_entity_ = nullptr;
+    scroll_follow_target_ = false;
     for(unsigned int i = 0; i < scroll_sync_.size(); ++ i)
         ScriptManager::getSingleton().ContinueScriptExecution(scroll_sync_[i]);
     scroll_sync_.clear();
@@ -344,17 +435,22 @@ void Background2D::SetScreenScroll(const Ogre::Vector2& position){
 }
 
 void Background2D::SetScroll(const Ogre::Vector2& position){
-    position_ = position;
-    Ogre::Vector3 pos_3d(position_.x, position_.y, 0);
+    position_ = ClampScroll(position);
+    applyScroll();
+}
+
+const Ogre::Vector2 Background2D::ClampScroll(const Ogre::Vector2& position) const{
+    Ogre::Vector2 clamped = position;
+    Ogre::Vector3 pos_3d(position.x, position.y, 0);
     if (!range_.contains(pos_3d)){
         Ogre::Vector3 max_3d(range_.getMaximum());
         Ogre::Vector3 min_3d(range_.getMinimum());
         Ogre::Vector2 max(max_3d.x, max_3d.y);
         Ogre::Vector2 min(min_3d.x, min_3d.y);
-        position_.makeCeil(min);
-        position_.makeFloor(max);
+        clamped.makeCeil(min);
+        clamped.makeFloor(max);
     }
-    applyScroll();
+    return clamped;
 }
 
 void Background2D::applyScroll(){
@@ -415,7 +511,11 @@ void Background2D::AddTile(const VGears::Tile& tile){
         }
         else depth = 0.9999f;
     }
-    AddTile(tile.destination, tile.width, tile.height, depth, tile.uv, tile.blending);
+    AddTile(
+      static_cast<int>(tile.destination.x), static_cast<int>(tile.destination.y),
+      tile.width, tile.height, depth, tile.uv.x, tile.uv.y, tile.uv.z, tile.uv.w, tile.blending,
+      tile.area, tile.state
+    );
 }
 
 void Background2D::AddTile(
@@ -437,7 +537,8 @@ void Background2D::virtualScreenToWorldSpace(Ogre::Vector2& pos) const{
 
 void Background2D::AddTile(
   const int x, const int y, const int width, const int height, const float depth, const float u1,
-  const float v1, const float u2, const float v2, const Blending blending
+  const float v1, const float u2, const float v2, const Blending blending, const int area,
+  const int state
 ){
     Ogre::RenderOperation render_op;
     Ogre::HardwareVertexBufferSharedPtr vertex_buffer;
@@ -475,6 +576,9 @@ void Background2D::AddTile(
     tile.height = height;
     tile.start_vertex_index = render_op.vertexData->vertexCount;
     tile.blending = blending;
+    tile.area = area;
+    tile.state = state;
+    tile.visible = (area == 0) || IsAreaStateOn(area, state);
     //size_t index(tiles_.size());
     tiles_.push_back(tile);
     Ogre::Vector2 top_left(static_cast<Ogre::Real>(x), static_cast<Ogre::Real>(-y));
@@ -558,6 +662,8 @@ void Background2D::AddTile(
     *write_iterator ++ = v2;
     render_op.vertexData->vertexCount += TILE_VERTEX_COUNT;
     vertex_buffer->unlock();
+    // Tiles of an area that is off are written at their position, and then collapsed.
+    if (!tiles_.back().visible) WriteTilePosition(tiles_.back());
 }
 
 void Background2D::UpdateTileUV(
@@ -766,11 +872,17 @@ void Background2D::load(const VGears::Background2DFile::TileList& tiles){
     VGears::Background2DFile::TileList::const_iterator it(tiles.begin());
     VGears::Background2DFile::TileList::const_iterator it_end(tiles.end());
     size_t tile_index(0);
+    size_t area_tiles(0);
     while (it != it_end) {
         AddTile(*it);
         load(tile_index ++, it->animations);
+        if (it->area != 0) ++ area_tiles;
         ++ it;
     }
+    LOG_TRIVIAL(
+      "Background2D: " + std::to_string(tile_index) + " tiles loaded, " + std::to_string(area_tiles)
+      + " of them in scripted areas."
+    );
 }
 
 void Background2D::load(const size_t tile_index, const VGears::AnimationMap& animations){

+ 139 - 3
src/core/Background2D.h

@@ -15,6 +15,8 @@
 
 #pragma once
 
+#include <map>
+#include <set>
 #include <OgreHardwareVertexBuffer.h>
 #include <OgreRenderQueueListener.h>
 #include <OgreRoot.h>
@@ -130,10 +132,35 @@ class Background2D : public Ogre::RenderQueueListener{
         /**
          * Scrolls the background to the position of the playable character.
          *
+         * Background2D::NONE centers the view instantly and resumes following the character. Any
+         * other type moves the view over time, and leaves it in place once it arrives.
+         *
          * @param[in] type Scroll type.
          * @param[in] seconds Scroll duration.
          */
-        void ScriptScrollToPlayer(const SCROLL_TYPE type, const unsigned int seconds);
+        void ScriptScrollToPlayer(const SCROLL_TYPE type, const float seconds);
+
+        /**
+         * Scrolls the background to an entity, and keeps following it once it arrives.
+         *
+         * @param[in] entity Entity to scroll to.
+         * @param[in] type Scroll type.
+         * @param[in] seconds Scroll duration.
+         */
+        void ScriptScrollToEntity(Entity* entity, const SCROLL_TYPE type, const float seconds);
+
+        /**
+         * Updates the destination of the current scroll, if it is moving towards an entity.
+         */
+        void UpdateScrollTarget();
+
+        /**
+         * Retrieves the scroll position that centers an entity in the view.
+         *
+         * @param[in] entity Entity to center.
+         * @return Scroll position in game internal screen coordinates.
+         */
+        const Ogre::Vector2 GetEntityScroll(const Entity* entity) const;
 
         /**
          * Scrolls the background to a position.
@@ -148,7 +175,7 @@ class Background2D : public Ogre::RenderQueueListener{
         );
 
         /**
-         * Waits for the scroll to complete.
+         * Waits for the scroll to complete. If no scroll is active, the script just continues.
          *
          * @return -1.
          */
@@ -162,6 +189,33 @@ class Background2D : public Ogre::RenderQueueListener{
          */
         void ScriptOffset(const float x, const float y);
 
+        /**
+         * Shows the tiles of a background area that are in a layer.
+         *
+         * Tiles can belong to a scripted area, and to the layers of it given by a bit mask: layer
+         * N is the bit 1 << N. They are only drawn while one of their layers is on. Tiles that
+         * don't belong to an area are always drawn. Areas start with all their layers off.
+         *
+         * @param[in] area Area ID.
+         * @param[in] layer Layer of the area to turn on, from 0 to 31.
+         */
+        void ScriptAreaOn(const int area, const int layer);
+
+        /**
+         * Hides the tiles of a background area that are in a layer.
+         *
+         * @param[in] area Area ID.
+         * @param[in] layer Layer of the area to turn off, from 0 to 31.
+         */
+        void ScriptAreaOff(const int area, const int layer);
+
+        /**
+         * Hides all the tiles of a background area, whatever their layer.
+         *
+         * @param[in] area Area ID.
+         */
+        void ScriptAreaClear(const int area);
+
         /**
          * Stops the current scrolling.
          */
@@ -282,12 +336,15 @@ class Background2D : public Ogre::RenderQueueListener{
          * @param[in] u2
          * @param[in] v2
          * @param[in] blending Blending mode for the tile
+         * @param[in] area Scripted area the tile belongs to, or 0 if it always has to be drawn.
+         * @param[in] state Bit mask with the layers of the area in which the tile is drawn.
          * @todo Depth is z-index??
          * @todo What are v1, v2, u1 and u2?
          */
         void AddTile(
           const int x, const int y, const int width, const int height, const float depth,
-          const float u1, const float v1, const float u2, const float v2, const Blending blending
+          const float u1, const float v1, const float u2, const float v2, const Blending blending,
+          const int area = 0, const int state = 0
         );
 
         /**
@@ -417,10 +474,50 @@ class Background2D : public Ogre::RenderQueueListener{
              * Tile blending mode.
              */
             Blending blending;
+
+            /**
+             * Scripted area the tile belongs to, or 0 if it is always drawn.
+             */
+            int area;
+
+            /**
+             * Bit mask with the layers of the area in which the tile is drawn.
+             */
+            int state;
+
+            /**
+             * Whether the tile is drawn.
+             */
+            bool visible;
         };
 
         typedef std::vector<Tile> TileList;
 
+        /**
+         * Checks if any of the layers in a bit mask is on for an area.
+         *
+         * @param[in] area Area ID.
+         * @param[in] layers Bit mask with layers of the area.
+         * @return True if one of the layers is on.
+         */
+        bool IsAreaStateOn(const int area, const int layers) const;
+
+        /**
+         * Draws or hides the tiles of an area according to its states.
+         *
+         * @param[in] area Area ID.
+         */
+        void RefreshArea(const int area);
+
+        /**
+         * Writes the position of a tile in its vertex buffer.
+         *
+         * A tile that is not visible is written collapsed, so that nothing is rasterised.
+         *
+         * @param[in] tile Tile to write.
+         */
+        void WriteTilePosition(const Tile& tile);
+
         /**
          * Loads a background.
          *
@@ -450,6 +547,30 @@ class Background2D : public Ogre::RenderQueueListener{
          */
         virtual void applyScroll();
 
+        /**
+         * Starts a scroll, replacing the current one if there is any.
+         *
+         * A scroll without type or duration is applied immediately.
+         *
+         * @param[in] position Position to scroll to.
+         * @param[in] type Scroll type.
+         * @param[in] seconds Scroll duration.
+         * @param[in] target_entity Entity the scroll moves towards, or nullptr.
+         * @param[in] follow_target Keep following target_entity when the scroll ends.
+         */
+        void StartScroll(
+          const Ogre::Vector2& position, const SCROLL_TYPE type, const float seconds,
+          Entity* target_entity, const bool follow_target
+        );
+
+        /**
+         * Limits a scroll position to the background range.
+         *
+         * @param[in] position Position to limit.
+         * @return The position, inside the background range.
+         */
+        const Ogre::Vector2 ClampScroll(const Ogre::Vector2& position) const;
+
         /**
          * Calculates the screen scale.
          *
@@ -512,6 +633,11 @@ class Background2D : public Ogre::RenderQueueListener{
          */
         TileList tiles_;
 
+        /**
+         * The layers that are on for each scripted area, as a bit mask.
+         */
+        std::map<int, unsigned int> area_states_on_;
+
         /**
          * Alpha blending render operation.
          */
@@ -602,6 +728,16 @@ class Background2D : public Ogre::RenderQueueListener{
          */
         float scroll_current_seconds_;
 
+        /**
+         * Entity the current scroll action moves towards, or nullptr.
+         */
+        Entity* scroll_target_entity_;
+
+        /**
+         * Whether to keep following the target entity when the current scroll action ends.
+         */
+        bool scroll_follow_target_;
+
         /**
          * @todo Document.
          */

+ 2 - 4
src/core/Enemy.cpp

@@ -216,10 +216,8 @@ Ogre::Vector3& Enemy::GetPos(){return pos_;}
 
 void Enemy::SetPos(const Ogre::Vector3 &pos){pos_ = pos;}
 
-void Enemy::ScriptGetPos() const{
-    ScriptManager::getSingleton().AddValueToStack(pos_.x);
-    ScriptManager::getSingleton().AddValueToStack(pos_.y);
-    ScriptManager::getSingleton().AddValueToStack(pos_.z);
+std::tuple<float, float, float> Enemy::ScriptGetPos() const{
+    return std::make_tuple(pos_.x, pos_.y, pos_.z);
 }
 
 bool Enemy::IsFront() const{return front_;}

+ 3 - 3
src/core/Enemy.h

@@ -15,6 +15,7 @@
 
 #pragma once
 
+#include <tuple>
 #include <OgreString.h>
 #include "ScriptManager.h"
 
@@ -555,10 +556,9 @@ class Enemy{
         /**
          * Retrieves the enemy position.
          *
-         * To be used from Lua scripts only. It doesn't return anything, but adds three values to
-         * the stack for the X, Y and Z coordinates.
+         * @return The X, Y and Z coordinates for Lua scripts.
          */
-        void ScriptGetPos() const;
+        std::tuple<float, float, float> ScriptGetPos() const;
 
         /**
          * Checks if the enemy is in the front row.

+ 12 - 5
src/core/Entity.cpp

@@ -40,6 +40,7 @@ Entity::Entity(const Ogre::String& name, Ogre::SceneNode* node):
   move_position_(Ogre::Vector3(0, 0, 0)),
   move_entity_(nullptr),
   move_stop_distance_(0.0f),
+  scripted_move_(false),
   move_triangle_id_(-1),
   move_auto_rotation_(true),
   move_auto_animation_(true),
@@ -246,11 +247,9 @@ void Entity::ScriptSetPosition(const float x, const float y, const float z){
 
 const Ogre::Vector3 Entity::GetPosition() const{return scene_node_->getPosition();}
 
-void Entity::ScriptGetPosition() const{
+std::tuple<float, float, float> Entity::ScriptGetPosition() const{
     Ogre::Vector3 position = scene_node_->getPosition();
-    ScriptManager::getSingleton().AddValueToStack(position.x);
-    ScriptManager::getSingleton().AddValueToStack(position.y);
-    ScriptManager::getSingleton().AddValueToStack(position.z);
+  return std::make_tuple(position.x, position.y, position.z);
 }
 
 void Entity::SetOffset(const Ogre::Vector3& position){
@@ -360,12 +359,17 @@ void Entity::SetMoveRunSpeed(const float speed){move_run_speed_ = speed;}
 
 float Entity::GetMoveRunSpeed() const{return move_run_speed_;}
 
-void Entity::SetMovePosition(const Ogre::Vector3& target){move_position_ = target;}
+void Entity::SetMovePosition(const Ogre::Vector3& target){
+  move_position_ = target;
+  scripted_move_ = false;
+}
 
 const Ogre::Vector3& Entity::GetMovePosition() const{return move_position_;}
 
 float Entity::GetMoveStopDistance() const{return move_stop_distance_;}
 
+bool Entity::IsScriptedMove() const{return scripted_move_;}
+
 void Entity::SetMoveTriangleId(const int triangle){move_triangle_id_ = triangle;}
 
 int Entity::GetMoveTriangleId() const{return move_triangle_id_;}
@@ -387,6 +391,7 @@ void Entity::ScriptMoveToPosition(const float x, const float y){
     move_position_ = Ogre::Vector3(x, y, 0);
     move_entity_ = nullptr;
     move_stop_distance_ = 0;
+    scripted_move_ = true;
     LOG_TRIVIAL(
       "Entity '" + name_ + "' set move to walkmesh position '"
       + Ogre::StringConverter::toString(move_position_) + "'."
@@ -398,6 +403,7 @@ void Entity::ScriptMoveToEntity(Entity* entity){
     move_position_ = entity->GetPosition();
     move_entity_ = entity;
     move_stop_distance_ = GetSolidRadius() + entity->GetSolidRadius();
+    scripted_move_ = true;
     LOG_TRIVIAL("Entity '" + name_ + "' set move to entity '" + entity->GetName() + "'.");
 }
 
@@ -415,6 +421,7 @@ void Entity::UnsetMove(){
     state_ = Entity::NONE;
     move_entity_ = nullptr;
     move_stop_distance_ = 0;
+    scripted_move_ = false;
     for (const auto &script : sync_) ScriptManager::getSingleton().ContinueScriptExecution(script);
     sync_.clear();
 }

+ 12 - 3
src/core/Entity.h

@@ -15,6 +15,7 @@
 
 #pragma once
 
+#include <tuple>
 #include <OgreString.h>
 #include "EntityCollision.h"
 #include "EntityDirection.h"
@@ -243,10 +244,9 @@ class Entity{
         /**
          * Retrieves the entity position.
          *
-         * To be used from Lua scripts only. It doesn't return anything, but adds three values to
-         * the stack for the X, Y and Z coordinates.
+         * @return The X, Y and Z coordinates for Lua scripts.
          */
-        void ScriptGetPosition() const;
+        std::tuple<float, float, float> ScriptGetPosition() const;
 
         /**
          * Sets the entity position.
@@ -534,6 +534,13 @@ class Entity{
          */
         float GetMoveStopDistance() const;
 
+        /**
+         * Checks whether the current move was requested by a script.
+         *
+         * @return True for scripted movement.
+         */
+        bool IsScriptedMove() const;
+
         /**
          * Sets the destination triangle in the walkmesh.
          *
@@ -1214,6 +1221,8 @@ class Entity{
          */
         float move_stop_distance_;
 
+        bool scripted_move_;
+
         /**
          * Entity's movement destination triangle ID.
          */

+ 37 - 15
src/core/EntityManager.cpp

@@ -204,17 +204,8 @@ void EntityManager::UpdateField(){
 
         // Update screen scroll.
         Entity* scroll_entity = background_2d_.GetAutoScrollEntity();
-        if (scroll_entity == entity_[i]){
-            CameraManager &camera_manager(CameraManager::getSingleton());
-            Ogre::Vector3 view = camera_manager.ProjectPointToScreen(scroll_entity->GetPosition());
-            Ogre::Vector2 pos = background_2d_.GetScreenScroll();
-            Ogre::Viewport *viewport(camera_manager.getViewport());
-            float width = float(viewport->getActualWidth());
-            float height = float(viewport->getActualHeight());
-            view.x = view.x - width / 2 - pos.x;
-            view.y = view.y - height / 2 - pos.y;
-            background_2d_.SetScreenScroll(Ogre::Vector2(-view.x, -view.y));
-        }
+        if (scroll_entity == entity_[i])
+            background_2d_.SetScroll(background_2d_.GetEntityScroll(scroll_entity));
         // Update offset.
         if (entity_[i]->GetOffsetType() != AT_NONE) SetNextOffsetStep(entity_[i]);
         // Update turn.
@@ -243,7 +234,19 @@ void EntityManager::UpdateField(){
                             if (player_run_ == true) speed *= 4;
                         }
                         else speed = entity_[i]->GetMoveAutoSpeed();
-                        bool is_move = PerformWalkmeshMove(entity_[i], speed);
+                        float remaining_time = Timer::getSingleton().GetGameTimeDelta();
+                        bool is_move = false;
+                        if (entity_[i]->IsScriptedMove() && speed > 0.0f){
+                            const float max_step_time = 0.2f / speed;
+                            while (remaining_time > 0.0f && entity_[i]->GetState() == Entity::WALKMESH){
+                                const float step_time = std::min(remaining_time, max_step_time);
+                                const bool moved = PerformWalkmeshMove(entity_[i], speed, step_time);
+                                is_move = is_move || moved;
+                                if (!moved) break;
+                                remaining_time -= step_time;
+                            }
+                        }
+                        else is_move = PerformWalkmeshMove(entity_[i], speed, remaining_time);
                         if (entity_[i]->GetMoveAutoAnimation() == true){
                             if (is_move == true){
                                 if (speed >= entity_[i]->GetMoveRunSpeed()){
@@ -842,7 +845,11 @@ bool EntityManager::SetEntityOnWalkmesh(Entity* entity){
     return true;
 }
 
-bool EntityManager::PerformWalkmeshMove(Entity* entity, const float speed){
+float EntityManager::ScriptedMoveStep(float distance, float stop_distance, float max_step){
+    return std::min(max_step, std::max(0.0f, distance - stop_distance));
+}
+
+bool EntityManager::PerformWalkmeshMove(Entity* entity, const float speed, const float delta_time){
     if (IsBattleModule()){
         LOG_WARNING("Tried to perform a walkmesh move, but the EntityManager is in battle mode");
         return false;
@@ -850,9 +857,22 @@ bool EntityManager::PerformWalkmeshMove(Entity* entity, const float speed){
     Ogre::Vector3 start_point = entity->GetPosition();
     Ogre::Vector3 move_vector = entity->GetMovePosition() - start_point;
     Ogre::Vector2 direction(move_vector.x, move_vector.y);
+    const float distance_to_target = direction.length();
+    const bool scripted_move = entity->IsScriptedMove();
+    const float stop_distance = entity->GetMoveStopDistance();
+    if (scripted_move && distance_to_target <= stop_distance + 0.0001f){
+        entity->UnsetMove();
+        return false;
+    }
     direction.normalise();
-    direction *= Timer::getSingleton().GetGameTimeDelta();
+    direction *= delta_time;
     direction *= speed;
+    if (scripted_move && !direction.isZeroLength()){
+        direction.normalise();
+        direction *= ScriptedMoveStep(
+          distance_to_target, stop_distance, speed * delta_time
+        );
+    }
     // If movement is still needed but speed or time don't allow it, just return for now.
     if (direction.isZeroLength() == true){
         if (player_entity_ == entity) CheckTriggers(entity, entity->GetPosition());
@@ -1090,7 +1110,8 @@ bool EntityManager::PerformWalkmeshMove(Entity* entity, const float speed){
 
     // If the entity has come to destination point, finish movement.
     Ogre::Vector3 final = entity->GetMovePosition() - end_point;
-    if (Ogre::Vector2(final.x, final.y).length() <= entity->GetMoveStopDistance() + speed / 10)
+    const float tolerance = scripted_move ? 0.0001f : speed / 10;
+    if (Ogre::Vector2(final.x, final.y).length() <= stop_distance + tolerance)
         entity->UnsetMove();
 
     return true;
@@ -1514,6 +1535,7 @@ void EntityManager::SetNextJumpStep(Entity* entity){
 
 void EntityManager::SetNextScrollStep(){
     // TODO: Allow this on battle mode?
+    background_2d_.UpdateScrollTarget();
     Background2D::SCROLL_TYPE type = background_2d_.GetScrollType();
     float total = background_2d_.GetScrollSeconds();
     float current = background_2d_.GetScrollCurrentSeconds();

+ 12 - 1
src/core/EntityManager.h

@@ -43,6 +43,16 @@ class EntityManager : public Manager, public Ogre::Singleton<EntityManager>{
          */
         virtual ~EntityManager();
 
+        /**
+         * Limits a scripted movement step to its remaining distance.
+         *
+         * @param[in] distance Distance to the target.
+         * @param[in] stop_distance Desired distance from the target.
+         * @param[in] max_step Maximum distance to move this frame.
+         * @return Distance to move this frame.
+         */
+        static float ScriptedMoveStep(float distance, float stop_distance, float max_step);
+
         /**
          * Handles an input event.
          *
@@ -533,10 +543,11 @@ class EntityManager : public Manager, public Ogre::Singleton<EntityManager>{
          *
          * @param[in] entity Entity to move.
          * @param[in] speed Movement speed.
+         * @param[in] delta_time Time elapsed for this movement step.
          * @return True if the movement was possible and the entity was moved, false otherwise.
          * If the manager is in battle mode, it will always return false.
          */
-        bool PerformWalkmeshMove(Entity* entity, const float speed);
+        bool PerformWalkmeshMove(Entity* entity, const float speed, const float delta_time);
 
         /**
          * Checks if the entity is crossing a walkmesh triangle border.

+ 2 - 4
src/core/EntityPoint.cpp

@@ -60,12 +60,10 @@ EntityPoint::GetPosition() const
 }
 
 
-void
+std::tuple<float, float, float>
 EntityPoint::ScriptGetPosition() const
 {
-    ScriptManager::getSingleton().AddValueToStack(position_.x);
-    ScriptManager::getSingleton().AddValueToStack(position_.y);
-    ScriptManager::getSingleton().AddValueToStack(position_.z);
+    return std::make_tuple(position_.x, position_.y, position_.z);
 }
 
 

+ 5 - 2
src/core/EntityPoint.h

@@ -14,6 +14,7 @@
  */
 
 #pragma once
+#include <tuple>
 #include <OgreRoot.h>
 
 /**
@@ -62,9 +63,11 @@ class EntityPoint{
         const Ogre::Vector3& GetPosition() const;
 
         /**
-         * Informs the script manager of the point position.
+         * Retrieves the point position for Lua scripts.
+         *
+         * @return The X, Y and Z coordinates.
          */
-        void ScriptGetPosition() const;
+        std::tuple<float, float, float> ScriptGetPosition() const;
 
         /**
          * Sets the point orientation.

+ 30 - 12
src/core/ScriptManager.cpp

@@ -204,10 +204,9 @@ void ScriptManager::Update(const ScriptManager::Type type){
                           + "\" for entity \"" + current_script_id_.entity + "\"."
                         );
 
-                        if (script_entity_[i].queue[0].paused_script_start.entity != ""){
-                            ContinueScriptExecution(script_entity_[i].queue[0].paused_script_start);
-                            script_entity_[i].queue[0].paused_script_start.entity = "";
-                        }
+                        for (const ScriptId& waiting : script_entity_[i].queue[0].paused_scripts_start)
+                            ContinueScriptExecution(waiting);
+                        script_entity_[i].queue[0].paused_scripts_start.clear();
                         sol::object table = GetTableByEntityName(
                           type, current_script_id_.entity,
                           script_entity_[i].queue[0].state
@@ -469,10 +468,9 @@ void ScriptManager::RemoveEntityTopScript(ScriptEntity& entity){
         // Delete the thread.
         luaL_unref(lua_state_, LUA_REGISTRYINDEX, entity.queue[0].state_id);
 
-        if (entity.queue[0].paused_script_end.entity != ""){
-            ContinueScriptExecution(entity.queue[0].paused_script_end);
-            entity.queue[0].paused_script_end.entity = "";
-        }
+        for (const ScriptId& waiting : entity.queue[0].paused_scripts_end)
+            ContinueScriptExecution(waiting);
+        entity.queue[0].paused_scripts_end.clear();
         entity.queue.erase(entity.queue.begin());
     }
 }
@@ -601,6 +599,17 @@ int ScriptManager::ScriptRequestStartSync(
         return 1;
     }
 
+    // A script that already started can't be waited for to start: nothing would resume the caller.
+    for (const QueueScript& queued : script_entity->queue){
+        if (queued.function == function && queued.yield){
+            LOG_TRIVIAL(
+              "[SCRIPT] script:request_start_sync: Function \"" + Ogre::String(function)
+              + "\" for entity \"" + Ogre::String(entity) + "\" already started."
+            );
+            return 1;
+        }
+    }
+
     bool added = ScriptRequest(script_entity, function, priority, "", "", true, false);
 
     if (added == false){
@@ -671,15 +680,24 @@ bool ScriptManager::ScriptRequest(
         script.seconds_to_wait = 0;
         script.wait = false;
         script.yield = false;
-        if (start_sync == true)
-            script.paused_script_start = GetCurrentScriptId();
-        if (end_sync == true) script.paused_script_end = GetCurrentScriptId();
+        if (start_sync == true) script.paused_scripts_start.push_back(GetCurrentScriptId());
+        if (end_sync == true) script.paused_scripts_end.push_back(GetCurrentScriptId());
 
-        // If the script is already running, don't queue it.
+        // If the script is already queued or running, don't queue it again. Whoever waits for the
+        // request must wait for the existing script, because nothing else would resume it.
         if (allow_duplicate == false){
           for (int i = 0; i < script_entity->queue.size(); i ++){
              if (script_entity->queue[i].function == function){
                script_entity->resort = true;
+               QueueScript& existing = script_entity->queue[i];
+               for (const ScriptId& waiting : script.paused_scripts_end)
+                   existing.paused_scripts_end.push_back(waiting);
+               // A script that already started isn't waited for. See ScriptRequestStartSync.
+               if (!existing.yield){
+                   for (const ScriptId& waiting : script.paused_scripts_start)
+                       existing.paused_scripts_start.push_back(waiting);
+               }
+               luaL_unref(lua_state_, LUA_REGISTRYINDEX, script.state_id);
                return true;
             }
             }

+ 10 - 4
src/core/ScriptManager.h

@@ -122,14 +122,20 @@ struct QueueScript{
     bool yield;
 
     /**
-     * The script paused by call of this script
+     * The scripts that wait for this one to start.
+     *
+     * More than one script can wait: a request for a script that is already queued adds a
+     * waiter to the queued one.
      */
-    ScriptId paused_script_start;
+    std::vector<ScriptId> paused_scripts_start;
 
     /**
-     * The script paused by call of this script
+     * The scripts that wait for this one to end.
+     *
+     * More than one script can wait: a request for a script that is already queued or running
+     * adds a waiter to the existing one.
      */
-    ScriptId paused_script_end;
+    std::vector<ScriptId> paused_scripts_end;
 };
 
 

+ 4 - 1
src/core/ScriptManagerBinds.h

@@ -176,7 +176,6 @@ void ScriptManager::InitBinds(){
     // Entity individual point commands
     lua.new_usertype<EntityPoint>(
       "EntityPoint",
-      // Internally returns 3 values:
       "get_position", &EntityPoint::ScriptGetPosition,
       "get_rotation", &EntityPoint::ScriptGetRotation
     );
@@ -360,8 +359,12 @@ void ScriptManager::InitBinds(){
       "Background2D",
       "autoscroll_to_entity", &Background2D::ScriptAutoScrollToEntity,
       "scroll_to_player", &Background2D::ScriptScrollToPlayer,
+      "scroll_to_entity", &Background2D::ScriptScrollToEntity,
       "scroll_to_position", &Background2D::ScriptScrollToPosition,
       "offset", &Background2D::ScriptOffset,
+      "area_on", &Background2D::ScriptAreaOn,
+      "area_off", &Background2D::ScriptAreaOff,
+      "area_clear", &Background2D::ScriptAreaClear,
       "play_animation_looped", &Background2D::ScriptPlayAnimationLooped,
       "play_animation_once", &Background2D::ScriptPlayAnimationOnce
     );

+ 5 - 1
src/core/XmlBackground2DFile.cpp

@@ -68,9 +68,13 @@ void XmlBackground2DFile::Load(){
                   = CameraManager::getSingleton().GetCurrentCamera()
                     ->getProjectionMatrixWithRSDepth() * distance;
                 res = res / res.w;
+                // Tiles that belong to a scripted area are only drawn while a script turns on
+                // the state they belong to.
+                const int area = GetInt(node, "area", 0);
+                const int state = GetInt(node, "state", 0);
                 background->AddTile(
                   static_cast<int>(destination.x), static_cast<int>(destination.y),
-                  width, height, res.z, uv.x, uv.y, uv.z, uv.w, blending
+                  width, height, res.z, uv.x, uv.y, uv.z, uv.w, blending, area, state
                 );
                 TiXmlNode* node2 = node->FirstChild();
                 while (node2 != nullptr){

+ 14 - 2
src/data/VGearsAFile.cpp

@@ -23,6 +23,10 @@
 
 namespace VGears{
 
+  static Ogre::Vector3 ConvertRootTranslation(const Ogre::Vector3& translation, bool flip_forward_axis){
+    return Ogre::Vector3(translation.x, translation.y, flip_forward_axis ? -translation.z : translation.z);
+  }
+
    const Ogre::Real AFile::FRAME_DURATION(1.0f / 30.0f);
 
    const String AFile::RESOURCE_TYPE("VGearsAFile");
@@ -39,6 +43,14 @@ namespace VGears{
 
     void AFile::SetBoneCount(const uint32 bone_count){bone_count_ = bone_count;}
 
+    Ogre::AxisAlignedBox AFile::GetRootTranslationBounds(bool flip_forward_axis) const{
+      Ogre::AxisAlignedBox bounds(Ogre::Vector3::ZERO, Ogre::Vector3::ZERO);
+      for (const Frame& frame : frames_){
+        bounds.merge(ConvertRootTranslation(frame.root_translation, flip_forward_axis));
+      }
+      return bounds;
+    }
+
     void AFile::loadImpl(){
         AFileSerializer serializer;
         Ogre::DataStreamPtr stream(openResource());
@@ -77,7 +89,7 @@ namespace VGears{
         key_frame->setRotation(rot);
     }
 
-    void AFile::AddTo(Ogre::SkeletonPtr skeleton, const String& name) const{
+    void AFile::AddTo(Ogre::SkeletonPtr skeleton, const String& name, bool flip_forward_axis) const{
         if(skeleton->hasAnimation(name)) return;
         Ogre::Real length((frames_.size() - 1) * FRAME_DURATION);
         Ogre::Animation *anim(skeleton->createAnimation(name, length));
@@ -94,7 +106,7 @@ namespace VGears{
         ){
             time = (index++) * FRAME_DURATION;
             Ogre::TransformKeyFrame* key_frame(track->createNodeKeyFrame(time));
-            key_frame->setTranslate(frame->root_translation);
+            key_frame->setTranslate(ConvertRootTranslation(frame->root_translation, flip_forward_axis));
             SetFrameRotation(key_frame, frame->root_rotation);
         }
         for(uint32 i(0); i < bone_count_; ++ i){

+ 10 - 1
src/data/VGearsAFile.h

@@ -74,8 +74,17 @@ namespace VGears{
              *
              * @param[in,out] skeleton Skeleton to add the animation to.
              * @param[in] name Animation name.
+             * @param[in] flip_forward_axis Reverse the root translation's forward axis.
              */
-            void AddTo(Ogre::SkeletonPtr skeleton, const String &name) const;
+            void AddTo(Ogre::SkeletonPtr skeleton, const String &name, bool flip_forward_axis = false) const;
+
+            /**
+             * Retrieves the bounds of the animation's root translations.
+             *
+             * @param[in] flip_forward_axis Apply the field-model coordinate conversion.
+             * @return Bounds containing the origin and all root translations.
+             */
+            Ogre::AxisAlignedBox GetRootTranslationBounds(bool flip_forward_axis = false) const;
 
             typedef std::vector<Ogre::Vector3> BoneRotationList;
 

+ 2 - 3
src/data/VGearsFLevelFileSerializer.cpp

@@ -230,9 +230,8 @@ namespace VGears{
         WalkmeshFile::TriangleList& triangles(walkmesh->GetTriangles());
         WalkmeshFile::TriangleList::iterator it(triangles.begin());
         WalkmeshFile::TriangleList::const_iterator it_end(triangles.end());
-        Ogre::Real scale
-          = static_cast<Ogre::Real>(dest->GetCameraMatrix()->GetCount());
-        scale *= FIELD_POSITION_SCALE;
+        // The field's own scale is stored in its script, the installer applies it on top of this.
+        const Ogre::Real scale = static_cast<Ogre::Real>(FIELD_POSITION_SCALE);
         while (it != it_end){
             it->a /= scale;
             it->b /= scale;

+ 55 - 12
src/installer/FieldDataInstaller.cpp

@@ -35,6 +35,19 @@ std::string FieldDataInstaller::FIELD_MAPS_DIR("fields");
 
 int FieldDataInstaller::INACTIVE_GATEWAY_ID(32767);
 
+const std::vector<int> FieldDataInstaller::TEMPORARY_BANKS{5};
+
+std::string FieldDataInstaller::FieldScriptHeader(){
+    std::string header = "EntityContainer = {}\n\n";
+    for (const int bank : TEMPORARY_BANKS){
+        header += "-- Temporary variables start at zero in every field.\n"
+          "for address = 0, Banks.SIZE do\n"
+          "    Banks[" + std::to_string(bank) + "][address] = 0\n"
+          "end\n\n";
+    }
+    return header;
+}
+
 std::string FieldDataInstaller::CreateGateWayScript(
   const std::string& gateway_entity_name, const std::string& target_map_name,
   const std::string& source_spawn_point_name
@@ -357,12 +370,26 @@ void FieldDataInstaller::ConvertModels(std::string model_name){
         auto mesh_name = VGears::NameLookup::model(base_name) + ".mesh";
         Ogre::MeshPtr mesh(Ogre::MeshManager::getSingleton().load(mesh_name, "FFVII"));
         Ogre::SkeletonPtr skeleton(mesh->getSkeleton());
+        const Ogre::AxisAlignedBox bind_bounds = mesh->getBounds();
+        Ogre::AxisAlignedBox translation_bounds(Ogre::Vector3::ZERO, Ogre::Vector3::ZERO);
         for (auto& anim : model->second){
             VGears::AFileManager &afl_mgr(VGears::AFileManager::GetSingleton());
             VGears::AFilePtr a = afl_mgr.load(anim, "FFVII").staticCast<VGears::AFile>();
             // Convert the FF7 name to a more readable name set in the meta data.
             VGears::StringUtil::splitBase(anim, base_name);
-            a->AddTo(skeleton, VGears::NameLookup::Animation(base_name));
+            a->AddTo(skeleton, VGears::NameLookup::Animation(base_name), true);
+                        translation_bounds.merge(a->GetRootTranslationBounds(true));
+                }
+                if (bind_bounds.isFinite()){
+                        const Ogre::AxisAlignedBox animated_bounds(
+                            bind_bounds.getMinimum() + translation_bounds.getMinimum(),
+                            bind_bounds.getMaximum() + translation_bounds.getMaximum()
+                        );
+                        mesh->_setBounds(animated_bounds, false);
+                        mesh->_setBoundingSphereRadius(std::max(
+                            mesh->getBoundingSphereRadius(),
+                            animated_bounds.getCenter().length() + animated_bounds.getHalfSize().length()
+                        ));
         }
         ExportMesh(output_dir_ + FIELD_MODELS_DIR + "/", mesh);
     }
@@ -511,7 +538,7 @@ void FieldDataInstaller::PcFieldToVGearsField(VGears::FLevelFilePtr& field){
 
         // Decompile to LUA.
         decompiled = FieldDecompiler::Decompile(
-          field->getName(), raw_field_data, formatter, gateway_script_data, "EntityContainer = {}\n\n"
+          field->getName(), raw_field_data, formatter, gateway_script_data, FieldScriptHeader()
         );
         std::ofstream script_file(output_dir_ + "/" + FIELD_MAPS_DIR + "/" + field->getName() + "/script.lua");
         if (script_file.is_open()){
@@ -611,7 +638,8 @@ void FieldDataInstaller::PcFieldToVGearsField(VGears::FLevelFilePtr& field){
                       << field->GetWalkmesh()->GetTriangles().size() << ")" << std::endl;
                     triangle_index = 0;
                 }
-                const float z_of_triangle_with_id = field->GetWalkmesh()->GetTriangles().at(triangle_index).a.z;
+                const float z_of_triangle_with_id
+                  = field->GetWalkmesh()->GetTriangles().at(triangle_index).a.z / GetFieldScaleFactor(this_field_id);
                 const Ogre::Vector3 position(
                   gateway.destination.x / downscaler_next, gateway.destination.y / downscaler_next,
                   z_of_triangle_with_id
@@ -628,6 +656,7 @@ void FieldDataInstaller::PcFieldToVGearsField(VGears::FLevelFilePtr& field){
 
         // Get lines. Add them to a list, so they aren't processed later as regular entities.
         std::vector<std::string> line_entities;
+        const float line_scale = LINE_SCALE_FACTOR / GetFieldScaleFactor(this_field_id);
         for (FieldDecompiler::Line line : decompiled.lines){
 
             std::unique_ptr<TiXmlElement> xml_entity_trigger(new TiXmlElement("entity_trigger"));
@@ -635,15 +664,15 @@ void FieldDataInstaller::PcFieldToVGearsField(VGears::FLevelFilePtr& field){
             xml_entity_trigger->SetAttribute("name", line.name);
             xml_entity_trigger->SetAttribute(
               "point1",
-              std::to_string(line.point_a[0] * LINE_SCALE_FACTOR)
-                + " " + std::to_string(line.point_a[1] * LINE_SCALE_FACTOR)
-                + " " + std::to_string(line.point_a[2] * LINE_SCALE_FACTOR)
+              std::to_string(line.point_a[0] * line_scale)
+                + " " + std::to_string(line.point_a[1] * line_scale)
+                + " " + std::to_string(line.point_a[2] * line_scale)
             );
             xml_entity_trigger->SetAttribute(
               "point2",
-              std::to_string(line.point_b[0] * LINE_SCALE_FACTOR)
-                + " " + std::to_string(line.point_b[1] * LINE_SCALE_FACTOR)
-                + " " + std::to_string(line.point_b[2] * LINE_SCALE_FACTOR)
+              std::to_string(line.point_b[0] * line_scale)
+                + " " + std::to_string(line.point_b[1] * line_scale)
+                + " " + std::to_string(line.point_b[2] * line_scale)
             );
             xml_entity_trigger->SetAttribute("enabled", "true");
             element->LinkEndChild(xml_entity_trigger.release());
@@ -816,6 +845,18 @@ void FieldDataInstaller::PcFieldToVGearsField(VGears::FLevelFilePtr& field){
                     // TODO: Should probably throw to fail conversion:
                     else blending_str = "unknown";
                     xml_element->SetAttribute("blending", blending_str);
+                    // Sprites in an area the script controls (BGON, BGOFF, BGCLR) are drawn only
+                    // while it turns on one of their layers. The rest are always drawn.
+                    if (
+                      sprite.animation_id != 0
+                      && (
+                        decompiled.background_areas_dynamic
+                        || decompiled.background_areas.count(sprite.animation_id) > 0
+                      )
+                    ){
+                        xml_element->SetAttribute("area", static_cast<int>(sprite.animation_id));
+                        xml_element->SetAttribute("state", static_cast<int>(sprite.animation_frame));
+                    }
                     bg_element->LinkEndChild(xml_element.release());
                 }
                 //}
@@ -827,13 +868,15 @@ void FieldDataInstaller::PcFieldToVGearsField(VGears::FLevelFilePtr& field){
     {
         // Save out the walk mesh as XML.
         const VGears::WalkmeshFilePtr& walkmesh = field->GetWalkmesh();
+        // Scripts, gateways and the camera use the field's scale, so the walkmesh must too.
+        const float field_scale = GetFieldScaleFactor(GetFieldId(field->getName(), map_list_));
         TiXmlDocument doc;
         std::unique_ptr<TiXmlElement> wmesh_element(new TiXmlElement("walkmesh"));
         for (VGears::WalkmeshFile::Triangle& tri : walkmesh->GetTriangles()){
             std::unique_ptr<TiXmlElement> xml_element(new TiXmlElement("triangle"));
-            xml_element->SetAttribute("a", Ogre::StringConverter::toString(tri.a));
-            xml_element->SetAttribute("b", Ogre::StringConverter::toString(tri.b));
-            xml_element->SetAttribute("c", Ogre::StringConverter::toString(tri.c));
+            xml_element->SetAttribute("a", Ogre::StringConverter::toString(tri.a / field_scale));
+            xml_element->SetAttribute("b", Ogre::StringConverter::toString(tri.b / field_scale));
+            xml_element->SetAttribute("c", Ogre::StringConverter::toString(tri.c / field_scale));
             xml_element->SetAttribute("a_b", std::to_string(tri.access_side[0]));
             xml_element->SetAttribute("b_c", std::to_string(tri.access_side[1]));
             xml_element->SetAttribute("c_a", std::to_string(tri.access_side[2]));

+ 43 - 0
src/installer/FieldDataInstaller.h

@@ -288,6 +288,32 @@ class FF7FieldScriptFormatter : public BaseFF7FieldScriptFormatter{
             return VGears::NameLookup::Animation(base_name);
         }
 
+        /**
+         * Retrieves the animation a model must play when its script doesn't set a default one.
+         *
+         * In the original game a model without a requested animation plays the first one of its
+         * list.
+         *
+         * @param[in] char_id ID of the model, as given to the CHAR opcode.
+         * @return Name of the first animation of the model. An empty string if the model already
+         * has an animation called "Idle", or it has no animations.
+         */
+        virtual std::string GetDefaultAnimationName(int char_id) override{
+            if (static_cast<unsigned int>(char_id) >= model_loader_->GetModels().size()) return "";
+            const auto& animations = model_loader_->GetModels().at(char_id).animations;
+            if (animations.empty()) return "";
+            std::string first;
+            for (size_t i = 0; i < animations.size(); ++ i){
+                Ogre::String base_name;
+                VGears::StringUtil::splitBase(animations[i].name, base_name);
+                VGears::StringUtil::toLowerCase(base_name);
+                const std::string friendly = VGears::NameLookup::Animation(base_name);
+                if (friendly == "Idle") return "";
+                if (i == 0) first = friendly;
+            }
+            return first;
+        }
+
     private:
 
         /**
@@ -395,6 +421,23 @@ class FieldDataInstaller{
          */
         void ConvertModels(std::string model_name);
 
+        /**
+         * Memory banks that are temporary: every field starts with them set to zero.
+         */
+        static const std::vector<int> TEMPORARY_BANKS;
+
+        /**
+         * Retrieves the Lua code every field script starts with.
+         *
+         * It creates the entity container, and sets the temporary memory banks to zero. In the
+         * original game a field can't see what the previous one left in them, but here the
+         * banks live as long as the game, so scripts that wait for a bank to change would see
+         * leftovers.
+         *
+         * @return Lua code to put at the beginning of a field script.
+         */
+        static std::string FieldScriptHeader();
+
     private:
 
         /**

+ 2 - 0
src/installer/decompiler/ScriptFormatter.cpp

@@ -21,6 +21,8 @@ std::string ScriptFormatter::GetFriendlyEntityName(const std::string& entity_nam
 
 std::string ScriptFormatter::GetFriendlyAnimationName(int animation_id, int id){return std::to_string(animation_id);}
 
+std::string ScriptFormatter::GetDefaultAnimationName(int char_id){return "";}
+
 std::string ScriptFormatter::GetFriendlyCharName(int char_id){return std::to_string(char_id);}
 
 std::string ScriptFormatter::GetFriendlyFunctionName(const std::string& entity_name, const std::string& function_name){

+ 12 - 0
src/installer/decompiler/ScriptFormatter.h

@@ -56,6 +56,18 @@ class ScriptFormatter{
          */
         virtual std::string GetFriendlyAnimationName(int animation_id, int id);
 
+        /**
+         * Retrieves the animation a model must play when its script doesn't set a default one.
+         *
+         * The engine plays the animation called "Idle" by default. Models that don't have one
+         * show no animation at all, and they are not drawn.
+         *
+         * @param[in] char_id ID of the model, as given to the CHAR opcode.
+         * @return Name of the animation to play, or an empty string if the model already has an
+         * animation called "Idle", or there is no animation to choose.
+         */
+        virtual std::string GetDefaultAnimationName(int char_id);
+
         /**
          * Retrieves a friendly name for a character.
          *

+ 35 - 1
src/installer/decompiler/field/FieldCodeGenerator.cpp

@@ -16,8 +16,13 @@
 #include <boost/algorithm/string/predicate.hpp>
 #include <stdexcept>
 #include "FieldCodeGenerator.h"
+#include "FieldDisassembler.h"
 #include "FieldEngine.h"
 
+const std::string FieldCodeGenerator::ON_UPDATE_FINISHED_CONDITION("self.on_update_finished");
+
+const std::string FieldCodeGenerator::ON_UPDATE_FINISHED_STATEMENT("self.on_update_finished = true");
+
 const std::string FunctionMetaData::DELIMITER("_");
 
 const std::string FunctionMetaData::START("start");
@@ -167,6 +172,7 @@ void FieldCodeGenerator::Generate(InstVec& insts, const Graph& graph){
                       "-- Overflowed jump to " + (boost::format("0x%1$X") % (*instruction)->GetDestAddress()).str()
                       + " (last address in function is " + (boost::format("0x%1$X)") % function->first.end_addr).str()
                     );
+                    if ("on_update" == function->first.name) AddOutputLine(ON_UPDATE_FINISHED_STATEMENT);
                     AddOutputLine("do return 0 end");
                 }
                 // Prevent backward jumps in the on_start script.
@@ -182,8 +188,10 @@ void FieldCodeGenerator::Generate(InstVec& insts, const Graph& graph){
         }
         for(; ends_needed > 0; ends_needed --) AddOutputLine("end -- clean-up", true, false);
         // Add missing return:
-        if ("return 0" != lines_.at(lines_.size() - 1).line && "do return 0 end" != lines_.at(lines_.size() - 1).line)
+        if ("return 0" != lines_.at(lines_.size() - 1).line && "do return 0 end" != lines_.at(lines_.size() - 1).line){
+            if ("on_update" == function->first.name) AddOutputLine(ON_UPDATE_FINISHED_STATEMENT);
             AddOutputLine("do return 0 end", false, false);
+        }
         OnEndFunction(function->first);
     }
 
@@ -219,6 +227,12 @@ void FieldCodeGenerator::OnStartFunction(const Function& func){
         AddOutputLine("entity_manager:set_player_entity(\"Cloud\")");
         AddOutputLine("background2d:autoscroll_to_entity(entity_manager:get_entity(\"Cloud\"))\n");
     }
+    // Don't run on_update again once it has reached its end.
+    if (func.name == "on_update"){
+        AddOutputLine("if " + ON_UPDATE_FINISHED_CONDITION + " then", false, true);
+        AddOutputLine("return 0");
+        AddOutputLine("end", true, false);
+    }
     // For lines, exit if they are off.
     if (func.name == "on_approach" || func.name == "on_cross" || func.name == "on_near" || func.name == "on_leave"){
         AddOutputLine("if self.on ~= nill and self.on == false then", false, true);
@@ -255,6 +269,26 @@ bool FieldCodeGenerator::OutputOnlyRequiredLabels() const{return true;}
 
 FieldScriptFormatter& FieldCodeGenerator::GetFormatter(){return formatter_;}
 
+const int FieldCodeGenerator::DEFAULT_WINDOW_X = 16;
+
+const int FieldCodeGenerator::DEFAULT_WINDOW_Y = 176;
+
+const int FieldCodeGenerator::DEFAULT_WINDOW_WIDTH = 288;
+
+const int FieldCodeGenerator::DEFAULT_WINDOW_HEIGHT = 56;
+
+bool FieldCodeGenerator::IsWindowDefined(uint32 window_id) const{
+    if (!defined_windows_collected_){
+        for (const auto& inst : insts_){
+            const uint32 opcode = inst->GetOpcode();
+            if (opcode == OPCODES::WINDOW || opcode == OPCODES::WSIZW)
+                defined_windows_.insert(inst->GetParam(0)->GetUnsigned());
+        }
+        defined_windows_collected_ = true;
+    }
+    return defined_windows_.find(window_id) != defined_windows_.end();
+}
+
 const std::string FieldCodeGenerator::FormatInstructionNotImplemented(
   const std::string& entity, uint32 address, uint32 opcode
 ){

+ 43 - 0
src/installer/decompiler/field/FieldCodeGenerator.h

@@ -16,6 +16,7 @@
 #pragma once
 
 #include <deque>
+#include <set>
 #include <unordered_map>
 #include <boost/algorithm/string.hpp>
 #include "FieldScriptFormatter.h"
@@ -171,6 +172,19 @@ class FieldCodeGenerator : public CodeGenerator{
 
     public:
 
+        /**
+         * Lua condition that tells an on_update script has already reached its end.
+         *
+         * The engine runs on_update again every time it finishes, but in the original game a
+         * script that returns is over for good.
+         */
+        static const std::string ON_UPDATE_FINISHED_CONDITION;
+
+        /**
+         * Lua statement that marks an on_update script as finished.
+         */
+        static const std::string ON_UPDATE_FINISHED_STATEMENT;
+
         /**
          * Type of data.
          */
@@ -346,7 +360,26 @@ class FieldCodeGenerator : public CodeGenerator{
          */
         FieldScriptFormatter& GetFormatter();
 
+        /**
+         * Checks if any script of the field defines a dialog window.
+         *
+         * A window is defined with the WINDOW or WSIZW opcodes. Fields can show messages in
+         * windows that are never defined.
+         *
+         * @param[in] window_id Window ID.
+         * @return True if the field defines the window.
+         */
+        bool IsWindowDefined(uint32 window_id) const;
 
+        /**
+         * Position and size of the window for messages shown in a window that is never defined.
+         *
+         * These are the same values the engine uses for its own script messages.
+         */
+        static const int DEFAULT_WINDOW_X;
+        static const int DEFAULT_WINDOW_Y;
+        static const int DEFAULT_WINDOW_WIDTH;
+        static const int DEFAULT_WINDOW_HEIGHT;
 
     protected:
 
@@ -418,4 +451,14 @@ class FieldCodeGenerator : public CodeGenerator{
          */
         FieldScriptFormatter& formatter_;
 
+        /**
+         * IDs of the dialog windows defined by the field. Filled the first time it is needed.
+         */
+        mutable std::set<uint32> defined_windows_;
+
+        /**
+         * Whether defined_windows_ has been filled.
+         */
+        mutable bool defined_windows_collected_ = false;
+
 };

+ 2 - 0
src/installer/decompiler/field/FieldDecompiler.cpp

@@ -53,5 +53,7 @@ FieldDecompiler::DecompiledScript FieldDecompiler::Decompile(
     ds.luaScript = text_before + output.str() + text_after;
     for (auto entity : engine.GetEntityList()) ds.entities.push_back(entity);
     for (auto line : engine.GetLineList()) ds.lines.push_back(line);
+    ds.background_areas = engine.GetBackgroundAreas();
+    ds.background_areas_dynamic = engine.BackgroundAreasAreDynamic();
     return ds;
 }

+ 15 - 0
src/installer/decompiler/field/FieldDecompiler.h

@@ -18,6 +18,7 @@
 
 #pragma once
 
+#include <set>
 #include <string>
 #include "FieldScriptFormatter.h"
 #include "installer/decompiler/Decompiler.h"
@@ -92,6 +93,20 @@ class FieldDecompiler: public Decompiler{
              * Lines in the field.
              */
             std::vector<Line> lines;
+
+            /**
+             * Background areas the script turns on or off.
+             *
+             * Background tiles of other areas are always drawn.
+             */
+            std::set<int> background_areas;
+
+            /**
+             * Whether the script picks background areas from variables.
+             *
+             * If so, it is not known which areas it controls, and any of them might be.
+             */
+            bool background_areas_dynamic = false;
         };
 
         /**

+ 9 - 0
src/installer/decompiler/field/FieldEngine.cpp

@@ -192,6 +192,15 @@ std::vector<FieldDecompiler::Line> FieldEngine::GetLineList() const{
     return lines;
 }
 
+void FieldEngine::AddBackgroundArea(int area){
+    if (area < 0) background_areas_dynamic_ = true;
+    else background_areas_.insert(area);
+}
+
+const std::set<int>& FieldEngine::GetBackgroundAreas() const{return background_areas_;}
+
+bool FieldEngine::BackgroundAreasAreDynamic() const{return background_areas_dynamic_;}
+
 void FieldEngine::AddEntityFunction(
   const std::string& entity_name, size_t entity_index, const std::string& func_name, size_t func_index
 ){

+ 32 - 0
src/installer/decompiler/field/FieldEngine.h

@@ -18,6 +18,7 @@
 
 #pragma once
 
+#include <set>
 #include <string>
 #include <vector>
 
@@ -268,6 +269,27 @@ class FieldEngine : public Engine{
          */
         std::vector<FieldDecompiler::Line> GetLineList() const;
 
+        /**
+         * Notes that the script turns a background area on or off.
+         *
+         * @param[in] area The area, or -1 if it is picked from a variable.
+         */
+        void AddBackgroundArea(int area);
+
+        /**
+         * Retrieves the background areas the script controls.
+         *
+         * @return The area IDs.
+         */
+        const std::set<int>& GetBackgroundAreas() const;
+
+        /**
+         * Checks if the script picks any background area from a variable.
+         *
+         * @return True if it does.
+         */
+        bool BackgroundAreasAreDynamic() const;
+
         /**
          * Retrieves all entities in the map.
          *
@@ -386,6 +408,16 @@ class FieldEngine : public Engine{
          */
         float scale_factor_;
 
+        /**
+         * Background areas the script controls.
+         */
+        std::set<int> background_areas_;
+
+        /**
+         * Whether the script picks any background area from a variable.
+         */
+        bool background_areas_dynamic_ = false;
+
         /**
          * The script name.
          */

+ 12 - 9
src/installer/decompiler/field/instruction/FieldBackgroundInstruction.cpp

@@ -33,11 +33,11 @@ void FieldBackgroundInstruction::ProcessInst(
         case OPCODES::BGPDH: code_gen->WriteTodo(md.GetEntityName(), "BGPDH");break;
         case OPCODES::BGSCR: code_gen->WriteTodo(md.GetEntityName(), "BGSCR"); break;
         case OPCODES::MPPAL: code_gen->WriteTodo(md.GetEntityName(), "MPPAL"); break;
-        case OPCODES::BGON: ProcessBGON(code_gen); break;
-        case OPCODES::BGOFF: ProcessBGOFF(code_gen); break;
+        case OPCODES::BGON: ProcessBGON(code_gen, static_cast<FieldEngine&>(*engine)); break;
+        case OPCODES::BGOFF: ProcessBGOFF(code_gen, static_cast<FieldEngine&>(*engine)); break;
         case OPCODES::BGROL: code_gen->WriteTodo(md.GetEntityName(), "BGROL"); break;
         case OPCODES::BGROL2: code_gen->WriteTodo(md.GetEntityName(), "BGROL2"); break;
-        case OPCODES::BGCLR: ProcessBGCLR(code_gen); break;
+        case OPCODES::BGCLR: ProcessBGCLR(code_gen, static_cast<FieldEngine&>(*engine)); break;
         case OPCODES::STPAL: ProcessSTPAL(code_gen); break;
         case OPCODES::LDPAL: ProcessLDPAL(code_gen); break;
         case OPCODES::CPPAL: ProcessCPPAL(code_gen); break;
@@ -56,7 +56,8 @@ void FieldBackgroundInstruction::ProcessInst(
     }
 }
 
-void FieldBackgroundInstruction::ProcessBGON(CodeGenerator* code_gen){
+void FieldBackgroundInstruction::ProcessBGON(CodeGenerator* code_gen, FieldEngine& engine){
+    engine.AddBackgroundArea(params_[0]->GetUnsigned() == 0 ? params_[2]->GetUnsigned() : -1);
     FieldCodeGenerator* cg = static_cast<FieldCodeGenerator*>(code_gen);
     const auto& background_id = FieldCodeGenerator::FormatValueOrVariable(
       cg->GetFormatter(), params_[0]->GetUnsigned(), params_[2]->GetUnsigned()
@@ -65,11 +66,12 @@ void FieldBackgroundInstruction::ProcessBGON(CodeGenerator* code_gen){
       cg->GetFormatter(), params_[1]->GetUnsigned(), params_[3]->GetUnsigned()
     );
     code_gen->AddOutputLine(
-      (boost::format("-- field:background_on(%1%, %2%)") % background_id % layer_id).str()
+      (boost::format("background2d:area_on(%1%, %2%)") % background_id % layer_id).str()
     );
 }
 
-void FieldBackgroundInstruction::ProcessBGOFF(CodeGenerator* code_gen){
+void FieldBackgroundInstruction::ProcessBGOFF(CodeGenerator* code_gen, FieldEngine& engine){
+    engine.AddBackgroundArea(params_[0]->GetUnsigned() == 0 ? params_[2]->GetUnsigned() : -1);
     FieldCodeGenerator* cg = static_cast<FieldCodeGenerator*>(code_gen);
     const auto& background_id = FieldCodeGenerator::FormatValueOrVariable(
       cg->GetFormatter(), params_[0]->GetUnsigned(), params_[2]->GetUnsigned()
@@ -78,17 +80,18 @@ void FieldBackgroundInstruction::ProcessBGOFF(CodeGenerator* code_gen){
       cg->GetFormatter(), params_[1]->GetUnsigned(), params_[3]->GetUnsigned()
     );
     code_gen->AddOutputLine(
-      (boost::format("-- field:background_off(%1%, %2%)") % background_id % layer_id).str()
+      (boost::format("background2d:area_off(%1%, %2%)") % background_id % layer_id).str()
     );
 }
 
-void FieldBackgroundInstruction::ProcessBGCLR(CodeGenerator* code_gen){
+void FieldBackgroundInstruction::ProcessBGCLR(CodeGenerator* code_gen, FieldEngine& engine){
+    engine.AddBackgroundArea(params_[1]->GetUnsigned() == 0 ? params_[2]->GetUnsigned() : -1);
     FieldCodeGenerator* cg = static_cast<FieldCodeGenerator*>(code_gen);
     const auto& background_id = FieldCodeGenerator::FormatValueOrVariable(
       cg->GetFormatter(), params_[1]->GetUnsigned(), params_[2]->GetUnsigned()
     );
     code_gen->AddOutputLine(
-      (boost::format("-- field:background_clear(%1%)") % background_id).str()
+      (boost::format("background2d:area_clear(%1%)") % background_id).str()
     );
 }
 

+ 3 - 3
src/installer/decompiler/field/instruction/FieldBackgroundInstruction.h

@@ -41,9 +41,9 @@ class FieldBackgroundInstruction : public KernelCallInstruction{
 
     private:
 
-        void ProcessBGON(CodeGenerator* code_gen);
-        void ProcessBGOFF(CodeGenerator* code_gen);
-        void ProcessBGCLR(CodeGenerator* code_gen);
+        void ProcessBGON(CodeGenerator* code_gen, FieldEngine& engine);
+        void ProcessBGOFF(CodeGenerator* code_gen, FieldEngine& engine);
+        void ProcessBGCLR(CodeGenerator* code_gen, FieldEngine& engine);
         void ProcessSTPAL(CodeGenerator* code_gen);
         void ProcessLDPAL(CodeGenerator* code_gen);
         void ProcessCPPAL(CodeGenerator* code_gen);

+ 42 - 6
src/installer/decompiler/field/instruction/FieldCameraInstruction.cpp

@@ -37,14 +37,14 @@ void FieldCameraInstruction::ProcessInst(
     case OPCODES::SCR2D: ProcessSCR2D(code_gen); break;
     case OPCODES::SCRCC: ProcessSCRCC(code_gen); break;
     case OPCODES::SCR2DC: ProcessSCR2DC(code_gen); break;
-    case OPCODES::SCRLW: code_gen->WriteTodo(md.GetEntityName(), "SCRLW"); break;
+    case OPCODES::SCRLW: code_gen->AddOutputLine("background2d:scroll_sync()"); break;
     case OPCODES::SCR2DL: ProcessSCR2DL(code_gen); break;
     case OPCODES::VWOFT: ProcessVWOFT(code_gen); break;
     case OPCODES::FADE: ProcessFADE(code_gen); break;
     case OPCODES::FADEW: code_gen->WriteTodo(md.GetEntityName(), "FADEW"); break;
     case OPCODES::SCRLP: code_gen->WriteTodo(md.GetEntityName(), "SCRLP"); break;
     case OPCODES::MVCAM: code_gen->WriteTodo(md.GetEntityName(), "MVCAM"); break;
-    case OPCODES::SCRLA: code_gen->WriteTodo(md.GetEntityName(), "SCRLA"); break;
+    case OPCODES::SCRLA: ProcessSCRLA(code_gen, static_cast<FieldEngine&>(*engine)); break;
     default:
         code_gen->AddOutputLine(FieldCodeGenerator::FormatInstructionNotImplemented(
           md.GetEntityName(), address_, opcode_
@@ -79,10 +79,46 @@ void FieldCameraInstruction::ProcessNFADE(CodeGenerator* code_gen){
 }
 
 void FieldCameraInstruction::ProcessSCRLC(CodeGenerator* code_gen){
-    std::string mode = "Background2D.NONE";
-    if (params_[3]->GetUnsigned() == 2) mode = "Background2D.LINEAR";
-    else if (params_[3]->GetUnsigned() == 3) mode = "Background2D.SMOOTH";
-    code_gen->AddOutputLine("background2d:scroll_to_player(" + mode + ", 0)");
+    std::string mode;
+    switch (params_[3]->GetUnsigned()){
+        case 2: mode = "Background2D.LINEAR"; break;
+        case 3: mode = "Background2D.SMOOTH"; break;
+        default:
+            // The other types don't move the camera, nor change what it is following.
+            code_gen->AddOutputLine("-- SCRLC without effect");
+            return;
+    }
+    FieldCodeGenerator* cg = static_cast<FieldCodeGenerator*>(code_gen);
+    const auto& seconds = FieldCodeGenerator::FormatValueOrVariable(
+      cg->GetFormatter(), params_[0]->GetUnsigned(), params_[1]->GetUnsigned(),
+      FieldCodeGenerator::ValueType::Float, 30.0f
+    );
+    code_gen->AddOutputLine("background2d:scroll_to_player(" + mode + ", " + seconds + ")");
+}
+
+void FieldCameraInstruction::ProcessSCRLA(CodeGenerator* code_gen, const FieldEngine& engine){
+    std::string mode;
+    switch (params_[4]->GetUnsigned()){
+        case 0:
+            code_gen->AddOutputLine("-- SCRLA without effect");
+            return;
+        case 1: mode = "Background2D.NONE"; break;
+        case 2: mode = "Background2D.LINEAR"; break;
+        case 3: mode = "Background2D.SMOOTH"; break;
+        default:
+            code_gen->WriteTodo("FieldCameraInstruction", "SCRLA with unsupported scroll type");
+            return;
+    }
+    FieldCodeGenerator* cg = static_cast<FieldCodeGenerator*>(code_gen);
+    const auto& seconds = FieldCodeGenerator::FormatValueOrVariable(
+      cg->GetFormatter(), params_[1]->GetUnsigned(), params_[2]->GetUnsigned(),
+      FieldCodeGenerator::ValueType::Float, 30.0f
+    );
+    const auto& entity = engine.EntityByIndex(params_[3]->GetUnsigned());
+    code_gen->AddOutputLine((
+      boost::format("background2d:scroll_to_entity(entity_manager:get_entity(\"%1%\"), %2%, %3%)")
+      % entity.GetName() % mode % seconds
+    ).str());
 }
 
 void FieldCameraInstruction::ProcessSCR2D(CodeGenerator* code_gen){

+ 33 - 0
src/installer/decompiler/field/instruction/FieldCameraInstruction.h

@@ -101,6 +101,39 @@ class FieldCameraInstruction : public KernelCallInstruction{
          */
         void ProcessSCRLC(CodeGenerator* code_gen);
 
+        /**
+         * Processes a SCRLA opcode.
+         *
+         * Opcode: 0x63
+         * Short name: SCRLA
+         * Long name: Scroll To Entity
+         *
+         * Memory layout (5 bytes)
+         * |0x63|B|S|E|T|
+         *
+         * Arguments
+         *
+         * - const UByte B: Bank for the scroll speed, or zero if it is specified as a literal
+         * value.
+         * - const UShort S: Speed of the scroll, in frames, or the address to find the speed if B
+         * is non-zero.
+         * - const UByte E: Entity ID to scroll to.
+         * - const UByte T: Type of scroll.
+         *
+         * Scrolls the current view so that entity E is in the center of the view. If the entity
+         * moves during the scroll, the scroll follows it, and so does the camera afterwards.
+         *
+         * Scroll Types:
+         * 0 - No scroll.
+         * 1 - Instantaneous.
+         * 2 - Linear.
+         * 3 - Smooth.
+         *
+         * @param[in,out] code_gen Code generator to append lines.
+         * @param[in] engine Engine, to find the entity.
+         */
+        void ProcessSCRLA(CodeGenerator* code_gen, const FieldEngine& engine);
+
         /**
          * Processes a SCRCC opcode.
          *

+ 4 - 0
src/installer/decompiler/field/instruction/FieldControlFlowInstruction.cpp

@@ -41,6 +41,10 @@ void FieldControlFlowInstruction::ProcessInst(
             //     'main' return is kept, the code of 'main' is never executed.
             //     There are safeguards forfunctions without a return in the end,
             //     so it's OK not to include it here.
+            // - The engine runs on_update again every time it finishes. In the original game a
+            //     script that returns is over for good, so on_update is marked as finished.
+            if (func.name == "on_update")
+                code_gen->AddOutputLine(FieldCodeGenerator::ON_UPDATE_FINISHED_STATEMENT);
             if (func.name != "on_start") code_gen->AddOutputLine("do return 0 end");
             break;
         case OPCODES::REQ: ProcessREQ(code_gen, eng); break;

+ 119 - 8
src/installer/decompiler/field/instruction/FieldModelInstruction.cpp

@@ -115,12 +115,20 @@ void FieldModelInstruction::ProcessInst(
         case OPCODES::CMOVE: code_gen->WriteTodo(md.GetEntityName(), "CMOVE"); break;
         case OPCODES::MOVA: code_gen->WriteTodo(md.GetEntityName(), "MOVA"); break;
         case OPCODES::TURA: ProcessTURA(code_gen, md.GetEntityName(), eng); break;
-        case OPCODES::ANIMW: code_gen->WriteTodo(md.GetEntityName(), "ANIMW"); break;
-        case OPCODES::FMOVE: code_gen->WriteTodo(md.GetEntityName(), "FMOVE"); break;
-        case OPCODES::ANIME2: code_gen->WriteTodo(md.GetEntityName(), "ANIME2"); break;
-        case OPCODES::ANIM_1: code_gen->WriteTodo(md.GetEntityName(), "ANIM_1"); break;
-        case OPCODES::CANIM1: code_gen->WriteTodo(md.GetEntityName(), "CANIM1"); break;
-        case OPCODES::CANM_1: code_gen->WriteTodo(md.GetEntityName(), "CANM_1"); break;
+        case OPCODES::ANIMW: ProcessANIMW(code_gen, md.GetEntityName()); break;
+        case OPCODES::FMOVE: ProcessFMOVE(code_gen, md.GetEntityName()); break;
+        case OPCODES::ANIME2:
+            ProcessANIME2(code_gen, md.GetEntityName(), md.GetCharacterId());
+            break;
+        case OPCODES::ANIM_1:
+            ProcessANIM_1(code_gen, md.GetEntityName(), md.GetCharacterId());
+            break;
+        case OPCODES::CANIM1:
+            ProcessCANIM1(code_gen, md.GetEntityName(), md.GetCharacterId());
+            break;
+        case OPCODES::CANM_1:
+            ProcessCANM_1(code_gen, md.GetEntityName(), md.GetCharacterId());
+            break;
         case OPCODES::MSPED: ProcessMSPED(code_gen, md.GetEntityName()); break;
         case OPCODES::DIR: ProcessDIR(code_gen, md.GetEntityName()); break;
         case OPCODES::TURNGEN: ProcessTURNGEN(code_gen, md.GetEntityName()); break;
@@ -232,6 +240,19 @@ void FieldModelInstruction::ProcessCHAR(CodeGenerator* code_gen, const std::stri
     code_gen->AddOutputLine(
       (boost::format("self.%1% = entity_manager:get_entity(\"%1%\")") % entity).str()
     );
+    // A model that has no animation playing is not drawn. The ones with no "Idle" animation need
+    // a default one, which a DFANM later in the script replaces.
+    FieldCodeGenerator* cg = static_cast<FieldCodeGenerator*>(code_gen);
+    const std::string animation = cg->GetFormatter().GetDefaultAnimationName(params_[0]->GetSigned());
+    if (!animation.empty()){
+        code_gen->AddOutputLine((
+          boost::format("self.%1%:set_default_animation(\"%2%\") -- No Idle animation")
+          % entity % animation
+        ).str());
+        code_gen->AddOutputLine(
+          (boost::format("self.%1%:play_animation(\"%2%\")") % entity % animation).str()
+        );
+    }
 }
 
 void FieldModelInstruction::ProcessDFANM(
@@ -267,6 +288,69 @@ void FieldModelInstruction::ProcessANIME1(
     code_gen->AddOutputLine((boost::format("self.%1%:animation_sync()") % entity).str());
 }
 
+void FieldModelInstruction::ProcessANIME2(
+  CodeGenerator* code_gen, const std::string& entity, int char_id
+){
+    // Like ANIME1, but the script doesn't wait for the animation to end.
+    FieldCodeGenerator* cg = static_cast<FieldCodeGenerator*>(code_gen);
+    auto animation_id = params_[0]->GetUnsigned();
+    auto speed = 1.0f / params_[1]->GetUnsigned();
+    code_gen->AddOutputLine((
+      boost::format("self.%1%:play_animation(\"%2%\") -- speed %3%")
+      % entity % cg->GetFormatter().GetFriendlyAnimationName(char_id, animation_id) % speed
+    ).str());
+}
+
+void FieldModelInstruction::ProcessANIM_1(
+  CodeGenerator* code_gen, const std::string& entity, int char_id
+){
+    // Like ANIM_2, but the script doesn't wait for the animation to end.
+    FieldCodeGenerator* cg = static_cast<FieldCodeGenerator*>(code_gen);
+    auto animation_id = params_[0]->GetUnsigned();
+    auto speed = 1.0f / params_[1]->GetUnsigned();
+    code_gen->AddOutputLine((
+      boost::format("self.%1%:play_animation_stop(\"%2%\") -- speed %3%")
+      % entity % cg->GetFormatter().GetFriendlyAnimationName(char_id, animation_id) % speed
+    ).str());
+}
+
+void FieldModelInstruction::ProcessCANIM1(
+  CodeGenerator* code_gen, const std::string& entity, int char_id
+){
+    // Like CANIM2, but the script doesn't wait for the animation to end.
+    FieldCodeGenerator* cg = static_cast<FieldCodeGenerator*>(code_gen);
+    auto animation_id = params_[0]->GetUnsigned();
+    auto start_frame = params_[1]->GetUnsigned() / 30.0f;
+    auto end_frame = params_[2]->GetUnsigned() / 30.0f;
+    auto speed = 1.0f / params_[3]->GetUnsigned();
+    code_gen->AddOutputLine((
+      boost::format("self.%1%:play_animation(\"%2%\", %3%, %4%) -- speed %5%")
+      % entity % cg->GetFormatter().GetFriendlyAnimationName(char_id, animation_id)
+      % start_frame % end_frame % speed
+    ).str());
+}
+
+void FieldModelInstruction::ProcessCANM_1(
+  CodeGenerator* code_gen, const std::string& entity, int char_id
+){
+    // Like CANM_2, but the script doesn't wait for the animation to end.
+    FieldCodeGenerator* cg = static_cast<FieldCodeGenerator*>(code_gen);
+    auto animation_id = params_[0]->GetUnsigned();
+    auto start_frame = params_[1]->GetUnsigned() / 30.0f;
+    auto end_frame = params_[2]->GetUnsigned() / 30.0f;
+    auto speed = 1.0f / params_[3]->GetUnsigned();
+    code_gen->AddOutputLine((
+      boost::format("self.%1%:play_animation_stop(\"%2%\", %3%, %4%) -- speed %5%")
+      % entity % cg->GetFormatter().GetFriendlyAnimationName(char_id, animation_id)
+      % start_frame % end_frame % speed
+    ).str());
+}
+
+void FieldModelInstruction::ProcessANIMW(CodeGenerator* code_gen, const std::string& entity){
+    // Waits for the animation that was started with one of the non-waiting opcodes.
+    code_gen->AddOutputLine((boost::format("self.%1%:animation_sync()") % entity).str());
+}
+
 void FieldModelInstruction::ProcessVISI(CodeGenerator* code_gen, const std::string& entity){
     code_gen->AddOutputLine((
       boost::format("self.%1%:set_visible(%2%)")
@@ -315,6 +399,26 @@ void FieldModelInstruction::ProcessMOVE(CodeGenerator* code_gen, const std::stri
     code_gen->AddOutputLine((boost::format("self.%1%:move_sync()") % entity).str());
 }
 
+void FieldModelInstruction::ProcessFMOVE(CodeGenerator* code_gen, const std::string& entity){
+    FieldCodeGenerator* cg = static_cast<FieldCodeGenerator*>(code_gen);
+    const float scale = 128.0f * cg->GetScaleFactor();
+    const auto& x = FieldCodeGenerator::FormatValueOrVariable(
+      cg->GetFormatter(), params_[0]->GetUnsigned(), params_[2]->GetSigned(),
+      FieldCodeGenerator::ValueType::Float, scale
+    );
+    const auto& y = FieldCodeGenerator::FormatValueOrVariable(
+      cg->GetFormatter(), params_[1]->GetUnsigned(), params_[3]->GetSigned(),
+      FieldCodeGenerator::ValueType::Float, scale
+    );
+    // Like MOVE, but the animation is the one the script chose, not the walking one.
+    code_gen->AddOutputLine((boost::format("self.%1%:move_auto_animation(false)") % entity).str());
+    code_gen->AddOutputLine(
+      (boost::format("self.%1%:move_to_position(%2%, %3%)") % entity % x % y).str()
+    );
+    code_gen->AddOutputLine((boost::format("self.%1%:move_sync()") % entity).str());
+    code_gen->AddOutputLine((boost::format("self.%1%:move_auto_animation(true)") % entity).str());
+}
+
 void FieldModelInstruction::ProcessTURA(
   CodeGenerator* code_gen, const std::string& entity, const FieldEngine& engine
 ){
@@ -605,8 +709,15 @@ void FieldModelInstruction::ProcessOFST(CodeGenerator* code_gen, const std::stri
     const bool literal_values
       = TryParseFloat(x_value, x) && TryParseFloat(y_value, y)
         && TryParseFloat(z_value, z) && TryParseFloat(speed_value, speed);
-    const std::string move_type
-      = params_[4]->GetUnsigned() ? "Entity.SMOOTH" : "Entity.LINEAR";
+    std::string move_type;
+    switch (params_[4]->GetUnsigned()){
+      case 0: move_type = "Entity.NONE"; break;
+      case 1: move_type = "Entity.LINEAR"; break;
+      case 2: move_type = "Entity.SMOOTH"; break;
+      default:
+        code_gen->WriteTodo(entity, "OFST with unsupported movement type");
+        return;
+    }
     if (literal_values){
         speed /= 30.0f;
         code_gen->AddOutputLine((

+ 78 - 0
src/installer/decompiler/field/instruction/FieldModelInstruction.h

@@ -156,6 +156,84 @@ class FieldModelInstruction : public KernelCallInstruction{
 
         void ProcessMOVE(CodeGenerator* code_gen, const std::string& entity);
 
+        /**
+         * Processes an ANIME2 opcode.
+         *
+         * Plays an animation of the model and goes back to the previous state, without waiting
+         * for it to end.
+         *
+         * @param[in,out] code_gen Code generator to append lines.
+         * @param[in] entity Name of the entity.
+         * @param[in] char_id ID of the model, to name the animation.
+         */
+        void ProcessANIME2(CodeGenerator* code_gen, const std::string& entity, int char_id);
+
+        /**
+         * Processes an ANIM!1 opcode.
+         *
+         * Plays an animation of the model and stops at its last frame, without waiting for it to
+         * end. It's the non-waiting version of ANIM!2.
+         *
+         * @param[in,out] code_gen Code generator to append lines.
+         * @param[in] entity Name of the entity.
+         * @param[in] char_id ID of the model, to name the animation.
+         */
+        void ProcessANIM_1(CodeGenerator* code_gen, const std::string& entity, int char_id);
+
+        /**
+         * Processes a CANIM1 opcode.
+         *
+         * Plays part of an animation and goes back to the previous state, without waiting for it
+         * to end. It's the non-waiting version of CANIM2.
+         *
+         * @param[in,out] code_gen Code generator to append lines.
+         * @param[in] entity Name of the entity.
+         * @param[in] char_id ID of the model, to name the animation.
+         */
+        void ProcessCANIM1(CodeGenerator* code_gen, const std::string& entity, int char_id);
+
+        /**
+         * Processes a CANM!1 opcode.
+         *
+         * Plays part of an animation and stops at its last frame, without waiting for it to end.
+         * It's the non-waiting version of CANM!2.
+         *
+         * @param[in,out] code_gen Code generator to append lines.
+         * @param[in] entity Name of the entity.
+         * @param[in] char_id ID of the model, to name the animation.
+         */
+        void ProcessCANM_1(CodeGenerator* code_gen, const std::string& entity, int char_id);
+
+        /**
+         * Processes an ANIMW opcode.
+         *
+         * Waits for the animation that was played by one of the non-waiting animation opcodes.
+         *
+         * @param[in,out] code_gen Code generator to append lines.
+         * @param[in] entity Name of the entity.
+         */
+        void ProcessANIMW(CodeGenerator* code_gen, const std::string& entity);
+
+        /**
+         * Processes an FMOVE opcode.
+         *
+         * Opcode: 0xAD
+         * Short name: FMOVE
+         *
+         * Memory layout (6 bytes)
+         * |0xAD|B1/B2|X|Y|
+         *
+         * Moves the entity's model to the position, and waits for it to arrive, like MOVE. The
+         * animation is the one the script chose, instead of the walking one. The documentation
+         * for this opcode in the repository is a copy of the one for CMOVE, so this is taken from
+         * how the scripts use it: they set a running animation, call FMOVE, and then set an idle
+         * animation.
+         *
+         * @param[in,out] code_gen Code generator to append lines.
+         * @param[in] entity Name of the entity.
+         */
+        void ProcessFMOVE(CodeGenerator* code_gen, const std::string& entity);
+
         /**
          * Processes a TURA opcode.
          *

+ 4 - 1
src/installer/decompiler/field/instruction/FieldModuleInstruction.cpp

@@ -104,8 +104,11 @@ void FieldModuleInstruction::ProcessInst(
 
 void FieldModuleInstruction::ProcessBATTLE(CodeGenerator* code_gen){
     FieldCodeGenerator* cg = static_cast<FieldCodeGenerator*>(code_gen);
+    // Operands: two bank nibbles, one of them always zero, and then the battle ID or the address
+    // to read it from.
     const auto& battle_id = FieldCodeGenerator::FormatValueOrVariable(
-      cg->GetFormatter(), params_[0]->GetUnsigned(), params_[1]->GetUnsigned()
+      cg->GetFormatter(), params_[0]->GetUnsigned() + params_[1]->GetUnsigned(),
+      params_[2]->GetUnsigned()
     );
     code_gen->AddOutputLine((boost::format("Battle.start(%1%)") % battle_id).str());
 }

+ 10 - 0
src/installer/decompiler/field/instruction/FieldWindowInstruction.cpp

@@ -222,6 +222,16 @@ void FieldWindowInstruction::ProcessMESSAGE(
     // Displays a dialog in the WINDOW that has previously been initialized to display this dialog.
     auto window_id = params_[0]->GetUnsigned();
     auto dialog_id = params_[1]->GetUnsigned();
+    // The engine needs a position and size to show a message. Some fields show messages in
+    // windows that are never defined, so these get default ones.
+    FieldCodeGenerator* cg = static_cast<FieldCodeGenerator*>(code_gen);
+    if (!cg->IsWindowDefined(window_id)){
+        code_gen->AddOutputLine((
+          boost::format("dialog:dialog_open(\"%1%\", %2%, %3%, %4%, %5%) -- Default, no WINDOW")
+          % window_id % FieldCodeGenerator::DEFAULT_WINDOW_X % FieldCodeGenerator::DEFAULT_WINDOW_Y
+          % FieldCodeGenerator::DEFAULT_WINDOW_WIDTH % FieldCodeGenerator::DEFAULT_WINDOW_HEIGHT
+        ).str());
+    }
     code_gen->AddOutputLine((
       boost::format("dialog:dialog_set_text(\"%1%\", \"%2%_%3%\")")
       % window_id % script_name % dialog_id

+ 2 - 0
src/map/VGearsBackground2DFileXMLSerializer.cpp

@@ -112,6 +112,8 @@ namespace VGears{
         ReadAttribute(node, "uv", dest.uv);
         ReadAttribute(node, "depth", dest.depth);
         ReadAttribute(node, "blending", dest.blending, B_ALPHA);
+        ReadAttribute(node, "area", dest.area, 0);
+        ReadAttribute(node, "state", dest.state, 0);
         ReadMap(node, "animation", "name", dest.animations);
     }
 

+ 10 - 0
src/map/VGearsTile.h

@@ -111,6 +111,16 @@ namespace VGears{
          */
         Blending blending;
 
+        /**
+         * Scripted area the tile belongs to, or 0 if it is always drawn.
+         */
+        int area = 0;
+
+        /**
+         * Bit mask with the layers of the area in which the tile is drawn.
+         */
+        int state = 0;
+
         /**
          * List of animations associated to the tile.
          */

+ 5 - 1
src/modules/worldmap/WorldmapModule.cpp

@@ -769,7 +769,11 @@ namespace VGears {
         );
         if (player_entity_ == nullptr) return;
 
-        const Ogre::AxisAlignedBox& bounds = player_entity_->GetModel()->getBoundingBox();
+        // Cloud's bounds without animations. The bounds in the mesh can't be used for sizing: the
+        // installer enlarges them with the animations' movement, so the model is not culled.
+        const Ogre::AxisAlignedBox bounds(
+          -8.7473f, -8.1671f, -4.1878f, 7.8685f, 5.6837f, 22.8135f
+        );
         const Ogre::Vector3 model_size = bounds.getSize();
         const Ogre::Real model_height = model_size.y;
         if (model_height > 0){

+ 13 - 5
test/core/EntityManager.cpp

@@ -16,11 +16,19 @@
 #include <boost/test/unit_test.hpp>
 #include "core/EntityManager.h"
 
-#pragma message("Unimplemented test file: " __FILE__)
-
 BOOST_AUTO_TEST_CASE(TestEntityManager){
-    // TODO
-    BOOST_WARN_MESSAGE(false, "Unimplemented test file: EntityManager");
-    BOOST_CHECK(0 == 0);
+    BOOST_CHECK_CLOSE(EntityManager::ScriptedMoveStep(0.5f, 0.0f, 0.1f), 0.1f, 0.01f);
+    BOOST_CHECK_CLOSE(EntityManager::ScriptedMoveStep(0.05f, 0.0f, 0.1f), 0.05f, 0.01f);
+    BOOST_CHECK_CLOSE(EntityManager::ScriptedMoveStep(0.55f, 0.5f, 0.1f), 0.05f, 0.01f);
+    BOOST_CHECK_EQUAL(EntityManager::ScriptedMoveStep(0.4f, 0.5f, 0.1f), 0.0f);
+    BOOST_CHECK_EQUAL(EntityManager::ScriptedMoveStep(1.0f, 0.0f, 0.0f), 0.0f);
+
+    for (float frame_time : {1.0f / 120.0f, 1.0f / 60.0f, 1.0f / 30.0f}){
+        float distance = 2.0f;
+        for (int step = 0; step < 1000 && distance > 0.0001f; ++step){
+            distance -= EntityManager::ScriptedMoveStep(distance, 0.0f, 6.0f * frame_time);
+        }
+        BOOST_CHECK_SMALL(distance, 0.0001f);
+    }
 }
 

+ 14 - 5
test/core/EntityPoint.cpp

@@ -14,13 +14,22 @@
  */
 
 #include <boost/test/unit_test.hpp>
+#include "LuaIncludes.h"
 #include "core/EntityPoint.h"
 
-#pragma message("Unimplemented test file: " __FILE__)
-
 BOOST_AUTO_TEST_CASE(TestEntityPoint){
-    // TODO
-    BOOST_WARN_MESSAGE(false, "Unimplemented test file: EntityPoint");
-    BOOST_CHECK(0 == 0);
+        EntityPoint point("entry");
+        point.SetPosition(Ogre::Vector3(4.0f, -2.0f, 5.5f));
+
+        sol::state lua;
+        lua.open_libraries(sol::lib::base, sol::lib::coroutine);
+        lua.new_usertype<EntityPoint>("EntityPoint", "get_position", &EntityPoint::ScriptGetPosition);
+        lua["point"] = &point;
+        const sol::protected_function_result result = lua.safe_script(
+            "local thread = coroutine.create(function() return point:get_position() end)\n"
+            "local ok, x, y, z = coroutine.resume(thread)\n"
+            "assert(ok and x == 4 and y == -2 and z == 5.5)"
+        );
+        BOOST_REQUIRE(result.valid());
 }
 

+ 32 - 5
test/data/VGearsAFile.cpp

@@ -14,13 +14,40 @@
  */
 
 #include <boost/test/unit_test.hpp>
+#include <OgreAnimation.h>
+#include <OgreAnimationTrack.h>
+#include <OgreKeyFrame.h>
+#include <OgreSkeleton.h>
 #include "data/VGearsAFile.h"
 
-#pragma message("Unimplemented test file: " __FILE__)
-
 BOOST_AUTO_TEST_CASE(TestVGearsAFile){
-    // TODO
-    BOOST_WARN_MESSAGE(false, "Unimplemented test file: VGearsAFile");
-    BOOST_CHECK(0 == 0);
+    VGears::AFile animation(nullptr, "animation", 0, "test", true);
+    VGears::AFile::Frame first_frame;
+    first_frame.root_rotation = Ogre::Vector3::ZERO;
+    first_frame.root_translation = Ogre::Vector3(2.0f, 3.0f, 4.0f);
+    animation.GetFrames().push_back(first_frame);
+    VGears::AFile::Frame last_frame;
+    last_frame.root_rotation = Ogre::Vector3::ZERO;
+    last_frame.root_translation = Ogre::Vector3(-1.0f, 5.0f, -6.0f);
+    animation.GetFrames().push_back(last_frame);
+
+    Ogre::SkeletonPtr skeleton(new Ogre::Skeleton(nullptr, "skeleton", 0, "test", true));
+    skeleton->createBone("root");
+    animation.AddTo(skeleton, "jump", true);
+
+    Ogre::NodeAnimationTrack* track = skeleton->getAnimation("jump")->getNodeTrack(0);
+    BOOST_CHECK(track->getNodeKeyFrame(0)->getTranslate() == Ogre::Vector3(2.0f, 3.0f, -4.0f));
+    BOOST_CHECK(track->getNodeKeyFrame(1)->getTranslate() == Ogre::Vector3(-1.0f, 5.0f, 6.0f));
+    animation.AddTo(skeleton, "unmodified");
+    track = skeleton->getAnimation("unmodified")->getNodeTrack(0);
+    BOOST_CHECK(track->getNodeKeyFrame(0)->getTranslate() == first_frame.root_translation);
+    BOOST_CHECK(track->getNodeKeyFrame(1)->getTranslate() == last_frame.root_translation);
+
+    const Ogre::AxisAlignedBox bounds = animation.GetRootTranslationBounds(true);
+    BOOST_CHECK(bounds.getMinimum() == Ogre::Vector3(-1.0f, 0.0f, -4.0f));
+    BOOST_CHECK(bounds.getMaximum() == Ogre::Vector3(2.0f, 5.0f, 6.0f));
+    const Ogre::AxisAlignedBox original_bounds = animation.GetRootTranslationBounds();
+    BOOST_CHECK(original_bounds.getMinimum() == Ogre::Vector3(-1.0f, 0.0f, -6.0f));
+    BOOST_CHECK(original_bounds.getMaximum() == Ogre::Vector3(2.0f, 5.0f, 4.0f));
 }
 

+ 11 - 0
test/installer/FieldDataInstaller.cpp

@@ -18,6 +18,17 @@
 
 #pragma message("Unimplemented test file: " __FILE__)
 
+BOOST_AUTO_TEST_CASE(TestFieldScriptHeaderResetsTemporaryBanks){
+    const std::string header = FieldDataInstaller::FieldScriptHeader();
+    // The container must exist before anything else in the script uses it.
+    BOOST_CHECK(header.find("EntityContainer = {}") == 0);
+    BOOST_CHECK(header.find("for address = 0, Banks.SIZE do") != std::string::npos);
+    BOOST_CHECK(header.find("Banks[5][address] = 0") != std::string::npos);
+    // Only the temporary banks are reset: saved variables must survive a field change.
+    BOOST_CHECK(header.find("Banks[1]") == std::string::npos);
+    BOOST_CHECK(header.find("Banks[2]") == std::string::npos);
+}
+
 BOOST_AUTO_TEST_CASE(TestFieldDataInstaller){
     // TODO
     BOOST_WARN_MESSAGE(false, "Unimplemented test file: FieldDataInstaller");

+ 292 - 5
test/installer/decompiler/field/FieldDecompiler.cpp

@@ -16,11 +16,298 @@
 #include <boost/test/unit_test.hpp>
 #include "installer/decompiler/field/FieldDecompiler.h"
 
-#pragma message("Unimplemented test file: " __FILE__)
-
 BOOST_AUTO_TEST_CASE(TestFieldDecompiler){
-    // TODO
-    BOOST_WARN_MESSAGE(false, "Unimplemented test file: FieldDecompiler");
-    BOOST_CHECK(0 == 0);
+    std::vector<unsigned char> script(104, 0);
+    script[0] = 0x02;
+    script[1] = 0x05;
+    script[2] = 1;
+    script[9] = 2;
+    script[32] = 'D';
+    script[33] = 'o';
+    script[34] = 'o';
+    script[35] = 'r';
+    for (size_t index = 0; index < 32; ++index) script[40 + index * 2] = 104;
+
+    for (unsigned char type = 0; type <= 2; ++type){
+        const unsigned char instruction[] = {0xc3, 0, 0, type, 128, 0, 0, 0, 0, 0, 30, 0};
+        script.insert(script.end(), std::begin(instruction), std::end(instruction));
+    }
+    script.push_back(0);
+    script[4] = static_cast<unsigned char>(script.size());
+    script[5] = static_cast<unsigned char>(script.size() >> 8);
+
+    FieldScriptFormatter formatter;
+    const std::string output = FieldDecompiler::Decompile("synthetic", script, formatter).luaScript;
+    BOOST_CHECK(output.find("offset_to_position(1, 0, 0, Entity.NONE, 1)") != std::string::npos);
+    BOOST_CHECK(output.find("offset_to_position(1, 0, 0, Entity.LINEAR, 1)") != std::string::npos);
+    BOOST_CHECK(output.find("offset_to_position(1, 0, 0, Entity.SMOOTH, 1)") != std::string::npos);
+}
+
+static std::string DecompileInstructions(const std::vector<unsigned char>& instructions){
+    std::vector<unsigned char> script(104, 0);
+    script[0] = 0x02;
+    script[1] = 0x05;
+    script[2] = 1;
+    script[9] = 2;
+    script[32] = 'D';
+    script[33] = 'o';
+    script[34] = 'o';
+    script[35] = 'r';
+    for (size_t index = 0; index < 32; ++index) script[40 + index * 2] = 104;
+    script.insert(script.end(), instructions.begin(), instructions.end());
+    script.push_back(0); // RET
+    script[4] = static_cast<unsigned char>(script.size());
+    script[5] = static_cast<unsigned char>(script.size() >> 8);
+    FieldScriptFormatter formatter;
+    return FieldDecompiler::Decompile("synthetic", script, formatter).luaScript;
+}
+
+namespace{
+    class DefaultAnimationFormatter : public FieldScriptFormatter{
+        public:
+            std::string GetDefaultAnimationName(int char_id) override{
+                return char_id == 2 ? "stand" : "";
+            }
+    };
+}
+
+static FieldDecompiler::DecompiledScript DecompileFully(
+  const std::vector<unsigned char>& instructions, FieldScriptFormatter& formatter
+){
+    std::vector<unsigned char> script(104, 0);
+    script[0] = 0x02;
+    script[1] = 0x05;
+    script[2] = 1;
+    script[9] = 2;
+    script[32] = 'D';
+    script[33] = 'o';
+    script[34] = 'o';
+    script[35] = 'r';
+    for (size_t index = 0; index < 32; ++index) script[40 + index * 2] = 104;
+    script.insert(script.end(), instructions.begin(), instructions.end());
+    script.push_back(0); // RET
+    script[4] = static_cast<unsigned char>(script.size());
+    script[5] = static_cast<unsigned char>(script.size() >> 8);
+    return FieldDecompiler::Decompile("synthetic", script, formatter);
+}
+
+static std::string DecompileWith(
+  const std::vector<unsigned char>& instructions, FieldScriptFormatter& formatter
+){return DecompileFully(instructions, formatter).luaScript;}
+
+BOOST_AUTO_TEST_CASE(TestFieldDecompilerModelWithoutIdleGetsDefaultAnimation){
+    DefaultAnimationFormatter formatter;
+    // CHAR 2: the formatter has an animation for it.
+    const std::string with_animation = DecompileWith({0xA1, 2}, formatter);
+    BOOST_CHECK(
+      with_animation.find("self.Door:set_default_animation(\"stand\") -- No Idle animation")
+      != std::string::npos
+    );
+    BOOST_CHECK(with_animation.find("self.Door:play_animation(\"stand\")") != std::string::npos);
+    // CHAR 1: the formatter has none, as when the model already has an Idle animation.
+    const std::string without_animation = DecompileWith({0xA1, 1}, formatter);
+    BOOST_CHECK(without_animation.find("self.Door = entity_manager") != std::string::npos);
+    BOOST_CHECK(without_animation.find("set_default_animation") == std::string::npos);
+    // The default formatter never adds one.
+    FieldScriptFormatter plain_formatter;
+    BOOST_CHECK(
+      DecompileWith({0xA1, 2}, plain_formatter).find("set_default_animation") == std::string::npos
+    );
+}
+
+namespace{
+    class NamedAnimationFormatter : public FieldScriptFormatter{
+        public:
+            std::string GetFriendlyAnimationName(int char_id, int id) override{
+                return "anim" + std::to_string(id);
+            }
+    };
+}
+
+BOOST_AUTO_TEST_CASE(TestFieldDecompilerNonWaitingAnimationsDontWait){
+    NamedAnimationFormatter formatter;
+    // ANIM!1 3, 1 - ANIME2 2, 1 - CANIM1 4, 0, 14, 1 - CANM!1 5, 15, 29, 1.
+    const std::string output = DecompileWith(
+      {0xAF, 3, 1, 0xAE, 2, 1, 0xB0, 4, 0, 14, 1, 0xB1, 5, 15, 29, 1}, formatter
+    );
+    BOOST_CHECK(output.find("self.Door:play_animation_stop(\"anim3\") -- speed 1") != std::string::npos);
+    BOOST_CHECK(output.find("self.Door:play_animation(\"anim2\") -- speed 1") != std::string::npos);
+    BOOST_CHECK(
+      output.find("self.Door:play_animation(\"anim4\", 0, 0.466667) -- speed 1") != std::string::npos
+    );
+    BOOST_CHECK(
+      output.find("self.Door:play_animation_stop(\"anim5\", 0.5, 0.966667) -- speed 1")
+      != std::string::npos
+    );
+    BOOST_CHECK(output.find("UNIMPLMENTED") == std::string::npos);
+    // None of them waits.
+    BOOST_CHECK(output.find("animation_sync") == std::string::npos);
+}
+
+BOOST_AUTO_TEST_CASE(TestFieldDecompilerAnimwWaitsForTheAnimation){
+    NamedAnimationFormatter formatter;
+    // ANIM!1 3, 1 and then ANIMW: the script waits after starting the animation.
+    const std::string output = DecompileWith({0xAF, 3, 1, 0xAC}, formatter);
+    const size_t start = output.find("self.Door:play_animation_stop(\"anim3\")");
+    const size_t wait = output.find("self.Door:animation_sync()");
+    BOOST_REQUIRE(start != std::string::npos && wait != std::string::npos);
+    BOOST_CHECK(start < wait);
+}
+
+BOOST_AUTO_TEST_CASE(TestFieldDecompilerBackgroundAreas){
+    FieldScriptFormatter formatter;
+    // BGON 0, 0, 2, 0 - BGOFF 0, 0, 8, 2 - BGCLR 0, 8.
+    const std::string output = DecompileWith(
+      {0xE0, 0, 2, 0, 0xE1, 0, 8, 2, 0xE4, 0, 8}, formatter
+    );
+    BOOST_CHECK(output.find("background2d:area_on(2, 0)") != std::string::npos);
+    BOOST_CHECK(output.find("background2d:area_off(8, 2)") != std::string::npos);
+    BOOST_CHECK(output.find("background2d:area_clear(8)") != std::string::npos);
+    // They used to be comments.
+    BOOST_CHECK(output.find("-- field:background_") == std::string::npos);
+}
+
+BOOST_AUTO_TEST_CASE(TestFieldDecompilerReportsTheBackgroundAreasItControls){
+    FieldScriptFormatter formatter;
+    const auto literal = DecompileFully({0xE0, 0, 2, 0, 0xE1, 0, 8, 2, 0xE4, 0, 5}, formatter);
+    BOOST_CHECK(literal.background_areas == std::set<int>({2, 5, 8}));
+    BOOST_CHECK(!literal.background_areas_dynamic);
+    // BGON with the area read from bank 1, address 3: any area could be controlled.
+    const auto from_variable = DecompileFully({0xE0, 0x10, 3, 0}, formatter);
+    BOOST_CHECK(from_variable.background_areas_dynamic);
+    // A script that doesn't touch the background controls no area.
+    const auto none = DecompileFully({}, formatter);
+    BOOST_CHECK(none.background_areas.empty());
+    BOOST_CHECK(!none.background_areas_dynamic);
+}
+
+BOOST_AUTO_TEST_CASE(TestFieldDecompilerFmoveWaitsAndKeepsTheAnimation){
+    FieldScriptFormatter formatter;
+    // FMOVE 0, 0, 128, -256: both bank nibbles zero, then X and Y as signed shorts.
+    const std::string output = DecompileWith({0xAD, 0, 0x80, 0x00, 0x00, 0xFF}, formatter);
+    BOOST_CHECK(output.find("UNIMPLMENTED INSTRUCTION: \"FMOVE\"") == std::string::npos);
+    const size_t off = output.find("self.Door:move_auto_animation(false)");
+    const size_t move = output.find("self.Door:move_to_position(");
+    const size_t sync = output.find("self.Door:move_sync()");
+    const size_t on = output.find("self.Door:move_auto_animation(true)");
+    BOOST_REQUIRE(off != std::string::npos && move != std::string::npos);
+    BOOST_REQUIRE(sync != std::string::npos && on != std::string::npos);
+    // The move must end before the automatic animation comes back.
+    BOOST_CHECK(off < move);
+    BOOST_CHECK(move < sync);
+    BOOST_CHECK(sync < on);
+}
+
+BOOST_AUTO_TEST_CASE(TestFieldDecompilerBattleKeepsItsId){
+    FieldScriptFormatter formatter;
+    // BATTLE 0, 0, 328 and BATTLE 0, 0, 7.
+    const std::string output = DecompileWith({0x70, 0, 0x48, 0x01, 0x70, 0, 0x07, 0x00}, formatter);
+    BOOST_CHECK(output.find("Battle.start(328)") != std::string::npos);
+    BOOST_CHECK(output.find("Battle.start(7)") != std::string::npos);
+    BOOST_CHECK(output.find("Battle.start(0)") == std::string::npos);
+}
+
+BOOST_AUTO_TEST_CASE(TestFieldDecompilerMessageInUndefinedWindow){
+    // MESSAGE 0, 0 and MESSAGE 1, 0, where no WINDOW defines any of them.
+    const std::string output = DecompileInstructions({0x40, 0, 0, 0x40, 1, 0});
+    BOOST_CHECK(
+      output.find("dialog:dialog_open(\"0\", 16, 176, 288, 56) -- Default, no WINDOW")
+      != std::string::npos
+    );
+    BOOST_CHECK(
+      output.find("dialog:dialog_open(\"1\", 16, 176, 288, 56) -- Default, no WINDOW")
+      != std::string::npos
+    );
+    // The window must be opened before the text is set.
+    BOOST_CHECK(
+      output.find("dialog_open(\"0\"") < output.find("dialog:dialog_set_text(\"0\", \"synthetic_0\")")
+    );
+}
+
+BOOST_AUTO_TEST_CASE(TestFieldDecompilerMessageInDefinedWindow){
+    // WINDOW 1, 83, 120, 138, 25 and then MESSAGE 1, 0. Window 0 is not defined.
+    const std::string output = DecompileInstructions(
+      {0x50, 1, 83, 0, 120, 0, 138, 0, 25, 0, 0x40, 1, 0, 0x40, 0, 0}
+    );
+    BOOST_CHECK(output.find("dialog:dialog_open(\"1\", 83, 120, 138, 25)") != std::string::npos);
+    BOOST_CHECK(output.find("dialog_open(\"1\", 16, 176") == std::string::npos);
+    BOOST_CHECK(output.find("dialog_open(\"0\", 16, 176, 288, 56)") != std::string::npos);
+}
+
+BOOST_AUTO_TEST_CASE(TestFieldDecompilerOnUpdateRunsOnce){
+    std::vector<unsigned char> script(104, 0);
+    script[0] = 0x02;
+    script[1] = 0x05;
+    script[2] = 1;
+    script[9] = 2;
+    script[32] = 'D';
+    script[33] = 'o';
+    script[34] = 'o';
+    script[35] = 'r';
+    for (size_t index = 0; index < 32; ++index) script[40 + index * 2] = 104;
+    // The first RET ends the init script (on_start), the code after it is the main script
+    // (on_update).
+    script.push_back(0); // RET
+    script.push_back(0); // RET
+    script[4] = static_cast<unsigned char>(script.size());
+    script[5] = static_cast<unsigned char>(script.size() >> 8);
+
+    FieldScriptFormatter formatter;
+    const std::string output = FieldDecompiler::Decompile("synthetic", script, formatter).luaScript;
+    const size_t on_update = output.find("on_update = function(self)");
+    BOOST_REQUIRE(on_update != std::string::npos);
+    const std::string body = output.substr(on_update);
+    // The engine runs on_update again once it ends, but a script that returns is over.
+    const size_t guard = body.find("if self.on_update_finished then");
+    const size_t mark = body.find("self.on_update_finished = true");
+    BOOST_CHECK(guard != std::string::npos);
+    BOOST_CHECK(mark != std::string::npos);
+    BOOST_CHECK(guard < mark);
+    // Other functions must keep running normally.
+    BOOST_CHECK(output.substr(0, on_update).find("on_update_finished") == std::string::npos);
+}
+
+BOOST_AUTO_TEST_CASE(TestFieldDecompilerScrollInstructions){
+    std::vector<unsigned char> script(104, 0);
+    script[0] = 0x02;
+    script[1] = 0x05;
+    script[2] = 1;
+    script[9] = 2;
+    script[32] = 'D';
+    script[33] = 'o';
+    script[34] = 'o';
+    script[35] = 'r';
+    for (size_t index = 0; index < 32; ++index) script[40 + index * 2] = 104;
+
+    const unsigned char instructions[] = {
+      0x63, 0, 128, 0, 0, 3, // SCRLA: Smooth scroll to entity 0, in 128 frames.
+      0x63, 0, 0, 0, 0, 1, // SCRLA: Instant scroll to entity 0.
+      0x67, // SCRLW
+      0x62, 0, 30, 0, 2, // SCRLC: Linear scroll to the player, in 30 frames.
+      0x62, 0, 0, 0, 1, // SCRLC: This type has no effect.
+      0x65 // SCRCC
+    };
+    script.insert(script.end(), std::begin(instructions), std::end(instructions));
+    script.push_back(0);
+    script[4] = static_cast<unsigned char>(script.size());
+    script[5] = static_cast<unsigned char>(script.size() >> 8);
+
+    FieldScriptFormatter formatter;
+    const std::string output = FieldDecompiler::Decompile("synthetic", script, formatter).luaScript;
+    const std::string get_door = "entity_manager:get_entity(\"Door\")";
+    BOOST_CHECK(
+      output.find("scroll_to_entity(" + get_door + ", Background2D.SMOOTH, 4.266667)")
+      != std::string::npos
+    );
+    BOOST_CHECK(
+      output.find("scroll_to_entity(" + get_door + ", Background2D.NONE, 0.000000)")
+      != std::string::npos
+    );
+    BOOST_CHECK(output.find("background2d:scroll_sync()") != std::string::npos);
+    BOOST_CHECK(
+      output.find("scroll_to_player(Background2D.LINEAR, 1.000000)") != std::string::npos
+    );
+    BOOST_CHECK(output.find("-- SCRLC without effect") != std::string::npos);
+    BOOST_CHECK(output.find("scroll_to_player(Background2D.NONE, 0)") != std::string::npos);
 }