Selaa lähdekoodia

Installer improvements:
- Fixed tile depth calculation in fields.
- Textures are now generated from .tex files and assigned to their models.
- RET opcode implementation modified, so each function only has one final return statement.
- The installer now adds a return statement to function that don't have one in the game data.
- OFST opcode implemented.
- Fixes in bit operations in map fields.
- Better logging and error messages

Iñigo Valentin 4 vuotta sitten
vanhempi
sitoutus
2c32eb4dc1

+ 97 - 78
V-Gears-Installer/src/ff7DataInstaller.cpp

@@ -39,6 +39,7 @@
 #include "data/QGearsPaletteFileManager.h"
 #include "data/QGearsPFileManager.h"
 #include "data/QGearsRSDFileManager.h"
+#include "data/QGearsTexFile.h"
 #include "data/QGearsTexCodec.h"
 #include "map/QGearsBackground2DFileManager.h"
 #include "map/QGearsWalkmeshFileManager.h"
@@ -93,33 +94,22 @@ int FF7DataInstaller::Progress(){
               application_.getRoot(), input_dir_ + "field/flevel.lgp",
               "LGP", "FFVIIFields"
             );
-            textures_lgp_ = std::make_unique<ScopedLgp>(
-              application_.getRoot(), input_dir_ + "field/flevel.lgp",
-              "TEX", "FFVIITextures"
-            );
-            // TODO: DEBUG
-            std::cout << "[INS] -> SPAWN_POINTS_AND_SCALE_FACTORS_INIT" << std::endl;
             installation_state_ = SPAWN_POINTS_AND_SCALE_FACTORS_INIT;
-            //installation_state_ = CONVERT_FIELD_MODELS;
+            // TODO: DEBUG:
+            //installation_state_ = CONVERT_FIELDS;
             return CalcProgress();
         case SPAWN_POINTS_AND_SCALE_FACTORS_INIT:
-            write_output_line("Collecting spawn points and scale factors");
-            std::cout << "[INS][PRE] InitCollectSpawnAndScaleFactors" << std::endl;
+            write_output_line("Collecting spawn points and scale factors...");
             InitCollectSpawnAndScaleFactors();
-            std::cout << "[INS][POST] InitCollectSpawnAndScaleFactors" << std::endl;
             return CalcProgress();
         case SPAWN_POINTS_AND_SCALE_FACTORS:
-            std::cout << "[INS][PRE] CollectSpawnAndScaleFactors" << std::endl;
             CollectionFieldSpawnAndScaleFactors();
-            std::cout << "[INS][POST] CollectSpawnAndScaleFactors" << std::endl;
             return CalcProgress();
         case CONVERT_FIELDS:
-            std::cout << "[INS][PRE] ConvertFieldsIteration" << std::endl;
             ConvertFieldsIteration();
-            std::cout << "[INS][POST] ConvertFieldsIteration" << std::endl;
             return CalcProgress();
         case WRITE_MAPS_INIT:
-            write_output_line("Write fields");
+            write_output_line("Writing fields...");
             WriteMapsXmlBegin();
             return CalcProgress();
         case WRITE_MAPS:
@@ -129,15 +119,13 @@ int FF7DataInstaller::Progress(){
             EndWriteMapsXml();
             return CalcProgress();
         case CONVERT_FIELD_MODELS_INIT:
-            write_output_line("Converting field models");
+            write_output_line("Converting field models...");
             ConvertFieldModelsBegin();
             return CalcProgress();
         case CONVERT_FIELD_MODELS:
             ConvertFieldModelsIteration();
             return CalcProgress();
         case DONE:
-            write_output_line("Installation done");
-            std::cout << "[INSTALL STEP] DONE" << std::endl;
         default:
             return 100;
     }
@@ -147,6 +135,49 @@ static std::string FieldModelDir(){return "models/ffvii/field/units";}
 
 std::vector<std::string> materials_;
 
