瀏覽代碼

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 天之前
父節點
當前提交
b1af0c86f5
共有 45 個文件被更改,包括 1417 次插入 和 199 次删除
  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
 resolution 1024 768 0
 
 
 bind F11 "toggle timer_scale_game 0.0 1.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 LAlt+F4 "quit"
 bind RAlt+F4 "quit"
 bind RAlt+F4 "quit"

+ 184 - 72
src/core/Background2D.cpp

@@ -41,6 +41,8 @@ Background2D::Background2D():
   scroll_type_(Background2D::NONE),
   scroll_type_(Background2D::NONE),
   scroll_seconds_(0),
   scroll_seconds_(0),
   scroll_current_seconds_(0),
   scroll_current_seconds_(0),
+  scroll_target_entity_(nullptr),
+  scroll_follow_target_(false),
   position_(Ogre::Vector2::ZERO),
   position_(Ogre::Vector2::ZERO),
   position_real_(Ogre::Vector2::ZERO),
   position_real_(Ogre::Vector2::ZERO),
   offset_(Ogre::Vector2::ZERO),
   offset_(Ogre::Vector2::ZERO),
@@ -187,65 +189,113 @@ void Background2D::calculateScreenScale(){
 
 
 void Background2D::OnResize(){
 void Background2D::OnResize(){
     calculateScreenScale();
     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();
     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(){
 void Background2D::Clear(){
     scroll_entity_ = nullptr;
     scroll_entity_ = nullptr;
     scroll_position_start_ = Ogre::Vector2::ZERO;
     scroll_position_start_ = Ogre::Vector2::ZERO;
     scroll_position_end_ = Ogre::Vector2::ZERO;
     scroll_position_end_ = Ogre::Vector2::ZERO;
     scroll_seconds_ = 0;
     scroll_seconds_ = 0;
     scroll_current_seconds_ = 0;
     scroll_current_seconds_ = 0;
+    scroll_target_entity_ = nullptr;
+    scroll_follow_target_ = false;
     position_ = Ogre::Vector2::ZERO;
     position_ = Ogre::Vector2::ZERO;
     position_real_ = Ogre::Vector2::ZERO;
     position_real_ = Ogre::Vector2::ZERO;
     offset_ = Ogre::Vector2::ZERO;
     offset_ = Ogre::Vector2::ZERO;
@@ -258,6 +308,7 @@ void Background2D::Clear(){
             ScriptManager::getSingleton().ContinueScriptExecution(animation_played_[i].sync[j]);
             ScriptManager::getSingleton().ContinueScriptExecution(animation_played_[i].sync[j]);
     animation_played_.clear();
     animation_played_.clear();
     tiles_.clear();
     tiles_.clear();
+    area_states_on_.clear();
     DestroyVertexBuffers();
     DestroyVertexBuffers();
     CreateVertexBuffers();
     CreateVertexBuffers();
 }
 }
@@ -266,17 +317,61 @@ void Background2D::ScriptAutoScrollToEntity(Entity* entity){scroll_entity_ = ent
 
 
 Entity* Background2D::GetAutoScrollEntity() const{return scroll_entity_;}
 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();
     Entity* player = EntityManager::getSingleton().ScriptGetPlayerEntity();
     if (player == nullptr) return;
     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_start_ = position_;
-    scroll_position_end_ = position;
+    scroll_position_end_ = ClampScroll(position);
     scroll_type_ = type;
     scroll_type_ = type;
     scroll_seconds_ = seconds;
     scroll_seconds_ = seconds;
     scroll_current_seconds_ = 0;
     scroll_current_seconds_ = 0;
-
-    return;
 }
 }
 
 
 void Background2D::ScriptScrollToPosition(
 void Background2D::ScriptScrollToPosition(
@@ -286,20 +381,13 @@ void Background2D::ScriptScrollToPosition(
       "Background2d set scroll to position '"
       "Background2d set scroll to position '"
       + Ogre::StringConverter::toString(Ogre::Vector2(x, y)) + "'."
       + Ogre::StringConverter::toString(Ogre::Vector2(x, y)) + "'."
     );
     );
-    Ogre::Vector2 position = Ogre::Vector2(x, y);
     scroll_entity_ = nullptr;
     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(){
 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();
     ScriptId script = ScriptManager::getSingleton().GetCurrentScriptId();
     LOG_TRIVIAL(
     LOG_TRIVIAL(
       "Wait Background2d scroll for function '"
       "Wait Background2d scroll for function '"
@@ -322,6 +410,9 @@ void Background2D::ScriptOffset(const float x, const float y){
 
 
 void Background2D::UnsetScroll(){
 void Background2D::UnsetScroll(){
     scroll_type_ = Background2D::NONE;
     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)
     for(unsigned int i = 0; i < scroll_sync_.size(); ++ i)
         ScriptManager::getSingleton().ContinueScriptExecution(scroll_sync_[i]);
         ScriptManager::getSingleton().ContinueScriptExecution(scroll_sync_[i]);
     scroll_sync_.clear();
     scroll_sync_.clear();
@@ -344,17 +435,22 @@ void Background2D::SetScreenScroll(const Ogre::Vector2& position){
 }
 }
 
 
 void Background2D::SetScroll(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)){
     if (!range_.contains(pos_3d)){
         Ogre::Vector3 max_3d(range_.getMaximum());
         Ogre::Vector3 max_3d(range_.getMaximum());
         Ogre::Vector3 min_3d(range_.getMinimum());
         Ogre::Vector3 min_3d(range_.getMinimum());
         Ogre::Vector2 max(max_3d.x, max_3d.y);
         Ogre::Vector2 max(max_3d.x, max_3d.y);
         Ogre::Vector2 min(min_3d.x, min_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(){
 void Background2D::applyScroll(){
@@ -415,7 +511,11 @@ void Background2D::AddTile(const VGears::Tile& tile){
         }
         }
         else depth = 0.9999f;
         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(
 void Background2D::AddTile(
@@ -437,7 +537,8 @@ void Background2D::virtualScreenToWorldSpace(Ogre::Vector2& pos) const{
 
 
 void Background2D::AddTile(
 void Background2D::AddTile(
   const int x, const int y, const int width, const int height, const float depth, const float u1,
   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::RenderOperation render_op;
     Ogre::HardwareVertexBufferSharedPtr vertex_buffer;
     Ogre::HardwareVertexBufferSharedPtr vertex_buffer;
@@ -475,6 +576,9 @@ void Background2D::AddTile(
     tile.height = height;
     tile.height = height;
     tile.start_vertex_index = render_op.vertexData->vertexCount;
     tile.start_vertex_index = render_op.vertexData->vertexCount;
     tile.blending = blending;
     tile.blending = blending;
+    tile.area = area;
+    tile.state = state;
+    tile.visible = (area == 0) || IsAreaStateOn(area, state);
     //size_t index(tiles_.size());
     //size_t index(tiles_.size());
     tiles_.push_back(tile);
     tiles_.push_back(tile);
     Ogre::Vector2 top_left(static_cast<Ogre::Real>(x), static_cast<Ogre::Real>(-y));
     Ogre::Vector2 top_left(static_cast<Ogre::Real>(x), static_cast<Ogre::Real>(-y));
@@ -558,6 +662,8 @@ void Background2D::AddTile(
     *write_iterator ++ = v2;
     *write_iterator ++ = v2;
     render_op.vertexData->vertexCount += TILE_VERTEX_COUNT;
     render_op.vertexData->vertexCount += TILE_VERTEX_COUNT;
     vertex_buffer->unlock();
     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(
 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(tiles.begin());
     VGears::Background2DFile::TileList::const_iterator it_end(tiles.end());
     VGears::Background2DFile::TileList::const_iterator it_end(tiles.end());
     size_t tile_index(0);
     size_t tile_index(0);
+    size_t area_tiles(0);
     while (it != it_end) {
     while (it != it_end) {
         AddTile(*it);
         AddTile(*it);
         load(tile_index ++, it->animations);
         load(tile_index ++, it->animations);
+        if (it->area != 0) ++ area_tiles;
         ++ it;
         ++ 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){
 void Background2D::load(const size_t tile_index, const VGears::AnimationMap& animations){

+ 139 - 3
src/core/Background2D.h

@@ -15,6 +15,8 @@
 
 
 #pragma once
 #pragma once
 
 
+#include <map>
+#include <set>
 #include <OgreHardwareVertexBuffer.h>
 #include <OgreHardwareVertexBuffer.h>
 #include <OgreRenderQueueListener.h>
 #include <OgreRenderQueueListener.h>
 #include <OgreRoot.h>
 #include <OgreRoot.h>
@@ -130,10 +132,35 @@ class Background2D : public Ogre::RenderQueueListener{
         /**
         /**
          * Scrolls the background to the position of the playable character.
          * 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] type Scroll type.
          * @param[in] seconds Scroll duration.
          * @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.
          * 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.
          * @return -1.
          */
          */
@@ -162,6 +189,33 @@ class Background2D : public Ogre::RenderQueueListener{
          */
          */
         void ScriptOffset(const float x, const float y);
         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.
          * Stops the current scrolling.
          */
          */
@@ -282,12 +336,15 @@ class Background2D : public Ogre::RenderQueueListener{
          * @param[in] u2
          * @param[in] u2
          * @param[in] v2
          * @param[in] v2
          * @param[in] blending Blending mode for the tile
          * @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 Depth is z-index??
          * @todo What are v1, v2, u1 and u2?
          * @todo What are v1, v2, u1 and u2?
          */
          */
         void AddTile(
         void AddTile(
           const int x, const int y, const int width, const int height, const float depth,
           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.
              * Tile blending mode.
              */
              */
             Blending blending;
             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;
         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.
          * Loads a background.
          *
          *
@@ -450,6 +547,30 @@ class Background2D : public Ogre::RenderQueueListener{
          */
          */
         virtual void applyScroll();
         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.
          * Calculates the screen scale.
          *
          *
@@ -512,6 +633,11 @@ class Background2D : public Ogre::RenderQueueListener{
          */
          */
         TileList tiles_;
         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.
          * Alpha blending render operation.
          */
          */
@@ -602,6 +728,16 @@ class Background2D : public Ogre::RenderQueueListener{
          */
          */
         float scroll_current_seconds_;
         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.
          * @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::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_;}
 bool Enemy::IsFront() const{return front_;}

+ 3 - 3
src/core/Enemy.h

@@ -15,6 +15,7 @@
 
 
 #pragma once
 #pragma once
 
 
+#include <tuple>
 #include <OgreString.h>
 #include <OgreString.h>
 #include "ScriptManager.h"
 #include "ScriptManager.h"
 
 
@@ -555,10 +556,9 @@ class Enemy{
         /**
         /**
          * Retrieves the enemy position.
          * 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.
          * 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_position_(Ogre::Vector3(0, 0, 0)),
   move_entity_(nullptr),
   move_entity_(nullptr),
   move_stop_distance_(0.0f),
   move_stop_distance_(0.0f),
+  scripted_move_(false),
   move_triangle_id_(-1),
   move_triangle_id_(-1),
   move_auto_rotation_(true),
   move_auto_rotation_(true),
   move_auto_animation_(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();}
 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();
     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){
 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_;}
 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_;}
 const Ogre::Vector3& Entity::GetMovePosition() const{return move_position_;}
 
 
 float Entity::GetMoveStopDistance() const{return move_stop_distance_;}
 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;}
 void Entity::SetMoveTriangleId(const int triangle){move_triangle_id_ = triangle;}
 
 
 int Entity::GetMoveTriangleId() const{return move_triangle_id_;}
 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_position_ = Ogre::Vector3(x, y, 0);
     move_entity_ = nullptr;
     move_entity_ = nullptr;
     move_stop_distance_ = 0;
     move_stop_distance_ = 0;
+    scripted_move_ = true;
     LOG_TRIVIAL(
     LOG_TRIVIAL(
       "Entity '" + name_ + "' set move to walkmesh position '"
       "Entity '" + name_ + "' set move to walkmesh position '"
       + Ogre::StringConverter::toString(move_position_) + "'."
       + Ogre::StringConverter::toString(move_position_) + "'."
@@ -398,6 +403,7 @@ void Entity::ScriptMoveToEntity(Entity* entity){
     move_position_ = entity->GetPosition();
     move_position_ = entity->GetPosition();
     move_entity_ = entity;
     move_entity_ = entity;
     move_stop_distance_ = GetSolidRadius() + entity->GetSolidRadius();
     move_stop_distance_ = GetSolidRadius() + entity->GetSolidRadius();
+    scripted_move_ = true;
     LOG_TRIVIAL("Entity '" + name_ + "' set move to entity '" + entity->GetName() + "'.");
     LOG_TRIVIAL("Entity '" + name_ + "' set move to entity '" + entity->GetName() + "'.");
 }
 }
 
 
@@ -415,6 +421,7 @@ void Entity::UnsetMove(){
     state_ = Entity::NONE;
     state_ = Entity::NONE;
     move_entity_ = nullptr;
     move_entity_ = nullptr;
     move_stop_distance_ = 0;
     move_stop_distance_ = 0;
+    scripted_move_ = false;
     for (const auto &script : sync_) ScriptManager::getSingleton().ContinueScriptExecution(script);
     for (const auto &script : sync_) ScriptManager::getSingleton().ContinueScriptExecution(script);
     sync_.clear();
     sync_.clear();
 }
 }

+ 12 - 3
src/core/Entity.h

@@ -15,6 +15,7 @@
 
 
 #pragma once
 #pragma once
 
 
+#include <tuple>
 #include <OgreString.h>
 #include <OgreString.h>
 #include "EntityCollision.h"
 #include "EntityCollision.h"
 #include "EntityDirection.h"
 #include "EntityDirection.h"
@@ -243,10 +244,9 @@ class Entity{
         /**
         /**
          * Retrieves the entity position.
          * 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.
          * Sets the entity position.
@@ -534,6 +534,13 @@ class Entity{
          */
          */
         float GetMoveStopDistance() const;
         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.
          * Sets the destination triangle in the walkmesh.
          *
          *
@@ -1214,6 +1221,8 @@ class Entity{
          */
          */
         float move_stop_distance_;
         float move_stop_distance_;
 
 
+        bool scripted_move_;
+
         /**
         /**
          * Entity's movement destination triangle ID.
          * Entity's movement destination triangle ID.
          */
          */

+ 37 - 15
src/core/EntityManager.cpp

@@ -204,17 +204,8 @@ void EntityManager::UpdateField(){
 
 
         // Update screen scroll.
         // Update screen scroll.
         Entity* scroll_entity = background_2d_.GetAutoScrollEntity();
         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.
         // Update offset.
         if (entity_[i]->GetOffsetType() != AT_NONE) SetNextOffsetStep(entity_[i]);
         if (entity_[i]->GetOffsetType() != AT_NONE) SetNextOffsetStep(entity_[i]);
         // Update turn.
         // Update turn.
@@ -243,7 +234,19 @@ void EntityManager::UpdateField(){
                             if (player_run_ == true) speed *= 4;
                             if (player_run_ == true) speed *= 4;
                         }
                         }
                         else speed = entity_[i]->GetMoveAutoSpeed();
                         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 (entity_[i]->GetMoveAutoAnimation() == true){
                             if (is_move == true){
                             if (is_move == true){
                                 if (speed >= entity_[i]->GetMoveRunSpeed()){
                                 if (speed >= entity_[i]->GetMoveRunSpeed()){
@@ -842,7 +845,11 @@ bool EntityManager::SetEntityOnWalkmesh(Entity* entity){
     return true;
     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()){
     if (IsBattleModule()){
         LOG_WARNING("Tried to perform a walkmesh move, but the EntityManager is in battle mode");
         LOG_WARNING("Tried to perform a walkmesh move, but the EntityManager is in battle mode");
         return false;
         return false;
@@ -850,9 +857,22 @@ bool EntityManager::PerformWalkmeshMove(Entity* entity, const float speed){
     Ogre::Vector3 start_point = entity->GetPosition();
     Ogre::Vector3 start_point = entity->GetPosition();
     Ogre::Vector3 move_vector = entity->GetMovePosition() - start_point;
     Ogre::Vector3 move_vector = entity->GetMovePosition() - start_point;
     Ogre::Vector2 direction(move_vector.x, move_vector.y);
     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.normalise();
-    direction *= Timer::getSingleton().GetGameTimeDelta();
+    direction *= delta_time;
     direction *= speed;
     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 movement is still needed but speed or time don't allow it, just return for now.
     if (direction.isZeroLength() == true){
     if (direction.isZeroLength() == true){
         if (player_entity_ == entity) CheckTriggers(entity, entity->GetPosition());
         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.
     // If the entity has come to destination point, finish movement.
     Ogre::Vector3 final = entity->GetMovePosition() - end_point;
     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();
         entity->UnsetMove();
 
 
     return true;
     return true;
@@ -1514,6 +1535,7 @@ void EntityManager::SetNextJumpStep(Entity* entity){
 
 
 void EntityManager::SetNextScrollStep(){
 void EntityManager::SetNextScrollStep(){
     // TODO: Allow this on battle mode?
     // TODO: Allow this on battle mode?
+    background_2d_.UpdateScrollTarget();
     Background2D::SCROLL_TYPE type = background_2d_.GetScrollType();
     Background2D::SCROLL_TYPE type = background_2d_.GetScrollType();
     float total = background_2d_.GetScrollSeconds();
     float total = background_2d_.GetScrollSeconds();
     float current = background_2d_.GetScrollCurrentSeconds();
     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();
         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.
          * Handles an input event.
          *
          *
@@ -533,10 +543,11 @@ class EntityManager : public Manager, public Ogre::Singleton<EntityManager>{
          *
          *
          * @param[in] entity Entity to move.
          * @param[in] entity Entity to move.
          * @param[in] speed Movement speed.
          * @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.
          * @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.
          * 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.
          * 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
 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
 #pragma once
+#include <tuple>
 #include <OgreRoot.h>
 #include <OgreRoot.h>
 
 
 /**
 /**
@@ -62,9 +63,11 @@ class EntityPoint{
         const Ogre::Vector3& GetPosition() const;
         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.
          * 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 + "\"."
                           + "\" 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(
                         sol::object table = GetTableByEntityName(
                           type, current_script_id_.entity,
                           type, current_script_id_.entity,
                           script_entity_[i].queue[0].state
                           script_entity_[i].queue[0].state
@@ -469,10 +468,9 @@ void ScriptManager::RemoveEntityTopScript(ScriptEntity& entity){
         // Delete the thread.
         // Delete the thread.
         luaL_unref(lua_state_, LUA_REGISTRYINDEX, entity.queue[0].state_id);
         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());
         entity.queue.erase(entity.queue.begin());
     }
     }
 }
 }
@@ -601,6 +599,17 @@ int ScriptManager::ScriptRequestStartSync(
         return 1;
         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);
     bool added = ScriptRequest(script_entity, function, priority, "", "", true, false);
 
 
     if (added == false){
     if (added == false){
@@ -671,15 +680,24 @@ bool ScriptManager::ScriptRequest(
         script.seconds_to_wait = 0;
         script.seconds_to_wait = 0;
         script.wait = false;
         script.wait = false;
         script.yield = 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){
         if (allow_duplicate == false){
           for (int i = 0; i < script_entity->queue.size(); i ++){
           for (int i = 0; i < script_entity->queue.size(); i ++){
              if (script_entity->queue[i].function == function){
              if (script_entity->queue[i].function == function){
                script_entity->resort = true;
                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;
                return true;
             }
             }
             }
             }

+ 10 - 4
src/core/ScriptManager.h

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

+ 5 - 1
src/core/XmlBackground2DFile.cpp

@@ -68,9 +68,13 @@ void XmlBackground2DFile::Load(){
                   = CameraManager::getSingleton().GetCurrentCamera()
                   = CameraManager::getSingleton().GetCurrentCamera()
                     ->getProjectionMatrixWithRSDepth() * distance;
                     ->getProjectionMatrixWithRSDepth() * distance;
                 res = res / res.w;
                 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(
                 background->AddTile(
                   static_cast<int>(destination.x), static_cast<int>(destination.y),
                   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();
                 TiXmlNode* node2 = node->FirstChild();
                 while (node2 != nullptr){
                 while (node2 != nullptr){

+ 14 - 2
src/data/VGearsAFile.cpp

@@ -23,6 +23,10 @@
 
 
 namespace VGears{
 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 Ogre::Real AFile::FRAME_DURATION(1.0f / 30.0f);
 
 
    const String AFile::RESOURCE_TYPE("VGearsAFile");
    const String AFile::RESOURCE_TYPE("VGearsAFile");
@@ -39,6 +43,14 @@ namespace VGears{
 
 
     void AFile::SetBoneCount(const uint32 bone_count){bone_count_ = bone_count;}
     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(){
     void AFile::loadImpl(){
         AFileSerializer serializer;
         AFileSerializer serializer;
         Ogre::DataStreamPtr stream(openResource());
         Ogre::DataStreamPtr stream(openResource());
@@ -77,7 +89,7 @@ namespace VGears{
         key_frame->setRotation(rot);
         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;
         if(skeleton->hasAnimation(name)) return;
         Ogre::Real length((frames_.size() - 1) * FRAME_DURATION);
         Ogre::Real length((frames_.size() - 1) * FRAME_DURATION);
         Ogre::Animation *anim(skeleton->createAnimation(name, length));
         Ogre::Animation *anim(skeleton->createAnimation(name, length));
@@ -94,7 +106,7 @@ namespace VGears{
         ){
         ){
             time = (index++) * FRAME_DURATION;
             time = (index++) * FRAME_DURATION;
             Ogre::TransformKeyFrame* key_frame(track->createNodeKeyFrame(time));
             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);
             SetFrameRotation(key_frame, frame->root_rotation);
         }
         }
         for(uint32 i(0); i < bone_count_; ++ i){
         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,out] skeleton Skeleton to add the animation to.
              * @param[in] name Animation name.
              * @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;
             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& triangles(walkmesh->GetTriangles());
         WalkmeshFile::TriangleList::iterator it(triangles.begin());
         WalkmeshFile::TriangleList::iterator it(triangles.begin());
         WalkmeshFile::TriangleList::const_iterator it_end(triangles.end());
         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){
         while (it != it_end){
             it->a /= scale;
             it->a /= scale;
             it->b /= 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);
 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(
 std::string FieldDataInstaller::CreateGateWayScript(
   const std::string& gateway_entity_name, const std::string& target_map_name,
   const std::string& gateway_entity_name, const std::string& target_map_name,
   const std::string& source_spawn_point_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";
         auto mesh_name = VGears::NameLookup::model(base_name) + ".mesh";
         Ogre::MeshPtr mesh(Ogre::MeshManager::getSingleton().load(mesh_name, "FFVII"));
         Ogre::MeshPtr mesh(Ogre::MeshManager::getSingleton().load(mesh_name, "FFVII"));
         Ogre::SkeletonPtr skeleton(mesh->getSkeleton());
         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){
         for (auto& anim : model->second){
             VGears::AFileManager &afl_mgr(VGears::AFileManager::GetSingleton());
             VGears::AFileManager &afl_mgr(VGears::AFileManager::GetSingleton());
             VGears::AFilePtr a = afl_mgr.load(anim, "FFVII").staticCast<VGears::AFile>();
             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.
             // Convert the FF7 name to a more readable name set in the meta data.
             VGears::StringUtil::splitBase(anim, base_name);
             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);
         ExportMesh(output_dir_ + FIELD_MODELS_DIR + "/", mesh);
     }
     }
@@ -511,7 +538,7 @@ void FieldDataInstaller::PcFieldToVGearsField(VGears::FLevelFilePtr& field){
 
 
         // Decompile to LUA.
         // Decompile to LUA.
         decompiled = FieldDecompiler::Decompile(
         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");
         std::ofstream script_file(output_dir_ + "/" + FIELD_MAPS_DIR + "/" + field->getName() + "/script.lua");
         if (script_file.is_open()){
         if (script_file.is_open()){
@@ -611,7 +638,8 @@ void FieldDataInstaller::PcFieldToVGearsField(VGears::FLevelFilePtr& field){
                       << field->GetWalkmesh()->GetTriangles().size() << ")" << std::endl;
                       << field->GetWalkmesh()->GetTriangles().size() << ")" << std::endl;
                     triangle_index = 0;
                     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(
                 const Ogre::Vector3 position(
                   gateway.destination.x / downscaler_next, gateway.destination.y / downscaler_next,
                   gateway.destination.x / downscaler_next, gateway.destination.y / downscaler_next,
                   z_of_triangle_with_id
                   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.
         // Get lines. Add them to a list, so they aren't processed later as regular entities.
         std::vector<std::string> line_entities;
         std::vector<std::string> line_entities;
+        const float line_scale = LINE_SCALE_FACTOR / GetFieldScaleFactor(this_field_id);
         for (FieldDecompiler::Line line : decompiled.lines){
         for (FieldDecompiler::Line line : decompiled.lines){
 
 
             std::unique_ptr<TiXmlElement> xml_entity_trigger(new TiXmlElement("entity_trigger"));
             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("name", line.name);
             xml_entity_trigger->SetAttribute(
             xml_entity_trigger->SetAttribute(
               "point1",
               "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(
             xml_entity_trigger->SetAttribute(
               "point2",
               "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");
             xml_entity_trigger->SetAttribute("enabled", "true");
             element->LinkEndChild(xml_entity_trigger.release());
             element->LinkEndChild(xml_entity_trigger.release());
@@ -816,6 +845,18 @@ void FieldDataInstaller::PcFieldToVGearsField(VGears::FLevelFilePtr& field){
                     // TODO: Should probably throw to fail conversion:
                     // TODO: Should probably throw to fail conversion:
                     else blending_str = "unknown";
                     else blending_str = "unknown";
                     xml_element->SetAttribute("blending", blending_str);
                     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());
                     bg_element->LinkEndChild(xml_element.release());
                 }
                 }
                 //}
                 //}
@@ -827,13 +868,15 @@ void FieldDataInstaller::PcFieldToVGearsField(VGears::FLevelFilePtr& field){
     {
     {
         // Save out the walk mesh as XML.
         // Save out the walk mesh as XML.
         const VGears::WalkmeshFilePtr& walkmesh = field->GetWalkmesh();
         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;
         TiXmlDocument doc;
         std::unique_ptr<TiXmlElement> wmesh_element(new TiXmlElement("walkmesh"));
         std::unique_ptr<TiXmlElement> wmesh_element(new TiXmlElement("walkmesh"));
         for (VGears::WalkmeshFile::Triangle& tri : walkmesh->GetTriangles()){
         for (VGears::WalkmeshFile::Triangle& tri : walkmesh->GetTriangles()){
             std::unique_ptr<TiXmlElement> xml_element(new TiXmlElement("triangle"));
             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("a_b", std::to_string(tri.access_side[0]));
             xml_element->SetAttribute("b_c", std::to_string(tri.access_side[1]));
             xml_element->SetAttribute("b_c", std::to_string(tri.access_side[1]));
             xml_element->SetAttribute("c_a", std::to_string(tri.access_side[2]));
             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);
             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:
     private:
 
 
         /**
         /**
@@ -395,6 +421,23 @@ class FieldDataInstaller{
          */
          */
         void ConvertModels(std::string model_name);
         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:
     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::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::GetFriendlyCharName(int char_id){return std::to_string(char_id);}
 
 
 std::string ScriptFormatter::GetFriendlyFunctionName(const std::string& entity_name, const std::string& function_name){
 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);
         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.
          * 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 <boost/algorithm/string/predicate.hpp>
 #include <stdexcept>
 #include <stdexcept>
 #include "FieldCodeGenerator.h"
 #include "FieldCodeGenerator.h"
+#include "FieldDisassembler.h"
 #include "FieldEngine.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::DELIMITER("_");
 
 
 const std::string FunctionMetaData::START("start");
 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()
                       "-- 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()
                       + " (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");
                     AddOutputLine("do return 0 end");
                 }
                 }
                 // Prevent backward jumps in the on_start script.
                 // 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);
         for(; ends_needed > 0; ends_needed --) AddOutputLine("end -- clean-up", true, false);
         // Add missing return:
         // 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);
             AddOutputLine("do return 0 end", false, false);
+        }
         OnEndFunction(function->first);
         OnEndFunction(function->first);
     }
     }
 
 
@@ -219,6 +227,12 @@ void FieldCodeGenerator::OnStartFunction(const Function& func){
         AddOutputLine("entity_manager:set_player_entity(\"Cloud\")");
         AddOutputLine("entity_manager:set_player_entity(\"Cloud\")");
         AddOutputLine("background2d:autoscroll_to_entity(entity_manager:get_entity(\"Cloud\"))\n");
         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.
     // For lines, exit if they are off.
     if (func.name == "on_approach" || func.name == "on_cross" || func.name == "on_near" || func.name == "on_leave"){
     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);
         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_;}
 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 FieldCodeGenerator::FormatInstructionNotImplemented(
   const std::string& entity, uint32 address, uint32 opcode
   const std::string& entity, uint32 address, uint32 opcode
 ){
 ){

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

@@ -16,6 +16,7 @@
 #pragma once
 #pragma once
 
 
 #include <deque>
 #include <deque>
+#include <set>
 #include <unordered_map>
 #include <unordered_map>
 #include <boost/algorithm/string.hpp>
 #include <boost/algorithm/string.hpp>
 #include "FieldScriptFormatter.h"
 #include "FieldScriptFormatter.h"
@@ -171,6 +172,19 @@ class FieldCodeGenerator : public CodeGenerator{
 
 
     public:
     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.
          * Type of data.
          */
          */
@@ -346,7 +360,26 @@ class FieldCodeGenerator : public CodeGenerator{
          */
          */
         FieldScriptFormatter& GetFormatter();
         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:
     protected:
 
 
@@ -418,4 +451,14 @@ class FieldCodeGenerator : public CodeGenerator{
          */
          */
         FieldScriptFormatter& formatter_;
         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;
     ds.luaScript = text_before + output.str() + text_after;
     for (auto entity : engine.GetEntityList()) ds.entities.push_back(entity);
     for (auto entity : engine.GetEntityList()) ds.entities.push_back(entity);
     for (auto line : engine.GetLineList()) ds.lines.push_back(line);
     for (auto line : engine.GetLineList()) ds.lines.push_back(line);
+    ds.background_areas = engine.GetBackgroundAreas();
+    ds.background_areas_dynamic = engine.BackgroundAreasAreDynamic();
     return ds;
     return ds;
 }
 }

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

@@ -18,6 +18,7 @@
 
 
 #pragma once
 #pragma once
 
 
+#include <set>
 #include <string>
 #include <string>
 #include "FieldScriptFormatter.h"
 #include "FieldScriptFormatter.h"
 #include "installer/decompiler/Decompiler.h"
 #include "installer/decompiler/Decompiler.h"
@@ -92,6 +93,20 @@ class FieldDecompiler: public Decompiler{
              * Lines in the field.
              * Lines in the field.
              */
              */
             std::vector<Line> lines;
             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;
     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(
 void FieldEngine::AddEntityFunction(
   const std::string& entity_name, size_t entity_index, const std::string& func_name, size_t func_index
   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
 #pragma once
 
 
+#include <set>
 #include <string>
 #include <string>
 #include <vector>
 #include <vector>
 
 
@@ -268,6 +269,27 @@ class FieldEngine : public Engine{
          */
          */
         std::vector<FieldDecompiler::Line> GetLineList() const;
         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.
          * Retrieves all entities in the map.
          *
          *
@@ -386,6 +408,16 @@ class FieldEngine : public Engine{
          */
          */
         float scale_factor_;
         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.
          * 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::BGPDH: code_gen->WriteTodo(md.GetEntityName(), "BGPDH");break;
         case OPCODES::BGSCR: code_gen->WriteTodo(md.GetEntityName(), "BGSCR"); break;
         case OPCODES::BGSCR: code_gen->WriteTodo(md.GetEntityName(), "BGSCR"); break;
         case OPCODES::MPPAL: code_gen->WriteTodo(md.GetEntityName(), "MPPAL"); 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::BGROL: code_gen->WriteTodo(md.GetEntityName(), "BGROL"); break;
         case OPCODES::BGROL2: code_gen->WriteTodo(md.GetEntityName(), "BGROL2"); 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::STPAL: ProcessSTPAL(code_gen); break;
         case OPCODES::LDPAL: ProcessLDPAL(code_gen); break;
         case OPCODES::LDPAL: ProcessLDPAL(code_gen); break;
         case OPCODES::CPPAL: ProcessCPPAL(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);
     FieldCodeGenerator* cg = static_cast<FieldCodeGenerator*>(code_gen);
     const auto& background_id = FieldCodeGenerator::FormatValueOrVariable(
     const auto& background_id = FieldCodeGenerator::FormatValueOrVariable(
       cg->GetFormatter(), params_[0]->GetUnsigned(), params_[2]->GetUnsigned()
       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()
       cg->GetFormatter(), params_[1]->GetUnsigned(), params_[3]->GetUnsigned()
     );
     );
     code_gen->AddOutputLine(
     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);
     FieldCodeGenerator* cg = static_cast<FieldCodeGenerator*>(code_gen);
     const auto& background_id = FieldCodeGenerator::FormatValueOrVariable(
     const auto& background_id = FieldCodeGenerator::FormatValueOrVariable(
       cg->GetFormatter(), params_[0]->GetUnsigned(), params_[2]->GetUnsigned()
       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()
       cg->GetFormatter(), params_[1]->GetUnsigned(), params_[3]->GetUnsigned()
     );
     );
     code_gen->AddOutputLine(
     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);
     FieldCodeGenerator* cg = static_cast<FieldCodeGenerator*>(code_gen);
     const auto& background_id = FieldCodeGenerator::FormatValueOrVariable(
     const auto& background_id = FieldCodeGenerator::FormatValueOrVariable(
       cg->GetFormatter(), params_[1]->GetUnsigned(), params_[2]->GetUnsigned()
       cg->GetFormatter(), params_[1]->GetUnsigned(), params_[2]->GetUnsigned()
     );
     );
     code_gen->AddOutputLine(
     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:
     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 ProcessSTPAL(CodeGenerator* code_gen);
         void ProcessLDPAL(CodeGenerator* code_gen);
         void ProcessLDPAL(CodeGenerator* code_gen);
         void ProcessCPPAL(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::SCR2D: ProcessSCR2D(code_gen); break;
     case OPCODES::SCRCC: ProcessSCRCC(code_gen); break;
     case OPCODES::SCRCC: ProcessSCRCC(code_gen); break;
     case OPCODES::SCR2DC: ProcessSCR2DC(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::SCR2DL: ProcessSCR2DL(code_gen); break;
     case OPCODES::VWOFT: ProcessVWOFT(code_gen); break;
     case OPCODES::VWOFT: ProcessVWOFT(code_gen); break;
     case OPCODES::FADE: ProcessFADE(code_gen); break;
     case OPCODES::FADE: ProcessFADE(code_gen); break;
     case OPCODES::FADEW: code_gen->WriteTodo(md.GetEntityName(), "FADEW"); break;
     case OPCODES::FADEW: code_gen->WriteTodo(md.GetEntityName(), "FADEW"); break;
     case OPCODES::SCRLP: code_gen->WriteTodo(md.GetEntityName(), "SCRLP"); break;
     case OPCODES::SCRLP: code_gen->WriteTodo(md.GetEntityName(), "SCRLP"); break;
     case OPCODES::MVCAM: code_gen->WriteTodo(md.GetEntityName(), "MVCAM"); 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:
     default:
         code_gen->AddOutputLine(FieldCodeGenerator::FormatInstructionNotImplemented(
         code_gen->AddOutputLine(FieldCodeGenerator::FormatInstructionNotImplemented(
           md.GetEntityName(), address_, opcode_
           md.GetEntityName(), address_, opcode_
@@ -79,10 +79,46 @@ void FieldCameraInstruction::ProcessNFADE(CodeGenerator* code_gen){
 }
 }
 
 
 void FieldCameraInstruction::ProcessSCRLC(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){
 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);
         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.
          * 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.
             //     'main' return is kept, the code of 'main' is never executed.
             //     There are safeguards forfunctions without a return in the end,
             //     There are safeguards forfunctions without a return in the end,
             //     so it's OK not to include it here.
             //     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");
             if (func.name != "on_start") code_gen->AddOutputLine("do return 0 end");
             break;
             break;
         case OPCODES::REQ: ProcessREQ(code_gen, eng); 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::CMOVE: code_gen->WriteTodo(md.GetEntityName(), "CMOVE"); break;
         case OPCODES::MOVA: code_gen->WriteTodo(md.GetEntityName(), "MOVA"); break;
         case OPCODES::MOVA: code_gen->WriteTodo(md.GetEntityName(), "MOVA"); break;
         case OPCODES::TURA: ProcessTURA(code_gen, md.GetEntityName(), eng); 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::MSPED: ProcessMSPED(code_gen, md.GetEntityName()); break;
         case OPCODES::DIR: ProcessDIR(code_gen, md.GetEntityName()); break;
         case OPCODES::DIR: ProcessDIR(code_gen, md.GetEntityName()); break;
         case OPCODES::TURNGEN: ProcessTURNGEN(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(
     code_gen->AddOutputLine(
       (boost::format("self.%1% = entity_manager:get_entity(\"%1%\")") % entity).str()
       (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(
 void FieldModelInstruction::ProcessDFANM(
@@ -267,6 +288,69 @@ void FieldModelInstruction::ProcessANIME1(
     code_gen->AddOutputLine((boost::format("self.%1%:animation_sync()") % entity).str());
     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){
 void FieldModelInstruction::ProcessVISI(CodeGenerator* code_gen, const std::string& entity){
     code_gen->AddOutputLine((
     code_gen->AddOutputLine((
       boost::format("self.%1%:set_visible(%2%)")
       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());
     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(
 void FieldModelInstruction::ProcessTURA(
   CodeGenerator* code_gen, const std::string& entity, const FieldEngine& engine
   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
     const bool literal_values
       = TryParseFloat(x_value, x) && TryParseFloat(y_value, y)
       = TryParseFloat(x_value, x) && TryParseFloat(y_value, y)
         && TryParseFloat(z_value, z) && TryParseFloat(speed_value, speed);
         && 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){
     if (literal_values){
         speed /= 30.0f;
         speed /= 30.0f;
         code_gen->AddOutputLine((
         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);
         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.
          * 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){
 void FieldModuleInstruction::ProcessBATTLE(CodeGenerator* code_gen){
     FieldCodeGenerator* cg = static_cast<FieldCodeGenerator*>(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(
     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());
     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.
     // Displays a dialog in the WINDOW that has previously been initialized to display this dialog.
     auto window_id = params_[0]->GetUnsigned();
     auto window_id = params_[0]->GetUnsigned();
     auto dialog_id = params_[1]->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((
     code_gen->AddOutputLine((
       boost::format("dialog:dialog_set_text(\"%1%\", \"%2%_%3%\")")
       boost::format("dialog:dialog_set_text(\"%1%\", \"%2%_%3%\")")
       % window_id % script_name % dialog_id
       % 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, "uv", dest.uv);
         ReadAttribute(node, "depth", dest.depth);
         ReadAttribute(node, "depth", dest.depth);
         ReadAttribute(node, "blending", dest.blending, B_ALPHA);
         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);
         ReadMap(node, "animation", "name", dest.animations);
     }
     }
 
 

+ 10 - 0
src/map/VGearsTile.h

@@ -111,6 +111,16 @@ namespace VGears{
          */
          */
         Blending blending;
         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.
          * 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;
         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::Vector3 model_size = bounds.getSize();
         const Ogre::Real model_height = model_size.y;
         const Ogre::Real model_height = model_size.y;
         if (model_height > 0){
         if (model_height > 0){

+ 13 - 5
test/core/EntityManager.cpp

@@ -16,11 +16,19 @@
 #include <boost/test/unit_test.hpp>
 #include <boost/test/unit_test.hpp>
 #include "core/EntityManager.h"
 #include "core/EntityManager.h"
 
 
-#pragma message("Unimplemented test file: " __FILE__)
-
 BOOST_AUTO_TEST_CASE(TestEntityManager){
 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 <boost/test/unit_test.hpp>
+#include "LuaIncludes.h"
 #include "core/EntityPoint.h"
 #include "core/EntityPoint.h"
 
 
-#pragma message("Unimplemented test file: " __FILE__)
-
 BOOST_AUTO_TEST_CASE(TestEntityPoint){
 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 <boost/test/unit_test.hpp>
+#include <OgreAnimation.h>
+#include <OgreAnimationTrack.h>
+#include <OgreKeyFrame.h>
+#include <OgreSkeleton.h>
 #include "data/VGearsAFile.h"
 #include "data/VGearsAFile.h"
 
 
-#pragma message("Unimplemented test file: " __FILE__)
-
 BOOST_AUTO_TEST_CASE(TestVGearsAFile){
 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__)
 #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){
 BOOST_AUTO_TEST_CASE(TestFieldDataInstaller){
     // TODO
     // TODO
     BOOST_WARN_MESSAGE(false, "Unimplemented test file: FieldDataInstaller");
     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 <boost/test/unit_test.hpp>
 #include "installer/decompiler/field/FieldDecompiler.h"
 #include "installer/decompiler/field/FieldDecompiler.h"
 
 
-#pragma message("Unimplemented test file: " __FILE__)
-
 BOOST_AUTO_TEST_CASE(TestFieldDecompiler){
 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);
 }
 }