ソースを参照

Merge branch 'feature/field_maps' into develop

Iñigo Valentin 4 日 前
親
コミット
f65f233d37
56 ファイル変更、3161 行追加、261 行削除
  1. 2 2
      data/data/config/config.cfg
  2. 2 0
      src/CMakeLists.txt
  3. 320 71
      src/core/Background2D.cpp
  4. 276 3
      src/core/Background2D.h
  5. 42 0
      src/core/ConfigCmdHandlerCommands.h
  6. 2 4
      src/core/Enemy.cpp
  7. 3 3
      src/core/Enemy.h
  8. 26 5
      src/core/Entity.cpp
  9. 27 3
      src/core/Entity.h
  10. 90 31
      src/core/EntityManager.cpp
  11. 65 1
      src/core/EntityManager.h
  12. 4 0
      src/core/EntityModel.cpp
  13. 2 4
      src/core/EntityPoint.cpp
  14. 5 2
      src/core/EntityPoint.h
  15. 196 0
      src/core/ScreenFade.cpp
  16. 160 0
      src/core/ScreenFade.h
  17. 170 0
      src/core/ScreenFadeState.cpp
  18. 226 0
      src/core/ScreenFadeState.h
  19. 30 12
      src/core/ScriptManager.cpp
  20. 10 4
      src/core/ScriptManager.h
  21. 17 1
      src/core/ScriptManagerBinds.h
  22. 5 1
      src/core/XmlBackground2DFile.cpp
  23. 14 2
      src/data/VGearsAFile.cpp
  24. 10 1
      src/data/VGearsAFile.h
  25. 2 3
      src/data/VGearsFLevelFileSerializer.cpp
  26. 55 12
      src/installer/FieldDataInstaller.cpp
  27. 61 0
      src/installer/FieldDataInstaller.h
  28. 4 0
      src/installer/decompiler/ScriptFormatter.cpp
  29. 24 0
      src/installer/decompiler/ScriptFormatter.h
  30. 35 1
      src/installer/decompiler/field/FieldCodeGenerator.cpp
  31. 43 0
      src/installer/decompiler/field/FieldCodeGenerator.h
  32. 2 0
      src/installer/decompiler/field/FieldDecompiler.cpp
  33. 15 0
      src/installer/decompiler/field/FieldDecompiler.h
  34. 8 1
      src/installer/decompiler/field/FieldDisassembler.cpp
  35. 9 0
      src/installer/decompiler/field/FieldEngine.cpp
  36. 32 0
      src/installer/decompiler/field/FieldEngine.h
  37. 12 9
      src/installer/decompiler/field/instruction/FieldBackgroundInstruction.cpp
  38. 3 3
      src/installer/decompiler/field/instruction/FieldBackgroundInstruction.h
  39. 70 41
      src/installer/decompiler/field/instruction/FieldCameraInstruction.cpp
  40. 51 0
      src/installer/decompiler/field/instruction/FieldCameraInstruction.h
  41. 1 1
      src/installer/decompiler/field/instruction/FieldCondJumpInstruction.cpp
  42. 4 0
      src/installer/decompiler/field/instruction/FieldControlFlowInstruction.cpp
  43. 203 17
      src/installer/decompiler/field/instruction/FieldModelInstruction.cpp
  44. 151 1
      src/installer/decompiler/field/instruction/FieldModelInstruction.h
  45. 4 1
      src/installer/decompiler/field/instruction/FieldModuleInstruction.cpp
  46. 11 1
      src/installer/decompiler/field/instruction/FieldWindowInstruction.cpp
  47. 2 0
      src/map/VGearsBackground2DFileXMLSerializer.cpp
  48. 10 0
      src/map/VGearsTile.h
  49. 5 1
      src/modules/worldmap/WorldmapModule.cpp
  50. 1 0
      test/CMakeLists.txt
  51. 64 4
      test/core/EntityManager.cpp
  52. 14 5
      test/core/EntityPoint.cpp
  53. 163 0
      test/core/ScreenFadeState.cpp
  54. 32 5
      test/data/VGearsAFile.cpp
  55. 11 0
      test/installer/FieldDataInstaller.cpp
  56. 355 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"

+ 2 - 0
src/CMakeLists.txt