+/**
+ * Converts a tex file to png.
+ *
+ * The image is saved in data/models/ffvii/field/units/
+ *
+ * @param name[in] Path to the tex file to convert.
+ */
+void TexToPng(const std::string name){
+    Ogre::DataStreamPtr stream(
+      Ogre::ResourceGroupManager::getSingleton().openResource(
+        name, "FFVIITextures", true, NULL
+      )
+    );
+    QGears::TexFile* tex = new QGears::TexFile();
+    tex->Read(stream);
+    Ogre::ImageCodec::ImageData* image_data = tex->GetImageData();
+    Ogre::Image* image = new Ogre::Image(
+      Ogre::PF_R8G8B8A8_UINT, image_data->width, image_data->height
+    );
+    for (int y = 0; y < image_data->height; y ++){
+        for (int x = 0; x < image_data->width; x ++){
+            // This is the position from which to read from the tex file colour
+            // data
+            // TODO: It doesn't work for non-square images. For example, the
+            // lower part of bxje is all wrong.
+            int i = (image_data->height * y) + x;
+            image->setColourAt(
+              Ogre::ColourValue(
+                tex->image_data_[i].comp.red / 255.0f,
+                tex->image_data_[i].comp.green / 255.0f,
+                tex->image_data_[i].comp.blue / 255.0f,
+                tex->image_data_[i].comp.alpha),
+              x, y, 0
+            );
+        }
+    }
+    Ogre::String base_name;
+    QGears::StringUtil::splitBase(name, base_name);
+    // TODO: Detect path
+    image->save("data/models/ffvii/field/units/" + base_name + ".png");
+
+}
+
 /**
  * Exports a mesh to a file.
  *
@@ -160,9 +191,7 @@ static void ExportMesh(std::string outdir, const Ogre::MeshPtr &mesh){
     // TODO: Share function with pc model exporter
     QGears::String base_mesh_name;
     QGears::StringUtil::splitFull(mesh->getName(), base_mesh_name);
-
     Ogre::MeshSerializer mesh_serializer;
-
     Ogre::SkeletonPtr skeleton(mesh->getSkeleton());
     Ogre::SkeletonSerializer skeleton_serializer;
     skeleton_serializer.exportSkeleton(
@@ -204,10 +233,11 @@ static void ExportMesh(std::string outdir, const Ogre::MeshPtr &mesh){
                                   unit
                                   && unit->getTextureName().empty() == false
                                 ){
+                                    // Convert the texture from .tex to .png.
+                                    TexToPng(unit->getTextureName());
                                     // Ensure the output material script
                                     // references png files rather than tex
                                     // files.
-                                    textures.insert(unit->getTextureName());
                                     Ogre::String base_name;
                                     QGears::StringUtil::splitBase(
                                       unit->getTextureName(), base_name
@@ -216,12 +246,20 @@ static void ExportMesh(std::string outdir, const Ogre::MeshPtr &mesh){
                                       FieldModelDir() + "/" + base_mesh_name
                                       + "_" + base_name + ".png"
                                     );
-                                    std::cout << "[TEXTURE] "
-                                      << unit->getTextureName() << " | "
-                                      << (
-                                        FieldModelDir() + "/" + base_mesh_name
-                                        + "_" + base_name + ".png"
-                                      ) << std::endl;
+                                    // Copy subtexture (xxxx.png) to
+                                    // model_xxxx.png
+                                    // TODO: obtain the "data" folder
+                                    // programatically.
+                                    boost::filesystem::copy_file(
+                                      "data/" + FieldModelDir() + "/"
+                                        + base_name + ".png",
+                                      "data/" + FieldModelDir() + "/"
+                                        + base_mesh_name + "_"
+                                        + base_name + ".png",
+                                        boost::filesystem::copy_option
+                                          ::overwrite_if_exists
+                                    );
+                                    textures.insert(unit->getTextureName());
                                 }
                             }
                         }
@@ -239,51 +277,29 @@ static void ExportMesh(std::string outdir, const Ogre::MeshPtr &mesh){
                 mat_ser.queueForExport(mat);
                 materials_.push_back(sub_mesh->getMaterialName());
             }
-            /*
-            else{
-                std::cout << "[MATERIAL] Not rewritting material "
-                  << sub_mesh->getMaterialName() << std::endl;
-            }
-            */
+
         }
         ++ i;
     }
     mat_ser.exportQueued(outdir + base_mesh_name + QGears::EXT_MATERIAL);
     for (auto& texture_name : textures){
-        //std::cout << "    [EXPORTMESH] Texture " << texture_name.c_str()
-        //  << std::endl;
         std::string tex_name = texture_name.c_str();
-        std::cout << "[EXPORTMESH] Texture name " << tex_name << std::endl;
         try{
             Ogre::TexturePtr texture_ptr
               = Ogre::TextureManager::getSingleton().load(
                 tex_name, "FFVIITextures" //"FFVII"
               );
-            //Ogre::TexturePtr texture_ptr =
-            //  Ogre::TextureManager::getSingleton().create(
-            //  tex_name, "FFVIITextures" //"FFVII"
-            //);
-            std::cout << "    [EXPORTMESH] Texture loaded" << std::endl;
+
             Ogre::Image image;
             texture_ptr->convertToImage(image);
-            //image.load()
-            std::cout << "    [EXPORTMESH] Texture converted" << std::endl;
             Ogre::String base_name;
             QGears::StringUtil::splitBase(texture_name, base_name);
-            std::cout << "    [EXPORTMESH] name split: "
-              << base_name << std::endl;
-            std::cout << "    [EXPORTMESH] saving: " << (
-              outdir + base_mesh_name + "_" + base_name + ".png"
-            ) << std::endl;
             image.save(outdir + base_mesh_name + "_" + base_name + ".png");
-            std::cout << "    [EXPORTMESH] Texture saved" << std::endl;
         }
         catch (std::exception const& ex){
             std::cerr << "[ERROR] Exception: " << ex.what() << std::endl;
         }
     }
-    //std::cout << "[EXPORTMESH] END TEXTURE LOOP "<< std::endl;
-    //std::cout << "[EXPORTMESH] END "<< std::endl;
 }
 
 /**
@@ -493,13 +509,13 @@ class FF7FieldScriptFormatter : public BaseFF7FieldScriptFormatter{
               static_cast<unsigned int>(char_id)
               >= model_loader_->GetModels().size()
             ){
-                std::cout << "FF7FieldScriptFormatter::AnimationName ERROR:"
+                std::cerr << "FF7FieldScriptFormatter::AnimationName ERROR:"
                   << "Char ID " << char_id << " out of bounds" << std::endl;
                 return std::to_string(id);
             }
             const auto& model_info = model_loader_->GetModels().at(char_id);
             if (static_cast<unsigned int>(id) >= model_info.animations.size()){
-                std::cout << "FF7FieldScriptFormatter::AnimationName ERROR:"
+                std::cerr << "FF7FieldScriptFormatter::AnimationName ERROR:"
                   << "In field " << field_name_ << " the model "
                   << model_info.name << " animation with ID " << id
                   << " is out of bounds (" << model_info.animations.size() << ")"
@@ -673,14 +689,18 @@ static void FF7PcFieldToQGearsField(
             }
         }
         else{
-            write_output_line("[ERROR] Failed to open script file for writing");
-            std::cerr << "[ERROR] Failed to open script file for writing"
-              << std::endl;
+            write_output_line(
+              "[ERROR] Failed to open script file from field "
+              + field->getName() + " for writing."
+            );
+            std::cerr << "[ERROR] Failed to open script file from field "
+              << field->getName() << "for writing." << std::endl;
         }
     }
     catch (const ::InternalDecompilerError& ex){
         write_output_line(
           "[ERROR] Internal decompiler error in field " + field->getName()
+          + ": " + ex.what()
         );
         std::cerr << "[ERROR] Internal decompiler error in field "
           << field->getName() << ": " << ex.what() << std::endl;
@@ -786,7 +806,15 @@ static void FF7PcFieldToQGearsField(
                 if (
                   triangle_index >= field->GetWalkmesh()->GetTriangles().size()
                 ){
-                    std::cout << "[WARNING] In field " << field->getName()
+                    write_output_line(
+                      "[WARNING] In field " + field->getName()
+                      + ": Map jump triangle ("
+                      + std::to_string(triangle_index) + ") out of bounds ("
+                      + std::to_string(
+                        field->GetWalkmesh()->GetTriangles().size()
+                      ) + ")"
+                    );
+                    std::cerr << "[WARNING] In field " << field->getName()
                       << ": Map jump triangle (" << triangle_index
                       << ") out of bounds ("
                       << field->GetWalkmesh()->GetTriangles().size() << ")"
@@ -1033,17 +1061,13 @@ static void FF7PcFieldToQGearsField(
                       + Ogre::StringConverter::toString(u1) + " "
                       + Ogre::StringConverter::toString(v1)
                     );
-
-                    // TODO: Figure out how to scale this value correctly.
-                    /*
                     xml_element->SetAttribute(
                       "depth",
                       Ogre::StringConverter::toString(
-                        static_cast<float>(sprite.depth) / (4096.0f/10.0f)
+                        // TODO: It works (on FFVII PC), but why this number?
+                        static_cast<float>(sprite.depth) * (0.03125f) // ??
                       )
                     );
-                    */
-                    xml_element->SetAttribute("depth", "999");
                     // TODO: Copied from DatFile::AddTile.
                     // Add to common method.
                     Ogre::String blending_str = "";