@@ -600,6 +600,8 @@ set(VGEARS_SOURCE_FILES
     core/AudioManager.cpp
     core/AudioManager.cpp
     core/Background2DAnimation.cpp
     core/Background2DAnimation.cpp
     core/Background2D.cpp
     core/Background2D.cpp
+    core/ScreenFade.cpp
+    core/ScreenFadeState.cpp
     core/BattleManager.cpp
     core/BattleManager.cpp
     core/CameraManager.cpp
     core/CameraManager.cpp
     core/ConfigCmd.cpp
     core/ConfigCmd.cpp

+ 320 - 71
src/core/Background2D.cpp

@@ -19,6 +19,7 @@
 #include <OgreException.h>
 #include <OgreException.h>
 #include <OgreViewport.h>
 #include <OgreViewport.h>
 #include <OgreTechnique.h>
 #include <OgreTechnique.h>
+#include <algorithm>
 #include "core/Background2D.h"
 #include "core/Background2D.h"
 #include "core/CameraManager.h"
 #include "core/CameraManager.h"
 #include "core/EntityManager.h"
 #include "core/EntityManager.h"
@@ -41,6 +42,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),
@@ -119,6 +122,11 @@ void Background2D::Hide(){scene_manager_->removeRenderQueueListener(this);}
 void Background2D::Show(){scene_manager_->addRenderQueueListener(this);}
 void Background2D::Show(){scene_manager_->addRenderQueueListener(this);}
 
 
 void Background2D::Update(){
 void Background2D::Update(){
+    if (has_pending_scroll_){
+        has_pending_scroll_ = false;
+        SetScroll(pending_scroll_);
+    }
+    UpdateShake(Timer::getSingleton().GetGameTimeDelta());
     for (unsigned int i = 0; i < animation_played_.size(); ++ i){
     for (unsigned int i = 0; i < animation_played_.size(); ++ i){
         for (unsigned int j = 0; j < animations_.size(); ++ j){
         for (unsigned int j = 0; j < animations_.size(); ++ j){
             if (animations_[j]->GetName() == animation_played_[i].name){
             if (animations_[j]->GetName() == animation_played_[i].name){
@@ -187,65 +195,217 @@ 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();
+}
+
+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);
+}
+
+namespace{
+    // Random factors the original game applies to each bounce of a shake.
+    const float SHAKE_FACTORS[] = {
+      -1.0f, 1.14f, -1.34f, 0.61f, -0.49f, 0.64f, -0.25f, 0.47f, -1.20f, -0.01f, 1.14f, -0.86f,
+      0.81f, -0.58f, 0.68f, -1.31f, 0.44f, -0.91f, 0.03f, -0.97f, 1.25f, -0.76f, 0.48f, -0.99f,
+      1.34f, -0.85f, 0.55f, -0.65f, 0.07f, -0.83f
+    };
+    const size_t SHAKE_FACTOR_COUNT = sizeof(SHAKE_FACTORS) / sizeof(SHAKE_FACTORS[0]);
+
+    // Frames per second the shake times are given in.
+    const float SHAKE_FRAMES_PER_SECOND = 30.0f;
+
+    // Pixels of the original screen.
+    const float ORIGINAL_SCREEN_WIDTH = 320.0f;
+
+    // Quadratic in and out easing.
+    float ShakeEase(const float t){return t < 0.5f ? 2 * t * t : 1 - 2 * (1 - t) * (1 - t);}
+}
+
+void Background2D::ShakeAxis::Configure(const float new_amplitude, const float new_seconds){
+    amplitude = new_amplitude;
+    seconds = std::max(new_seconds, 1.0f / SHAKE_FRAMES_PER_SECOND);
+    stopping = false;
+    if (!running){
+        running = true;
+        StartBounce();
+    }
+}
+
+void Background2D::ShakeAxis::Stop(const float seconds_to_center){
+    if (!running) return;
+    stopping = true;
+    stop_seconds = std::max(seconds_to_center, 1.0f / SHAKE_FRAMES_PER_SECOND);
+}
+
+void Background2D::ShakeAxis::StartBounce(){
+    from = value;
+    elapsed = 0;
+    if (stopping){
+        to = 0;
+        duration = stop_seconds;
     }
     }
+    else{
+        // Half the amplitude, times the random factor, is the distance in pixels.
+        to = amplitude / 2 * SHAKE_FACTORS[step % SHAKE_FACTOR_COUNT];
+        ++ step;
+        duration = seconds;
+    }
+}
+
+bool Background2D::ShakeAxis::Advance(const float delta){
+    if (!running) return false;
+    const float old_value = value;
+    elapsed += delta;
+    while (elapsed >= duration){
+        elapsed -= duration;
+        value = to;
+        if (stopping && to == 0){
+            // Back in the center: the shake is over.
+            *this = ShakeAxis();
+            return old_value != 0;
+        }
+        StartBounce();
+    }
+    value = from + (to - from) * ShakeEase(elapsed / duration);
+    return value != old_value;
+}
+
+void Background2D::UpdateShake(const float delta){
+    if (!shake_x_.running && !shake_y_.running) return;
+    const bool x_changed = shake_x_.Advance(delta);
+    const bool y_changed = shake_y_.Advance(delta);
+    if (!x_changed && !y_changed) return;
+    const float scale = virtual_screen_size_.x / ORIGINAL_SCREEN_WIDTH;
+    shake_offset_ = Ogre::Vector2(shake_x_.value * scale, shake_y_.value * scale);
     applyScroll();
     applyScroll();
 }
 }
 
 
+void Background2D::ScriptShake(
+  const int type, const int x_amplitude, const int x_frames, const int y_amplitude,
+  const int y_frames
+){
+    LOG_TRIVIAL("Background2d shake type " + std::to_string(type) + ".");
+    const float x_seconds = x_frames / SHAKE_FRAMES_PER_SECOND;
+    const float y_seconds = y_frames / SHAKE_FRAMES_PER_SECOND;
+    switch (type){
+        case 0:
+            {
+                const float seconds = std::max(x_seconds, y_seconds);
+                shake_x_.Stop(seconds);
+                shake_y_.Stop(seconds);
+            }
+            break;
+        case 1: shake_x_.Configure(static_cast<float>(x_amplitude), x_seconds); break;
+        case 2: shake_y_.Configure(static_cast<float>(y_amplitude), y_seconds); break;
+        case 3:
+            shake_x_.Configure(static_cast<float>(x_amplitude), x_seconds);
+            shake_y_.Configure(static_cast<float>(y_amplitude), y_seconds);
+            break;
+        default: LOG_WARNING("Background2d shake type " + std::to_string(type) + " is unknown.");
+    }
+}
+
+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;
+    has_pending_scroll_ = 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 +418,10 @@ 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();
+    shake_x_ = ShakeAxis();
+    shake_y_ = ShakeAxis();
+    shake_offset_ = Ogre::Vector2::ZERO;
     DestroyVertexBuffers();
     DestroyVertexBuffers();
     CreateVertexBuffers();
     CreateVertexBuffers();
 }
 }
@@ -266,17 +430,63 @@ 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
+){
+    // A newer scroll replaces an instant one that has not been applied yet.
+    has_pending_scroll_ = false;
+    // 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(
@@ -284,22 +494,37 @@ void Background2D::ScriptScrollToPosition(
 ){
 ){
     LOG_TRIVIAL(
     LOG_TRIVIAL(
       "Background2d set scroll to position '"
       "Background2d set scroll to position '"
-      + Ogre::StringConverter::toString(Ogre::Vector2(x, y)) + "'."
+      + Ogre::StringConverter::toString(Ogre::Vector2(x, y)) + "', current '"
+      + Ogre::StringConverter::toString(position_) + "', player would be at '"
+      + (EntityManager::getSingleton().ScriptGetPlayerEntity() != nullptr
+        ? Ogre::StringConverter::toString(
+            GetEntityScroll(EntityManager::getSingleton().ScriptGetPlayerEntity())
+          )
+        : Ogre::String("none"))
+      + "', range " + Ogre::StringConverter::toString(range_.getMinimum()) + " "
+      + Ogre::StringConverter::toString(range_.getMaximum()) + "."
     );
     );
-    Ogre::Vector2 position = Ogre::Vector2(x, y);
     scroll_entity_ = nullptr;
     scroll_entity_ = nullptr;
-    if (type == Background2D::NONE){
-        SetScroll(position);
+    if (type == Background2D::NONE || seconds <= 0){
+        // The scripts set a position and then scroll from it to another one, in the same frame.
+        // The camera is only moved at the end of the frame, so if another scroll comes first the
+        // movement starts from where the camera was, and there is no jump.
+        // TODO: Some scenes still show a small camera jump (mds7pb_1, when the NPCs go down the
+        // pinball machine and when Marlene says "Papa"), and the camera stays fixed after them
+        // until the script scrolls to the player. How the original game handles it is not known.
+        scroll_target_entity_ = nullptr;
+        scroll_follow_target_ = false;
+        UnsetScroll();
+        pending_scroll_ = Ogre::Vector2(x, y);
+        has_pending_scroll_ = true;
         return;
         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 +547,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,22 +572,27 @@ 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(){
     if(cv_background2d_manual.GetB() != true){
     if(cv_background2d_manual.GetB() != true){
-        position_real_ = GetScreenScroll();
+        position_real_ = (GetScroll() + shake_offset_) * screen_scale_;
         position_real_.x += offset_.x;
         position_real_.x += offset_.x;
         position_real_.y += offset_.y;
         position_real_.y += offset_.y;
         CameraManager::getSingleton().Set2DScroll(position_real_);
         CameraManager::getSingleton().Set2DScroll(position_real_);
@@ -415,7 +648,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 +674,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 +713,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 +799,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 +1009,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){

+ 276 - 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] seconds Scroll duration.
+         */
+        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] type Scroll type.
          * @param[in] seconds Scroll duration.
          * @param[in] seconds Scroll duration.
          */
          */
-        void ScriptScrollToPlayer(const SCROLL_TYPE type, const unsigned int seconds);
+        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,52 @@ 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);
+
+        /**
+         * Shakes the view.
+         *
+         * The shake is added to the scroll, and doesn't change it. Each axis is a chain of random
+         * bounces of the given amplitude, one every given number of frames. Changing the
+         * parameters of a shake that is running takes effect when the current bounce ends.
+         *
+         * @param[in] type 0 stops the shake and brings the view back, 1 shakes the X axis, 2 the
+         * Y axis and 3 both.
+         * @param[in] x_amplitude Amplitude of the bounces in the X axis, in original screen pixels.
+         * @param[in] x_frames Frames between bounces in the X axis.
+         * @param[in] y_amplitude Amplitude of the bounces in the Y axis, in original screen pixels.
+         * @param[in] y_frames Frames between bounces in the Y axis.
+         */
+        void ScriptShake(
+          const int type, const int x_amplitude, const int x_frames, const int y_amplitude,
+          const int y_frames
+        );
+
         /**
         /**
          * Stops the current scrolling.
          * Stops the current scrolling.
          */
          */
@@ -282,12 +355,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 +493,140 @@ 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;
+
+        /**
+         * A chain of random bounces in one axis of the view.
+         */
+        struct ShakeAxis{
+
+            /**
+             * Amplitude of the next bounces, in original screen pixels.
+             */
+            float amplitude = 0;
+
+            /**
+             * Duration of the next bounces, in seconds.
+             */
+            float seconds = 0;
+
+            /**
+             * Whether the axis is moving.
+             */
+            bool running = false;
+
+            /**
+             * Whether the axis is going back to the center to stop.
+             */
+            bool stopping = false;
+
+            /**
+             * Duration of the movement back to the center, in seconds.
+             */
+            float stop_seconds = 0;
+
+            /**
+             * Position at the start of the current bounce.
+             */
+            float from = 0;
+
+            /**
+             * Position at the end of the current bounce.
+             */
+            float to = 0;
+
+            /**
+             * Duration of the current bounce, in seconds.
+             */
+            float duration = 0;
+
+            /**
+             * Time since the start of the current bounce, in seconds.
+             */
+            float elapsed = 0;
+
+            /**
+             * Index of the next random factor.
+             */
+            size_t step = 0;
+
+            /**
+             * Current position, in original screen pixels.
+             */
+            float value = 0;
+
+            /**
+             * Changes the bounces of the axis, and starts it if it is stopped.
+             */
+            void Configure(const float new_amplitude, const float new_seconds);
+
+            /**
+             * Makes the axis go back to the center and stop after the current bounce.
+             */
+            void Stop(const float seconds_to_center);
+
+            /**
+             * Moves the axis forward in time.
+             *
+             * @return True if the position changed.
+             */
+            bool Advance(const float delta);
+
+            /**
+             * Starts the next bounce.
+             */
+            void StartBounce();
+        };
+
+        /**
+         * Moves the shake forward in time, and applies it to the view if it changed.
+         *
+         * @param[in] delta Seconds since the last frame.
+         */
+        void UpdateShake(const float delta);
+
+        /**
+         * 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 +656,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 +742,26 @@ 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_;
+
+        /**
+         * The shake of the view in the X axis.
+         */
+        ShakeAxis shake_x_;
+
+        /**
+         * The shake of the view in the Y axis.
+         */
+        ShakeAxis shake_y_;
+
+        /**
+         * The shake being applied to the scroll, in virtual screen units.
+         */
+        Ogre::Vector2 shake_offset_ = Ogre::Vector2::ZERO;
+
         /**
         /**
          * Alpha blending render operation.
          * Alpha blending render operation.
          */
          */
@@ -602,6 +852,29 @@ 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_;
+
+        /**
+         * Position of an instant scroll that waits for the end of the frame.
+         *
+         * A script that sets a position and scrolls to another one in the same frame must see the
+         * scroll start where the camera was, not at the first position. See ScriptScrollToPosition.
+         */
+        Ogre::Vector2 pending_scroll_;
+
+        /**
+         * Whether there is an instant scroll waiting for the end of the frame.
+         */
+        bool has_pending_scroll_ = false;
+
         /**
         /**
          * @todo Document.
          * @todo Document.
          */
          */

+ 42 - 0
src/core/ConfigCmdHandlerCommands.h

@@ -374,6 +374,46 @@ void CmdScreenshot(const Ogre::StringVector& params){
 
 
 
 
 
 
+/**
+ * Starts a screen fade, like the FADE opcode does. It is meant to test the fades by hand.
+ *
+ * @param[in] params Command parameters: the command name, the fade type, red, green, blue and
+ * speed.
+ */
+void CmdFade(const Ogre::StringVector& params){
+    if (params.size() != 6){
+        Console::getSingleton().AddTextToOutput(
+          "Usage: /fade <type 1-10> <red 0-255> <green 0-255> <blue 0-255> <speed>"
+        );
+        return;
+    }
+    EntityManager::getSingleton().GetScreenFade()->ScriptFade(
+      Ogre::StringConverter::parseInt(params[1]), Ogre::StringConverter::parseInt(params[2]),
+      Ogre::StringConverter::parseInt(params[3]), Ogre::StringConverter::parseInt(params[4]),
+      Ogre::StringConverter::parseInt(params[5])
+    );
+}
+
+/**
+ * Starts a screen fade, like the NFADE opcode does. It is meant to test the fades by hand.
+ *
+ * @param[in] params Command parameters: the command name, the fade type (0 clears it, 11 adds
+ * and 12 subtracts), red, green, blue and frames.
+ */
+void CmdNFade(const Ogre::StringVector& params){
+    if (params.size() != 6){
+        Console::getSingleton().AddTextToOutput(
+          "Usage: /nfade <type 0, 11 or 12> <red 0-255> <green 0-255> <blue 0-255> <frames>"
+        );
+        return;
+    }
+    EntityManager::getSingleton().GetScreenFade()->ScriptNFade(
+      Ogre::StringConverter::parseInt(params[1]), Ogre::StringConverter::parseInt(params[2]),
+      Ogre::StringConverter::parseInt(params[3]), Ogre::StringConverter::parseInt(params[4]),
+      Ogre::StringConverter::parseInt(params[5])
+    );
+}
+
 /*
 /*
 void CmdViewer(const Ogre::StringVector& params){
 void CmdViewer(const Ogre::StringVector& params){
     if (params.size() == 2 || params.size() > 3){
     if (params.size() == 2 || params.size() > 3){
@@ -448,6 +488,8 @@ void ConfigCmdHandler::InitCmd(){
     AddCommand("map", "Run game module", "", CmdMap, CmdMapCompletion);
     AddCommand("map", "Run game module", "", CmdMap, CmdMapCompletion);
     AddCommand("resolution", "Change resolution", "", CmdResolution, CmdResolutionCompletition);
     AddCommand("resolution", "Change resolution", "", CmdResolution, CmdResolutionCompletition);
     AddCommand("screenshot", "Capture current screen content", "", CmdScreenshot, NULL);
     AddCommand("screenshot", "Capture current screen content", "", CmdScreenshot, NULL);
+    AddCommand("fade", "Start a screen fade", "<type> <r> <g> <b> <speed>", CmdFade, NULL);
+    AddCommand("nfade", "Start a screen nfade", "<type> <r> <g> <b> <frames>", CmdNFade, NULL);
     //AddCommand(
     //AddCommand(
     //  "viewer", "Run viewer module", "", CmdViewer, CmdViewerCompletion
     //  "viewer", "Run viewer module", "", CmdViewer, CmdViewerCompletion
     //);
     //);

+ 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.

+ 26 - 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_;}
@@ -378,6 +382,11 @@ void Entity::SetMoveAutoAnimation(const bool animate){move_auto_animation_ = ani
 
 
 bool Entity::GetMoveAutoAnimation() const{return move_auto_animation_;}
 bool Entity::GetMoveAutoAnimation() const{return move_auto_animation_;}
 
 
+void Entity::SetMoveAnimations(const char* walk, const char* run){
+    move_animation_walk_ = walk;
+    move_animation_run_ = run;
+}
+
 const Ogre::String& Entity::GetMoveAnimationWalkName() const{return move_animation_walk_;}
 const Ogre::String& Entity::GetMoveAnimationWalkName() const{return move_animation_walk_;}
 
 
 const Ogre::String& Entity::GetMoveAnimationRunName() const{return move_animation_run_;}
 const Ogre::String& Entity::GetMoveAnimationRunName() const{return move_animation_run_;}
@@ -387,6 +396,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 +408,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 +426,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();
 }
 }
@@ -603,6 +615,15 @@ Entity::ScriptTurnToEntity(Entity* entity, const TurnDirection turn_direction, c
     LOG_TRIVIAL("Entity '" + name_ + "' turn to entity '" + entity->GetName() + ".");
     LOG_TRIVIAL("Entity '" + name_ + "' turn to entity '" + entity->GetName() + ".");
 }
 }
 
 
+void Entity::ScriptFaceEntity(Entity* entity){
+    if (entity == nullptr || entity == this){
+        LOG_ERROR("Face entity: Invalid entity pointer (NULL or self).");
+        return;
+    }
+    SetRotation(GetDirectionToEntity(entity));
+    LOG_TRIVIAL("Entity '" + name_ + "' faces entity '" + entity->GetName() + "'.");
+}
+
 int Entity::ScriptTurnSync(){
 int Entity::ScriptTurnSync(){
     ScriptId script = ScriptManager::getSingleton().GetCurrentScriptId();
     ScriptId script = ScriptManager::getSingleton().GetCurrentScriptId();
     LOG_TRIVIAL(
     LOG_TRIVIAL(

+ 27 - 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.
          *
          *
@@ -590,6 +597,14 @@ class Entity{
          */
          */
         const Ogre::String& GetMoveAnimationWalkName() const;
         const Ogre::String& GetMoveAnimationWalkName() const;
 
 
+        /**
+         * Sets the animations played when the entity moves on its own.
+         *
+         * @param[in] walk Name of the walk animation.
+         * @param[in] run Name of the run animation.
+         */
+        void SetMoveAnimations(const char* walk, const char* run);
+
         /**
         /**
          * Retrieves the entity's run animation name.
          * Retrieves the entity's run animation name.
          *
          *
@@ -874,6 +889,13 @@ class Entity{
           const float seconds
           const float seconds
         );
         );
 
 
+        /**
+         * Makes the entity face another entity at once.
+         *
+         * @param[in] entity Entity to face.
+         */
+        void ScriptFaceEntity(Entity* entity);
+
         /**
         /**
          * Adds the entity's turn to the sync queue.
          * Adds the entity's turn to the sync queue.
          *
          *
@@ -1214,6 +1236,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.
          */
          */

+ 90 - 31
src/core/EntityManager.cpp

@@ -15,6 +15,7 @@
 
 
 #include <iostream>
 #include <iostream>
 #include <cmath>
 #include <cmath>
+#include <limits>
 #include <OgreEntity.h>
 #include <OgreEntity.h>
 #include <OgreRoot.h>
 #include <OgreRoot.h>
 #include <OgreViewport.h>
 #include <OgreViewport.h>
@@ -204,17 +205,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 +235,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()){
@@ -290,6 +294,7 @@ void EntityManager::UpdateField(){
     player_run_ = false;
     player_run_ = false;
     if (background_2d_.GetScrollType() != Background2D::NONE) SetNextScrollStep();
     if (background_2d_.GetScrollType() != Background2D::NONE) SetNextScrollStep();
     background_2d_.Update();
     background_2d_.Update();
+    screen_fade_.Update(Timer::getSingleton().GetGameTimeDelta());
 }
 }
 
 
 void EntityManager::UpdateBattle(){
 void EntityManager::UpdateBattle(){
@@ -453,6 +458,7 @@ void EntityManager::OnResize(){background_2d_.OnResize();}
 void EntityManager::ClearField(){
 void EntityManager::ClearField(){
     walkmesh_.Clear();
     walkmesh_.Clear();
     background_2d_.Clear();
     background_2d_.Clear();
+    screen_fade_.Clear();
     DialogsManager::getSingleton().Clear();
     DialogsManager::getSingleton().Clear();
     for (unsigned int i = 0; i < entity_.size(); ++ i){
     for (unsigned int i = 0; i < entity_.size(); ++ i){
         ScriptManager::getSingleton().RemoveEntity(ScriptManager::ENTITY, entity_[i]->GetName());
         ScriptManager::getSingleton().RemoveEntity(ScriptManager::ENTITY, entity_[i]->GetName());
@@ -555,6 +561,8 @@ bool EntityManager::IsWorldPositionBlocked(
 
 
 Background2D* EntityManager::GetBackground2D(){return &background_2d_;}
 Background2D* EntityManager::GetBackground2D(){return &background_2d_;}
 
 
+ScreenFade* EntityManager::GetScreenFade(){return &screen_fade_;}
+
 void EntityManager::AddEntity(
 void EntityManager::AddEntity(
   const Ogre::String& name, const Ogre::String& file_name,
   const Ogre::String& name, const Ogre::String& file_name,
   const Ogre::Vector3& position, const Ogre::Degree& rotation, int index
   const Ogre::Vector3& position, const Ogre::Degree& rotation, int index
@@ -753,16 +761,17 @@ bool EntityManager::IsKeyOn(unsigned int key_code){
     // TODO: Maybe this will interfere with something down the road?
     // TODO: Maybe this will interfere with something down the road?
     // Some scene where the player can't move but must press a button?
     // Some scene where the player can't move but must press a button?
     if (player_lock_) return false;
     if (player_lock_) return false;
-    unsigned int code = key_code;
-    // Translate keycodes to game key codes.
-    // For example 32 is standard for "Enter", but in game is "Circle/Action".
-    // TODO: Finish this list.
-    // TOOD: Get values from config.lua
-    switch (key_code){
-        case 32: code = OIS::KC_SPACE; break;
-    }
-    bool pressed = InputManager::getSingleton().IsButtonPressed(code);
-    return pressed;
+    // The key code of the scripts is a mask of the pad buttons, and the check is true if any of
+    // them is pressed. Translate each to its key. For example 32 is "Circle/Action", the space bar.
+    // TODO: Get values from config.lua
+    static const struct{unsigned int mask; int key;} KEYS[] = {
+      {4, OIS::KC_PGUP}, {8, OIS::KC_PGDOWN}, {32, OIS::KC_SPACE}, {64, OIS::KC_ESCAPE},
+      {4096, OIS::KC_UP}, {8192, OIS::KC_RIGHT}, {16384, OIS::KC_DOWN}, {32768, OIS::KC_LEFT}
+    };
+    for (const auto& entry : KEYS)
+        if ((key_code & entry.mask) && InputManager::getSingleton().IsButtonPressed(entry.key))
+            return true;
+    return false;
 }
 }
 
 
 bool EntityManager::IsKeyOff(unsigned int key_code){return !IsKeyOn(key_code);}
 bool EntityManager::IsKeyOff(unsigned int key_code){return !IsKeyOn(key_code);}
@@ -842,7 +851,29 @@ bool EntityManager::SetEntityOnWalkmesh(Entity* entity){
     return true;
     return true;
 }
 }
 
 
-bool EntityManager::PerformWalkmeshMove(Entity* entity, const float speed){
+float EntityManager::TriggerReach(float solid_radius){
+    // The radius an entity has when a script doesn't change it.
+    const float default_radius = 0.21f;
+    return solid_radius < 0.01f ? default_radius : solid_radius;
+}
+
+bool EntityManager::WalkmeshEdgeBlocks(bool is_player, bool scripted_move){
+    return is_player || !scripted_move;
+}
+
+bool EntityManager::IsBetterInteractionTarget(
+  float angle, float distance, float best_angle, float best_distance
+){
+    const float same_angle = 0.001f;
+    if (angle < best_angle - same_angle) return true;
+    return std::fabs(angle - best_angle) <= same_angle && distance < best_distance;
+}
+
+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 +881,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 +1134,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;
@@ -1105,6 +1150,7 @@ bool EntityManager::WalkmeshBorderCross(
     }
     }
     int current_triangle = entity->GetMoveTriangleId();
     int current_triangle = entity->GetMoveTriangleId();
     if (current_triangle == -1) return true;
     if (current_triangle == -1) return true;
+    const bool free_walk = !WalkmeshEdgeBlocks(entity == player_entity_, entity->IsScriptedMove());
     Ogre::Vector2 pos = Ogre::Vector2(position.x, position.y);
     Ogre::Vector2 pos = Ogre::Vector2(position.x, position.y);
     for (;;){
     for (;;){
         Ogre::Vector3 A3 = walkmesh_.GetA(current_triangle);
         Ogre::Vector3 A3 = walkmesh_.GetA(current_triangle);
@@ -1127,14 +1173,15 @@ bool EntityManager::WalkmeshBorderCross(
         }
         }
         if (next_triangle >= 0){
         if (next_triangle >= 0){
             bool lock = walkmesh_.IsLocked(next_triangle);
             bool lock = walkmesh_.IsLocked(next_triangle);
-            if (lock == false){
+            if (lock == false || free_walk){
                 current_triangle = next_triangle;
                 current_triangle = next_triangle;
                 continue;
                 continue;
             }
             }
         }
         }
         position.z = PointElevation(pos, A3, B3, C3);
         position.z = PointElevation(pos, A3, B3, C3);
         entity->SetMoveTriangleId(current_triangle);
         entity->SetMoveTriangleId(current_triangle);
-        return true;
+        // A walk of a script goes on over the edge, keeping the last triangle.
+        return !free_walk;
     }
     }
 }
 }
 
 
@@ -1194,7 +1241,7 @@ void EntityManager::CheckTriggers(Entity* entity, const Ogre::Vector3& position)
 
 
             // Calculate distance
             // Calculate distance
             float square_dist = SquareDistanceToLine(entity_position, point_a, point_b, proj);
             float square_dist = SquareDistanceToLine(entity_position, point_a, point_b, proj);
-            float solid_radius = entity->GetSolidRadius();
+            float solid_radius = TriggerReach(entity->GetSolidRadius());
 
 
             // Calculate direction.
             // Calculate direction.
             const Ogre::Degree direction_to_line = GetDirectionToPoint(entity_position, proj);
             const Ogre::Degree direction_to_line = GetDirectionToPoint(entity_position, proj);
@@ -1312,6 +1359,7 @@ void EntityManager::CheckEntityInteract(){
     Ogre::Degree angle_pc = player_entity_->GetRotation();
     Ogre::Degree angle_pc = player_entity_->GetRotation();
     Entity* entity_to_interact = NULL;
     Entity* entity_to_interact = NULL;
     Ogre::Degree less_angle(90);
     Ogre::Degree less_angle(90);
+    float best_distance = std::numeric_limits<float>::max();
     Ogre::Real nearest_worldmap_distance = -1.0f;
     Ogre::Real nearest_worldmap_distance = -1.0f;
     for (unsigned int i = 0; i < entity_.size(); ++ i){
     for (unsigned int i = 0; i < entity_.size(); ++ i){
         if (entity_[i]->IsTalkable() == false) continue;
         if (entity_[i]->IsTalkable() == false) continue;
@@ -1350,8 +1398,15 @@ void EntityManager::CheckEntityInteract(){
                 Ogre::Degree angle = angle_pc - angle_to_model;
                 Ogre::Degree angle = angle_pc - angle_to_model;
                 angle = (angle < Ogre::Degree(0)) ? -angle : angle;
                 angle = (angle < Ogre::Degree(0)) ? -angle : angle;
                 angle = (angle >= Ogre::Degree(180)) ? Ogre::Degree(360) - angle : angle;
                 angle = (angle >= Ogre::Degree(180)) ? Ogre::Degree(360) - angle : angle;
-                if (angle < less_angle){
-                    angle = less_angle;
+                // With several entities in range, interact with the one the player faces most.
+                // Nobody 90 degrees or more away from where the player faces is considered.
+                if (
+                  angle < Ogre::Degree(90) && IsBetterInteractionTarget(
+                    angle.valueDegrees(), distance, less_angle.valueDegrees(), best_distance
+                  )
+                ){
+                    less_angle = angle;
+                    best_distance = distance;
                     entity_to_interact = entity_[i];
                     entity_to_interact = entity_[i];
                 }
                 }
             }
             }
@@ -1419,7 +1474,10 @@ void EntityManager::SetNextLinearStep(Entity* entity){
     Ogre::Vector3 end = entity->GetLinearEnd();
     Ogre::Vector3 end = entity->GetLinearEnd();
     Ogre::Vector3 current = entity->GetPosition();
     Ogre::Vector3 current = entity->GetPosition();
     float delta = Timer::getSingleton().GetGameTimeDelta();
     float delta = Timer::getSingleton().GetGameTimeDelta();
-    if (entity == player_entity_){
+    // The player climbs a ladder with the directional keys. While a scene has the player locked
+    // the keys have no effect, and the scene makes the player move on its own, as with any other
+    // entity.
+    if (entity == player_entity_ && !player_lock_){
         LinearMovement move = entity->GetLinearMovement();
         LinearMovement move = entity->GetLinearMovement();
         if (move == LM_DOWN_TO_UP || move == LM_UP_TO_DOWN){
         if (move == LM_DOWN_TO_UP || move == LM_UP_TO_DOWN){
             if (player_move_.y > 0){ // move up
             if (player_move_.y > 0){ // move up
@@ -1514,6 +1572,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();

+ 65 - 1
src/core/EntityManager.h

@@ -22,6 +22,7 @@
 #include "EntityTrigger.h"
 #include "EntityTrigger.h"
 #include "Event.h"
 #include "Event.h"
 #include "Manager.h"
 #include "Manager.h"
+#include "ScreenFade.h"
 #include "Walkmesh.h"
 #include "Walkmesh.h"
 
 
 class EntityModel;
 class EntityModel;
@@ -43,6 +44,55 @@ 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);
+
+        /**
+         * Decides whether an entity is a better target to interact with than the best one so far.
+         *
+         * The one the player faces more directly wins. If the angles are the same, the nearest
+         * one does.
+         *
+         * @param[in] angle Angle in degrees between where the player faces and the candidate.
+         * @param[in] distance Squared distance to the candidate.
+         * @param[in] best_angle Angle of the best candidate so far, or the largest one allowed.
+         * @param[in] best_distance Squared distance to the best candidate so far.
+         * @return True if the candidate is better.
+         */
+        static bool IsBetterInteractionTarget(
+          float angle, float distance, float best_angle, float best_distance
+        );
+
+        /**
+         * Retrieves how close to a line an entity has to be to trigger it.
+         *
+         * It's the solid radius of the entity, unless a script has made it too small, as scripts
+         * do while a scene is playing. Lines couldn't be triggered at all then.
+         *
+         * @param[in] solid_radius Solid radius of the entity.
+         * @return The distance to the line from which it triggers.
+         */
+        static float TriggerReach(float solid_radius);
+
+        /**
+         * Checks if the edge of the walkmesh stops an entity.
+         *
+         * It stops the player and the characters that wander. The characters that a script walks
+         * to a position are not stopped: the original game lets them walk off the walkmesh.
+         *
+         * @param[in] is_player Whether the entity is the player.
+         * @param[in] scripted_move Whether a script asked the entity to walk to a position.
+         * @return True if the edge of the walkmesh blocks the entity.
+         */
+        static bool WalkmeshEdgeBlocks(bool is_player, bool scripted_move);
+
         /**
         /**
          * Handles an input event.
          * Handles an input event.
          *
          *
@@ -108,6 +158,13 @@ class EntityManager : public Manager, public Ogre::Singleton<EntityManager>{
          */
          */
         Background2D* GetBackground2D();
         Background2D* GetBackground2D();
 
 
+        /**
+         * Retrieves the coloured fade of the screen.
+         *
+         * @return The screen fade.
+         */
+        ScreenFade* GetScreenFade();
+
         /**
         /**
          * Adds an entity to the manager.
          * Adds an entity to the manager.
          *
          *
@@ -533,10 +590,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.
@@ -639,6 +697,12 @@ class EntityManager : public Manager, public Ogre::Singleton<EntityManager>{
          */
          */
         Background2D background_2d_;
         Background2D background_2d_;
 
 
+        /**
+         * The coloured fade of the screen. It has to be created after the background, because the
+         * fade must be drawn over it.
+         */
+        ScreenFade screen_fade_;
+
         /**
         /**
          * The entity table name.
          * The entity table name.
          */
          */

+ 4 - 0
src/core/EntityModel.cpp

@@ -75,6 +75,10 @@ void EntityModel::PlayAnimation(
         animation_current_name_ = animation;
         animation_current_name_ = animation;
         animation_current_ = model_->getAnimationState(animation);
         animation_current_ = model_->getAnimationState(animation);
         animation_current_->setLoop((play_type == Entity::PLAY_LOOPED) ? true : false);
         animation_current_->setLoop((play_type == Entity::PLAY_LOOPED) ? true : false);
+        // Single frame animations have no length. A looping animation without length wraps its
+        // time position dividing by zero, ends up with a time that is not a number, and never
+        // finishes, so any script waiting for it would wait forever.
+        if (animation_current_->getLength() <= 0.0f) animation_current_->setLoop(false);
         animation_current_->setEnabled(true);
         animation_current_->setEnabled(true);
         animation_current_->setTimePosition(
         animation_current_->setTimePosition(
           (start == -1) ? animation_current_->getLength() : start
           (start == -1) ? animation_current_->getLength() : start

+ 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.

+ 196 - 0
src/core/ScreenFade.cpp

@@ -0,0 +1,196 @@
+/*
+ * Copyright (C) 2022 The V-Gears Team
+ *
+ * This file is part of V-Gears
+ *
+ * V-Gears is free software: you can redistribute it and/or modify it under
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation, version 3.0 (GPLv3) of the License.
+ *
+ * V-Gears is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ */
+
+#include <OgreHardwareBufferManager.h>
+#include <OgreMaterialManager.h>
+#include <OgreRoot.h>
+#include <OgreTechnique.h>
+#include "core/CameraManager.h"
+#include "core/EntityManager.h"
+#include "core/Logger.h"
+#include "core/ScreenFade.h"
+
+namespace{
+
+    // The quad is two triangles.
+    const unsigned int QUAD_VERTEX_COUNT = 6;
+
+    // Position (3), colour (4) and texture coordinates (2).
+    const unsigned int QUAD_FLOATS_PER_VERTEX = 9;
+
+    // Resource group of the materials. The background creates its own there as well.
+    const char* const MATERIAL_GROUP = "FIELDS";
+
+    /**
+     * Creates a material that is drawn over everything in the field, without lighting or texture.
+     */
+    Ogre::MaterialPtr CreateMaterial(const Ogre::String& name){
+        Ogre::MaterialPtr material = Ogre::MaterialManager::getSingleton().create(
+          name, MATERIAL_GROUP
+        );
+        Ogre::Pass* pass = material->getTechnique(0)->getPass(0);
+        pass->setVertexColourTracking(Ogre::TVC_AMBIENT);
+        pass->setCullingMode(Ogre::CULL_NONE);
+        pass->setDepthCheckEnabled(false);
+        pass->setDepthWriteEnabled(false);
+        pass->setLightingEnabled(false);
+        // No texture: the fade is a flat colour. A texture that can't be loaded is replaced with a
+        // dark placeholder, which turned every colour black.
+        return material;
+    }
+}
+
+ScreenFade::ScreenFade(){
+    scene_manager_ = Ogre::Root::getSingleton().getSceneManager("Scene");
+    render_system_ = Ogre::Root::getSingletonPtr()->getRenderSystem();
+
+    normal_material_ = CreateMaterial("ScreenFadeNormal");
+    normal_material_->getTechnique(0)->getPass(0)->setSceneBlending(Ogre::SBT_TRANSPARENT_ALPHA);
+
+    add_material_ = CreateMaterial("ScreenFadeAdd");
+    add_material_->getTechnique(0)->getPass(0)->setSceneBlending(Ogre::SBT_ADD);
+
+    // The colour is taken from the field: field - colour, and not colour - field.
+    subtract_material_ = CreateMaterial("ScreenFadeSubtract");
+    Ogre::Pass* pass = subtract_material_->getTechnique(0)->getPass(0);
+    pass->setSceneBlending(Ogre::SBF_ONE, Ogre::SBF_ONE);
+    pass->setSceneBlendingOperation(Ogre::SBO_REVERSE_SUBTRACT);
+
+    render_op_.vertexData = new Ogre::VertexData;
+    render_op_.vertexData->vertexStart = 0;
+    render_op_.vertexData->vertexCount = QUAD_VERTEX_COUNT;
+    Ogre::VertexDeclaration* declaration = render_op_.vertexData->vertexDeclaration;
+    size_t offset = 0;
+    declaration->addElement(0, 0, Ogre::VET_FLOAT3, Ogre::VES_POSITION);
+    offset += Ogre::VertexElement::getTypeSize(Ogre::VET_FLOAT3);
+    declaration->addElement(0, offset, Ogre::VET_FLOAT4, Ogre::VES_DIFFUSE);
+    offset += Ogre::VertexElement::getTypeSize(Ogre::VET_FLOAT4);
+    declaration->addElement(0, offset, Ogre::VET_FLOAT2, Ogre::VES_TEXTURE_COORDINATES);
+    vertex_buffer_ = Ogre::HardwareBufferManager::getSingletonPtr()->createVertexBuffer(
+      declaration->getVertexSize(0), QUAD_VERTEX_COUNT, Ogre::HardwareBuffer::HBU_DYNAMIC_WRITE_ONLY,
+      false
+    );
+    render_op_.vertexData->vertexBufferBinding->setBinding(0, vertex_buffer_);
+    render_op_.operationType = Ogre::RenderOperation::OT_TRIANGLE_LIST;
+    render_op_.useIndexes = false;
+
+    scene_manager_->addRenderQueueListener(this);
+}
+
+ScreenFade::~ScreenFade(){
+    scene_manager_->removeRenderQueueListener(this);
+    delete render_op_.vertexData;
+}
+
+void ScreenFade::Update(const float delta){
+    if (!state_.Advance(delta)) return;
+    for (const ScriptId& script : waiting_) ScriptManager::getSingleton().ContinueScriptExecution(script);
+    waiting_.clear();
+}
+
+void ScreenFade::Clear(){
+    state_.Reset();
+    draw_logged_ = false;
+    waiting_.clear();
+}
+
+void ScreenFade::ScriptFade(const int type, const int r, const int g, const int b, const int speed){
+    LOG_TRIVIAL(
+      "Screen fade type " + std::to_string(type) + ", colour " + std::to_string(r) + " "
+      + std::to_string(g) + " " + std::to_string(b) + ", speed " + std::to_string(speed) + "."
+    );
+    draw_logged_ = false;
+    if (!state_.StartFade(type, r, g, b, speed))
+        LOG_WARNING("Screen fade type " + std::to_string(type) + " is not known.");
+}
+
+void ScreenFade::ScriptNFade(
+  const int type, const int r, const int g, const int b, const int frames
+){
+    LOG_TRIVIAL(
+      "Screen nfade type " + std::to_string(type) + ", colour " + std::to_string(r) + " "
+      + std::to_string(g) + " " + std::to_string(b) + ", " + std::to_string(frames) + " frames."
+    );
+    draw_logged_ = false;
+    if (!state_.StartNFade(type, r, g, b, frames))
+        LOG_WARNING("Screen nfade type " + std::to_string(type) + " is not known.");
+}
+
+int ScreenFade::ScriptWait(){
+    if (!state_.IsFading()) return 1;
+    waiting_.push_back(ScriptManager::getSingleton().GetCurrentScriptId());
+    return -1;
+}
+
+void ScreenFade::WriteQuad(const FadeColour& colour, const float alpha){
+    // The corners of the screen, in the coordinates the background is drawn in.
+    const float corners[QUAD_VERTEX_COUNT][2] = {
+      {-1, 1}, {1, 1}, {1, -1}, {-1, 1}, {1, -1}, {-1, -1}
+    };
+    float* write_iterator
+      = static_cast<float*>(vertex_buffer_->lock(Ogre::HardwareBuffer::HBL_DISCARD));
+    for (unsigned int i = 0; i < QUAD_VERTEX_COUNT; ++ i){
+        *write_iterator ++ = corners[i][0];
+        *write_iterator ++ = corners[i][1];
+        *write_iterator ++ = 0;
+        *write_iterator ++ = colour.r;
+        *write_iterator ++ = colour.g;
+        *write_iterator ++ = colour.b;
+        *write_iterator ++ = alpha;
+        *write_iterator ++ = 0;
+        *write_iterator ++ = 0;
+    }
+    vertex_buffer_->unlock();
+}
+
+void ScreenFade::renderQueueStarted(
+  Ogre::uint8 queue_group_id, const Ogre::String& invocation, bool& skip_this_invocation
+){
+    if (queue_group_id != Ogre::RENDER_QUEUE_OVERLAY || !state_.IsVisible()) return;
+    if (!EntityManager::getSingleton().IsFieldModule()) return;
+
+    Ogre::MaterialPtr material;
+    switch (state_.GetBlend()){
+        case ScreenFadeState::BLEND_NORMAL: material = normal_material_; break;
+        case ScreenFadeState::BLEND_ADD: material = add_material_; break;
+        case ScreenFadeState::BLEND_SUBTRACT: material = subtract_material_; break;
+        default: return;
+    }
+    WriteQuad(state_.GetColour(), state_.GetAlpha());
+    if (!draw_logged_){
+        draw_logged_ = true;
+        LOG_TRIVIAL(
+          "Screen fade drawn, blend " + std::to_string(static_cast<int>(state_.GetBlend()))
+          + ", colour " + std::to_string(state_.GetColour().r) + " "
+          + std::to_string(state_.GetColour().g) + " " + std::to_string(state_.GetColour().b)
+          + ", alpha " + std::to_string(state_.GetAlpha()) + "."
+        );
+    }
+
+    // Same setup the background uses: the quad is given in screen coordinates.
+    Ogre::GpuProgramParametersPtr rs_params = render_system_->getFixedFunctionParams(
+      Ogre::TVC_NONE, Ogre::FOG_NONE
+    );
+    rs_params->setConstant(Ogre::GpuProgramParameters::ACT_WORLD_MATRIX, Ogre::Matrix4::IDENTITY);
+    rs_params->setConstant(
+      Ogre::GpuProgramParameters::ACT_PROJECTION_MATRIX, Ogre::Matrix4::IDENTITY
+    );
+    // TODO This is deprecated, but if not done the quad is drawn with the last view matrix.
+    render_system_->_setViewMatrix(Ogre::Matrix4::IDENTITY);
+    rs_params->setConstant(Ogre::GpuProgramParameters::ACT_VIEW_MATRIX, Ogre::Matrix4::IDENTITY);
+    render_system_->applyFixedFunctionParams(rs_params, Ogre::GPV_GLOBAL);
+    scene_manager_->_setPass(material->getTechnique(0)->getPass(0), true, false);
+    render_system_->_render(render_op_);
+}

+ 160 - 0
src/core/ScreenFade.h

@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2022 The V-Gears Team
+ *
+ * This file is part of V-Gears
+ *
+ * V-Gears is free software: you can redistribute it and/or modify it under
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation, version 3.0 (GPLv3) of the License.
+ *
+ * V-Gears is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ */
+
+#pragma once
+
+#include <vector>
+#include <OgreHardwareVertexBuffer.h>
+#include <OgreMaterial.h>
+#include <OgreRenderOperation.h>
+#include <OgreRenderQueueListener.h>
+#include <OgreRenderSystem.h>
+#include <OgreSceneManager.h>
+#include "ScreenFadeState.h"
+#include "ScriptManager.h"
+
+/**
+ * A coloured fade of the screen, for flashes and fades to a colour.
+ *
+ * The colour is drawn over the field and its models, and below the dialogs and the rest of the
+ * interface. The rules of the fades are in {@see ScreenFadeState}.
+ */
+class ScreenFade : public Ogre::RenderQueueListener{
+
+    public:
+
+        /**
+         * Constructor.
+         */
+        ScreenFade();
+
+        /**
+         * Destructor.
+         */
+        virtual ~ScreenFade();
+
+        /**
+         * Run each frame. Moves the fade forward, and resumes the scripts that wait for its end.
+         *
+         * @param[in] delta Seconds since the last frame.
+         */
+        void Update(const float delta);
+
+        /**
+         * Removes the fade. Run when a field is unloaded.
+         */
+        void Clear();
+
+        /**
+         * Starts a fade like the FADE opcode.
+         *
+         * @param[in] type Fade type, from 1 to 10.
+         * @param[in] r Red component, from 0 to 255.
+         * @param[in] g Green component, from 0 to 255.
+         * @param[in] b Blue component, from 0 to 255.
+         * @param[in] speed Speed of the fade. 8 takes a second.
+         */
+        void ScriptFade(const int type, const int r, const int g, const int b, const int speed);
+
+        /**
+         * Starts a fade like the NFADE opcode.
+         *
+         * @param[in] type Fade type: 0, 11 or 12.
+         * @param[in] r Red component, from 0 to 255.
+         * @param[in] g Green component, from 0 to 255.
+         * @param[in] b Blue component, from 0 to 255.
+         * @param[in] frames Duration of the fade, in frames of 30 per second.
+         */
+        void ScriptNFade(const int type, const int r, const int g, const int b, const int frames);
+
+        /**
+         * Waits for the fade to end, like the FADEW opcode. It continues at once if there is no
+         * fade running.
+         *
+         * @return 1 if the script doesn't have to wait, and -1 if it does.
+         */
+        int ScriptWait();
+
+        /**
+         * Draws the fade, over the field and below the interface.
+         *
+         * It's drawn when the overlay queue starts, which is when the interface is drawn, and not
+         * after the main queue, where it would depend on the order of the listeners and be
+         * covered by the background if the background registered itself again.
+         */
+        void renderQueueStarted(
+          Ogre::uint8 queue_group_id, const Ogre::String& invocation, bool& skip_this_invocation
+        ) override;
+
+    private:
+
+        /**
+         * Writes the colour in the vertices of the screen quad.
+         *
+         * @param[in] colour The colour.
+         * @param[in] alpha The alpha.
+         */
+        void WriteQuad(const FadeColour& colour, const float alpha);
+
+        /**
+         * The rules and the progress of the fade.
+         */
+        ScreenFadeState state_;
+
+        /**
+         * Whether the current fade has been logged as drawn.
+         */
+        bool draw_logged_ = false;
+
+        /**
+         * The scripts that wait for the fade to end.
+         */
+        std::vector<ScriptId> waiting_;
+
+        /**
+         * The scene manager.
+         */
+        Ogre::SceneManager* scene_manager_;
+
+        /**
+         * The render system.
+         */
+        Ogre::RenderSystem* render_system_;
+
+        /**
+         * Material that covers the field.
+         */
+        Ogre::MaterialPtr normal_material_;
+
+        /**
+         * Material that adds to the field.
+         */
+        Ogre::MaterialPtr add_material_;
+
+        /**
+         * Material that subtracts from the field.
+         */
+        Ogre::MaterialPtr subtract_material_;
+
+        /**
+         * Render operation of the screen quad.
+         */
+        Ogre::RenderOperation render_op_;
+
+        /**
+         * Vertex buffer of the screen quad.
+         */
+        Ogre::HardwareVertexBufferSharedPtr vertex_buffer_;
+};

+ 170 - 0
src/core/ScreenFadeState.cpp

@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2022 The V-Gears Team
+ *
+ * This file is part of V-Gears
+ *
+ * V-Gears is free software: you can redistribute it and/or modify it under
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation, version 3.0 (GPLv3) of the License.
+ *
+ * V-Gears is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ */
+
+#include <algorithm>
+#include "core/ScreenFadeState.h"
+
+namespace{
+
+    // Frames per second the NFADE durations are given in.
+    const float FRAMES_PER_SECOND = 30.0f;
+
+    // The colour of the opcode as it is given.
+    FadeColour Standard(const int r, const int g, const int b){
+        FadeColour colour;
+        colour.r = r / 255.0f;
+        colour.g = g / 255.0f;
+        colour.b = b / 255.0f;
+        return colour;
+    }
+
+    // 255 minus each component of the colour.
+    FadeColour Inverse1(const int r, const int g, const int b){
+        FadeColour colour;
+        colour.r = (255 - r) / 255.0f;
+        colour.g = (255 - g) / 255.0f;
+        colour.b = (255 - b) / 255.0f;
+        return colour;
+    }
+
+    // Four times 255 minus each component of the colour. It can go over full intensity.
+    FadeColour Inverse4(const int r, const int g, const int b){
+        FadeColour colour = Inverse1(r, g, b);
+        colour.r *= 4;
+        colour.g *= 4;
+        colour.b *= 4;
+        return colour;
+    }
+
+    // Black adds or subtracts nothing.
+    FadeColour Black(){return FadeColour();}
+
+    float Clamp(const float value){return std::min(1.0f, std::max(0.0f, value));}
+}
+
+float ScreenFadeState::SpeedToSeconds(const int speed){
+    if (speed <= 0) return 0;
+    return 8.0f / speed;
+}
+
+void ScreenFadeState::Begin(
+  const Blend blend, const FadeColour& from, const FadeColour& to, const float alpha_from,
+  const float alpha_to, const float seconds, const bool hold
+){
+    blend_ = blend;
+    from_ = from;
+    to_ = to;
+    alpha_from_ = alpha_from;
+    alpha_to_ = alpha_to;
+    duration_ = seconds;
+    elapsed_ = 0;
+    hold_ = hold;
+    fading_ = seconds > 0;
+    holding_ = !fading_ && hold;
+    if (!fading_ && !hold) Reset();
+}
+
+bool ScreenFadeState::StartFade(
+  const int type, const int r, const int g, const int b, const int speed
+){
+    const float seconds = SpeedToSeconds(speed);
+    switch (type){
+        case 1:
+            Begin(BLEND_SUBTRACT, Inverse4(r, g, b), Black(), 1, 1, seconds, false);
+            return true;
+        case 2:
+            Begin(BLEND_SUBTRACT, Black(), Inverse4(r, g, b), 1, 1, seconds, true);
+            return true;
+        case 4:
+            // The screen goes black at once and stays that way.
+            Begin(BLEND_NORMAL, Black(), Black(), 1, 1, 0, true);
+            return true;
+        case 5:
+            Begin(BLEND_ADD, Standard(r, g, b), Black(), 1, 1, seconds, false);
+            return true;
+        case 6:
+            Begin(BLEND_NORMAL, Standard(r, g, b), Standard(r, g, b), 0, 1, seconds, true);
+            return true;
+        case 7:
+            Begin(BLEND_SUBTRACT, Inverse1(r, g, b), Inverse1(r, g, b), 1, 1, seconds, false);
+            return true;
+        case 8:
+            Begin(BLEND_SUBTRACT, Inverse1(r, g, b), Inverse1(r, g, b), 1, 1, seconds, true);
+            return true;
+        case 9:
+            Begin(BLEND_ADD, Standard(r, g, b), Standard(r, g, b), 1, 1, seconds, false);
+            return true;
+        case 10:
+            Begin(BLEND_ADD, Standard(r, g, b), Standard(r, g, b), 1, 1, seconds, true);
+            return true;
+        default: return false;
+    }
+}
+
+bool ScreenFadeState::StartNFade(
+  const int type, const int r, const int g, const int b, const int frames
+){
+    const float seconds = frames > 0 ? frames / FRAMES_PER_SECOND : 0;
+    switch (type){
+        case 0:
+            Reset();
+            return true;
+        case 11:
+            Begin(BLEND_ADD, Black(), Standard(r, g, b), 1, 1, seconds, true);
+            return true;
+        case 12:
+            Begin(BLEND_SUBTRACT, Black(), Standard(r, g, b), 1, 1, seconds, true);
+            return true;
+        default: return false;
+    }
+}
+
+bool ScreenFadeState::Advance(const float seconds){
+    if (!fading_) return false;
+    elapsed_ += seconds;
+    if (elapsed_ < duration_) return false;
+    fading_ = false;
+    holding_ = hold_;
+    if (!holding_) Reset();
+    return true;
+}
+
+void ScreenFadeState::Reset(){*this = ScreenFadeState();}
+
+bool ScreenFadeState::IsFading() const{return fading_;}
+
+bool ScreenFadeState::IsVisible() const{return fading_ || holding_;}
+
+ScreenFadeState::Blend ScreenFadeState::GetBlend() const{return IsVisible() ? blend_ : BLEND_NONE;}
+
+FadeColour ScreenFadeState::GetColour() const{
+    FadeColour colour = to_;
+    if (fading_){
+        const float progress = duration_ > 0 ? Clamp(elapsed_ / duration_) : 1.0f;
+        colour.r = from_.r + (to_.r - from_.r) * progress;
+        colour.g = from_.g + (to_.g - from_.g) * progress;
+        colour.b = from_.b + (to_.b - from_.b) * progress;
+    }
+    colour.r = Clamp(colour.r);
+    colour.g = Clamp(colour.g);
+    colour.b = Clamp(colour.b);
+    return colour;
+}
+
+float ScreenFadeState::GetAlpha() const{
+    if (!fading_) return Clamp(alpha_to_);
+    const float progress = duration_ > 0 ? Clamp(elapsed_ / duration_) : 1.0f;
+    return Clamp(alpha_from_ + (alpha_to_ - alpha_from_) * progress);
+}

+ 226 - 0
src/core/ScreenFadeState.h

@@ -0,0 +1,226 @@
+/*
+ * Copyright (C) 2022 The V-Gears Team
+ *
+ * This file is part of V-Gears
+ *
+ * V-Gears is free software: you can redistribute it and/or modify it under
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation, version 3.0 (GPLv3) of the License.
+ *
+ * V-Gears is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ */
+
+#pragma once
+
+/**
+ * A colour whose components go from 0 (none) to 1 (full intensity).
+ */
+struct FadeColour{
+
+    /**
+     * Red component.
+     */
+    float r = 0;
+
+    /**
+     * Green component.
+     */
+    float g = 0;
+
+    /**
+     * Blue component.
+     */
+    float b = 0;
+};
+
+/**
+ * The rules of the screen fades of the FADE and NFADE opcodes.
+ *
+ * A fade is a linear tween of a colour that is drawn over the field, blended with it. Depending
+ * on the fade type the colour is added to the field, subtracted from it, or mixed with it. Some
+ * fades keep their last colour when the tween ends, and the others go back to showing the field.
+ * A black colour in an additive or subtractive fade changes nothing.
+ *
+ * This class has no dependency on the rendering engine, so the rules can be tested.
+ */
+class ScreenFadeState{
+
+    public:
+
+        /**
+         * How the colour is combined with the field.
+         */
+        enum Blend{
+
+            /**
+             * Nothing is drawn.
+             */
+            BLEND_NONE,
+
+            /**
+             * The colour covers the field in proportion to the alpha.
+             */
+            BLEND_NORMAL,
+
+            /**
+             * The colour is added to the field.
+             */
+            BLEND_ADD,
+
+            /**
+             * The colour is subtracted from the field.
+             */
+            BLEND_SUBTRACT
+        };
+
+        /**
+         * Starts a fade of the FADE opcode, replacing the one that is running.
+         *
+         * @param[in] type Fade type, from 1 to 10. 3 is not used.
+         * @param[in] r Red component, from 0 to 255.
+         * @param[in] g Green component, from 0 to 255.
+         * @param[in] b Blue component, from 0 to 255.
+         * @param[in] speed Speed of the fade. 8 takes a second, and double speed halves the time.
+         * @return False if the type is unknown, and then nothing changes.
+         */
+        bool StartFade(const int type, const int r, const int g, const int b, const int speed);
+
+        /**
+         * Starts a fade of the NFADE opcode, replacing the one that is running.
+         *
+         * @param[in] type Fade type: 0 clears the fade, 11 adds and 12 subtracts.
+         * @param[in] r Red component, from 0 to 255.
+         * @param[in] g Green component, from 0 to 255.
+         * @param[in] b Blue component, from 0 to 255.
+         * @param[in] frames Duration of the fade, in frames of 30 per second.
+         * @return False if the type is unknown, and then nothing changes.
+         */
+        bool StartNFade(const int type, const int r, const int g, const int b, const int frames);
+
+        /**
+         * Moves the fade forward in time.
+         *
+         * @param[in] seconds Time since the last call.
+         * @return True if the tween of the fade ended with this call.
+         */
+        bool Advance(const float seconds);
+
+        /**
+         * Removes any fade, including the colour it holds.
+         */
+        void Reset();
+
+        /**
+         * Checks if a tween is running. A script waiting for the fade waits while it is.
+         *
+         * @return True if the fade is changing.
+         */
+        bool IsFading() const;
+
+        /**
+         * Checks if something has to be drawn: a tween, or the colour it holds.
+         *
+         * @return True if the fade covers the field.
+         */
+        bool IsVisible() const;
+
+        /**
+         * Retrieves how the colour has to be combined with the field.
+         *
+         * @return The blending.
+         */
+        Blend GetBlend() const;
+
+        /**
+         * Retrieves the colour to draw now, with all its components between 0 and 1.
+         *
+         * @return The colour.
+         */
+        FadeColour GetColour() const;
+
+        /**
+         * Retrieves how much the colour covers the field. Only the normal blending uses it.
+         *
+         * @return The alpha, between 0 and 1.
+         */
+        float GetAlpha() const;
+
+        /**
+         * Converts the speed of the FADE opcode to the seconds the fade takes.
+         *
+         * @param[in] speed The speed. 1 is 8 seconds, 2 is 4, 4 is 2 and 8 is 1.
+         * @return The seconds. 0 for a speed that is not positive, meaning instant.
+         */
+        static float SpeedToSeconds(const int speed);
+
+    private:
+
+        /**
+         * Starts a fade.
+         *
+         * @param[in] blend How the colour is combined with the field.
+         * @param[in] from Colour at the start.
+         * @param[in] to Colour at the end.
+         * @param[in] alpha_from Alpha at the start.
+         * @param[in] alpha_to Alpha at the end.
+         * @param[in] seconds Duration. If it is 0 the fade is instant.
+         * @param[in] hold Whether the final colour stays on the screen.
+         */
+        void Begin(
+          const Blend blend, const FadeColour& from, const FadeColour& to, const float alpha_from,
+          const float alpha_to, const float seconds, const bool hold
+        );
+
+        /**
+         * How the colour is combined with the field.
+         */
+        Blend blend_ = BLEND_NONE;
+
+        /**
+         * Colour at the start of the tween.
+         */
+        FadeColour from_;
+
+        /**
+         * Colour at the end of the tween.
+         */
+        FadeColour to_;
+
+        /**
+         * Alpha at the start of the tween.
+         */
+        float alpha_from_ = 1;
+
+        /**
+         * Alpha at the end of the tween.
+         */
+        float alpha_to_ = 1;
+
+        /**
+         * Duration of the tween.
+         */
+        float duration_ = 0;
+
+        /**
+         * Time since the start of the tween.
+         */
+        float elapsed_ = 0;
+
+        /**
+         * Whether the tween is running.
+         */
+        bool fading_ = false;
+
+        /**
+         * Whether the final colour stays on the screen when the tween ends.
+         */
+        bool hold_ = false;
+
+        /**
+         * Whether the final colour is on the screen after the tween.
+         */
+        bool holding_ = false;
+};

+ 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;
 };
 };
 
 
 
 

+ 17 - 1
src/core/ScriptManagerBinds.h

@@ -135,12 +135,14 @@ void ScriptManager::InitBinds(){
       "set_move_triangle_id", &Entity::SetMoveTriangleId,
       "set_move_triangle_id", &Entity::SetMoveTriangleId,
       "move_auto_rotation", &Entity::SetMoveAutoRotation,
       "move_auto_rotation", &Entity::SetMoveAutoRotation,
       "move_auto_animation", &Entity::SetMoveAutoAnimation,
       "move_auto_animation", &Entity::SetMoveAutoAnimation,
+      "set_move_animations", &Entity::SetMoveAnimations,
       "move_to_position", &Entity::ScriptMoveToPosition,
       "move_to_position", &Entity::ScriptMoveToPosition,
       "move_to_entity", &Entity::ScriptMoveToEntity,
       "move_to_entity", &Entity::ScriptMoveToEntity,
       "linear_to_position", &Entity::ScriptLinearToPosition,
       "linear_to_position", &Entity::ScriptLinearToPosition,
       "jump_to_position", &Entity::ScriptJumpToPosition,
       "jump_to_position", &Entity::ScriptJumpToPosition,
       "offset_to_position", &Entity::ScriptOffsetToPosition,
       "offset_to_position", &Entity::ScriptOffsetToPosition,
       "turn_to_entity", &Entity::ScriptTurnToEntity,
       "turn_to_entity", &Entity::ScriptTurnToEntity,
+      "face_entity", &Entity::ScriptFaceEntity,
       "turn_to_direction", &Entity::ScriptTurnToDirection,
       "turn_to_direction", &Entity::ScriptTurnToDirection,
       "set_animation_speed", &Entity::ScriptSetAnimationSpeed,
       "set_animation_speed", &Entity::ScriptSetAnimationSpeed,
       "play_animation", sol::overload(
       "play_animation", sol::overload(
@@ -176,7 +178,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,11 +361,25 @@ 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,
+      "shake", &Background2D::ScriptShake,
       "play_animation_looped", &Background2D::ScriptPlayAnimationLooped,
       "play_animation_looped", &Background2D::ScriptPlayAnimationLooped,
       "play_animation_once", &Background2D::ScriptPlayAnimationOnce
       "play_animation_once", &Background2D::ScriptPlayAnimationOnce
     );
     );
+    // Coloured fades of the screen
+    lua.new_usertype<ScreenFade>(
+      "ScreenFade",
+      "fade", &ScreenFade::ScriptFade,
+      "nfade", &ScreenFade::ScriptNFade
+    );
+    sol::table screen_fade_type = lua["ScreenFade"];
+    BindYielding(screen_fade_type, "wait", &ScreenFade::ScriptWait);
+
     sol::table background_type = lua["Background2D"];
     sol::table background_type = lua["Background2D"];
     BindYielding(background_type, "scroll_sync", &Background2D::ScriptScrollSync);
     BindYielding(background_type, "scroll_sync", &Background2D::ScriptScrollSync);
     BindYielding(background_type, "animation_sync", &Background2D::ScriptAnimationSync);
     BindYielding(background_type, "animation_sync", &Background2D::ScriptAnimationSync);
@@ -490,6 +505,7 @@ void ScriptManager::InitBinds(){
     lua["audio_manager"] = AudioManager::getSingletonPtr();
     lua["audio_manager"] = AudioManager::getSingletonPtr();
     lua["savemap_manager"] = SavemapHandler::getSingletonPtr();
     lua["savemap_manager"] = SavemapHandler::getSingletonPtr();
     lua["background2d"] = EntityManager::getSingletonPtr()->GetBackground2D();
     lua["background2d"] = EntityManager::getSingletonPtr()->GetBackground2D();
+    lua["screen_fade"] = EntityManager::getSingletonPtr()->GetScreenFade();
     lua["walkmesh"] = EntityManager::getSingletonPtr()->GetWalkmesh();
     lua["walkmesh"] = EntityManager::getSingletonPtr()->GetWalkmesh();
     lua["text_manager"] = TextHandler::getSingletonPtr();
     lua["text_manager"] = TextHandler::getSingletonPtr();
     lua["dialog"] = DialogsManager::getSingletonPtr();
     lua["dialog"] = DialogsManager::getSingletonPtr();

+ 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]));

+ 61 - 0
src/installer/FieldDataInstaller.h

@@ -288,6 +288,50 @@ 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;
+        }
+
+        /**
+         * Retrieves the animation a model plays when it walks or runs on its own.
+         *
+         * @param[in] char_id ID of the model, as given to the CHAR opcode.
+         * @param[in] index 1 for the walk animation, 2 for the run animation.
+         * @return Name of the animation, or an empty string if the model hasn't got it.
+         */
+        virtual std::string GetMoveAnimationName(int char_id, int index) override{
+            if (char_id < 0 || static_cast<unsigned int>(char_id) >= model_loader_->GetModels().size())
+                return "";
+            const auto& animations = model_loader_->GetModels().at(char_id).animations;
+            if (static_cast<unsigned int>(index) >= animations.size()) return "";
+            Ogre::String base_name;
+            VGears::StringUtil::splitBase(animations[index].name, base_name);
+            VGears::StringUtil::toLowerCase(base_name);
+            return VGears::NameLookup::Animation(base_name);
+        }
+
     private:
     private:
 
 
         /**
         /**
@@ -395,6 +439,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:
 
 
         /**
         /**

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

@@ -21,6 +21,10 @@ 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::GetMoveAnimationName(int char_id, int index){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){

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

@@ -56,6 +56,30 @@ 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 the animation a model plays when it walks or runs on its own.
+         *
+         * In the original game the second animation of a model is its walk, and the third its
+         * run. Only some models have those called "Walk" and "Run".
+         *
+         * @param[in] char_id ID of the model, as given to the CHAR opcode.
+         * @param[in] index 1 for the walk animation, 2 for the run animation.
+         * @return Name of the animation, or an empty string if the model hasn't got it.
+         */
+        virtual std::string GetMoveAnimationName(int char_id, int index);
+
         /**
         /**
          * 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;
         };
         };
 
 
         /**
         /**

+ 8 - 1
src/installer/decompiler/field/FieldDisassembler.cpp

@@ -114,11 +114,18 @@ std::unique_ptr<Function> FieldDisassembler::StartFunction(size_t script_index){
 
 
 void FieldDisassembler::DoDisassemble(){
 void FieldDisassembler::DoDisassemble(){
     // Loop through the scripts for each entity.
     // Loop through the scripts for each entity.
+    std::set<std::string> used_entity_names;
     for (size_t entity_number = 0; entity_number < header_.entity_scripts.size(); entity_number ++){
     for (size_t entity_number = 0; entity_number < header_.entity_scripts.size(); entity_number ++){
         std::string original_name = header_.field_entity_names[entity_number].data();
         std::string original_name = header_.field_entity_names[entity_number].data();
         // If the entity name was blank in the file then use a consistent generated name
         // If the entity name was blank in the file then use a consistent generated name
         if (original_name.empty()) original_name = "entity_" + std::to_string(entity_number);
         if (original_name.empty()) original_name = "entity_" + std::to_string(entity_number);
-        const std::string entity_name = formatter_.GetFriendlyEntityName(original_name);
+        std::string entity_name = formatter_.GetFriendlyEntityName(original_name);
+        // The original game allows several entities to share a name, such as a line and a model.
+        // Here a name identifies the entity in scripts, the map and the engine, so keep it unique.
+        if (!used_entity_names.insert(entity_name).second){
+            entity_name += "_" + std::to_string(entity_number);
+            used_entity_names.insert(entity_name);
+        }
         // Only parse each script one.
         // Only parse each script one.
         std::set<uint16> parsed_scripts;
         std::set<uint16> parsed_scripts;
         std::vector<ScriptInfo> script_info;
         std::vector<ScriptInfo> script_info;

+ 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);

+ 70 - 41
src/installer/decompiler/field/instruction/FieldCameraInstruction.cpp

@@ -31,20 +31,20 @@ void FieldCameraInstruction::ProcessInst(
     //std::cout << "Camera opcode: " << opcode_ << "\n";
     //std::cout << "Camera opcode: " << opcode_ << "\n";
     switch (opcode_){
     switch (opcode_){
     case OPCODES::NFADE: ProcessNFADE(code_gen); break;
     case OPCODES::NFADE: ProcessNFADE(code_gen); break;
-    case OPCODES::SHAKE: code_gen->WriteTodo(md.GetEntityName(), "SHAKE"); break;
+    case OPCODES::SHAKE: ProcessSHAKE(code_gen); break;
     case OPCODES::SCRLO: code_gen->WriteTodo(md.GetEntityName(), "SCRLO"); break;
     case OPCODES::SCRLO: code_gen->WriteTodo(md.GetEntityName(), "SCRLO"); break;
     case OPCODES::SCRLC: ProcessSCRLC(code_gen); break;
     case OPCODES::SCRLC: ProcessSCRLC(code_gen); break;
     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->AddOutputLine("screen_fade:wait()"); 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_
@@ -53,14 +53,9 @@ void FieldCameraInstruction::ProcessInst(
 }
 }
 
 
 void FieldCameraInstruction::ProcessNFADE(CodeGenerator* code_gen){
 void FieldCameraInstruction::ProcessNFADE(CodeGenerator* code_gen){
-    // TODO: Not fully reversed.
-    auto raw_type = params_[4]->GetUnsigned();
-    if (raw_type == 0){
-        code_gen->AddOutputLine("-- fade:clear()");
-        return;
-    }
+    // Operands: the banks of R, G and B (the third nibble is always zero), the type, R, G, B
+    // and the duration in frames.
     FieldCodeGenerator* cg = static_cast<FieldCodeGenerator*>(code_gen);
     FieldCodeGenerator* cg = static_cast<FieldCodeGenerator*>(code_gen);
-    const std::string type = raw_type == 12 ? "Fade.SUBTRACT" : "Fade.ADD";
     const auto& r = FieldCodeGenerator::FormatValueOrVariable(
     const auto& r = FieldCodeGenerator::FormatValueOrVariable(
       cg->GetFormatter(), params_[0]->GetUnsigned(), params_[5]->GetUnsigned()
       cg->GetFormatter(), params_[0]->GetUnsigned(), params_[5]->GetUnsigned()
     );
     );
@@ -68,21 +63,69 @@ void FieldCameraInstruction::ProcessNFADE(CodeGenerator* code_gen){
       cg->GetFormatter(), params_[1]->GetUnsigned(), params_[6]->GetUnsigned()
       cg->GetFormatter(), params_[1]->GetUnsigned(), params_[6]->GetUnsigned()
     );
     );
     const auto& b = FieldCodeGenerator::FormatValueOrVariable(
     const auto& b = FieldCodeGenerator::FormatValueOrVariable(
-      cg->GetFormatter(), params_[2]->GetUnsigned(), params_[7]->GetUnsigned()
-    );
-    const auto& unknown = FieldCodeGenerator::FormatValueOrVariable(
-      cg->GetFormatter(), params_[3]->GetUnsigned(), params_[8]->GetUnsigned()
-    );
-    code_gen->AddOutputLine(
-      (boost::format("-- fade:fade(%2%, %3%, %4%, %1%, %5%)") % type % r % g % b % unknown).str()
+      cg->GetFormatter(), params_[3]->GetUnsigned(), params_[7]->GetUnsigned()
     );
     );
+    code_gen->AddOutputLine((
+      boost::format("screen_fade:nfade(%1%, %2%, %3%, %4%, %5%)")
+      % params_[4]->GetUnsigned() % r % g % b % params_[8]->GetUnsigned()
+    ).str());
+}
+
+void FieldCameraInstruction::ProcessSHAKE(CodeGenerator* code_gen){
+    // Operands: two banks for the amplitudes (always zero in the game data), the type, and the
+    // amplitude and frames between bounces of each axis.
+    if (params_[0]->GetUnsigned() != 0 || params_[1]->GetUnsigned() != 0){
+        code_gen->WriteTodo("FieldCameraInstruction", "SHAKE with variable parameters");
+        return;
+    }
+    code_gen->AddOutputLine((
+      boost::format("background2d:shake(%1%, %2%, %3%, %4%, %5%)")
+      % params_[2]->GetUnsigned() % params_[3]->GetUnsigned() % params_[4]->GetUnsigned()
+      % params_[5]->GetUnsigned() % params_[6]->GetUnsigned()
+    ).str());
 }
 }
 
 
 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){
@@ -156,20 +199,8 @@ void FieldCameraInstruction::ProcessVWOFT(CodeGenerator* code_gen){
 }
 }
 
 
 void FieldCameraInstruction::ProcessFADE(CodeGenerator* code_gen){
 void FieldCameraInstruction::ProcessFADE(CodeGenerator* code_gen){
-    // TODO: not fully reversed
-    auto raw_type = params_[8]->GetUnsigned();
-    std::string type;
-    switch (raw_type) {
-        case 1:
-        case 2:
-        case 7:
-        case 8:
-            type = "Fade.SUBTRACT";
-            break;
-        case 4: code_gen->AddOutputLine("-- fade:black()"); return;
-        default: type = "Fade.ADD"; break;
-    }
-
+    // Operands: the banks of R, G and B (the third nibble is always zero), R, G, B, the speed,
+    // the type and the adjust, which doesn't change what is seen.
     FieldCodeGenerator* cg = static_cast<FieldCodeGenerator*>(code_gen);
     FieldCodeGenerator* cg = static_cast<FieldCodeGenerator*>(code_gen);
     const auto& r = FieldCodeGenerator::FormatValueOrVariable(
     const auto& r = FieldCodeGenerator::FormatValueOrVariable(
       cg->GetFormatter(), params_[0]->GetUnsigned(), params_[4]->GetUnsigned()
       cg->GetFormatter(), params_[0]->GetUnsigned(), params_[4]->GetUnsigned()
@@ -178,12 +209,10 @@ void FieldCameraInstruction::ProcessFADE(CodeGenerator* code_gen){
       cg->GetFormatter(), params_[1]->GetUnsigned(), params_[5]->GetUnsigned()
       cg->GetFormatter(), params_[1]->GetUnsigned(), params_[5]->GetUnsigned()
     );
     );
     const auto& b = FieldCodeGenerator::FormatValueOrVariable(
     const auto& b = FieldCodeGenerator::FormatValueOrVariable(
-      cg->GetFormatter(), params_[2]->GetUnsigned(), params_[6]->GetUnsigned()
+      cg->GetFormatter(), params_[3]->GetUnsigned(), params_[6]->GetUnsigned()
     );
     );
-    // TODO: needs to be divided by 30.0f?
-    auto speed = params_[7]->GetUnsigned();
-    auto start = params_[9]->GetUnsigned();
     code_gen->AddOutputLine((
     code_gen->AddOutputLine((
-      boost::format("-- fade:fade(%2%, %3%, %4%, %1%, %5%, %6%)") % type % r % g % b % speed % start
+      boost::format("screen_fade:fade(%1%, %2%, %3%, %4%, %5%)")
+      % params_[8]->GetUnsigned() % r % g % b % params_[7]->GetUnsigned()
     ).str());
     ).str());
 }
 }

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

@@ -43,6 +43,24 @@ class FieldCameraInstruction : public KernelCallInstruction{
 
 
         void ProcessNFADE(CodeGenerator* code_gen);
         void ProcessNFADE(CodeGenerator* code_gen);
 
 
+        /**
+         * Processes a SHAKE opcode.
+         *
+         * Opcode: 0x5E
+         * Short name: SHAKE
+         * Long name: Shake Camera
+         *
+         * Memory layout (8 bytes)
+         * |0x5E|B1|B2|T|XA|XF|YA|YF|
+         *
+         * Adds a shake to the view, without changing the scroll. The type T is 0 to stop it, 1 to
+         * shake the X axis, 2 to shake the Y axis and 3 to shake both. XA and YA are the amplitude
+         * of the bounces, and XF and YF the frames between them.
+         *
+         * @param[in,out] code_gen Code generator to append lines.
+         */
+        void ProcessSHAKE(CodeGenerator* code_gen);
+
         /**
         /**
          * Prodesses a SCR2D opcode.
          * Prodesses a SCR2D opcode.
          *
          *
@@ -101,6 +119,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.
          *
          *

+ 1 - 1
src/installer/decompiler/field/instruction/FieldCondJumpInstruction.cpp

@@ -31,7 +31,7 @@ void FieldCondJumpInstruction::ProcessInst(
     std::string func_name;
     std::string func_name;
     if (opcode_ == OPCODES::IFKEYON) func_name = "entity_manager:is_key_on";
     if (opcode_ == OPCODES::IFKEYON) func_name = "entity_manager:is_key_on";
     else if (opcode_ == OPCODES::IFKEYOFF) func_name = "entity_manager:is_key_off";
     else if (opcode_ == OPCODES::IFKEYOFF) func_name = "entity_manager:is_key_off";
-    else if (opcode_ == OPCODES::IFKEY) func_name = "Key";
+    else if (opcode_ == OPCODES::IFKEY) func_name = "entity_manager:is_key_on";
     else if (opcode_ == OPCODES::IFMEMBQ) func_name = "IFMEMBQ";
     else if (opcode_ == OPCODES::IFMEMBQ) func_name = "IFMEMBQ";
     else if (opcode_ == OPCODES::IFPRTYQ) func_name = "IFPRTYQ";
     else if (opcode_ == OPCODES::IFPRTYQ) func_name = "IFPRTYQ";
 
 

+ 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;

+ 203 - 17
src/installer/decompiler/field/instruction/FieldModelInstruction.cpp

@@ -113,19 +113,27 @@ void FieldModelInstruction::ProcessInst(
         case OPCODES::XYZ: code_gen->WriteTodo(md.GetEntityName(), "XYZ"); break;
         case OPCODES::XYZ: code_gen->WriteTodo(md.GetEntityName(), "XYZ"); break;
         case OPCODES::MOVE: ProcessMOVE(code_gen, md.GetEntityName()); break;
         case OPCODES::MOVE: ProcessMOVE(code_gen, md.GetEntityName()); break;
         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: ProcessMOVA(code_gen, md.GetEntityName(), eng); 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;
         case OPCODES::TURN: code_gen->WriteTodo(md.GetEntityName(), "TURN"); break;
         case OPCODES::TURN: code_gen->WriteTodo(md.GetEntityName(), "TURN"); break;
-        case OPCODES::DIRA: code_gen->WriteTodo(md.GetEntityName(), "DIRA"); break;
+        case OPCODES::DIRA: ProcessDIRA(code_gen, md.GetEntityName(), eng); break;
         case OPCODES::GETDIR: code_gen->WriteTodo(md.GetEntityName(), "GETDIR"); break;
         case OPCODES::GETDIR: code_gen->WriteTodo(md.GetEntityName(), "GETDIR"); break;
         case OPCODES::GETAXY: code_gen->WriteTodo(md.GetEntityName(), "GETAXY"); break;
         case OPCODES::GETAXY: code_gen->WriteTodo(md.GetEntityName(), "GETAXY"); break;
         case OPCODES::GETAI: ProcessGETAI(code_gen, eng); break;
         case OPCODES::GETAI: ProcessGETAI(code_gen, eng); break;
@@ -138,19 +146,19 @@ void FieldModelInstruction::ProcessInst(
         case OPCODES::CANM_2:
         case OPCODES::CANM_2:
             ProcessCANM_2(code_gen, md.GetEntityName(), md.GetCharacterId());
             ProcessCANM_2(code_gen, md.GetEntityName(), md.GetCharacterId());
             break;
             break;
-        case OPCODES::ASPED: code_gen->WriteTodo(md.GetEntityName(), "ASPED"); break;
+        case OPCODES::ASPED: ProcessASPED(code_gen, md.GetEntityName()); break;
         case OPCODES::CC: ProcessCC(code_gen, eng); break;
         case OPCODES::CC: ProcessCC(code_gen, eng); break;
         case OPCODES::JUMP: ProcessJUMP(code_gen, md.GetEntityName()); break;
         case OPCODES::JUMP: ProcessJUMP(code_gen, md.GetEntityName()); break;
         case OPCODES::AXYZI: ProcessAXYZI(code_gen); break;
         case OPCODES::AXYZI: ProcessAXYZI(code_gen); break;
-        case OPCODES::LADER: ProcessLADER(code_gen, md.GetEntityName()); break;
+        case OPCODES::LADER: ProcessLADER(code_gen, md.GetEntityName(), md.GetCharacterId()); break;
         case OPCODES::OFST: ProcessOFST(code_gen, md.GetEntityName()); break;
         case OPCODES::OFST: ProcessOFST(code_gen, md.GetEntityName()); break;
         case OPCODES::OFSTW:
         case OPCODES::OFSTW:
             code_gen->AddOutputLine("self." + md.GetEntityName() + ":offset_sync()");
             code_gen->AddOutputLine("self." + md.GetEntityName() + ":offset_sync()");
             break;
             break;
-        case OPCODES::TALKR: code_gen->WriteTodo(md.GetEntityName(), "TALKR"); break;
+        case OPCODES::TALKR: ProcessTALKR(code_gen, md.GetEntityName()); break;
         case OPCODES::SLIDR: ProcessSLIDR(code_gen, md.GetEntityName()); break;
         case OPCODES::SLIDR: ProcessSLIDR(code_gen, md.GetEntityName()); break;
         case OPCODES::SOLID: ProcessSOLID(code_gen, md.GetEntityName()); break;
         case OPCODES::SOLID: ProcessSOLID(code_gen, md.GetEntityName()); break;
-        case OPCODES::TLKR2: code_gen->WriteTodo(md.GetEntityName(), "TLKR2"); break;
+        case OPCODES::TLKR2: ProcessTALKR(code_gen, md.GetEntityName()); break;
         case OPCODES::SLDR2: code_gen->WriteTodo(md.GetEntityName(), "SLDR2"); break;
         case OPCODES::SLDR2: code_gen->WriteTodo(md.GetEntityName(), "SLDR2"); break;
         case OPCODES::CCANM: code_gen->WriteTodo(md.GetEntityName(), "CCANM"); break;
         case OPCODES::CCANM: code_gen->WriteTodo(md.GetEntityName(), "CCANM"); break;
         case OPCODES::FCFIX: code_gen->WriteTodo(md.GetEntityName(), "FCFIX"); break;
         case OPCODES::FCFIX: code_gen->WriteTodo(md.GetEntityName(), "FCFIX"); break;
@@ -232,6 +240,27 @@ 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());
+    // The walk and run animations are the second and the third of the model.
+    const std::string walk = cg->GetFormatter().GetMoveAnimationName(params_[0]->GetSigned(), 1);
+    const std::string run = cg->GetFormatter().GetMoveAnimationName(params_[0]->GetSigned(), 2);
+    if (!walk.empty() && !run.empty() && (walk != "Walk" || run != "Run")){
+        code_gen->AddOutputLine((
+          boost::format("self.%1%:set_move_animations(\"%2%\", \"%3%\")") % entity % walk % run
+        ).str());
+    }
+    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 +296,134 @@ 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::ProcessMOVA(
+  CodeGenerator* code_gen, const std::string& entity, const FieldEngine& engine
+){
+    // Walks to the other entity, and waits until it is there.
+    const auto& target = engine.EntityByIndex(params_[0]->GetUnsigned());
+    code_gen->AddOutputLine((
+      boost::format("self.%1%:move_to_entity(entity_manager:get_entity(\"%2%\"))")
+      % entity % target.GetName()
+    ).str());
+    code_gen->AddOutputLine((boost::format("self.%1%:move_sync()") % entity).str());
+}
+
+void FieldModelInstruction::ProcessTALKR(CodeGenerator* code_gen, const std::string& entity){
+    // The range is given in the same units as the positions.
+    FieldCodeGenerator* cg = static_cast<FieldCodeGenerator*>(code_gen);
+    const float scale = 128.0f * cg->GetScaleFactor();
+    const std::string range_value = FieldCodeGenerator::FormatValueOrVariable(
+      cg->GetFormatter(), params_[0]->GetUnsigned() + params_[1]->GetUnsigned(),
+      params_[2]->GetUnsigned(), FieldCodeGenerator::ValueType::Float, scale
+    );
+    float range = 0;
+    if (TryParseFloat(range_value, range)){
+        code_gen->AddOutputLine(
+          (boost::format("self.%1%:set_talk_radius(%2%)") % entity % range).str()
+        );
+    }
+    else{
+        // A variable: the scale is not applied yet.
+        code_gen->AddOutputLine(
+          "self." + entity + ":set_talk_radius((" + range_value + ") / " + std::to_string(scale) + ")"
+        );
+    }
+}
+
+void FieldModelInstruction::ProcessDIRA(
+  CodeGenerator* code_gen, const std::string& entity, const FieldEngine& engine
+){
+    // Turns the model towards another entity at once.
+    const auto& target = engine.EntityByIndex(params_[0]->GetUnsigned());
+    code_gen->AddOutputLine((
+      boost::format("self.%1%:face_entity(entity_manager:get_entity(\"%2%\"))")
+      % entity % target.GetName()
+    ).str());
+}
+
+void FieldModelInstruction::ProcessASPED(CodeGenerator* code_gen, const std::string& entity){
+    // The value is the speed of the animations: 16 is the normal one, 32 double and 8 half.
+    FieldCodeGenerator* cg = static_cast<FieldCodeGenerator*>(code_gen);
+    const std::string speed_value = FieldCodeGenerator::FormatValueOrVariable(
+      cg->GetFormatter(), params_[0]->GetUnsigned() + params_[1]->GetUnsigned(),
+      params_[2]->GetUnsigned()
+    );
+    float speed = 0;
+    if (TryParseFloat(speed_value, speed)){
+        code_gen->AddOutputLine((
+          boost::format("self.%1%:set_animation_speed(%2%)") % entity % (speed / 16.0f)
+        ).str());
+    }
+    else{
+        code_gen->AddOutputLine(
+          "self." + entity + ":set_animation_speed((" + speed_value + ") / 16.0)"
+        );
+    }
+}
+
+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 +472,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
 ){
 ){
@@ -514,7 +691,9 @@ void FieldModelInstruction::ProcessAXYZI(CodeGenerator* code_gen){
     ).str());
     ).str());
 }
 }
 
 
-void FieldModelInstruction::ProcessLADER(CodeGenerator* code_gen, const std::string& entity){
+void FieldModelInstruction::ProcessLADER(
+  CodeGenerator* code_gen, const std::string& entity, int char_id
+){
     FieldCodeGenerator* cg = static_cast<FieldCodeGenerator*>(code_gen);
     FieldCodeGenerator* cg = static_cast<FieldCodeGenerator*>(code_gen);
     const float scale = 128.0f * cg->GetScaleFactor();
     const float scale = 128.0f * cg->GetScaleFactor();
     const auto& x = FieldCodeGenerator::FormatValueOrVariable(
     const auto& x = FieldCodeGenerator::FormatValueOrVariable(
@@ -540,13 +719,13 @@ void FieldModelInstruction::ProcessLADER(CodeGenerator* code_gen, const std::str
       FieldCodeGenerator::ValueType::Float, 256.0f / 360.0f
       FieldCodeGenerator::ValueType::Float, 256.0f / 360.0f
     );
     );
     uint speed = params_[11]->GetUnsigned();
     uint speed = params_[11]->GetUnsigned();
-    // TODO: Animation hardcoded as "btce".
     // TODO: Orientation and speed not set.
     // TODO: Orientation and speed not set.
     code_gen->AddOutputLine((
     code_gen->AddOutputLine((
       boost::format(
       boost::format(
-        "self.%1%:linear_to_position(%2%, %3%, %4%, %5%, \"Climb\", %6%, %7%) "
+        "self.%1%:linear_to_position(%2%, %3%, %4%, %5%, \"%10%\", %6%, %7%) "
         "-- Animation %8% -- Speed %9%"
         "-- Animation %8% -- Speed %9%"
       ) % entity % x % y % z % keys % orientation % end_triangle % animation % speed
       ) % entity % x % y % z % keys % orientation % end_triangle % animation % speed
+        % cg->GetFormatter().GetFriendlyAnimationName(char_id, animation)
     ).str());
     ).str());
     code_gen->AddOutputLine((boost::format("self.%1%:linear_sync()") % entity).str());
     code_gen->AddOutputLine((boost::format("self.%1%:linear_sync()") % entity).str());
 }
 }
@@ -605,8 +784,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((

+ 151 - 1
src/installer/decompiler/field/instruction/FieldModelInstruction.h

@@ -156,6 +156,155 @@ class FieldModelInstruction : public KernelCallInstruction{
 
 
         void ProcessMOVE(CodeGenerator* code_gen, const std::string& entity);
         void ProcessMOVE(CodeGenerator* code_gen, const std::string& entity);
 
 
+        /**
+         * Processes a MOVA opcode.
+         *
+         * Opcode: 0xAA
+         * Short name: MOVA
+         * Long name: Move to Entity
+         *
+         * Memory layout (2 bytes)
+         * |0xAA|E|
+         *
+         * Walks the model towards the entity with ID E, up to the point where the two touch, and
+         * waits until it gets there.
+         *
+         * @param[in,out] code_gen Code generator to append lines.
+         * @param[in] entity Name of the entity.
+         * @param[in] engine Engine, to find the entity.
+         */
+        void ProcessMOVA(
+          CodeGenerator* code_gen, const std::string& entity, const FieldEngine& engine
+        );
+
+        /**
+         * Processes a TALKR or TLKR2 opcode.
+         *
+         * Opcodes: 0xC5 and 0xD6
+         * Short names: TALKR and TLKR2
+         * Long names: Talk Range and Talk Range (16-bit)
+         *
+         * Sets how far from the model the player can start a conversation with it. It's the same
+         * for both opcodes, which only differ in the size of the value.
+         *
+         * @param[in,out] code_gen Code generator to append lines.
+         * @param[in] entity Name of the entity.
+         */
+        void ProcessTALKR(CodeGenerator* code_gen, const std::string& entity);
+
+        /**
+         * Processes a DIRA opcode.
+         *
+         * Opcode: 0xB6
+         * Short name: DIRA
+         * Long name: Direction to Entity
+         *
+         * Memory layout (2 bytes)
+         * |0xB6|E|
+         *
+         * Turns the model towards the entity with ID E at once.
+         *
+         * @param[in,out] code_gen Code generator to append lines.
+         * @param[in] entity Name of the entity.
+         * @param[in] engine Engine, to find the entity.
+         */
+        void ProcessDIRA(
+          CodeGenerator* code_gen, const std::string& entity, const FieldEngine& engine
+        );
+
+        /**
+         * Processes an ASPED opcode.
+         *
+         * Opcode: 0xBD
+         * Short name: ASPED
+         * Long name: Animation Speed
+         *
+         * Sets the speed of all the animations the model plays: 16 is normal speed, 32 is double
+         * and 8 is half.
+         *
+         * @param[in,out] code_gen Code generator to append lines.
+         * @param[in] entity Name of the entity.
+         */
+        void ProcessASPED(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.
          *
          *
@@ -340,8 +489,9 @@ class FieldModelInstruction : public KernelCallInstruction{
          *
          *
          * @param[in,out] code_gen Code generator to append lines.
          * @param[in,out] code_gen Code generator to append lines.
          * @param[in] entity The name of the entity.
          * @param[in] entity The name of the entity.
+         * @param[in] char_id ID of the character model, to name the climbing animation.
          */
          */
-        void ProcessLADER(CodeGenerator* code_gen, const std::string& entity);
+        void ProcessLADER(CodeGenerator* code_gen, const std::string& entity, int char_id);
 
 
         /**
         /**
          * Processes an SLIDR opcode.
          * Processes an SLIDR 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());
 }
 }

+ 11 - 1
src/installer/decompiler/field/instruction/FieldWindowInstruction.cpp

@@ -47,7 +47,7 @@ void FieldWindowInstruction::ProcessInst(
     //std::cout << "Window opcode: " << opcode_ << "\n";
     //std::cout << "Window opcode: " << opcode_ << "\n";
     switch (opcode_){
     switch (opcode_){
         case OPCODES::TUTOR: code_gen->WriteTodo(md.GetEntityName(), "TUTOR"); break;
         case OPCODES::TUTOR: code_gen->WriteTodo(md.GetEntityName(), "TUTOR"); break;
-        case OPCODES::WCLS: code_gen->WriteTodo(md.GetEntityName(), "WCLS"); break;
+        case OPCODES::WCLS: ProcessWCLSE(code_gen); break;
         case OPCODES::WSIZW: ProcessWSIZW(code_gen); break;
         case OPCODES::WSIZW: ProcessWSIZW(code_gen); break;
         case OPCODES::WSPCL: ProcessWSPCL(code_gen); break;
         case OPCODES::WSPCL: ProcessWSPCL(code_gen); break;
         case OPCODES::WNUMB: code_gen->WriteTodo(md.GetEntityName(), "WNUMB"); break;
         case OPCODES::WNUMB: code_gen->WriteTodo(md.GetEntityName(), "WNUMB"); break;
@@ -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){

+ 1 - 0
test/CMakeLists.txt

@@ -91,6 +91,7 @@ set(SOURCE_FILES
     core/AudioManager.cpp
     core/AudioManager.cpp
     core/Background2D.cpp
     core/Background2D.cpp
     core/Background2DAnimation.cpp
     core/Background2DAnimation.cpp
+    core/ScreenFadeState.cpp
     core/CameraManager.cpp
     core/CameraManager.cpp
     core/ConfigCmd.cpp
     core/ConfigCmd.cpp
     core/ConfigCmdHandler.cpp
     core/ConfigCmdHandler.cpp

+ 64 - 4
test/core/EntityManager.cpp

@@ -16,11 +16,71 @@
 #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(TestEntityManagerInteractionPicksTheEntityTheCharacterFaces){
+    // The maximum angle is 90 degrees, and no candidate yet.
+    const float max_angle = 90.0f;
+    const float no_distance = 1000.0f;
+    BOOST_CHECK(EntityManager::IsBetterInteractionTarget(30.0f, 1.0f, max_angle, no_distance));
+    // Facing the character more directly is better, whatever the order.
+    BOOST_CHECK(EntityManager::IsBetterInteractionTarget(5.0f, 1.0f, 30.0f, 1.0f));
+    BOOST_CHECK(!EntityManager::IsBetterInteractionTarget(30.0f, 1.0f, 5.0f, 1.0f));
+    // A candidate further from the front than the best one is not better.
+    BOOST_CHECK(!EntityManager::IsBetterInteractionTarget(120.0f, 1.0f, max_angle, no_distance));
+    // The same angle: the nearest wins, and the first one is kept on a tie.
+    BOOST_CHECK(EntityManager::IsBetterInteractionTarget(10.0f, 0.5f, 10.0f, 1.0f));
+    BOOST_CHECK(!EntityManager::IsBetterInteractionTarget(10.0f, 1.0f, 10.0f, 0.5f));
+    BOOST_CHECK(!EntityManager::IsBetterInteractionTarget(10.0f, 1.0f, 10.0f, 1.0f));
+
+    // Two characters side by side, in the list order Jessie then Wedge: facing Jessie must
+    // choose Jessie, and facing Wedge must choose Wedge.
+    const float jessie_angle_facing_jessie = 4.0f;
+    const float wedge_angle_facing_jessie = 40.0f;
+    float best_angle = max_angle;
+    float best_distance = no_distance;
+    int chosen = -1;
+    const float angles[] = {jessie_angle_facing_jessie, wedge_angle_facing_jessie};
+    for (int i = 0; i < 2; ++i){
+        if (EntityManager::IsBetterInteractionTarget(angles[i], 0.2f, best_angle, best_distance)){
+            best_angle = angles[i];
+            best_distance = 0.2f;
+            chosen = i;
+        }
+    }
+    BOOST_CHECK_EQUAL(chosen, 0);
+}
+
+BOOST_AUTO_TEST_CASE(TestEntityManagerLinesTriggerEvenIfAScriptZeroedTheRadius){
+    // A script sets the radius of the player to zero while a scene plays: lines must still work.
+    BOOST_CHECK_CLOSE(EntityManager::TriggerReach(0.0f), 0.21f, 0.01f);
+    BOOST_CHECK_CLOSE(EntityManager::TriggerReach(0.005f), 0.21f, 0.01f);
+    // A radius that a script chose is respected.
+    BOOST_CHECK_CLOSE(EntityManager::TriggerReach(0.265625f), 0.265625f, 0.01f);
+    BOOST_CHECK_CLOSE(EntityManager::TriggerReach(0.078125f), 0.078125f, 0.01f);
+}
+
+BOOST_AUTO_TEST_CASE(TestEntityManagerScriptedWalksAreNotStoppedByTheWalkmeshEdge){
+    // The player and the characters that wander are stopped.
+    BOOST_CHECK(EntityManager::WalkmeshEdgeBlocks(true, false));
+    BOOST_CHECK(EntityManager::WalkmeshEdgeBlocks(false, false));
+    // Even if a script moves the player, the player stays on the walkmesh.
+    BOOST_CHECK(EntityManager::WalkmeshEdgeBlocks(true, true));
+    // A character a script walks to a position is not.
+    BOOST_CHECK(!EntityManager::WalkmeshEdgeBlocks(false, true));
+}
 
 
 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());
 }
 }
 
 

+ 163 - 0
test/core/ScreenFadeState.cpp

@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2022 The V-Gears Team
+ *
+ * This file is part of V-Gears
+ *
+ * V-Gears is free software: you can redistribute it and/or modify it under
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation, version 3.0 (GPLv3) of the License.
+ *
+ * V-Gears is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ */
+
+#include <boost/test/unit_test.hpp>
+#include "core/ScreenFadeState.h"
+
+BOOST_AUTO_TEST_CASE(TestScreenFadeStateSpeedToSeconds){
+    BOOST_CHECK_CLOSE(ScreenFadeState::SpeedToSeconds(1), 8.0f, 0.01f);
+    BOOST_CHECK_CLOSE(ScreenFadeState::SpeedToSeconds(2), 4.0f, 0.01f);
+    BOOST_CHECK_CLOSE(ScreenFadeState::SpeedToSeconds(4), 2.0f, 0.01f);
+    BOOST_CHECK_CLOSE(ScreenFadeState::SpeedToSeconds(8), 1.0f, 0.01f);
+    BOOST_CHECK_CLOSE(ScreenFadeState::SpeedToSeconds(16), 0.5f, 0.01f);
+    BOOST_CHECK_CLOSE(ScreenFadeState::SpeedToSeconds(32), 0.25f, 0.01f);
+    // A speed that is not positive is instant.
+    BOOST_CHECK_EQUAL(ScreenFadeState::SpeedToSeconds(0), 0.0f);
+    BOOST_CHECK_EQUAL(ScreenFadeState::SpeedToSeconds(-3), 0.0f);
+}
+
+BOOST_AUTO_TEST_CASE(TestScreenFadeStateNothingByDefault){
+    ScreenFadeState fade;
+    BOOST_CHECK(!fade.IsFading());
+    BOOST_CHECK(!fade.IsVisible());
+    BOOST_CHECK_EQUAL(fade.GetBlend(), ScreenFadeState::BLEND_NONE);
+    BOOST_CHECK(!fade.Advance(1.0f));
+}
+
+BOOST_AUTO_TEST_CASE(TestScreenFadeStateRedFlashAddsAndGoesBack){
+    // FADE type 5: the colour is added, and goes down to nothing. It's the red flash.
+    ScreenFadeState fade;
+    BOOST_REQUIRE(fade.StartFade(5, 255, 0, 0, 8));
+    BOOST_CHECK(fade.IsFading());
+    BOOST_CHECK(fade.IsVisible());
+    BOOST_CHECK_EQUAL(fade.GetBlend(), ScreenFadeState::BLEND_ADD);
+    BOOST_CHECK_CLOSE(fade.GetColour().r, 1.0f, 0.01f);
+    BOOST_CHECK_SMALL(fade.GetColour().g, 0.0001f);
+
+    BOOST_CHECK(!fade.Advance(0.5f));
+    BOOST_CHECK_CLOSE(fade.GetColour().r, 0.5f, 0.01f);
+    // It ends once, and then the field is shown again.
+    BOOST_CHECK(fade.Advance(0.5f));
+    BOOST_CHECK(!fade.IsFading());
+    BOOST_CHECK(!fade.IsVisible());
+    BOOST_CHECK_EQUAL(fade.GetBlend(), ScreenFadeState::BLEND_NONE);
+    BOOST_CHECK(!fade.Advance(1.0f));
+}
+
+BOOST_AUTO_TEST_CASE(TestScreenFadeStateFadeToColourHoldsIt){
+    // FADE type 10: the colour is added for the time of the fade, and stays.
+    ScreenFadeState fade;
+    BOOST_REQUIRE(fade.StartFade(10, 40, 80, 120, 16));
+    BOOST_CHECK(fade.Advance(0.5f));
+    BOOST_CHECK(!fade.IsFading());
+    BOOST_CHECK(fade.IsVisible());
+    BOOST_CHECK_EQUAL(fade.GetBlend(), ScreenFadeState::BLEND_ADD);
+    BOOST_CHECK_CLOSE(fade.GetColour().b, 120.0f / 255.0f, 0.01f);
+
+    // Type 9 is the same, but it goes back to the field.
+    BOOST_REQUIRE(fade.StartFade(9, 40, 80, 120, 16));
+    BOOST_CHECK(fade.Advance(0.5f));
+    BOOST_CHECK(!fade.IsVisible());
+}
+
+BOOST_AUTO_TEST_CASE(TestScreenFadeStateSubtractiveTypes){
+    // Type 2 goes from nothing to the inverse of the colour times four, and holds it.
+    ScreenFadeState fade;
+    BOOST_REQUIRE(fade.StartFade(2, 255, 255, 255, 8));
+    BOOST_CHECK_EQUAL(fade.GetBlend(), ScreenFadeState::BLEND_SUBTRACT);
+    BOOST_CHECK(fade.Advance(1.0f));
+    // White inverted is black: nothing is subtracted.
+    BOOST_CHECK_SMALL(fade.GetColour().r, 0.0001f);
+
+    // Black inverted four times is brighter than the maximum: it is limited, and takes it all.
+    BOOST_REQUIRE(fade.StartFade(2, 0, 0, 0, 8));
+    BOOST_CHECK(fade.Advance(1.0f));
+    BOOST_CHECK_CLOSE(fade.GetColour().r, 1.0f, 0.01f);
+    BOOST_CHECK(fade.IsVisible());
+
+    // Type 1 goes the other way, and ends with the field.
+    BOOST_REQUIRE(fade.StartFade(1, 0, 0, 0, 8));
+    BOOST_CHECK_CLOSE(fade.GetColour().r, 1.0f, 0.01f);
+    BOOST_CHECK(fade.Advance(1.0f));
+    BOOST_CHECK(!fade.IsVisible());
+}
+
+BOOST_AUTO_TEST_CASE(TestScreenFadeStateInstantBlackAndNormalFade){
+    // Type 4 makes the screen black at once, and nobody has to wait for it.
+    ScreenFadeState fade;
+    BOOST_REQUIRE(fade.StartFade(4, 12, 34, 56, 8));
+    BOOST_CHECK(!fade.IsFading());
+    BOOST_CHECK(fade.IsVisible());
+    BOOST_CHECK_EQUAL(fade.GetBlend(), ScreenFadeState::BLEND_NORMAL);
+    BOOST_CHECK_CLOSE(fade.GetAlpha(), 1.0f, 0.01f);
+    BOOST_CHECK_SMALL(fade.GetColour().r, 0.0001f);
+
+    // Type 6 covers the field with the colour little by little, and keeps it.
+    BOOST_REQUIRE(fade.StartFade(6, 255, 255, 255, 8));
+    BOOST_CHECK_EQUAL(fade.GetBlend(), ScreenFadeState::BLEND_NORMAL);
+    BOOST_CHECK_SMALL(fade.GetAlpha(), 0.0001f);
+    fade.Advance(0.5f);
+    BOOST_CHECK_CLOSE(fade.GetAlpha(), 0.5f, 0.01f);
+    BOOST_CHECK(fade.Advance(0.5f));
+    BOOST_CHECK(fade.IsVisible());
+    BOOST_CHECK_CLOSE(fade.GetAlpha(), 1.0f, 0.01f);
+}
+
+BOOST_AUTO_TEST_CASE(TestScreenFadeStateNFade){
+    // NFADE type 11 adds the colour in 30 frames, which is a second, and keeps it.
+    ScreenFadeState fade;
+    BOOST_REQUIRE(fade.StartNFade(11, 200, 100, 0, 30));
+    BOOST_CHECK_EQUAL(fade.GetBlend(), ScreenFadeState::BLEND_ADD);
+    BOOST_CHECK_SMALL(fade.GetColour().r, 0.0001f);
+    BOOST_CHECK(!fade.Advance(0.5f));
+    BOOST_CHECK_CLOSE(fade.GetColour().r, 0.5f * 200.0f / 255.0f, 0.01f);
+    BOOST_CHECK(fade.Advance(0.5f));
+    BOOST_CHECK(fade.IsVisible());
+    BOOST_CHECK_CLOSE(fade.GetColour().r, 200.0f / 255.0f, 0.01f);
+
+    // Type 12 does the same, taking the colour from the field.
+    BOOST_REQUIRE(fade.StartNFade(12, 200, 100, 0, 30));
+    BOOST_CHECK_EQUAL(fade.GetBlend(), ScreenFadeState::BLEND_SUBTRACT);
+
+    // Type 0 clears whatever there is.
+    BOOST_REQUIRE(fade.StartNFade(0, 0, 0, 0, 0));
+    BOOST_CHECK(!fade.IsVisible());
+    BOOST_CHECK(!fade.IsFading());
+}
+
+BOOST_AUTO_TEST_CASE(TestScreenFadeStateUnknownTypesAndReplacement){
+    ScreenFadeState fade;
+    // Unknown types change nothing.
+    BOOST_CHECK(!fade.StartFade(3, 255, 0, 0, 8));
+    BOOST_CHECK(!fade.StartFade(99, 255, 0, 0, 8));
+    BOOST_CHECK(!fade.StartNFade(5, 255, 0, 0, 8));
+    BOOST_CHECK(!fade.IsVisible());
+
+    // A new fade replaces the one that is running.
+    BOOST_REQUIRE(fade.StartFade(5, 255, 0, 0, 8));
+    fade.Advance(0.5f);
+    BOOST_REQUIRE(fade.StartFade(10, 0, 255, 0, 8));
+    BOOST_CHECK_EQUAL(fade.GetBlend(), ScreenFadeState::BLEND_ADD);
+    BOOST_CHECK_SMALL(fade.GetColour().r, 0.0001f);
+    BOOST_CHECK_CLOSE(fade.GetColour().g, 1.0f, 0.01f);
+
+    // An unknown type doesn't stop the one that is running.
+    BOOST_CHECK(!fade.StartFade(3, 0, 0, 0, 8));
+    BOOST_CHECK(fade.IsFading());
+
+    fade.Reset();
+    BOOST_CHECK(!fade.IsVisible());
+    BOOST_CHECK(!fade.IsFading());
+}

+ 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");

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

@@ -16,11 +16,361 @@
 #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(TestFieldDecompilerMovaWalksToTheEntityAndWaits){
+    FieldScriptFormatter formatter;
+    // MOVA 0: the only entity of the synthetic field.
+    const std::string output = DecompileWith({0xAA, 0}, formatter);
+    const size_t walk = output.find("self.Door:move_to_entity(entity_manager:get_entity(\"Door\"))");
+    const size_t wait = output.find("self.Door:move_sync()");
+    BOOST_REQUIRE(walk != std::string::npos && wait != std::string::npos);
+    BOOST_CHECK(walk < wait);
+    BOOST_CHECK(output.find("UNIMPLMENTED") == std::string::npos);
+}
+
+BOOST_AUTO_TEST_CASE(TestFieldDecompilerScreenFades){
+    FieldScriptFormatter formatter;
+    // FADE 255, 0, 0, speed 8, type 5 - NFADE type 11, 10, 20, 30, 15 frames - FADEW.
+    const std::string output = DecompileWith(
+      {0x6B, 0, 0, 255, 0, 0, 8, 5, 0, 0x25, 0, 0, 11, 10, 20, 30, 15, 0, 0x6C}, formatter
+    );
+    BOOST_CHECK(output.find("screen_fade:fade(5, 255, 0, 0, 8)") != std::string::npos);
+    BOOST_CHECK(output.find("screen_fade:nfade(11, 10, 20, 30, 15)") != std::string::npos);
+    BOOST_CHECK(output.find("screen_fade:wait()") != std::string::npos);
+    // They used to be comments.
+    BOOST_CHECK(output.find("-- fade:") == std::string::npos);
+    BOOST_CHECK(output.find("UNIMPLMENTED") == std::string::npos);
+}
+
+BOOST_AUTO_TEST_CASE(TestFieldDecompilerWindowCloseTalkRangeAndFacing){
+    FieldScriptFormatter formatter;
+    // WCLS 2 - TALKR 64 - TLKR2 128 - DIRA 0.
+    const std::string output = DecompileWith(
+      {0x2E, 2, 0xC5, 0, 64, 0xD6, 0, 128, 0, 0xB6, 0}, formatter
+    );
+    BOOST_CHECK(output.find("dialog:dialog_close(\"2\")") != std::string::npos);
+    BOOST_CHECK(output.find("self.Door:set_talk_radius(0.5)") != std::string::npos);
+    BOOST_CHECK(output.find("self.Door:set_talk_radius(1)") != std::string::npos);
+    BOOST_CHECK(
+      output.find("self.Door:face_entity(entity_manager:get_entity(\"Door\"))") != std::string::npos
+    );
+    BOOST_CHECK(output.find("UNIMPLMENTED") == std::string::npos);
+}
+
+BOOST_AUTO_TEST_CASE(TestFieldDecompilerAspedSetsTheAnimationSpeed){
+    FieldScriptFormatter formatter;
+    // ASPED 16 (normal speed), ASPED 32 (double) and ASPED 8 (half).
+    const std::string output = DecompileWith(
+      {0xBD, 0, 16, 0, 0xBD, 0, 32, 0, 0xBD, 0, 8, 0}, formatter
+    );
+    BOOST_CHECK(output.find("self.Door:set_animation_speed(1)") != std::string::npos);
+    BOOST_CHECK(output.find("self.Door:set_animation_speed(2)") != std::string::npos);
+    BOOST_CHECK(output.find("self.Door:set_animation_speed(0.5)") != std::string::npos);
+    BOOST_CHECK(output.find("UNIMPLMENTED") == std::string::npos);
+}
+
+BOOST_AUTO_TEST_CASE(TestFieldDecompilerShakeKeepsItsParameters){
+    FieldScriptFormatter formatter;
+    // SHAKE 0, 0, 2, 0, 0, 5, 2 (shake the Y axis) and SHAKE 0, 0, 0, 0, 1, 0, 1 (stop).
+    const std::string output = DecompileWith(
+      {0x5E, 0, 0, 2, 0, 0, 5, 2, 0x5E, 0, 0, 0, 0, 1, 0, 1}, formatter
+    );
+    BOOST_CHECK(output.find("background2d:shake(2, 0, 0, 5, 2)") != std::string::npos);
+    BOOST_CHECK(output.find("background2d:shake(0, 0, 1, 0, 1)") != std::string::npos);
+    BOOST_CHECK(output.find("UNIMPLMENTED") == 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);
 }
 }