@@ -1052,10 +1076,8 @@ static void FF7PcFieldToQGearsField(
                     else if (sprite.blending == 1 || sprite.blending == 3)
                         blending_str = "add";
                     else if (sprite.blending == 2) blending_str = "subtract";
-                    else{
-                        // TODO: Should probably throw to fail conversion.
-                        blending_str = "unknown";
-                    }
+                    // TODO: Should probably throw to fail conversion:
+                    else blending_str = "unknown";
                     xml_element->SetAttribute("blending", blending_str);
                     element->LinkEndChild(xml_element.release());
                 }
@@ -1436,10 +1458,10 @@ void FF7DataInstaller::ConvertFieldsIteration(){
             else{
                 write_output_line(
                   "[ERROR] Skip field " + resource_name
-                  + " due to crash or hang issue"
+                  + " due to crash or hang issue."
                 );
-                std::cout << "[ERROR] Skip field: " << resource_name
-                  << " as it has a crash or hang issue" << std::endl;
+                std::cerr << "[ERROR] Skip field: " << resource_name
+                  << " due to crash or hang issue." << std::endl;
             }
         }
         iterator_counter_ ++;
@@ -1488,7 +1510,6 @@ void FF7DataInstaller::EndWriteMapsXml(){
 }
 
 void FF7DataInstaller::ConvertFieldModelsBegin(){
-    std::cout << "[INSTALL] Step: ConvertFieldModels" << std::endl;
     // TODO: Convert models and animations in model_animation_db.
     progress_step_num_elements_ = 1;
     field_models_lgp_ = std::make_unique<ScopedLgp>(
@@ -1551,16 +1572,16 @@ void FF7DataInstaller::ConvertFieldModelsIteration(){
                   "[ERROR] Ogre exception converting model "
                   + model_animation_map_iterator_->first + ": " + ex.what()
                 );
-                std::cout << "[ERROR] Exception converting model "
+                std::cerr << "[ERROR] Ogre exception converting model "
                   << model_animation_map_iterator_->first <<": " << ex.what()
                   << std::endl;
             }
             catch (const std::exception& ex){
                 write_output_line(
-                  "[ERROR] Ogre exception converting model "
+                  "[ERROR] Exception converting model "
                   + model_animation_map_iterator_->first + ": " + ex.what()
                 );
-                std::cout << "[ERROR] Exception converting model "
+                std::cerr << "[ERROR] Exception converting model "
                   << model_animation_map_iterator_->first << ": " << ex.what()
                   << std::endl;
             }
@@ -1570,10 +1591,8 @@ void FF7DataInstaller::ConvertFieldModelsIteration(){
         }
     }
     else{
-        std::cout << "[INSTALL] Finished" << std::endl;
-        write_output_line("[INSTALL] Finished");
+        std::cout << "Installation finished." << std::endl;
+        write_output_line("Installation finished.");
         installation_state_ = DONE;
     }
-    //std::cout << "ConvertFieldModelsIteration: " << iterator_counter_
-    //  << "/" << used_models_and_anims_.map.size() << "\n";
 }

+ 8 - 5
V-Gears/src/core/ScriptManager.cpp

@@ -223,17 +223,20 @@ void ScriptManager::Update(const ScriptManager::Type type){
                                   script_entity_[i].queue[0].argument2.c_str()
                                 );
                             }
-                            catch(luabind::error& /*e*/){
+                            catch(luabind::error& e){
                                 LOG_ERROR(
-                                  "LUA error in " + script_entity_[i].name
+                                  "LUA error in entity "
+                                  + script_entity_[i].name + " ("
+                                  + current_script_id_.entity + ") "
                                   + " in function "
-                                  + script_entity_[i].queue[0].function
-                                  + " details: "
+                                  + script_entity_[i].queue[0].function + " ("
+                                  + current_script_id_.function + ") "
+                                  + ". Details: "
                                   + Ogre::String(
                                     lua_tostring(
                                       script_entity_[i].queue[0].state, -1
                                     )
-                                  )
+                                  ) + " : " + e.what()
                                 );
                             }
 

+ 1 - 1
V-Gears/src/data/QGearsFLevelFile.cpp

@@ -142,7 +142,7 @@ namespace QGears{
     const String& FLevelFile::GetResourceType(void) const{return RESOURCE_TYPE;}
 
     size_t FLevelFile::calculateSize() const{
-        // Data is only stored in section resources
+        // Data is only stored in section resources.
         return 0;
     }
 

+ 3 - 4
V-Gears/src/modules/worldmap/WorldMapModule.cpp

@@ -389,7 +389,7 @@ namespace QGears {
 
         // TODO: Get correct path
         // TODO: What is this file? Where does it come from?
-        const char* file_name = "~/.gears/data/wm/wm0.txz";
+        const char* file_name = "data/wm/wm0.txz";
         std::ifstream ifs(file_name, std::ifstream::binary);
         Ogre::DataStreamPtr stream(
           OGRE_NEW Ogre::FileStreamDataStream(&ifs, false)
@@ -425,12 +425,11 @@ namespace QGears {
                  */
                 TestFile() : QGears::WorldMapFile(nullptr, "", 0, "") {}
         };
-
-        const char* file_name = "~/.gears/data/wm/WM0.MAP";
+        const char* file_name = "data/wm/WM0.MAP";
         std::ifstream ifs(file_name, std::ifstream::binary);
         if (!ifs.is_open()){
             LogManager::getSingleton().logMessage(
-              "ERROR: Failed to open ~/.gears/data/wm/WM0.MAP"
+              "ERROR: Failed to open data/wm/WM0.MAP"
             );
             return;
         }

+ 4 - 0
lib/SUDM/decompiler/ff7_field/ff7_field_codegen.cpp

@@ -90,6 +90,7 @@ void FF7::FF7SimpleCodeGenerator::generate(InstVec& insts, const Graph& /*g*/)
             }
         }
 
+        std::string last_instruction = "";
         for (auto instruction = function->second.begin(); instruction != function->second.end(); ++instruction)
         {
             auto label = labels.find((*instruction)->_address);
@@ -140,6 +141,9 @@ void FF7::FF7SimpleCodeGenerator::generate(InstVec& insts, const Graph& /*g*/)
             // else, was already output'd
         }
 
+        if ("return 0" != mLines.at(mLines.size() - 1)._line)
+            addOutputLine("return 0 -- Missing original return", false, false);
+
         onEndFunction(function->first);
     }
 

+ 50 - 7
lib/SUDM/decompiler/ff7_field/ff7_field_engine.cpp

@@ -280,7 +280,8 @@ void FF7::FF7CondJumpInstruction::processInst(Function&, ValueStack &stack, Engi
 
     case 9:
     {
-        op = "hasbit("+ source + ", bit(" + destination + "))";
+        op = "bit(" + _params[0]->getString() + ", " + _params[2]->getString()
+          + ", " + destination + ") == 1";
         ValuePtr v = new UnqotedStringValue(op);
         stack.push(v);
     }
@@ -288,7 +289,8 @@ void FF7::FF7CondJumpInstruction::processInst(Function&, ValueStack &stack, Engi
 
     case 0xA:
     {
-        op = "not hasbit(" + source + ", bit(" + destination + "))";
+        op = "bit(" + _params[0]->getString() + ", " + _params[2]->getString()
+          + ", " + destination + ") == 0";
         ValuePtr v = new UnqotedStringValue(op);
         stack.push(v);
     }
@@ -418,7 +420,7 @@ void FF7::FF7ControlFlowInstruction::processInst(Function& func, ValueStack&, En
     {
     case eOpcodes::RET:
         // TODO: Lua won't allow return if not followeb by end;
-        codeGen->addOutputLine("return 0"); // Seems all our functions must return zero
+        codeGen->addOutputLine("do return 0 end"); // Seems all our functions must return zero
         //codeGen->addOutputLine("do return end");
         //codeGen->addOutputLine("-- do return end");
         break;
@@ -1503,7 +1505,8 @@ void FF7::FF7ModelInstruction::processInst(Function& func, ValueStack&, Engine*
         break;
 
     case eOpcodes::OFST:
-    {
+        processOFST(codeGen, md.EntityName());
+    //{
         /* TODO: Implement me
         FF7SimpleCodeGenerator* cg = static_cast<FF7SimpleCodeGenerator*>(codeGen);
 
@@ -1532,13 +1535,14 @@ void FF7::FF7ModelInstruction::processInst(Function& func, ValueStack&, Engine*
             ((type == 2) ? "Entity.SMOOTH" : "Entity.LINEAR") + ", " +
             ParseGetVariable(GetU8(script + 2) & 0x0F, GetU16LE(script + 10), 0, 30.0f) + " )\n");
             */
-        WriteTodo(codeGen, md.EntityName(), "OFST");
+       // WriteTodo(codeGen, md.EntityName(), "OFST");
       
-    }
+    //}
         break;
 
     case eOpcodes::OFSTW:
-        codeGen->addOutputLine(md.EntityName() + ":offset_sync()");
+        //codeGen->addOutputLine(md.EntityName() + ":offset_sync()");
+        codeGen->addOutputLine("self." + md.EntityName() + ":offset_sync()");
         break;
 
     case eOpcodes::TALKR:
@@ -1767,6 +1771,45 @@ void FF7::FF7ModelInstruction::processSOLID(CodeGenerator* codeGen, const std::s
     codeGen->addOutputLine((boost::format("self.%1%:set_solid( %2% )") % entity % FF7CodeGeneratorHelpers::FormatInvertedBool(_params[0]->getUnsigned())).str());
 }
 
+void FF7::FF7ModelInstruction::processOFST(CodeGenerator* codeGen, const std::string& entity){
+    // IVV TODO
+    FF7SimpleCodeGenerator* cg = static_cast<FF7SimpleCodeGenerator*>(codeGen);
+    float x = atoi(
+      FF7CodeGeneratorHelpers::FormatValueOrVariable(
+        cg->mFormatter, _params[0]->getUnsigned(), _params[5]->getSigned()
+      ).c_str()
+    );
+    float y = atoi(
+      FF7CodeGeneratorHelpers::FormatValueOrVariable(
+        cg->mFormatter, _params[1]->getUnsigned(),_params[6]->getSigned()
+      ).c_str()
+    );
+    float z = atoi(
+      FF7CodeGeneratorHelpers::FormatValueOrVariable(
+        cg->mFormatter, _params[2]->getUnsigned(), _params[7]->getSigned()
+      ).c_str()
+    );
+    float speed = atoi(
+      FF7CodeGeneratorHelpers::FormatValueOrVariable(
+        cg->mFormatter, _params[3]->getUnsigned(), _params[8]->getUnsigned()
+      ).c_str()
+    );
+    // Spatial coordinates need to be scaled down
+    // TODO: This number is empirically deducted. Why this number?
+    x *= 0.00390f;
+    y *= 0.00390f;
+    z *= 0.00390f;
+    // Speed needs to be scaled down by the frame rate.
+    speed /= 30.0f;
+    codeGen->addOutputLine(
+      (
+        boost::format("self.%1%:offset_to_position( %2%, %3%, %4%, %5%, %6%)")
+          % entity % x % y % z
+          % (_params[4]->getUnsigned() ? "Entity.SMOOTH" : "Entity.LINEAR")
+          % speed
+      ).str());
+}
+
 void FF7::FF7WalkmeshInstruction::processInst(Function& func, ValueStack&, Engine* /*engine*/, CodeGenerator *codeGen)
 {
     //FF7::FF7FieldEngine& eng = static_cast<FF7::FF7FieldEngine&>(*engine);

+ 52 - 0
lib/SUDM/decompiler/ff7_field/ff7_field_engine.h

@@ -777,6 +777,58 @@ namespace FF7{
         void processCANM_2(CodeGenerator* codeGen, const std::string& entity, int charId);
         void processCC(CodeGenerator* codeGen, const FF7FieldEngine& engine);
         void processSOLID(CodeGenerator* codeGen, const std::string& entity);
+
+        /**
+         * Processes an OFST opcode.
+         *
+         * Opcode: 0xC3
+         * Short name: OFST
+         * Long name: Offset Object
+         *
+         * Memory layout (8 bytes)
+         * |0xC3|B1/B2|B3/B4|T|X|Y|Z|S|
+         *
+         * Arguments:
+         * - const Bit[4] B1: Bank to retrieve X offset, or zero if X is
+         * specified as a literal.
+         * - const Bit[4] B2: Bank to retrieve Y offset, or zero if Y is
+         * specified as a literal.
+         * - const Bit[4] B3: Bank to retrieve Z offset, or zero if Z is
+         * specified as a literal.
+         * - const Bit[4] B4: Bank to retrieve speed, or zero if S is specified
+         * as a literal.
+         * - const UByte T: Type of movement.
+         * - const Short X: X offset amount, relative to current position, or
+         * address to find X offset, if B1 is non-zero.
+         * - const Short Y: Y offset amount, relative to current position, or
+         * address to find Y offset, if B2 is non-zero.
+         * - const Short Z: Z offset amount, relative to current position, or
+         * address to find Z offset, if B3 is non-zero.
+         * - const UShort S: Speed of the offset movement, if type is non-zero,
+         * or address to find speed, if B4 is non-zero.
+         *
+         * Offsets the field object, belonging to the entity whose script this
+         * opcode resides in, by a certain amount. After being offset, the
+         * character continues to be constrained in movement as defined by the
+         * walkmesh's shape, but at a certain distance away from the normal
+         * walkmesh position. Other field objects are unaffected, and their
+         * position or movements are maintained on the walkmesh's original
+         * position. If B1, B2, B3 or B4 is non-zero, then the value for that
+         * particular component is taken from memory using the corresponding
+         * bank and address specified, rather than as a literal value. Both
+         * retrieved values and literals can be used for different components.
+         * If using T, X, Y or S as addresses, the lower byte should hold the
+         * address whilst the higher byte should be zero. The amount to offset
+         * is specified relative to the current position. If Type is specified,
+         * the object moves gradually from its current point to the offset
+         * position; this can be used to simulate movements such as elevators.
+         * Any type outside the range in the table will cause the offset not to
+         * occur. If the object is set to move gradually, then the speed of
+         * offset can be set; the greater the number, the slower the object
+         * moves to its target offset. Script execution may also be halted
+         * until the gradual offset has been completed. For this, see OFSTW.
+         */
+        void processOFST(CodeGenerator* codegen, const std::string& entity);
     };
 
     class FF7WalkmeshInstruction : public KernelCallInstruction