Преглед на файлове

Let there be sound!

- Sound related features:
  - The installer now extracts all the sound effects from the original data and converts them to a compatible format.
  - Some AKAO operations implemented in the installer and in the engine (field sounds sound crappy, though).
  - Music can also be played, but the installer doesn't extract it yet.
  - Sound effects implemented in the existing menu screens.

- Materia related features:
  - The installer now extract all the materia information, with all of their effects.
  - Materia can be easily added to the inventory.
  - Several improvements to the materia menu: Materia can be equipped and removed, and enemy skill data is nicely displayed.
Iñigo Valentin преди 3 години
родител
ревизия
e66295d06a
променени са 46 файла, в които са добавени 1204 реда и са изтрити 150 реда
  1. 10 0
      V-Gears-Installer/include/DataInstaller.h
  2. 82 0
      V-Gears-Installer/include/MediaDataInstaller.h
  3. 12 0
      V-Gears-Installer/src/DataInstaller.cpp
  4. 2 0
      V-Gears-Installer/src/MainWindow.cpp
  5. 103 1
      V-Gears-Installer/src/MediaDataInstaller.cpp
  6. 87 13
      V-Gears-Installer/src/decompiler/field/instruction/FieldMediaInstruction.cpp
  7. 6 15
      V-Gears/CMakeLists.txt
  8. 108 8
      V-Gears/include/core/AudioManager.h
  9. 19 1
      V-Gears/include/core/ScriptManagerBinds.h
  10. 43 0
      V-Gears/include/core/XmlSoundsFile.h
  11. 1 5
      V-Gears/src/Main.cpp
  12. 89 9
      V-Gears/src/core/AudioManager.cpp
  13. 40 0
      V-Gears/src/core/XmlSoundsFile.cpp
  14. 6 0
      data/data/audio/music/README.txt
  15. 6 0
      data/data/audio/sound/README.txt
  16. 4 0
      data/data/fields/README.txt
  17. 6 0
      data/data/game/README.txt
  18. 7 0
      data/data/images/characters/README.txt
  19. 5 0
      data/data/images/fonts/README.txt
  20. 6 0
      data/data/images/icons/README.txt
  21. 6 0
      data/data/images/other/README.txt
  22. 4 0
      data/data/images/reels/README.txt
  23. 6 0
      data/data/images/window/README.txt
  24. 2 16
      data/data/maps.xml
  25. 9 0
      data/data/models/fields/entities/README.txt
  26. 2 0
      data/data/screens/README.txt
  27. 1 1
      data/data/screens/equip_menu/equip_menu.xml
  28. 3 3
      data/data/screens/item_menu/item_menu.xml
  29. 1 1
      data/data/screens/main_menu/main_menu.xml
  30. 105 29
      data/data/screens/materia_menu/materia_menu.xml
  31. 1 1
      data/data/screens/name_menu/name_menu.xml
  32. 1 0
      data/data/scripts/README.txt
  33. 3 0
      data/data/scripts/data.lua
  34. 17 5
      data/data/scripts/debug_data.lua
  35. 3 0
      data/data/scripts/menu/begin_menu.lua
  36. 12 2
      data/data/scripts/menu/equip_menu.lua
  37. 42 2
      data/data/scripts/menu/item_menu.lua
  38. 14 0
      data/data/scripts/menu/main_menu.lua
  39. 273 35
      data/data/scripts/menu/materia_menu.lua
  40. 16 1
      data/data/scripts/menu/name_menu.lua
  41. 5 0
      data/data/scripts/menu/pause_menu.lua
  42. 23 2
      data/data/scripts/system.lua
  43. 4 0
      data/data/sounds.xml
  44. 1 0
      data/data/system/README.txt
  45. 2 0
      data/data/texts/README.txt
  46. 6 0
      doc/INSTALL_DATA.md

+ 10 - 0
V-Gears-Installer/include/DataInstaller.h

@@ -185,6 +185,16 @@ class DataInstaller{
              */
             MEDIA_IMAGES,
 
+            /**
+             * Extract game sounds.
+             */
+            MEDIA_SOUNDS,
+
+            /**
+             * Build sounds index.
+             */
+            MEDIA_SOUNDS_INDEX,
+
             /**
              * Initializer for {@see SPAWN_POINTS_AND_SCALE_FACTORS}.
              */

+ 82 - 0
V-Gears-Installer/include/MediaDataInstaller.h

@@ -15,6 +15,7 @@
 
 #pragma once
 
+#include <unordered_map>
 #include "data/VGearsLGPArchive.h"
 #include "common/BinGZipFile.h"
 #include "TexFile.h"
@@ -40,6 +41,11 @@ class MediaDataInstaller{
          */
         ~MediaDataInstaller();
 
+        /**
+         * Populates the audio name maps.
+         */
+        void PopulateMaps();
+
         /**
          * Extracts all images contained in the menu LGP file and installs them.
          *
@@ -48,8 +54,72 @@ class MediaDataInstaller{
          */
         void InstallSprites();
 
+        /**
+         * Extracts all sound files contained in the audio.dat file and installs them.
+         *
+         */
+        void InstallSounds();
+
+        /**
+         * Writes the XML file with all audio entries.
+         */
+        void WriteSoundIndex();
+
     private:
 
+        /**
+         * Number of sound files to extract.
+         */
+        static int TOTAL_SOUNDS;
+
+        /**
+         * The standard WAV header.
+         *
+         * 78 bytes to be written to every wav file before anything else.
+         */
+        static u8 WAV_HEADER[78];
+
+        /**
+         * The structure of each audio file pointer in a sound FMT file. 74 bytes.
+         */
+        struct FmtFile{
+
+            /**
+             * Size of the wav file in the dat. 4 bytes.
+             */
+            u32 size;
+
+            /**
+             * Offset of the wav file in the dat. 4 bytes.
+             */
+            u32 offset;
+
+            /**
+             * Information for sound looping. 12 bytes.
+             */
+            u8 loop_metadata[16];
+
+            /**
+             * Microsoft WAVFORMATEX header for the wav file. 44 bytes.
+             */
+            u8 wav_header[18];
+
+            /**
+             * Samples per block. 2 bytes.
+             */
+            u16 samples_per_block;
+
+            /**
+             * Number of ADPCM coefficients (used for compression, should always be 7). 2 bytes.
+             */
+            u16 adpcm;
+
+            /**
+             * Standard Microsoft ADPCMCoefSets. 28 bytes.
+             */
+            u8 adpcm_sets[28];
+        };
+
         /**
          * The path to the directory from which to read the PC game data.
          */
@@ -70,4 +140,16 @@ class MediaDataInstaller{
          */
         BinGZipFile window_;
 
+        /**
+         * Map for sound files with descriptive names.
+         */
+        std::unordered_map<int, std::string> sound_map_;
+
+        /**
+         * Sound data for each entry to write to the XML file
+         */
+        std::vector<std::string> sounds_;
+
+
+
 };

+ 12 - 0
V-Gears-Installer/src/DataInstaller.cpp

@@ -136,6 +136,16 @@ int DataInstaller::Progress(){
         case MEDIA_IMAGES:
             write_output_line_("Extracting game images...", 2, true);
             media_installer_->InstallSprites();
+            installation_state_ = MEDIA_SOUNDS;
+            return CalcProgress();
+        case MEDIA_SOUNDS:
+            write_output_line_("Extracting sounds...", 2, true);
+            media_installer_->InstallSounds();
+            installation_state_ = MEDIA_SOUNDS_INDEX;
+            return CalcProgress();
+        case MEDIA_SOUNDS_INDEX:
+            write_output_line_("Building sound index...", 2, true);
+            media_installer_->WriteSoundIndex();
             installation_state_ = FIELD_SPAWN_POINTS_AND_SCALE_FACTORS_INIT;
             return CalcProgress();
         case FIELD_SPAWN_POINTS_AND_SCALE_FACTORS_INIT:
@@ -225,6 +235,8 @@ void DataInstaller::CreateDirectories(){
     CreateDir("images/reels");
     CreateDir("images/window");
     CreateDir("models/fields/entities");
+    CreateDir("audio/fx");
+    CreateDir("audio/music");
     application_.ResMgr()->addResourceLocation("data/temp/char/", "FileSystem", "FFVII", true, true);
     application_.ResMgr()->addResourceLocation("data/models/", "FileSystem", "FFVII", true, true);
     fields_lgp_ = std::make_unique<ScopedLgp>(

+ 2 - 0
V-Gears-Installer/src/MainWindow.cpp

@@ -175,6 +175,8 @@ void MainWindow::on_btn_data_run_clicked(){
           "data/field/flevel.lgp",
           "data/kernel/KERNEL.BIN",
           "data/menu/menu_us.lgp",
+          "data/sound/audio.fmt",
+          "data/sound/audio.dat",
           "ff7.exe"
         };
         // Ensure required files are in the input dir

+ 103 - 1
V-Gears-Installer/src/MediaDataInstaller.cpp

@@ -20,16 +20,39 @@
 #include <OgreResourceGroupManager.h>
 #include <OgreImage.h>
 #include <OgreColourValue.h>
+#include <boost/format.hpp>
+#include <tinyxml.h>
 #include "MediaDataInstaller.h"
 #include "data/VGearsLGPArchive.h"
 #include "data/VGearsTexFile.h"
 #include "TexFile.h"
 
+int MediaDataInstaller::TOTAL_SOUNDS = 723;
+
+u8 MediaDataInstaller::WAV_HEADER[] = {
+  0x52, 0x49, 0x46, 0x46, 0x70, 0x0B, 0x00, 0x00, 0x57, 0x41, 0x56, 0x45, 0x66, 0x6D, 0x74, 0x20,
+  0x32, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01, 0x00, 0x44, 0xAC, 0x00, 0x00, 0x00, 0x54, 0x00, 0x00,
+  0x00, 0x04, 0x04, 0x00, 0x20, 0x00, 0xF4, 0x07, 0x07, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02,
+  0x00, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x40, 0x00, 0xF0, 0x00, 0x00, 0x00, 0xCC, 0x01,
+  0x30, 0xFF, 0x88, 0x01, 0x18, 0xFF, 0x64, 0x61, 0x74, 0x61, 0x2A, 0x0B, 0x00, 0x00
+};
 
 MediaDataInstaller::MediaDataInstaller(const std::string input_dir, const std::string output_dir):
   input_dir_(input_dir), output_dir_(output_dir),
   menu_(input_dir + "data/menu/menu_us.lgp", "LGP"), window_(input_dir + "data/kernel/WINDOW.BIN")
-{}
+{PopulateMaps();}
+
+void MediaDataInstaller::PopulateMaps(){
+    // TODO: Names ending in "_?" are sounds that I think they match their name, but I'm not 100%
+    // sure.
+    sound_map_[0] = "Cursor";
+    sound_map_[1] = "Window";
+    sound_map_[2] = "Error";
+    sound_map_[3] = "Back";
+    sound_map_[4] = "Slash_Miss_?";
+    sound_map_[6] = "Chirp";
+    // TODO: Fill this list with somewhat descriptive names.
+}
 
 MediaDataInstaller::~MediaDataInstaller(){}
 
@@ -273,3 +296,82 @@ void MediaDataInstaller::InstallSprites(){
         std::remove((output_dir_ + "images/" + f.file_name).c_str());
     }
 }
+
+void MediaDataInstaller::InstallSounds(){
+    File fmt(input_dir_ + "data/sound/audio.fmt");
+    File dat(input_dir_ + "data/sound/audio.dat");
+
+    for (int i = 0; i < TOTAL_SOUNDS; i ++){
+        FmtFile header;
+
+        header.size = fmt.readU32LE();
+        // If size is 0, this is a bad header. There are 112 bytes of bad data, and after that,
+        // the next header.
+        if (header.size == 0){
+            for (int b = 0; b < 112; b += 4) fmt.readU32LE();
+            continue;
+        }
+
+        header.offset = fmt.readU32LE();
+        // If the offset is less than the previous one, also bad header. 34 bytes of bad data, and
+        // after that, the next header.
+        if (header.offset < dat.GetCurrentOffset()){
+            for (int b = 0; b < 34; b += 2) fmt.readU16LE();
+            continue;
+        }
+
+        // This should never happen, but just in case, never read outside the file
+        if (header.offset + header.size > dat.GetFileSize()) continue;
+
+        for (int l = 0; l < 16; l ++) header.loop_metadata[l] = fmt.readU8();
+        for (int l = 0; l < 18; l ++) header.wav_header[l] = fmt.readU8();
+        header.samples_per_block = fmt.readU16LE();
+        header.adpcm = fmt.readU16LE();
+        for (int l = 0; l < 28; l ++) header.adpcm_sets[l] = fmt.readU8();
+
+        dat.SetOffset(header.offset);
+        std::ofstream out(
+          output_dir_ + "audio/sound/" + std::to_string(i) + ".wav", std::ios::out | std::ios::binary
+        );
+
+
+        // Write the standard wav header.
+        for (int b = 0; b < 78; b ++) out.put(WAV_HEADER[b]);
+        // Write the data from the dat file.
+        for (int b = 0; b < header.size; b ++) out.put(dat.readU8());
+        out.close();
+
+        // Convert to OGG.
+        // TODO: Don't use system calls! Integrate libav or something that can do the conversion
+        // natively
+        std::string command = (boost::format(
+          "ffmpeg -hide_banner -loglevel panic -y -i %1%audio/sound/%2%.wav %1%audio/sound/%2%.ogg;"
+          "rm %1%audio/sound/%2%.wav"
+        ) % output_dir_ % i).str();
+        std::system(command.c_str());
+
+        sounds_.push_back("audio/sound/" + std::to_string(i) + ".ogg");
+    }
+}
+
+void MediaDataInstaller::WriteSoundIndex(){
+    TiXmlDocument xml;
+    std::unique_ptr<TiXmlElement> container(new TiXmlElement("sounds"));
+    //for (string item : items_){
+    for (int id = 0; id < sounds_.size(); id ++){
+        std::string path = sounds_[id];
+        std::unique_ptr<TiXmlElement> xml_sound(new TiXmlElement("sound"));
+        xml_sound->SetAttribute("file_name", path);
+        xml_sound->SetAttribute("name", id);
+        container->LinkEndChild(xml_sound.release());
+        // If there is a friendly name for this sound, add another entry
+        if (sound_map_.count(id) != 0){
+            std::unique_ptr<TiXmlElement> xml_sound_name(new TiXmlElement("sound"));
+            xml_sound_name->SetAttribute("file_name", path);
+            xml_sound_name->SetAttribute("name", sound_map_[id]);
+            container->LinkEndChild(xml_sound_name.release());
+        }
+    }
+    xml.LinkEndChild(container.release());
+    xml.SaveFile(output_dir_ + "sounds.xml");
+}

+ 87 - 13
V-Gears-Installer/src/decompiler/field/instruction/FieldMediaInstruction.cpp

@@ -30,7 +30,7 @@ void FieldMediaInstruction::ProcessInst(
     FunctionMetaData md(func.metadata);
     switch (opcode_){
         case OPCODES::BGMOVIE: code_gen->WriteTodo(md.GetEntityName(), "BGMOVIE"); break;
-        case OPCODES::AKAO2: ProcessAKAO2(code_gen); break;
+        case OPCODES::AKAO2: ProcessAKAO(code_gen); break;
         case OPCODES::MUSIC: ProcessMUSIC(code_gen); break;
         case OPCODES::SOUND: ProcessSOUND(code_gen); break;
         case OPCODES::AKAO: ProcessAKAO(code_gen); break;
@@ -77,10 +77,9 @@ void FieldMediaInstruction::ProcessAKAO2(CodeGenerator* code_gen){
 }
 
 void FieldMediaInstruction::ProcessMUSIC(CodeGenerator* code_gen){
-    code_gen->AddOutputLine((
-      boost::format("-- music:execute_akao(0x10, pointer_to_field_AKAO_%1%)")
-      % params_[0]->GetUnsigned()
-    ).str());
+    code_gen->AddOutputLine(
+      (boost::format("-- play_map_music(%1%)") % params_[0]->GetUnsigned()).str()
+    );
 }
 
 void FieldMediaInstruction::ProcessSOUND(CodeGenerator* code_gen){
@@ -91,9 +90,9 @@ void FieldMediaInstruction::ProcessSOUND(CodeGenerator* code_gen){
     const auto& panning = FieldCodeGenerator::FormatValueOrVariable(
       cg->GetFormatter(), params_[1]->GetUnsigned(), params_[3]->GetUnsigned()
     );
-    code_gen->AddOutputLine(
-      (boost::format("-- music:execute_akao(0x20, %1%, %2%)") % soundId % panning).str()
-    );
+    code_gen->AddOutputLine((
+      boost::format("audio_manager:play_sound(\"%1%\") -- Direction: %2%)") % soundId % panning
+    ).str());
 }
 
 void FieldMediaInstruction::ProcessAKAO(CodeGenerator* code_gen){
@@ -113,11 +112,86 @@ void FieldMediaInstruction::ProcessAKAO(CodeGenerator* code_gen){
     const auto& param5 = FieldCodeGenerator::FormatValueOrVariable(
       cg->GetFormatter(), params_[5]->GetUnsigned(), params_[11]->GetUnsigned()
     );
-    auto op = params_[6]->GetUnsigned();
-    code_gen->AddOutputLine((
-      boost::format("-- music:execute_akao(0x%6$02x, %1%, %2%, %3%, %4%, %5%)")
-      % param1 % param2 % param3 % param4 % param5 % op
-    ).str());
+    int op = static_cast<int>(params_[6]->GetUnsigned());
+    switch (op){
+        /*code_gen->AddOutputLine((
+            boost::format("-- music:execute_akao(0x%6$02x, %1%, %2%, %3%, %4%, %5%)")
+            % param1 % param2 % param3 % param4 % param5 % op
+        ).str());*/
+        case 0x10: // Play music.
+        case 0x14: // Play music.
+            code_gen->AddOutputLine(
+              (boost::format("audio_manager:play_music(\"%1%\")") % param1).str()
+            );
+            break;
+        case 0x18: // Resume music.
+        case 0x19: // Resume music.
+            code_gen->AddOutputLine(
+              (boost::format("audio_manager:play_music(\"%1%\")") % param1).str()
+            );
+            break;
+        case 0x20: // Play 1 sound, channel 1.
+        case 0x24: // Play 1 sound, channel 1.
+            code_gen->AddOutputLine((
+              boost::format("audio_manager:play_sounds(\"%1%\", nil, nil, nil) -- Panning %2%")
+              % param2 % param1
+            ).str());
+            break;
+        case 0x21: // Play 2 sounds, channels 1 and 2.
+        case 0x25: // Play 2 sounds, channels 1 and 2.
+            code_gen->AddOutputLine((
+              boost::format("audio_manager:play_sounds(\"%1%\", \"%2%\", nil, nil) -- Panning %3%")
+              % param2 % param3 % param1
+            ).str());
+            break;
+        case 0x22: // Play 3 sounds, channels 1, 2 and 3.
+        case 0x26: // Play 3 sounds, channels 1, 2 and 3.
+            code_gen->AddOutputLine((
+              boost::format(
+                "audio_manager:play_sounds(\"%1%\", \"%2%\", \"%3%\", nil) -- Panning %4%"
+              ) % param2 % param3 % param4 % param1
+            ).str());
+            break;
+        case 0x23: // Play 4 sounds, channels 1, 2, 3 and 4.
+        case 0x27: // Play 4 sounds, channels 1, 2, 3 and 4.
+            code_gen->AddOutputLine((
+              boost::format(
+                "audio_manager:play_sounds(\"%1%\", \"%2%\", \"%3%\", \"%4%\") -- Panning %5%"
+              ) % param2 % param3 % param4 % param5 % param1
+            ).str());
+            break;
+        case 0x28: // Play sound, channel 1.
+            code_gen->AddOutputLine(
+              (boost::format("audio_manager:play_sound(\"%1%\", 1)") % param1).str()
+            );
+            break;
+        case 0x29: // Play sound, channel 2.
+            code_gen->AddOutputLine(
+              (boost::format("audio_manager:play_sound(\"%1%\", 2)") % param1).str()
+            );
+            break;
+        case 0x2A: // Play sound, channel 3.
+            code_gen->AddOutputLine(
+              (boost::format("audio_manager:play_sound(\"%1%\", 3)") % param1).str()
+            );
+            break;
+        case 0x2B: // Play sound, channel 4.
+            code_gen->AddOutputLine(
+              (boost::format("audio_manager:play_sound(\"%1%\", 4)") % param1).str()
+            );
+            break;
+        case 0x30: // Play sound, channel 5.
+            code_gen->AddOutputLine(
+              (boost::format("audio_manager:play_sound(\"%1%\", 5)") % param1).str()
+            );
+            break;
+        case 0x15: // Unused.
+        default:
+            code_gen->AddOutputLine((
+                boost::format("-- Unknown akao operation AKAO(0x%6$02x, %1%, %2%, %3%, %4%, %5%)")
+                % param1 % param2 % param3 % param4 % param5 % op
+            ).str());
+    }
 }
 
 void FieldMediaInstruction::ProcessMULCK(CodeGenerator* code_gen){

+ 6 - 15
V-Gears/CMakeLists.txt

@@ -47,6 +47,7 @@ set(HEADER_FILES
     include/core/particles/ParticleTechniqueTranslator.h
     include/core/particles/ParticleVisual.h
     include/core/Assert.h
+    include/core/AudioManager.h
     include/core/Background2D.h
     include/core/Background2DAnimation.h
     include/core/CameraManager.h
@@ -89,10 +90,12 @@ set(HEADER_FILES
     include/core/XmlFontFile.h
     include/core/XmlFontsFile.h
     include/core/XmlMapFile.h
+    include/core/XmlMusicsFile.h
     include/core/XmlPrototypesFile.h
     include/core/XmlScreenFile.h
     include/core/XmlScreensFile.h
     include/core/XmlScriptsFile.h
+    include/core/XmlSoundsFile.h
     include/core/XmlTextFile.h
     include/core/XmlTextsFile.h
     include/core/DialogsManager.h
@@ -239,6 +242,7 @@ set(SOURCE_FILES
     src/core/particles/ParticleVisual.cpp
     src/core/particles/renderer/ParticleEntityRenderer.cpp
     src/core/particles/renderer/ParticleEntityRendererDictionary.cpp
+    src/core/AudioManager.cpp
     src/core/Background2D.cpp
     src/core/Background2DAnimation.cpp
     src/core/CameraManager.cpp
@@ -274,10 +278,12 @@ set(SOURCE_FILES
     src/core/XmlFontsFile.cpp
     src/core/XmlMapFile.cpp
     src/core/XmlMapsFile.cpp
+    src/core/XmlMusicsFile.cpp
     src/core/XmlPrototypesFile.cpp
     src/core/XmlScreenFile.cpp
     src/core/XmlScreensFile.cpp
     src/core/XmlScriptsFile.cpp
+    src/core/XmlSoundsFile.cpp
     src/core/XmlTextFile.cpp
     src/core/XmlTextsFile.cpp
     src/core/DialogsManager.cpp
@@ -299,20 +305,6 @@ set(SOURCE_FILES
     src/FF7Common.cpp
 )
 
-# If sound, add more files.
-if(VGears_SOUND)
-    set(HEADER_FILES
-        ${HEADER_FILES}
-        include/core/AudioManager.h
-        include/core/XmlMusicsFile.h
-    )
-    set(SOURCE_FILES
-        ${SOURCE_FILES}
-        src/core/AudioManager.cpp
-        src/core/XmlMusicsFile.cpp
-    )
-endif()
-
 # Find LibOIS. TODO: Find it.
 set(ois_lib "/usr/lib/x86_64-linux-gnu/libOIS.so")
 
@@ -321,7 +313,6 @@ include_directories(
     ${VGears_INCLUDE_DIRS}
     ${CMAKE_CURRENT_SOURCE_DIR}/include
     /usr/include/OIS/
-
 )
 
 # Compiler options

+ 108 - 8
V-Gears/include/core/AudioManager.h

@@ -64,6 +64,15 @@ class AudioManager : public Ogre::Singleton<AudioManager>{
          */
         void MusicPause();
 
+        /**
+         * Plays a music track.
+         *
+         * To be called from Lua scripts
+         *
+         * @param[in] name Name of the track to play.
+         */
+        void ScriptPlayMusic(const char* name);
+
         /**
          * Plays a music track.
          *
@@ -71,6 +80,46 @@ class AudioManager : public Ogre::Singleton<AudioManager>{
          */
         void MusicPlay(const Ogre::String& name);
 
+        /**
+         * Plays a sound.
+         *
+         * To be called from Lua scripts
+         *
+         * @param[in] name Name of the sound to play.
+         */
+        void ScriptPlaySound(const char* name);
+
+        /**
+         * Plays a sound in a channel.
+         *
+         * To be called from Lua scripts
+         *
+         * @param[in] name Name of the sound to play.
+         * @param[in] channel Channel to play the sound in (1-5).
+         */
+        void ScriptPlaySound(const char* name, const int channel);
+
+        /**
+         * Plays up to 4 sounds, in 4 different channels.
+         *
+         * To be called from Lua scripts.
+         *
+         * @param[in] name1 Name of the first sound to play.
+         * @param[in] name2 Name of the second sound to play.
+         * @param[in] name3 Name of the third sound to play.
+         * @param[in] name4 Name of the fourth sound to play.
+         */
+        void AudioManager::ScriptPlaySounds(
+          const char* name1, const char* name2, const char* name3, const char* name4
+        );
+
+        /**
+         * Plays a sound.
+         *
+         * @param[in] name Name of the sound to play.
+         */
+        void SoundPlay(const Ogre::String& name);
+
         /**
          * Stops the currently playing music.
          *
@@ -81,7 +130,7 @@ class AudioManager : public Ogre::Singleton<AudioManager>{
         /**
          * Music structure.
          *
-         * Defines a music track.
+         * Defines a music entry in musics.xml.
          */
         struct Music{
 
@@ -101,7 +150,25 @@ class AudioManager : public Ogre::Singleton<AudioManager>{
              * @todo How is a loop done with only one value? shouldn't it be
              * start and end of loop?
              */
-            float        loop;
+            float loop;
+        };
+
+        /**
+         * Music structure.
+         *
+         * Defines a sound entry in sound.xml.
+         */
+        struct Sound{
+
+            /**
+             * The name of the sound.
+             */
+            Ogre::String name;
+
+            /**
+             * Sound filename
+             */
+            Ogre::String file;
         };
 
         /**
@@ -112,14 +179,28 @@ class AudioManager : public Ogre::Singleton<AudioManager>{
         void AddMusic(const AudioManager::Music& music);
 
         /**
-         * Retrieves a muisc track by name.
+         * Retrieves a music track by name.
          *
-         * @param[in] name The track name
-         * @return The music track, or nullptr if there is no track by that
-         * name.
+         * @param[in] name The track name.
+         * @return The music track, or nullptr if there is no track by that name.
          */
         AudioManager::Music* GetMusic(const Ogre::String& name);
 
+        /**
+         * Adds a sound to the audio manager.
+         *
+         * @param[in] sound The sound to add.
+         */
+        void AddSound(const AudioManager::Sound& sound);
+
+        /**
+         * Retrieves a sound by name.
+         *
+         * @param[in] name The sound name.
+         * @return The sound track, or nullptr if there is no sound by that name.
+         */
+        AudioManager::Sound* GetSound(const Ogre::String& name);
+
     private:
 
         /**
@@ -234,6 +315,11 @@ class AudioManager : public Ogre::Singleton<AudioManager>{
                  */
                 bool stream_finished_;
 
+                /**
+                 * Audio buffer.
+                 */
+                char* buffer_;
+
                 /**
                  * Audio source.
                  */
@@ -266,6 +352,8 @@ class AudioManager : public Ogre::Singleton<AudioManager>{
 
         /**
          * Audio buffer.
+         *
+         * TODO: Unused? Not there are buffers per player. Remove.
          */
         char* buffer_;
 
@@ -275,7 +363,7 @@ class AudioManager : public Ogre::Singleton<AudioManager>{
         boost::recursive_mutex update_mutex_;
 
         /**
-         * Thread to handle consurrent operations.
+         * Thread to handle concurrent operations.
          */
         boost::thread* update_thread_;
 
@@ -285,15 +373,27 @@ class AudioManager : public Ogre::Singleton<AudioManager>{
         bool thread_continue_;
 
         /**
-         * Music player
+         * Music player.
          */
         AudioManager::Player music_;
 
+        /**
+         * Sound effect player.
+         *
+         * TODO: Unused? remove.
+         */
+        AudioManager::Player fx_;
+
         /**
          * List of music.
          */
         std::list<AudioManager::Music> music_list_;
 
+        /**
+         * List of music.
+         */
+        std::list<AudioManager::Sound> sound_list_;
+
         /**
          * Size of a channel buffer.
          *

+ 19 - 1
V-Gears/include/core/ScriptManagerBinds.h

@@ -18,6 +18,7 @@
 #include "Logger.h"
 #include "Entity.h"
 #include "EntityManager.h"
+#include "AudioManager.h"
 #include "Timer.h"
 #include "UiManager.h"
 #include "UiWidget.h"
@@ -189,7 +190,7 @@ void ScriptManager::InitBinds(){
 
     // Entity individual point commands
     luabind::module(lua_state_)[
-        luabind::class_< EntityPoint >("EntityPoint")
+        luabind::class_<EntityPoint>("EntityPoint")
           // Internally returns 3 values:
           .def("get_position", (void(EntityPoint::*)()) &EntityPoint::ScriptGetPosition)
           .def("get_rotation", (float(EntityPoint::*)()) &EntityPoint::ScriptGetRotation)
@@ -255,6 +256,22 @@ void ScriptManager::InitBinds(){
           )
     ];
 
+    // Commands for the entity manager, not related to any particular entity.
+    luabind::module(lua_state_)[
+        luabind::class_<AudioManager>("AudioManager")
+          .def("play_music", (void(AudioManager::*)(const char*)) &AudioManager::ScriptPlayMusic)
+          .def("play_sound", (void(AudioManager::*)(const char*)) &AudioManager::ScriptPlaySound)
+          .def(
+            "play_sound",
+            (void(AudioManager::*)(const char*, const int)) &AudioManager::ScriptPlaySound
+          )
+          .def(
+            "play_sounds",
+            (void(AudioManager::*)(const char*, const char*, const char*, const char*))
+            &AudioManager::ScriptPlaySounds
+          )
+    ];
+
     // 2D background and camera commands
     luabind::module(lua_state_)[
         luabind::class_<Background2D>("Background2D")
@@ -448,6 +465,7 @@ void ScriptManager::InitBinds(){
     //auto b = luabind::globals(lua_state_)["entity_manager"];
     luabind::globals(lua_state_)["entity_manager"]
       = boost::ref(*(EntityManager::getSingletonPtr()));
+    luabind::globals(lua_state_)["audio_manager"] = boost::ref(*(AudioManager::getSingletonPtr()));
     luabind::globals(lua_state_)["background2d"]
       = boost::ref(*(EntityManager::getSingletonPtr()->GetBackground2D()));
     luabind::globals(lua_state_)["walkmesh"]

+ 43 - 0
V-Gears/include/core/XmlSoundsFile.h

@@ -0,0 +1,43 @@
+/*
+ * 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 "XmlFile.h"
+
+/**
+ * Handles the main sounds file.
+ */
+class XmlSoundsFile : public XmlFile{
+
+    public:
+
+        /**
+         * Constructor.
+         *
+         * @param[in] file Path to the main sounds file.
+         */
+        XmlSoundsFile(const Ogre::String& file);
+
+        /**
+         * Destructor.
+         */
+        virtual ~XmlSoundsFile();
+
+        /**
+         * Parses the file and loads the sounds.
+         */
+        void LoadSounds();
+};

+ 1 - 5
V-Gears/src/Main.cpp

@@ -21,9 +21,7 @@
 #include <OgreOverlayManager.h>
 #include <OgreOverlaySystem.h>
 #include "VGearsGameState.h"
-#ifdef VGears_SOUND
 #include "core/AudioManager.h"
-#endif
 #include "common/VGearsApplication.h"
 #include "core/CameraManager.h"
 #include "core/ConfigCmdManager.h"
@@ -106,10 +104,8 @@ int main(int argc, char *argv[]){
         auto input_manager = std::make_unique<InputManager>();
 
 
-#ifdef VGears_SOUND
-        //auto audio_manager = std::make_unique<AudioManager>();
+        auto audio_manager = std::make_unique<AudioManager>();
         //audio_manager->MusicPlay( "loop1" );
-#endif
 
         // Create this earlier than DisplayFrameListener cause it can fire
         // events there

+ 89 - 9
V-Gears/src/core/AudioManager.cpp

@@ -13,10 +13,12 @@
  * GNU General Public License for more details.
  */
 
+#include <iostream>
 #include <list>
 #include <boost/thread.hpp>
 #include "core/AudioManager.h"
 #include "core/XmlMusicsFile.h"
+#include "core/XmlSoundsFile.h"
 #include "core/Logger.h"
 
 template<>AudioManager *Ogre::Singleton<AudioManager>::msSingleton = nullptr;
@@ -25,7 +27,8 @@ ALsizei AudioManager::channel_buffer_number_ = 2;
 int AudioManager::channel_buffer_size_ = 96 * 1024;
 
 AudioManager::AudioManager():
-  initialized_(false), thread_continue_(true), update_mutex_(), music_(&update_mutex_)
+  initialized_(false), thread_continue_(true), update_mutex_(), music_(&update_mutex_),
+  fx_(&update_mutex_)
 {
     al_device_ = alcOpenDevice(nullptr);
     if (al_device_ != nullptr){
@@ -52,9 +55,13 @@ AudioManager::AudioManager():
     }
     else LOG_ERROR("AudioManager failed to initialised. There's no default sound device.");
 
-    // Load musics
+    // Load musics.
     XmlMusicsFile musics("./data/musics.xml");
     musics.LoadMusics();
+
+    // Load sounds.
+    XmlSoundsFile sounds("./data/sounds.xml");
+    sounds.LoadSounds();
 }
 
 AudioManager::~AudioManager(){
@@ -85,6 +92,7 @@ void AudioManager::operator()(){
 void AudioManager::Update(){
     boost::recursive_mutex::scoped_lock lock(update_mutex_);
     music_.Update();
+    fx_.Update();
 }
 
 void AudioManager::MusicPause(){
@@ -92,7 +100,13 @@ void AudioManager::MusicPause(){
     music_.Pause();
 }
 
+void AudioManager::ScriptPlayMusic(const char* name){
+    const Ogre::String name_str = Ogre::String(name);
+    MusicPlay(name_str);
+}
+
 void AudioManager::MusicPlay(const Ogre::String& name){
+    std::cout << "[MUSIC] play " << name << "\n";
     if (initialized_){
         boost::recursive_mutex::scoped_lock lock(update_mutex_);
         AudioManager::Music* music = GetMusic(name);
@@ -105,6 +119,37 @@ void AudioManager::MusicPlay(const Ogre::String& name){
     }
 }
 
+void AudioManager::SoundPlay(const Ogre::String& name){
+    std::cout << "[FX] play " << name << "\n";
+    if (initialized_){
+        boost::recursive_mutex::scoped_lock lock(update_mutex_);
+        AudioManager::Sound* fx = GetSound(name);
+        if (fx == nullptr){
+            LOG_ERROR("No sound found with name \"" + name + "\".");
+            return;
+        }
+        //Player player(&update_mutex_);
+        //player.SetLoop(-1);
+        //player.Play(fx->file);
+        fx_.SetLoop(-1);
+        fx_.Play(fx->file);
+    }
+}
+
+void AudioManager::ScriptPlaySound(const char* name){
+    const Ogre::String name_str = Ogre::String(name);
+    SoundPlay(name_str);
+}
+
+void ScriptPlaySound(const char* name, const int channel){ScriptPlaySound(name1);}
+
+void AudioManager::ScriptPlaySounds(
+  const char* name1, const char* name2, const char* name3, const char* name4
+){
+    // TODO: Playing only name1, implement the rest.
+    ScriptPlaySound(name1);
+}
+
 void AudioManager::MusicStop(){
     boost::recursive_mutex::scoped_lock lock(update_mutex_);
     music_.Stop();
@@ -126,11 +171,33 @@ void AudioManager::AddMusic(const AudioManager::Music& music){
     music_list_.push_back(music);
 }
 
+void AudioManager::AddSound(const AudioManager::Sound& sound){
+    std::cout << "[ADD_SOUND] " << sound.name << "\n";
+    boost::recursive_mutex::scoped_lock lock(update_mutex_);
+    for (
+      std::list<AudioManager::Sound>::iterator it = sound_list_.begin();
+      it != sound_list_.end();
+      ++ it
+    ){
+        if (it->name == sound.name){
+            LOG_ERROR("Sound with name '" + sound.name + "' already exists.");
+            return;
+        }
+    }
+    sound_list_.push_back(sound);
+}
+
 AudioManager::Music* AudioManager::GetMusic(const Ogre::String& name){
     boost::recursive_mutex::scoped_lock lock(update_mutex_);
-    for (auto it = music_list_.begin(); it != music_list_.end(); ++ it){
+    for (auto it = music_list_.begin(); it != music_list_.end(); ++ it)
+        if (it->name == name) return &(*it);
+    return nullptr;
+}
+
+AudioManager::Sound* AudioManager::GetSound(const Ogre::String& name){
+    boost::recursive_mutex::scoped_lock lock(update_mutex_);
+    for (auto it = sound_list_.begin(); it != sound_list_.end(); ++ it)
         if (it->name == name) return &(*it);
-    }
     return nullptr;
 }
 
@@ -154,7 +221,9 @@ const char* AudioManager::ALCError(const ALCdevice* device){
 AudioManager::Player::Player(boost::recursive_mutex* mutex):
   loop_(-1.0), vorbis_info_(nullptr), vorbis_section_(0),
   stream_finished_(false), update_mutex_(mutex)
-{}
+{
+    buffer_ = new char[1024 * 96];
+}
 
 AudioManager::Player::~Player(){Stop();}
 
@@ -185,7 +254,8 @@ void AudioManager::Player::Play(const Ogre::String &file){
         if (buffer_size){
             alBufferData(
               buffers[i], vorbis_info_->channels == 1 ? AL_FORMAT_MONO16 : AL_FORMAT_STEREO16,
-              static_cast<const ALvoid*>(AudioManager::getSingleton().buffer_),
+              //static_cast<const ALvoid*>(AudioManager::getSingleton().buffer_),
+              static_cast<const ALvoid*>(buffer_),
               static_cast<ALsizei>(buffer_size), static_cast<ALsizei>(vorbis_info_->rate)
             );
             alSourceQueueBuffers(source_, 1, &buffers[i]);
@@ -216,6 +286,7 @@ void AudioManager::Player::Stop(){
     alDeleteSources(1, &source_);
     // Cleanup
     ov_clear(&vorbis_file_);
+    stream_finished_ = false;
 }
 
 void AudioManager::Player::SetLoop(const float loop){
@@ -236,18 +307,26 @@ void AudioManager::Player::Update(){
             alBufferData(
               buffer_id, vorbis_info_->channels == 1 ?
                 AL_FORMAT_MONO16 : AL_FORMAT_STEREO16,
-              static_cast<const ALvoid*>(AudioManager::getSingleton().buffer_),
+              //static_cast<const ALvoid*>(AudioManager::getSingleton().buffer_),
+              static_cast<const ALvoid*>(buffer_),
               static_cast<ALsizei>(buffer_size), static_cast<ALsizei>(vorbis_info_->rate)
             );
             alSourceQueueBuffers(source_, 1, &buffer_id);
+            std::cout << "Stream unfinished\n";
         }
         // Finished reading stream
         else{
+            std::cout << "Stream finished\n";
             stream_finished_ = true;
             break;
         }
     }
 
+    if (stream_finished_ && loop_ < 0){
+        std::cout << "Stopping\n";
+        Stop();
+    }
+
     // Manage source state
     alGetSourcei(source_, AL_BUFFERS_PROCESSED, &processed);
     int queued;
@@ -263,7 +342,8 @@ void AudioManager::Player::Update(){
 ALsizei AudioManager::Player::FillBuffer(){
     ALsizei read = 0;
     if (stream_finished_) return read;
-    char *&buffer = AudioManager::getSingleton().buffer_;
+    //char *&buffer = AudioManager::getSingleton().buffer_;
+    char *&buffer = buffer_;
     bool finished = false;
     do{
         long result = ov_read(
@@ -282,7 +362,7 @@ ALsizei AudioManager::Player::FillBuffer(){
             case 0:
                 // If there isn't loop point or can't seek
                 if (loop_ < 0.0f || ov_time_seek(&vorbis_file_, loop_)) finished = true;
-            break;
+                break;
             // Readed "result" bytes
             default:
                 read += result;

+ 40 - 0
V-Gears/src/core/XmlSoundsFile.cpp

@@ -0,0 +1,40 @@
+/*
+ * 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 "core/AudioManager.h"
+#include "core/Logger.h"
+#include "core/XmlSoundsFile.h"
+
+XmlSoundsFile::XmlSoundsFile(const Ogre::String& file): XmlFile(file){}
+
+XmlSoundsFile::~XmlSoundsFile(){}
+
+void XmlSoundsFile::LoadSounds(){
+    TiXmlNode* node = file_.RootElement();
+    if (node == nullptr || node->ValueStr() != "sounds"){
+        LOG_ERROR(file_.ValueStr() + " is not a valid sounds file! No <sounds> in root.");
+        return;
+    }
+    node = node->FirstChild();
+    while (node != nullptr){
+        if (node->Type() == TiXmlNode::TINYXML_ELEMENT && node->ValueStr() == "sound"){
+            AudioManager::Sound sound;
+            sound.name = GetString(node, "name");
+            sound.file = "./data/" + GetString(node, "file_name");
+            AudioManager::getSingleton().AddSound(sound);
+        }
+        node = node->NextSibling();
+    }
+}

+ 6 - 0
data/data/audio/music/README.txt

@@ -0,0 +1,6 @@
+This directory is intentionally empty. It will be populated by the data installer.
+
+Once the data installation is complete, musics files will be installed here. They will have very
+un-descriptive file names, and there is no metadata in them, such as titles. But, if you want to
+find a specific one, a looc at the file data/musics.xml (also generated by the installer) may help
+you. There, some files in this folder may be referenced by a friendlier name.

+ 6 - 0
data/data/audio/sound/README.txt

@@ -0,0 +1,6 @@
+This directory is intentionally empty. It will be populated by the data installer.
+
+Once the data installation is complete, sound files will be installed here. They will have very
+un-descriptive file names, and there is no metadata in them, such as titles. But, if you want to
+find a specific one, a looc at the file data/sounds.xml (also generated by the installer) may help
+you. There, some files in this folder may be referenced by a friendlier name.

+ 4 - 0
data/data/fields/README.txt

@@ -0,0 +1,4 @@
+This directory is intentionally empty. It will be populated by the data installer.
+
+Once the data installation is complete, every field file will be installed here, grouped by field
+name. Field descriptions, backgrounds, walkeshes and scripts will be generated.

+ 6 - 0
data/data/game/README.txt

@@ -0,0 +1,6 @@
+This directory is intentionally empty. It will be populated by the data installer.
+
+Once the data installation is complete, several Lua scripts with static game data will be
+installed here. Those scripts only define game constants and settings, and are not to be written
+during a normal playthrough. If you are a modder, those scripts are a good place to start.
+

+ 7 - 0
data/data/images/characters/README.txt

@@ -0,0 +1,7 @@
+This directory is intentionally empty. It will be populated by the data installer.
+
+Once the data installation is complete, character portraits will be installed here, named after
+the character IDs. If you are a modder, it's perfectly fine to edit them as you see fit, but try
+to keep their aspect ratio. If changed, menu screens, and probably some other things, will need to
+be revised.
+

+ 5 - 0
data/data/images/fonts/README.txt

@@ -0,0 +1,5 @@
+This directory is intentionally empty. It will be populated by the data installer.
+
+Once the data installation is complete, images with game fonts will be installed here. As of the
+time of writting this, they are not used for anything.
+

+ 6 - 0
data/data/images/icons/README.txt

@@ -0,0 +1,6 @@
+This directory is intentionally empty. It will be populated by the data installer.
+
+Once the data installation is complete, several icons used in the game will be installed here
+If you are a modder, it's perfectly fine to edit them as you see fit, but try to keep their size
+and names intact. If changed, menu screens, and probably some other things, will need to be
+revised.

+ 6 - 0
data/data/images/other/README.txt

@@ -0,0 +1,6 @@
+This directory is intentionally empty. It will be populated by the data installer.
+
+Once the data installation is complete, several images used in the game will be installed here
+If you are a modder, it's perfectly fine to edit them as you see fit, but try to keep their size
+and names intact. If changed, menu screens, and probably some other things, will need to be
+revised.

+ 4 - 0
data/data/images/reels/README.txt

@@ -0,0 +1,4 @@
+This directory is intentionally empty. It will be populated by the data installer.
+
+Once the data installation is complete, several icons used in the game reels will be installed
+here. As of the time of this writting, they are not used for anything.

+ 6 - 0
data/data/images/window/README.txt

@@ -0,0 +1,6 @@
+This directory is intentionally empty. It will be populated by the data installer.
+
+Once the data installation is complete, several images used for window decorations will be
+installed here. If you are a modder, it's perfectly fine to edit them as you see fit, but try to
+keep their size and names intact. If changed, menu screens, and probably some other things, will
+need to be revised.

+ 2 - 16
data/data/maps.xml

@@ -1,18 +1,4 @@
+<!-- This file is to be overwritten by the installer -->
 <maps>
-    <map name="elevtr1" file_name="fields/elevtr1/map.xml" />
-    <map name="md1_1" file_name="fields/md1_1/map.xml" />
-    <map name="md1_2" file_name="fields/md1_2/map.xml" />
-    <map name="md1stin" file_name="fields/md1stin/map.xml" />
-    <map name="md8_1" file_name="fields/md8_1/map.xml" />
-    <map name="md8_4" file_name="fields/md8_4/map.xml" />
-    <map name="nmkin_1" file_name="fields/nmkin_1/map.xml" />
-    <map name="nmkin_2" file_name="fields/nmkin_2/map.xml" />
-    <map name="nmkin_3" file_name="fields/nmkin_3/map.xml" />
-    <map name="nmkin_4" file_name="fields/nmkin_4/map.xml" />
-    <map name="nmkin_5" file_name="fields/nmkin_5/map.xml" />
-    <map name="nrthmk" file_name="fields/nrthmk/map.xml" />
-    <map name="rootmap" file_name="fields/rootmap/map.xml" />
-    <map name="startmap" file_name="fields/startmap/map.xml" />
-    <map name="tin_1" file_name="fields/tin_1/map.xml" />
-    <map name="tin_2" file_name="fields/tin_2/map.xml" />
+    <map name="example" file_name="fields/example/map.xml" />
 </maps>

+ 9 - 0
data/data/models/fields/entities/README.txt

@@ -0,0 +1,9 @@
+This directory is intentionally empty. It will be populated by the data installer.
+
+Once the data installation is complete, a lot of files will be installed here. They contain
+information about 3D models and textures. Most of them will have very cryptic names. If you are a
+modder and want to manually edit the 3D models, I have a few pointers for you:
+
+ 1. Good luck.
+ 2. Make backups.
+ 3. Document everything! If you are succesfull, I would love to know how you did it.

+ 2 - 0
data/data/screens/README.txt

@@ -0,0 +1,2 @@
+This folder contains the definition for every static screen in the game, be it menus, dialog
+windows...

+ 1 - 1
data/data/screens/equip_menu/equip_menu.xml

@@ -2,7 +2,7 @@
     <widget name="Container" width="40%" height="40%" x="0" y="0" align="left" valign="top" scale="2.5 2.5" visible="true">
         <widget name="Character" width="100%" height="70" visible="true">
             <prototype name="Window"/>
-            <sprite name="Portrait" image="images/other/choco.png" x="10" y="12" width="48" height="48" visible="true" />
+            <sprite name="Portrait" image="images/other/choco.png" x="10" y="12" width="42" height="48" visible="true" />
             <widget name="Data" x="66" y="14" width="100%-66" height="100%-14" visible="true">
                 <prototype name="MenuCharacterData" />
             </widget>

+ 3 - 3
data/data/screens/item_menu/item_menu.xml

@@ -22,19 +22,19 @@
         <widget name="ItemMenuCharacters" x="0" y="48" width="50%" height="100%-48" visible="true">
             <prototype name="Window" />
             <widget name="Character1" x="0" y="0" width="100%" height="55" visible="true">
-                <sprite name="Portrait" image="images/other/choco.png" x="10" y="12" width="48" height="48" visible="true" />
+                <sprite name="Portrait" image="images/other/choco.png" x="10" y="12" width="42" height="48" visible="true" />
                 <widget name="Data" x="66" y="14" width="100%" height="100%" visible="true">
                     <prototype name="MenuCharacterData" />
                 </widget>
             </widget>
             <widget name="Character2" x="0" y="55" width="100%" height="55" visible="true">
-                <sprite name="Portrait" image="images/other/choco.png" x="10" y="12" width="48" height="48" visible="true" />
+                <sprite name="Portrait" image="images/other/choco.png" x="10" y="12" width="42" height="48" visible="true" />
                 <widget name="Data" x="66" y="14" width="100%" height="100%" visible="true">
                     <prototype name="MenuCharacterData" />
                 </widget>
             </widget>
             <widget name="Character3" x="0" y="110" width="100%" height="55" visible="true">
-                <sprite name="Portrait" image="images/other/choco.png" x="10" y="12" width="48" height="48" visible="true" />
+                <sprite name="Portrait" image="images/other/choco.png" x="10" y="12" width="42" height="48" visible="true" />
                 <widget name="Data" x="66" y="14" width="100%" height="100%" visible="true">
                     <prototype name="MenuCharacterData" />
                 </widget>

+ 1 - 1
data/data/screens/main_menu/main_menu.xml

@@ -5,7 +5,7 @@
             <animation name="Appear" length="0.3" x="0:105%,0.3:5%" />
             <animation name="Disappear" length="0.3" x="0:5%,0.3:105%" />
             <widget name="Character1" x="2%" y="2%" width="100%" height="30%" visible="true">
-                <sprite name="Portrait" image="images/other/choco.png" x="5" y="12" width="56" height="56" visible="true">
+                <sprite name="Portrait" image="images/other/choco.png" x="5" y="12" width="49" height="56" visible="true">
                     <animation name="RowFront" length="0" x="0:5" />
                     <animation name="RowBack" length="0" x="0:25" />
                 </sprite>

+ 105 - 29
data/data/screens/materia_menu/materia_menu.xml

@@ -2,7 +2,7 @@
     <widget name="Container" width="40%" height="40%" x="0" y="0" align="left" valign="top" scale="2.5 2.5" visible="true">
         <widget name="Character" width="100%" height="75" visible="true">
             <prototype name="Window"/>
-            <sprite name="Portrait" image="images/other/choco.png" x="10" y="12" width="48" height="58" visible="true" />
+            <sprite name="Portrait" image="images/other/choco.png" x="10" y="8" width="50" height="58" visible="true" />
             <widget name="Data" x="66" y="14" width="100%-66" height="100%-14" visible="true">
                 <prototype name="MenuCharacterData" />
             </widget>
@@ -85,9 +85,73 @@
             <prototype name="Window" />
             <text_area name="Help" x="16" y="6" font="FFVIIFont" visible="true" />
         </widget>
-        <widget name="Details" x="0" y="94" width="230" height="100%-99" visible="true">
+        <widget name="Details" x="0" y="99" width="230" height="100%-99" visible="true">
             <prototype name="Window" />
-            <widget name="Materia" x="0" y ="0" width="100%" height="100%" visible="true">
+            <widget name="Empty" x="0" y ="0" width="100%" height="100%" visible="false">
+            </widget>
+            <widget name="Commands" x="0" y ="0" width="100%" height="100%" visible="false">
+                <widget name="Box" x="20" y ="20" width="100%-40" height="60" visible="true">
+                    <prototype name="Window" />
+                    <!-- TODO -->
+                </widget>
+            </widget>
+            <widget name="EnemySkill" x="0" y ="0" width="100%" height="100%" visible="false">
+                <sprite name="Icon" image="images/icons/materia_3.png" x="5" y="5" width="18" height="18" visible="true"/>
+                <text_area name="Name" x="29" y="9" font="FFVIIFont" visible="true" />
+                <widget name="Stars" x="5%" y="25" width="90%" height="32" visible="true">
+                    <sprite name="Star1" image="images/icons/materia_star_3.png" x="0" y="0" width="15" height="15" visible="true"/>
+                    <sprite name="Star2" image="images/icons/materia_star_3.png" x="16" y="0" width="15" height="15" visible="true"/>
+                    <sprite name="Star3" image="images/icons/materia_star_3.png" x="32" y="0" width="15" height="15" visible="true"/>
+                    <sprite name="Star4" image="images/icons/materia_star_3.png" x="48" y="0" width="15" height="15" visible="true"/>
+                    <sprite name="Star5" image="images/icons/materia_star_3.png" x="64" y="0" width="15" height="15" visible="true"/>
+                    <sprite name="Star6" image="images/icons/materia_star_3.png" x="80" y="0" width="15" height="15" visible="true"/>
+                    <sprite name="Star7" image="images/icons/materia_star_3.png" x="96" y="0" width="15" height="15" visible="true"/>
+                    <sprite name="Star8" image="images/icons/materia_star_3.png" x="112" y="0" width="15" height="15" visible="true"/>
+                    <sprite name="Star9" image="images/icons/materia_star_3.png" x="128" y="0" width="15" height="15" visible="true"/>
+                    <sprite name="Star10" image="images/icons/materia_star_3.png" x="144" y="0" width="15" height="15" visible="true"/>
+                    <sprite name="Star11" image="images/icons/materia_star_3.png" x="160" y="0" width="15" height="15" visible="true"/>
+                    <sprite name="Star12" image="images/icons/materia_star_3.png" x="176" y="0" width="15" height="15" visible="true"/>
+                    <sprite name="Star13" image="images/icons/materia_star_3.png" x="0" y="16" width="15" height="15" visible="true"/>
+                    <sprite name="Star14" image="images/icons/materia_star_3.png" x="16" y="16" width="15" height="15" visible="true"/>
+                    <sprite name="Star15" image="images/icons/materia_star_3.png" x="32" y="16" width="15" height="15" visible="true"/>
+                    <sprite name="Star16" image="images/icons/materia_star_3.png" x="48" y="16" width="15" height="15" visible="true"/>
+                    <sprite name="Star17" image="images/icons/materia_star_3.png" x="64" y="16" width="15" height="15" visible="true"/>
+                    <sprite name="Star18" image="images/icons/materia_star_3.png" x="80" y="16" width="15" height="15" visible="true"/>
+                    <sprite name="Star19" image="images/icons/materia_star_3.png" x="96" y="16" width="15" height="15" visible="true"/>
+                    <sprite name="Star20" image="images/icons/materia_star_3.png" x="112" y="16" width="15" height="15" visible="true"/>
+                    <sprite name="Star21" image="images/icons/materia_star_3.png" x="128" y="16" width="15" height="15" visible="true"/>
+                    <sprite name="Star22" image="images/icons/materia_star_3.png" x="144" y="16" width="15" height="15" visible="true"/>
+                    <sprite name="Star23" image="images/icons/materia_star_3.png" x="160" y="16" width="15" height="15" visible="true"/>
+                    <sprite name="Star24" image="images/icons/materia_star_3.png" x="176" y="16" width="15" height="15" visible="true"/>
+                </widget>
+                <widget name="Skills" x="5%" y="57" width="95%" height="120" visible="true" >
+                    <text_area name="Skill1" text_name="MateriaMenuAbilities" x="0%" y="0" font="FFVIIFont" visible="true" />
+                    <text_area name="Skill2" text_name="MateriaMenuAbilities" x="50%" y="0" font="FFVIIFont" visible="true" />
+                    <text_area name="Skill3" text_name="MateriaMenuAbilities" x="0%" y="10" font="FFVIIFont" visible="true" />
+                    <text_area name="Skill4" text_name="MateriaMenuAbilities" x="50%" y="10" font="FFVIIFont" visible="true" />
+                    <text_area name="Skill5" text_name="MateriaMenuAbilities" x="0%" y="20" font="FFVIIFont" visible="true" />
+                    <text_area name="Skill6" text_name="MateriaMenuAbilities" x="50%" y="20" font="FFVIIFont" visible="true" />
+                    <text_area name="Skill7" text_name="MateriaMenuAbilities" x="0%" y="30" font="FFVIIFont" visible="true" />
+                    <text_area name="Skill8" text_name="MateriaMenuAbilities" x="50%" y="30" font="FFVIIFont" visible="true" />
+                    <text_area name="Skill9" text_name="MateriaMenuAbilities" x="0%" y="40" font="FFVIIFont" visible="true" />
+                    <text_area name="Skill10" text_name="MateriaMenuAbilities" x="50%" y="40" font="FFVIIFont" visible="true" />
+                    <text_area name="Skill11" text_name="MateriaMenuAbilities" x="0%" y="50" font="FFVIIFont" visible="true" />
+                    <text_area name="Skill12" text_name="MateriaMenuAbilities" x="50%" y="50" font="FFVIIFont" visible="true" />
+                    <text_area name="Skill13" text_name="MateriaMenuAbilities" x="0%" y="60" font="FFVIIFont" visible="true" />
+                    <text_area name="Skill14" text_name="MateriaMenuAbilities" x="50%" y="60" font="FFVIIFont" visible="true" />
+                    <text_area name="Skill15" text_name="MateriaMenuAbilities" x="0%" y="70" font="FFVIIFont" visible="true" />
+                    <text_area name="Skill16" text_name="MateriaMenuAbilities" x="50%" y="70" font="FFVIIFont" visible="true" />
+                    <text_area name="Skill17" text_name="MateriaMenuAbilities" x="0%" y="80" font="FFVIIFont" visible="true" />
+                    <text_area name="Skill18" text_name="MateriaMenuAbilities" x="50%" y="80" font="FFVIIFont" visible="true" />
+                    <text_area name="Skill19" text_name="MateriaMenuAbilities" x="0%" y="90" font="FFVIIFont" visible="true" />
+                    <text_area name="Skill20" text_name="MateriaMenuAbilities" x="50%" y="90" font="FFVIIFont" visible="true" />
+                    <text_area name="Skill21" text_name="MateriaMenuAbilities" x="0%" y="100" font="FFVIIFont" visible="true" />
+                    <text_area name="Skill22" text_name="MateriaMenuAbilities" x="50%" y="100" font="FFVIIFont" visible="true" />
+                    <text_area name="Skill23" text_name="MateriaMenuAbilities" x="0%" y="110" font="FFVIIFont" visible="true" />
+                    <text_area name="Skill24" text_name="MateriaMenuAbilities" x="50%" y="110" font="FFVIIFont" visible="true" />
+                </widget>
+            </widget>
+            <widget name="Materia" x="0" y ="0" width="100%" height="100%" visible="false">
                 <sprite name="Icon" image="images/icons/materia_3.png" x="5" y="5" width="18" height="18" visible="true"/>
                 <text_area name="Name" x="29" y="9" font="FFVIIFont" visible="true" />
                 <widget name="Stars" x="100%-90" y="8" width="86" height="15" visible="true">
@@ -111,36 +175,48 @@
                     <text_area name="Ability5" text_name="MateriaMenuAbilities" x="0" y="60" width="100%" height="15" font="FFVIIFont" visible="true" />
                 </widget>
                 <widget name="Effects" x="50%+20" y="80" width="50%-20" height="100%-80" visible="true">
-                    <text_area name="Stat1" text_name="MateriaMenuEffects" x="0" y="0" width="60%" height="15" font="FFVIIFont" visible="true" />
-                    <text_area name="Stat2" text_name="MateriaMenuEffects" x="0" y="15" width="60%" height="15" font="FFVIIFont" visible="true" />
-                    <text_area name="Stat3" text_name="MateriaMenuEffects" x="0" y="30" width="60%" height="15" font="FFVIIFont" visible="true" />
-                    <text_area name="Stat4" text_name="MateriaMenuEffects" x="0" y="45" width="60%" height="15" font="FFVIIFont" visible="true" />
-                    <text_area name="Stat5" text_name="MateriaMenuEffects" x="0" y="60" width="60%" height="15" font="FFVIIFont" visible="true" />
-                    <text_area name="Stat6" text_name="MateriaMenuEffects" x="0" y="75" width="60%" height="15" font="FFVIIFont" visible="true" />
-                    <text_area name="Stat7" text_name="MateriaMenuEffects" x="0" y="90" width="60%" height="15" font="FFVIIFont" visible="true" />
-                    <text_area name="Val1" text_name="MateriaMenuAP" x="60%" y="2" width="30%" height="15" font="FFVIIMenuDigits" visible="true" />
-                    <text_area name="Val2" text_name="MateriaMenuAP" x="60%" y="17" width="30%" height="15" font="FFVIIMenuDigits" visible="true" />
-                    <text_area name="Val3" text_name="MateriaMenuAP" x="60%" y="32" width="30%" height="15" font="FFVIIMenuDigits" visible="true" />
-                    <text_area name="Val4" text_name="MateriaMenuAP" x="60%" y="47" width="30%" height="15" font="FFVIIMenuDigits" visible="true" />
-                    <text_area name="Val5" text_name="MateriaMenuAP" x="60%" y="62" width="30%" height="15" font="FFVIIMenuDigits" visible="true" />
-                    <text_area name="Val6" text_name="MateriaMenuAP" x="60%" y="77" width="30%" height="15" font="FFVIIMenuDigits" visible="true" />
-                    <text_area name="Val7" text_name="MateriaMenuAP" x="60%" y="92" width="30%" height="15" font="FFVIIMenuDigits" visible="true" />
+                    <text_area name="Stat1" x="0" y="0" width="60%" height="15" font="FFVIIFont" visible="true" />
+                    <text_area name="Stat2" x="0" y="15" width="60%" height="15" font="FFVIIFont" visible="true" />
+                    <text_area name="Stat3" x="0" y="30" width="60%" height="15" font="FFVIIFont" visible="true" />
+                    <text_area name="Stat4" x="0" y="45" width="60%" height="15" font="FFVIIFont" visible="true" />
+                    <text_area name="Stat5" x="0" y="60" width="60%" height="15" font="FFVIIFont" visible="true" />
+                    <text_area name="Stat6" x="0" y="75" width="60%" height="15" font="FFVIIFont" visible="true" />
+                    <text_area name="Stat7" x="0" y="90" width="60%" height="15" font="FFVIIFont" visible="true" />
+                    <text_area name="Val1" x="60%" y="2" width="30%" height="15" font="FFVIIMenuDigits" visible="true" />
+                    <text_area name="Val2" x="60%" y="17" width="30%" height="15" font="FFVIIMenuDigits" visible="true" />
+                    <text_area name="Val3" x="60%" y="32" width="30%" height="15" font="FFVIIMenuDigits" visible="true" />
+                    <text_area name="Val4" x="60%" y="47" width="30%" height="15" font="FFVIIMenuDigits" visible="true" />
+                    <text_area name="Val5" x="60%" y="62" width="30%" height="15" font="FFVIIMenuDigits" visible="true" />
+                    <text_area name="Val6" x="60%" y="77" width="30%" height="15" font="FFVIIMenuDigits" visible="true" />
+                    <text_area name="Val7" x="60%" y="92" width="30%" height="15" font="FFVIIMenuDigits" visible="true" />
                 </widget>
             </widget>
         </widget>
-        <widget name="List" x="230" y="94" width="100%-230" height="100%-99" visible="true">
+        <widget name="List" x="230" y="99" width="100%-230" height="100%-99" visible="true">
             <prototype name="Window" />
-            <text_area name="Item1" x="15" y="4%" font="FFVIIFont" visible="true" />
-            <text_area name="Item2" x="15" y="13%" font="FFVIIFont" visible="true" />
-            <text_area name="Item3" x="15" y="22%" font="FFVIIFont" visible="true" />
-            <text_area name="Item4" x="15" y="31%" font="FFVIIFont" visible="true" />
-            <text_area name="Item5" x="15" y="40%" font="FFVIIFont" visible="true" />
-            <text_area name="Item6" x="15" y="49%" font="FFVIIFont" visible="true" />
-            <text_area name="Item7" x="15" y="58%" font="FFVIIFont" visible="true" />
-            <text_area name="Item8" x="15" y="67%" font="FFVIIFont" visible="true" />
-            <text_area name="Item9" x="15" y="76%" font="FFVIIFont" visible="true" />
-            <text_area name="Item10" x="15" y="85%" font="FFVIIFont" visible="true" />
-            <sprite name="Cursor" image="images/icons/cursor.png" x="-10" y="8" width="24" height="17" visible="true">
+            <sprite name="ScrollBg" x="100%-12" y="2" width="10" height="100%-6" colour="0.2 0.2 0.2" alpha="0.8" visible="true"/>
+            <sprite name="Scroll" x="100%-11" y="3" width="8" height="18" colour="0.7 0.7 0.7" visible="true"/>
+            <sprite name="Icon1" image="images/icons/materia_3.png" x="5" y="4%-1" width="12" height="12" visible="true"/>
+            <sprite name="Icon2" image="images/icons/materia_3.png" x="5" y="13%-1" width="12" height="12" visible="true"/>
+            <sprite name="Icon3" image="images/icons/materia_3.png" x="5" y="22%-1" width="12" height="12" visible="true"/>
+            <sprite name="Icon4" image="images/icons/materia_3.png" x="5" y="31%-1" width="12" height="12" visible="true"/>
+            <sprite name="Icon5" image="images/icons/materia_3.png" x="5" y="40%-1" width="12" height="12" visible="true"/>
+            <sprite name="Icon6" image="images/icons/materia_3.png" x="5" y="49%-1" width="12" height="12" visible="true"/>
+            <sprite name="Icon7" image="images/icons/materia_3.png" x="5" y="58%-1" width="12" height="12" visible="true"/>
+            <sprite name="Icon8" image="images/icons/materia_3.png" x="5" y="67%-1" width="12" height="12" visible="true"/>
+            <sprite name="Icon9" image="images/icons/materia_3.png" x="5" y="76%-1" width="12" height="12" visible="true"/>
+            <sprite name="Icon10" image="images/icons/materia_3.png" x="5" y="85%-1" width="12" height="12" visible="true"/>
+            <text_area name="Name1" x="20" y="4%" font="FFVIIFont" visible="true" />
+            <text_area name="Name2" x="20" y="13%" font="FFVIIFont" visible="true" />
+            <text_area name="Name3" x="20" y="22%" font="FFVIIFont" visible="true" />
+            <text_area name="Name4" x="20" y="31%" font="FFVIIFont" visible="true" />
+            <text_area name="Name5" x="20" y="40%" font="FFVIIFont" visible="true" />
+            <text_area name="Name6" x="20" y="49%" font="FFVIIFont" visible="true" />
+            <text_area name="Name7" x="20" y="58%" font="FFVIIFont" visible="true" />
+            <text_area name="Name8" x="20" y="67%" font="FFVIIFont" visible="true" />
+            <text_area name="Name9" x="20" y="76%" font="FFVIIFont" visible="true" />
+            <text_area name="Name10" x="20" y="85%" font="FFVIIFont" visible="true" />
+            <sprite name="Cursor" image="images/icons/cursor.png" x="-15" y="8" width="24" height="17" visible="true">
                 <animation name="Position1" length="0" y="0:4%+3" />
                 <animation name="Position2" length="0" y="0:13%+3" />
                 <animation name="Position3" length="0" y="0:22%+3" />

+ 1 - 1
data/data/screens/name_menu/name_menu.xml

@@ -7,7 +7,7 @@
         </widget>
         <widget name="Character" width="100%" x="0" y="24" height="70" visible="true">
             <prototype name="Window"/>
-            <sprite name="Portrait" image="images/other/choco.png" x="10" y="12" width="48" height="48" visible="true" />
+            <sprite name="Portrait" image="images/other/choco.png" x="10" y="12" width="42" height="48" visible="true" />
             <widget name="Name" width="72" height="16" x="80" y="50%-8" visible="true">
                 <text_area name="Char1" x="1" y="0" width="8" height="16" visible="true" font="FFVIIFont" />
                 <text_area name="Char2" x="11" y="0" width="8" height="16" visible="true" font="FFVIIFont" />

+ 1 - 0
data/data/scripts/README.txt

@@ -0,0 +1 @@
+This folders contains the scripts that make the game work.

+ 3 - 0
data/data/scripts/data.lua

@@ -127,6 +127,9 @@ Materia.TYPE = {
     SUMMON = 5
 }
 
+--- ID of the enemy skill materia. This is handled differently.
+Materia.ENEMY_SKILL_ID = 44
+
 --- Abilities confered by independent materia.
 Materia.ABILITY = {
     STR_PLUS = 0,

+ 17 - 5
data/data/scripts/debug_data.lua

@@ -29,7 +29,7 @@ Inventory.add_item(240, 4)
 Inventory.add_item(289, 4)
 Inventory.add_item(290, 1)
 Inventory.add_item(294, 2)
---[[Inventory.add_key_item(0)
+Inventory.add_key_item(0)
 Inventory.add_key_item(1)
 Inventory.add_key_item(5)
 Inventory.add_key_item(18)
@@ -38,10 +38,10 @@ Inventory.add_key_item(20)
 Inventory.add_key_item(21)
 Inventory.add_key_item(22)
 Inventory.add_key_item(33)
-Inventory.add_key_item(38)]]
-for i = 0, 50 do
+Inventory.add_key_item(38)
+--[[for i = 0, 50 do
     Inventory.add_key_item(i)
-end
+end]]
 Inventory.money = 1234567
 Inventory.sort(Inventory.ORDER.NAME)
 Characters[0].stats.hp.current = 12345
@@ -49,10 +49,22 @@ Characters[0].stats.hp.base = 54321
 Characters[0].armor.id = 268
 Characters[0].armor.materia = {
     [0] = {id = 62, ap = 5340},
-    [2] = {id = 72, ap = 5},
+    [2] = {id = 44, ap = 0, skills = {}}, -- E. Skill
     [3] = {id = 78, ap = 520},
     [4] = {id = 36, ap = 0},
     [5] = {id = 36, ap = 50000},
     [6] = {id = 0, ap = 0},
     [7] = {id = 26, ap = 0},
 }
+for i = 1, 24 do -- E. Skill list
+    if i == 1 or i == 6 or i == 9 or i == 13 or i == 14 or i == 15 or i == 23 or i == 24 then
+        Characters[0].armor.materia[2].skills[i] = true
+    else
+        Characters[0].armor.materia[2].skills[i] = false
+    end
+end
+Materia.add(4)
+Materia.add(44)
+Materia.add(12)
+Materia.add(15, 2000)
+Materia.add(24)

+ 3 - 0
data/data/scripts/menu/begin_menu.lua

@@ -22,6 +22,7 @@ UiContainer.BeginMenu = {
 
             if button == "Enter" and event == "Press" then
                 if self.position == 1 then
+                    audio_manager:play_sound("Window")
                     load_field_map_request( "md1stin", "" )
                     console( "camera_free false" )
                     --console( "debug_walkmesh true" )
@@ -87,12 +88,14 @@ UiContainer.BeginMenu = {
                     FFVII.MenuSettings.pause_available = true
                 end
             elseif button == "Down" then
+                audio_manager:play_sound("Cursor")
                 self.position = self.position + 1
                 if self.position > self.position_total then
                     self.position = 1;
                 end
                 cursor:set_default_animation( "Position" .. self.position )
             elseif button == "Up" then
+                audio_manager:play_sound("Cursor")
                 self.position = self.position - 1
                 if self.position <= 0 then
                     self.position = self.position_total;

+ 12 - 2
data/data/scripts/menu/equip_menu.lua

@@ -64,9 +64,11 @@ UiContainer.EquipMenu = {
                 -- TODO: Handle L1/R1 (change character)
                 -- TODO: Handle square: Materia menu
                 if button == "Escape" and event == "Press" then
+                    audio_manager:play_sound("Back")
                     UiContainer.current_menu = "main"
                     script:request_end_sync(Script.UI, "EquipMenu", "hide", 0)
                 elseif button == "Down" then
+                    audio_manager:play_sound("Cursor")
                     self.equip_position = self.equip_position + 1
                     if self.equip_position > self.equip_position_total then
                         self.equip_position = 1
@@ -74,6 +76,7 @@ UiContainer.EquipMenu = {
                     ui_manager:get_widget("EquipMenu.Container.Character.Cursor"):set_default_animation("Position" .. self.equip_position)
                     self.populate_details(self, false)
                 elseif button == "Up" then
+                    audio_manager:play_sound("Cursor")
                     self.equip_position = self.equip_position - 1
                     if self.equip_position < 1 then
                         self.equip_position = self.equip_position_total
@@ -88,6 +91,7 @@ UiContainer.EquipMenu = {
                     elseif self.equip_position == 3 then
                         self.selecting_slot = Inventory.ITEM_TYPE.ACCESSORY
                     end
+                    audio_manager:play_sound("Cursor")
                     self.submenu_select(self)
                 else
                     return 0
@@ -102,8 +106,10 @@ UiContainer.EquipMenu = {
                     list = self.avail_accessories
                 end
                 if button == "Escape" and event == "Press" then
+                    audio_manager:play_sound("Back")
                     self.submenu_none(self)
                 elseif button == "Down" then
+                    audio_manager:play_sound("Cursor")
                     -- Move one position down only if there is a next item
                     if #(list) <= self.list_item_selected + 1 then
                         return 0
@@ -122,6 +128,7 @@ UiContainer.EquipMenu = {
                     self.populate_details(self, true)
                     self.calculate_stat_diffs(self)
                 elseif button == "Up" then
+                    audio_manager:play_sound("Cursor")
                     -- Move one position up only if not in the first.
                     if self.list_item_selected <= 1 then
                         return 0
@@ -150,9 +157,11 @@ UiContainer.EquipMenu = {
                         item_id = self.avail_accessories[self.list_item_selected + 1]
                     end
                     if Characters.equip(self.char_id, item_id) == false then
-                        print("BEEP (error)")
+                        audio_manager:play_sound("Error")
                         return 0
                     end
+                    -- TODO: Replace for "Equip" sound
+                    audio_manager:play_sound("Cursor")
                     self.submenu_none(self)
                 end
             end
@@ -179,6 +188,7 @@ UiContainer.EquipMenu = {
         UiContainer.populate_character_data("EquipMenu.Container.Character", Characters[self.char_id])
         ui_manager:get_widget("EquipMenu.Container.Character.Portrait"):set_image("images/characters/" .. tostring(self.char_id) .. ".png")
         -- TODO: Do something for chars 9 and 10
+        ui_manager:get_widget("EquipMenu.Container.Character.WpnIcon"):set_image("images/icons/item_weapon_" .. tostring(self.char_id) .. ".png")
         ui_manager:get_widget("EquipMenu.Container.Character.WpnLbl"):set_text(Game.Items[Characters[self.char_id].weapon.id].name)
         ui_manager:get_widget("EquipMenu.Container.Character.ArmLbl"):set_text(Game.Items[Characters[self.char_id].armor.id].name)
         if Characters[self.char_id].accessory == nil then
@@ -218,7 +228,7 @@ UiContainer.EquipMenu = {
         self.list_position = 1
         self.list_item_selected = 1
         if self.populate_item_list(self) == false then
-            print("BEEP")
+            audio_manager:play_sound("Error")
             return
         end
         UiContainer.current_submenu = "item_select"

+ 42 - 2
data/data/scripts/menu/item_menu.lua

@@ -87,9 +87,11 @@ UiContainer.ItemMenu = {
         if UiContainer.current_menu == "item" then
             if UiContainer.current_submenu == "bar_selection" then
                 if button == "Escape" and event == "Press" then
+                    audio_manager:play_sound("Back")
                     UiContainer.current_menu = "main"
                     script:request_end_sync(Script.UI, "ItemMenu", "hide", 0)
                 elseif button == "Right" then
+                    audio_manager:play_sound("Cursor")
                     self.bar_position = self.bar_position + 1
                     if self.bar_position > self.bar_position_total then
                         self.bar_position = 1
@@ -102,6 +104,7 @@ UiContainer.ItemMenu = {
                         ui_manager:get_widget("ItemMenu.Container.KeyItems"):set_visible(false)
                     end
                 elseif button == "Left" then
+                    audio_manager:play_sound("Cursor")
                     self.bar_position = self.bar_position - 1
                     if self.bar_position < 1 then
                         self.bar_position = self.bar_position_total
@@ -123,25 +126,30 @@ UiContainer.ItemMenu = {
                     else
                         return 0
                     end
+                    audio_manager:play_sound("Cursor")
                 else
                     return 0
                 end
             elseif UiContainer.current_submenu == "order_selection" then
                 if button == "Escape" and event == "Press" then
+                    audio_manager:play_sound("Back")
                     self.submenu_bar(self)
                 elseif button == "Down" then
+                    audio_manager:play_sound("Cursor")
                     self.order_cursor_position = self.order_cursor_position + 1
                     if self.order_cursor_position > self.order_cursor_position_total then
                         self.order_cursor_position = 1
                     end
                     ui_manager:get_widget("ItemMenu.Container.ItemOrder.Cursor"):set_default_animation("Position" .. self.order_cursor_position)
                 elseif button == "Up" then
+                    audio_manager:play_sound("Cursor")
                     self.order_cursor_position = self.order_cursor_position - 1
                     if self.order_cursor_position <= 0 then
                         self.order_cursor_position = self.order_cursor_position_total
                     end
                     ui_manager:get_widget("ItemMenu.Container.ItemOrder.Cursor"):set_default_animation("Position" .. self.order_cursor_position)
                 elseif button == "Enter" then
+                    audio_manager:play_sound("Cursor")
                     if self.order_cursor_position == 1 then
                         Inventory.sort(Inventory.ORDER.CUSTOM)
                     elseif self.order_cursor_position == 2 then
@@ -168,8 +176,10 @@ UiContainer.ItemMenu = {
                 if button == "Down" then
                     if self.item_cursor_item_selected >= self.inventory_size then
                         -- At the very end, do nothing
+                        audio_manager:play_sound("Error")
                         return 0
                     end
+                    audio_manager:play_sound("Cursor")
                     self.item_cursor_item_selected = self.item_cursor_item_selected + 1
                     if self.item_cursor_position == self.item_cursor_position_total then
                         -- The cursor is at the end. Scroll page, but dont change cursor.
@@ -189,8 +199,10 @@ UiContainer.ItemMenu = {
                 elseif button == "Up" then
                     if self.item_cursor_item_selected == 0 then
                         -- At the very begining, do nothing
+                        audio_manager:play_sound("Error")
                         return 0
                     end
+                    audio_manager:play_sound("Cursor")
                     self.item_cursor_item_selected = self.item_cursor_item_selected - 1
                     if self.item_cursor_position == 1 then
                         -- The cursor is at the end. Scroll page, but dont change cursor.
@@ -209,6 +221,7 @@ UiContainer.ItemMenu = {
                     end
                 elseif button == "PGDN" then -- TODO: Also, the bind to R1
                     if self.first_item_in_window + self.item_cursor_position_total < self.inventory_size - self.item_cursor_position_total then
+                        audio_manager:play_sound("Cursor")
                         self.item_cursor_item_selected = self.item_cursor_item_selected + self.item_cursor_position_total
                         self.first_item_in_window = self.first_item_in_window + self.item_cursor_position_total
                         self.populate_items(self)
@@ -217,9 +230,12 @@ UiContainer.ItemMenu = {
                         else
                             desc_label:set_text("")
                         end
+                    else
+                        audio_manager:play_sound("Error")
                     end
                 elseif button == "PGUP" then -- TODO: Also, the bind to L1
                     if self.first_item_in_window - self.item_cursor_position_total > 1 then
+                        audio_manager:play_sound("Cursor")
                         self.item_cursor_item_selected = self.item_cursor_item_selected - self.item_cursor_position_total
                         self.first_item_in_window = self.first_item_in_window - self.item_cursor_position_total
                         self.populate_items(self)
@@ -228,13 +244,17 @@ UiContainer.ItemMenu = {
                         else
                             desc_label:set_text("")
                         end
+                    else
+                        audio_manager:play_sound("Error")
                     end
                 elseif button == "Escape" and event == "Press" then
+                    audio_manager:play_sound("Back")
                     self.submenu_bar(self)
                     UiContainer.current_submenu = "bar_selection"
                     ui_manager:get_widget("ItemMenu.Container.ItemList.Cursor"):set_visible(false)
                 elseif button == "Enter" then
                     if Game.Items[Inventory[self.item_cursor_item_selected].item].menu == 1 then
+                        audio_manager:play_sound("Cursor")
                         self.item_in_use = Inventory[self.item_cursor_item_selected].item
                         if (Game.Items[Inventory[self.item_cursor_item_selected - 1].item].target.default_multiple) == 1 then
                             self.item_in_use_party = true
@@ -244,7 +264,7 @@ UiContainer.ItemMenu = {
                             self.submenu_chars(self, false)
                         end
                     else
-                        print("BEEP")
+                        audio_manager:play_sound("Error")
                     end
                 end
             elseif UiContainer.current_submenu == "char_selection" then
@@ -252,6 +272,7 @@ UiContainer.ItemMenu = {
                     if self.item_in_use_party == true then
                         return 0
                     else
+                        audio_manager:play_sound("Cursor")
                         self.char_position = self.char_position + 1
                         if self.char_position > self.char_position_total then
                             self.char_position = 1
@@ -262,6 +283,7 @@ UiContainer.ItemMenu = {
                     if self.item_in_use_party == true then
                         return 0
                     else
+                        audio_manager:play_sound("Cursor")
                         self.char_position = self.char_position - 1
                         if self.char_position < 1 then
                             self.char_position = self.char_position_total
@@ -269,6 +291,7 @@ UiContainer.ItemMenu = {
                         ui_manager:get_widget("ItemMenu.Container.ItemMenuCharacters.Cursor"):set_default_animation("Position" .. self.char_position)
                     end
                 elseif button == "Escape" then
+                    audio_manager:play_sound("Back")
                     self.submenu_items(self)
                 elseif button == "Enter" then
                     local use_result = false
@@ -278,15 +301,19 @@ UiContainer.ItemMenu = {
                         use_result = self.use_item(self, self.item_in_use, self.char_position)
                     end
                     if use_result == false then
-                        print("USE BEEP")
+                        audio_manager:play_sound("Error")
+                    else
+                        -- TODO: Audio here? or in use_item?
                     end
                 end
             elseif UiContainer.current_submenu == "key_item_selection" then
                 if button == "Down" then
                     if self.key_items_cursor_y_position == self.key_items_cursor_y_position_total and self.first_key_item_row_in_window == self.key_items_total_rows - self.key_items_cursor_y_position then
                         -- Last key item selected, do nothing
+                        audio_manager:play_sound("Error")
                         return 0
                     end
+                    audio_manager:play_sound("Cursor")
                     if self.key_items_cursor_y_position == self.key_items_cursor_y_position_total then
                         -- Scroll down by 1, don't move cursor
                         self.first_key_item_row_in_window = self.first_key_item_row_in_window + 1
@@ -306,8 +333,10 @@ UiContainer.ItemMenu = {
                 elseif button == "Up" then
                     if self.key_items_cursor_y_position == 1 and self.first_key_item_row_in_window == 1 then
                         -- First item, do nothing.
+                        audio_manager:play_sound("Error")
                         return 0
                     end
+                    audio_manager:play_sound("Cursor")
                     if self.key_items_cursor_y_position == 1 then
                         -- Scroll down by 1, don't move cursor
                         self.first_key_item_row_in_window = self.first_key_item_row_in_window - 1
@@ -328,8 +357,10 @@ UiContainer.ItemMenu = {
                     local k_selected_id = 2 * (self.first_key_item_row_in_window + self.key_items_cursor_y_position - 2) + self.key_items_cursor_x_position - 1
                     if k_selected_id >= 50 then
                         -- Already in last item, do nothing
+                        audio_manager:play_sound("Error")
                         return 0
                     end
+                    audio_manager:play_sound("Cursor")
                     k_selected_id = k_selected_id + 1
                     if self.key_items_cursor_x_position == 1 then
                         -- If on the left, move to the right. No scrolling.
@@ -357,8 +388,10 @@ UiContainer.ItemMenu = {
                     local k_selected_id = 2 * (self.first_key_item_row_in_window + self.key_items_cursor_y_position - 2) + self.key_items_cursor_x_position - 1
                     if k_selected_id <= 0 then
                         -- Already in first item, do nothing
+                        audio_manager:play_sound("Error")
                         return 0
                     end
+                    audio_manager:play_sound("Cursor")
                     k_selected_id = k_selected_id - 1
                     if self.key_items_cursor_x_position == 2 then
                         -- If on the right, move to the left. No scrolling.
@@ -384,6 +417,7 @@ UiContainer.ItemMenu = {
                     end
                 elseif button == "PGDN" then -- TODO: Also, the bind to R1
                     if self.first_key_item_row_in_window + self.key_items_cursor_y_position_total < self.key_items_total_rows - self.key_items_cursor_y_position_total then
+                        audio_manager:play_sound("Cursor")
                         self.first_key_item_row_in_window = self.first_key_item_row_in_window + self.key_items_cursor_y_position_total
                         self.populate_items(self)
                         -- Show description of the selected item, if any.
@@ -393,9 +427,12 @@ UiContainer.ItemMenu = {
                         else
                             ui_manager:get_widget("ItemMenu.Container.ItemMenuSecondBar.ItemText"):set_text("")
                         end
+                    else
+                        audio_manager:play_sound("Error")
                     end
                 elseif button == "PGUP" then -- TODO: Also, the bind to L1
                     if self.first_key_item_row_in_window + self.key_items_cursor_y_position_total > 1 then
+                        audio_manager:play_sound("Cursor")
                         self.first_key_item_row_in_window = self.first_key_item_row_in_window - self.key_items_cursor_y_position_total
                         self.populate_items(self)
                         -- Show description of the selected item, if any.
@@ -405,8 +442,11 @@ UiContainer.ItemMenu = {
                         else
                             ui_manager:get_widget("ItemMenu.Container.ItemMenuSecondBar.ItemText"):set_text("")
                         end
+                    else
+                        audio_manager:play_sound("Cursor")
                     end
                 elseif button == "Escape" then
+                    audio_manager:play_sound("Back")
                     self.submenu_bar(self)
                 end
 

+ 14 - 0
data/data/scripts/menu/main_menu.lua

@@ -41,28 +41,36 @@ UiContainer.MainMenu = {
             local location = ui_manager:get_widget("MainMenu.Container.Location")
             if UiContainer.current_submenu == "" then
                 if button == "Escape" and event == "Press" then
+                    audio_manager:play_sound("Back")
                     script:request_end_sync(Script.UI, "MainMenu", "hide", 0)
                 elseif button == "Enter" and event == "Press" then
                     if self.position == 1 then -- Item menu
+                        audio_manager:play_sound("Cursor")
                         script:request_end_sync(Script.UI, "ItemMenu", "show", 0)
                     elseif self.position == 8 then -- Config menu
+                        audio_manager:play_sound("Error")
                         print("TODO: Open config menu")
                     elseif self.position == 9 then -- PHS menu
+                        audio_manager:play_sound("Error")
                         print("TODO: Open PHS menu")
                     elseif self.position == 10 then -- Save menu
+                        audio_manager:play_sound("Error")
                         print("TODO: Open save menu")
                     else -- Any other menu that needs a character
+                        audio_manager:play_sound("Cursor")
                         self.select_character_for_menu = self.position
                         UiContainer.current_submenu = "main_character"
                         ui_manager:get_widget("MainMenu.Container.Characters.Cursor"):set_visible(true)
                     end
                 elseif button == "Down" then
+                    audio_manager:play_sound("Cursor")
                     self.position = self.position + 1
                     if self.position > self.position_total then
                         self.position = 1;
                     end
                     menu_cursor:set_default_animation("Position" .. self.position)
                 elseif button == "Up" then
+                    audio_manager:play_sound("Cursor")
                     self.position = self.position - 1
                     if self.position <= 0 then
                         self.position = self.position_total;
@@ -71,9 +79,11 @@ UiContainer.MainMenu = {
                 end
             elseif UiContainer.current_submenu == "main_character" then
                 if button == "Escape" and event == "Press" then
+                    audio_manager:play_sound("Back")
                     UiContainer.current_submenu = ""
                     ui_manager:get_widget("MainMenu.Container.Characters.Cursor"):set_visible(false)
                 elseif button == "Down" then
+                    audio_manager:play_sound("Cursor")
                     -- TODO: Skip empty character slots
                     self.character_position = self.character_position + 1
                     if self.character_position > self.character_position_total then
@@ -81,6 +91,7 @@ UiContainer.MainMenu = {
                     end
                     ui_manager:get_widget("MainMenu.Container.Characters.Cursor"):set_default_animation("Position" .. self.character_position)
                 elseif button == "Up" then
+                    audio_manager:play_sound("Cursor")
                     -- TODO: Skip empty character slots
                     self.character_position = self.character_position - 1
                     if self.character_position < 1 then
@@ -88,6 +99,8 @@ UiContainer.MainMenu = {
                     end
                     ui_manager:get_widget("MainMenu.Container.Characters.Cursor"):set_default_animation("Position" .. self.character_position)
                 elseif button == "Enter" then
+                    -- TODO: Check for empty character.
+                    audio_manager:play_sound("Cursor")
                     if self.select_character_for_menu == 2 then -- Magic menu
                         print("Open magic menu for char in slot " .. self.character_position)
                     elseif self.select_character_for_menu == 3 then -- Materia menu
@@ -107,6 +120,7 @@ UiContainer.MainMenu = {
             end
         elseif ui_manager:get_widget("MainMenu"):is_visible() == false and FFVII.MenuSettings.available == true then
             if button == "Escape" and event == "Press" then
+                audio_manager:play_sound("Back")
                 script:request_end_sync(Script.UI, "MainMenu", "show", 0)
             end
         else

+ 273 - 35
data/data/scripts/menu/materia_menu.lua

@@ -18,12 +18,18 @@ UiContainer.MateriaMenu = {
     --- ID of the character the menu is open for.
     char_id = -1,
 
+    --- Cursor position in the materia list.
     list_position = 1,
 
+    --- Max cursor postion in the materia list.
     list_position_total = 10,
 
+    --- First visible position in the materia list
     list_first_visible = 1,
 
+    --- Max materias in the list
+    list_size = 200,
+
     --- Run when the menu is creaded.
     --
     -- It does nothing.
@@ -42,9 +48,11 @@ UiContainer.MateriaMenu = {
                 -- TODO: Handle L1/R1 (change character)
                 -- TODO: Handle square: Materia menu
                 if button == "Escape" and event == "Press" then
+                    audio_manager:play_sound("Back")
                     UiContainer.current_menu = "main"
                     script:request_end_sync(Script.UI, "MateriaMenu", "hide", 0)
                 elseif button == "Down" then
+                    audio_manager:play_sound("Cursor")
                     self.header_position_row = self.header_position_row + 1
                     if self.header_position_row > self.header_position_row_total then
                         self.header_position_row = 1
@@ -52,6 +60,7 @@ UiContainer.MateriaMenu = {
                     ui_manager:get_widget("MateriaMenu.Container.Character.Cursor"):set_default_animation("Position" .. self.header_position_row .. "-" .. self.header_position)
                     self.populate_details(self, false)
                 elseif button == "Up" then
+                    audio_manager:play_sound("Cursor")
                     self.header_position_row = self.header_position_row - 1
                     if self.header_position_row < 1 then
                         self.header_position_row = self.header_position_row_total
@@ -59,6 +68,7 @@ UiContainer.MateriaMenu = {
                     ui_manager:get_widget("MateriaMenu.Container.Character.Cursor"):set_default_animation("Position" .. self.header_position_row .. "-" .. self.header_position)
                     self.populate_details(self, false)
                 elseif button == "Right" then
+                    audio_manager:play_sound("Cursor")
                     self.header_position = self.header_position + 1
                     if self.header_position > self.header_position_total then
                         self.header_position = 1
@@ -66,6 +76,7 @@ UiContainer.MateriaMenu = {
                     ui_manager:get_widget("MateriaMenu.Container.Character.Cursor"):set_default_animation("Position" .. self.header_position_row .. "-" .. self.header_position)
                     self.populate_details(self, false)
                 elseif button == "Left" then
+                    audio_manager:play_sound("Cursor")
                     self.header_position = self.header_position - 1
                     if self.header_position < 1 then
                         self.header_position = self.header_position_total
@@ -73,18 +84,89 @@ UiContainer.MateriaMenu = {
                     ui_manager:get_widget("MateriaMenu.Container.Character.Cursor"):set_default_animation("Position" .. self.header_position_row .. "-" .. self.header_position)
                     self.populate_details(self, false)
                 elseif button == "Enter" then
-                    --[[if self.equip_position == 1 then
-                        self.selecting_slot = Inventory.ITEM_TYPE.WEAPON
-                    elseif self.equip_position == 2 then
-                        self.selecting_slot = Inventory.ITEM_TYPE.ARMOR
-                    elseif self.equip_position == 3 then
-                        self.selecting_slot = Inventory.ITEM_TYPE.ACCESSORY
+                    if self.header_position_row == 1 and self.header_position == 1 then
+                        -- TODO: Command menu
+                    elseif self.header_position_row == 2 and self.header_position == 1 then
+                        -- TODO: Order menu
+                    else
+                        -- TODO: Check for  avalid slot before entering list
+                        if self.header_position_row == 1 and #(Game.Items[Characters[self.char_id].weapon.id].slots) > self.header_position - 2 then
+                            self.submenu_list(self)
+                        elseif self.header_position_row == 2 and #(Game.Items[Characters[self.char_id].armor.id].slots) > self.header_position -2  then
+                            self.submenu_list(self)
+                        else
+                            audio_manager:play_sound("Error")
+                            return 0
+                        end
+                        audio_manager:play_sound("Cursor")
                     end
-                    self.submenu_select(self)]]
                 -- TODO: Triangle->Remove
                 else
                     return 0
                 end
+            elseif UiContainer.current_submenu == "list" then
+                if button == "Down" then
+                    if self.list_first_visible + self.list_position > self.list_size then
+                        -- At the very end, do nothing
+                        audio_manager:play_sound("Error")
+                        return 0
+                    end
+                    audio_manager:play_sound("Cursor")
+                    if self.list_position == self.list_position_total then
+                        -- The cursor is at the end. Scroll page, but dont change cursor.
+                        self.list_first_visible = self.list_first_visible + 1
+                        self.populate_list(self)
+                    else
+                        -- The cursor is not at the end, move cursor down.
+                        self.list_position = self.list_position + 1
+                    end
+                    ui_manager:get_widget("MateriaMenu.Container.List.Cursor"):set_default_animation("Position" .. self.list_position)
+                    -- Show details
+                    self.populate_details(self, true)
+                elseif button == "Up" then
+                    if self.list_first_visible == 1 and self.list_position == 1 then
+                        -- At the very begining, do nothing
+                        audio_manager:play_sound("Error")
+                        return 0
+                    end
+                    audio_manager:play_sound("Cursor")
+                    if self.list_position == 1 then
+                        -- The cursor is at the end. Scroll page, but dont change cursor.
+                        self.list_first_visible = self.list_first_visible - 1
+                        self.populate_list(self)
+                    else
+                        -- The cursor is not at the end, move cursor down.
+                        self.list_position = self.list_position - 1
+                    end
+                    ui_manager:get_widget("MateriaMenu.Container.List.Cursor"):set_default_animation("Position" .. self.list_position)
+                    -- Show details
+                    self.populate_details(self, true)
+                elseif button == "PGDN" then -- TODO: Also, the bind to R1
+                    if self.list_first_visible + self.list_position_total < self.list_size - self.list_position_total then
+                        audio_manager:play_sound("Cursor")
+                        self.list_first_visible = self.list_first_visible + self.list_position_total
+                        self.populate_list(self)
+                        self.populate_details(self, true)
+                    else
+                        audio_manager:play_sound("Error")
+                    end
+                elseif button == "PGUP" then -- TODO: Also, the bind to L1
+                    if self.list_first_visible - self.list_position_total > 1 then
+                        audio_manager:play_sound("Cursor")
+                        self.list_first_visible = self.list_first_visible - self.list_position_total
+                        self.populate_list(self)
+                        self.populate_details(self, true)
+                    else
+                        audio_manager:play_sound("Error")
+                    end
+                elseif button == "Escape" and event == "Press" then
+                    audio_manager:play_sound("Back")
+                    self.submenu_none(self)
+                elseif button == "Enter" then
+                    -- TODO: Replace for "Equip materia" sound.
+                    audio_manager:play_sound("Cursor")
+                    self.equip_materia(self)
+                end
             end
         end
         return 0
@@ -100,7 +182,11 @@ UiContainer.MateriaMenu = {
         UiContainer.current_submenu = ""
         self.header_position_row = 1
         self.header_position = 1
+        self.list_position = 1
+        self.list_first_visible = 1
+        self.populate_list(self)
         self.submenu_none(self)
+        self.populate_commands(self)
         return 0;
     end,
 
@@ -111,33 +197,22 @@ UiContainer.MateriaMenu = {
         ui_manager:get_widget("MateriaMenu.Container.Character.Portrait"):set_image("images/characters/" .. tostring(self.char_id) .. ".png")
         self.populate_equip(self)
         -- TODO: Do something for chars 9 and 10
-
-
-        --[[self.load_availables(self)
+        ui_manager:get_widget("MateriaMenu.Container.Character.WpnIcon"):set_image("images/icons/item_weapon_" .. tostring(self.char_id) .. ".png")
+        ui_manager:get_widget("MateriaMenu.Container.List.Cursor"):set_visible(false)
+        --TODO: Uncomment when implemented: ui_manager:get_widget("MateriaMenu.Container.Order.Cursor"):set_visible(false)
+        ui_manager:get_widget("MateriaMenu.Container.Character.Cursor"):set_visible(true)
         self.populate_details(self, false)
-        self.populate_current_stats(self)
-        self.calculate_stat_diffs(self)
-        -- Hide stat arrows and difs
-        ui_manager:get_widget("EquipMenu.Container.Stats.AtkDif"):set_visible(false)
-        ui_manager:get_widget("EquipMenu.Container.Stats.AccDif"):set_visible(false)
-        ui_manager:get_widget("EquipMenu.Container.Stats.DefDif"):set_visible(false)
-        ui_manager:get_widget("EquipMenu.Container.Stats.EvaDif"):set_visible(false)
-        ui_manager:get_widget("EquipMenu.Container.Stats.MAtkDif"):set_visible(false)
-        ui_manager:get_widget("EquipMenu.Container.Stats.MDefDif"):set_visible(false)
-        ui_manager:get_widget("EquipMenu.Container.Stats.MEvaDif"):set_visible(false)
-        ui_manager:get_widget("EquipMenu.Container.Stats.AtkArw"):set_visible(false)
-        ui_manager:get_widget("EquipMenu.Container.Stats.AccArw"):set_visible(false)
-        ui_manager:get_widget("EquipMenu.Container.Stats.DefArw"):set_visible(false)
-        ui_manager:get_widget("EquipMenu.Container.Stats.EvaArw"):set_visible(false)
-        ui_manager:get_widget("EquipMenu.Container.Stats.MAtkArw"):set_visible(false)
-        ui_manager:get_widget("EquipMenu.Container.Stats.MDefArw"):set_visible(false)
-        ui_manager:get_widget("EquipMenu.Container.Stats.MEvaArw"):set_visible(false)
-        ui_manager:get_widget("EquipMenu.Container.List.Cursor"):set_visible(false)
-        for i = 1, self.list_position_total do
-            ui_manager:get_widget("EquipMenu.Container.List.Item" .. tostring(i)):set_text("")
-        end]]
     end,
 
+    submenu_list = function(self)
+        UiContainer.current_submenu = "list"
+        ui_manager:get_widget("MateriaMenu.Container.List.Cursor"):set_visible(true)
+        self.populate_details(self, true)
+    end,
+
+    --- Populate the equipment section.
+    --
+    -- It fills the header with weapn and armor names, slots, and current materia.
     populate_equip = function(self)
         local weapon = Game.Items[Characters[self.char_id].weapon.id]
         local armor = Game.Items[Characters[self.char_id].armor.id]
@@ -192,12 +267,53 @@ UiContainer.MateriaMenu = {
     -- Must be called when the cursor goes into an empty slot
     hide_details = function(self)
         -- TODO
+        ui_manager:get_widget("MateriaMenu.Container.Details.Empty"):set_visible(true)
+        ui_manager:get_widget("MateriaMenu.Container.Details.Commands"):set_visible(false)
+        ui_manager:get_widget("MateriaMenu.Container.Details.EnemySkill"):set_visible(false)
+        ui_manager:get_widget("MateriaMenu.Container.Details.Materia"):set_visible(false)
+        ui_manager:get_widget("MateriaMenu.Container.Help.Help"):set_text("")
     end,
 
     --- Show details of an enemy skill materia.
     --
-    -- Must be called when the cursor goes into an empty slot
-    populate_e_skill_details = function(self)
+    -- Must be called when the cursor goes over an enemy skill materia
+    --
+    -- @param materia Selected Enemy Skill materia.
+    populate_e_skill_details = function(self, materia)
+        if materia.id ~= Materia.ENEMY_SKILL_ID then
+            print("ERROR: Tried to get enemy skill info form non-enemy skill materia with ID " .. tostring(materia.id))
+            return 0
+        elseif materia.skills == nil then
+            print("ERROR: Enemy skill materia has no skill information")
+            --return 0
+        end
+        ui_manager:get_widget("MateriaMenu.Container.Help.Help"):set_text(Game.Materia[materia.id].description)
+        ui_manager:get_widget("MateriaMenu.Container.Details.EnemySkill.Name"):set_text(Game.Materia[materia.id].name)
+        ui_manager:get_widget("MateriaMenu.Container.Details.Empty"):set_visible(false)
+        ui_manager:get_widget("MateriaMenu.Container.Details.Commands"):set_visible(false)
+        ui_manager:get_widget("MateriaMenu.Container.Details.EnemySkill"):set_visible(true)
+        ui_manager:get_widget("MateriaMenu.Container.Details.Materia"):set_visible(false)
+        for s = 1, 24 do
+            if materia.skills[s] ~= nil and materia.skills[s] == true then
+                ui_manager:get_widget("MateriaMenu.Container.Details.EnemySkill.Stars.Star" .. tostring(s)):set_image("images/icons/materia_star_3.png")
+                ui_manager:get_widget("MateriaMenu.Container.Details.EnemySkill.Skills.Skill" .. tostring(s)):set_text(Game.Attacks[71 + s].name)
+                ui_manager:get_widget("MateriaMenu.Container.Details.EnemySkill.Skills.Skill" .. tostring(s)):set_visible(true)
+            else
+                ui_manager:get_widget("MateriaMenu.Container.Details.EnemySkill.Stars.Star" .. tostring(s)):set_image("images/icons/materia_star_empty_3.png")
+                ui_manager:get_widget("MateriaMenu.Container.Details.EnemySkill.Skills.Skill" .. tostring(s)):set_visible(false)
+            end
+        end
+
+
+    end,
+
+    --- Displays the command list in the details window
+    populate_commands = function(self)
+        ui_manager:get_widget("MateriaMenu.Container.Details.Empty"):set_visible(false)
+        ui_manager:get_widget("MateriaMenu.Container.Details.Commands"):set_visible(true)
+        ui_manager:get_widget("MateriaMenu.Container.Details.EnemySkill"):set_visible(false)
+        ui_manager:get_widget("MateriaMenu.Container.Details.Materia"):set_visible(false)
+        ui_manager:get_widget("MateriaMenu.Container.Help.Help"):set_text("")
         -- TODO
     end,
 
@@ -212,9 +328,16 @@ UiContainer.MateriaMenu = {
         local level
         local max_level
         local master = false
-        local materia
+        local materia -- Materia static information, as in Game.Materia[]
+        local materia_selected -- Materia instance information, as in Materia[]
         if from_list == false then
+            -- Get data from equiped materia
             if self.header_position_row == 1 then
+                -- "Check" highlighted
+                if self.header_position == 1 then
+                    self.populate_commands(self)
+                    return 0
+                end
                 -- Weapon materia
                 local weapon_id = Characters[self.char_id].weapon.id
                 local weapon = Game.Items[weapon_id]
@@ -223,10 +346,16 @@ UiContainer.MateriaMenu = {
                     self.hide_details(self)
                     return 0
                 end
+                materia_selected = Characters[self.char_id].weapon.materia[self.header_position - 2]
                 id = Characters[self.char_id].weapon.materia[self.header_position - 2].id
                 ap = Characters[self.char_id].weapon.materia[self.header_position - 2].ap
                 materia = Game.Materia[id]
             elseif self.header_position_row == 2 then
+                -- "Order" highlighted
+                if self.header_position == 1 then
+                    self.hide_details(self)
+                    return 0
+                end
                 -- Armor materia
                 local armor_id = Characters[self.char_id].armor.id
                 local armor = Game.Items[armor_id]
@@ -235,6 +364,7 @@ UiContainer.MateriaMenu = {
                     self.hide_details(self)
                     return 0
                 end
+                materia_selected = Characters[self.char_id].armor.materia[self.header_position - 2]
                 id = Characters[self.char_id].armor.materia[self.header_position - 2].id
                 ap = Characters[self.char_id].armor.materia[self.header_position - 2].ap
                 materia = Game.Materia[id]
@@ -244,9 +374,36 @@ UiContainer.MateriaMenu = {
                 return 0
             end
         else
-            -- TODO: Get materia data from list
+            --Get materia data from list.
+            local mat_position = self.list_first_visible + self.list_position - 1
+            if Materia[mat_position] == nil then
+                self.hide_details(self)
+                return 0
+            end
+            materia_selected = Materia[mat_position]
+            id = Materia[mat_position].id
+            ap = Materia[mat_position].ap
+            materia = Game.Materia[Materia[mat_position].id]
+        end
+
+        -- Materia should be set, but just in case, one last check.
+        if materia == nil then
+            self.hide_details(self)
+            return
+        end
+
+        -- Check for enemy skill. It is handled by another function.
+        if id == Materia.ENEMY_SKILL_ID then
+            self.populate_e_skill_details(self, materia_selected)
+            return 0
         end
 
+        ui_manager:get_widget("MateriaMenu.Container.Details.Empty"):set_visible(false)
+        ui_manager:get_widget("MateriaMenu.Container.Details.Commands"):set_visible(false)
+        ui_manager:get_widget("MateriaMenu.Container.Details.EnemySkill"):set_visible(false)
+        ui_manager:get_widget("MateriaMenu.Container.Details.Materia"):set_visible(true)
+        ui_manager:get_widget("MateriaMenu.Container.Help.Help"):set_text(Game.Materia[id].description)
+
         -- Get level and max level, and check for master.
         level = 0
         for lv, lv_ap in ipairs(materia.levels_ap) do
@@ -471,6 +628,87 @@ UiContainer.MateriaMenu = {
         end
     end,
 
+    --- Populates the materia list.
+    populate_list = function(self)
+        local pos = 1
+        for i = self.list_first_visible, self.list_first_visible + self.list_position_total - 1 do
+            if Materia[i] ~= nil then
+                local type = Game.Materia[Materia[i].id].type
+                local name = Game.Materia[Materia[i].id].name
+                ui_manager:get_widget("MateriaMenu.Container.List.Icon" .. tostring(pos)):set_image("images/icons/materia_" .. tostring(type) .. ".png")
+                ui_manager:get_widget("MateriaMenu.Container.List.Icon" .. tostring(pos)):set_visible(true)
+                ui_manager:get_widget("MateriaMenu.Container.List.Name" .. tostring(pos)):set_text(name)
+                ui_manager:get_widget("MateriaMenu.Container.List.Name" .. tostring(pos)):set_visible(true)
+            else
+                ui_manager:get_widget("MateriaMenu.Container.List.Icon" .. tostring(pos)):set_visible(false)
+                ui_manager:get_widget("MateriaMenu.Container.List.Name" .. tostring(pos)):set_visible(false)
+            end
+            pos = pos + 1
+        end
+         -- Set scroll bar position
+        local scroll = ui_manager:get_widget("MateriaMenu.Container.List.Scroll")
+        -- first_item_in_window == 1 then Top -> 0%3
+        -- first_item_in_window == inventory_size - first_item_in_window then top = 100%-21
+        local percent = math.floor((self.list_first_visible - 1) * 100 / (self.list_size - self.list_position_total))
+        local fixed = 3 + (-1 * math.floor(self.list_first_visible - 1) * 25 / (self.list_size - self.list_position_total))
+        scroll:set_y(percent, fixed)
+    end,
+
+    --- Equips the materia selected in the list at the currently selected slot.
+    --
+    -- It can also be used to swap or remove materia.
+    equip_materia = function(self)
+        local equip
+        local equip_slot = self.header_position - 2 -- 0-8
+        local list_pos = self.list_first_visible + self.list_position - 1
+        local materia_equip = {id = nil, ap = 0}
+        local materia_list = {id = nil, ap = 0}
+        --- Get info about the seleceted materias (equipped and in the list)
+        if self.header_position_row == 1 then
+            equip = Inventory.ITEM_TYPE.WEAPON
+            if equip_slot + 1 > #(Game.Items[Characters[self.char_id].weapon.id].slots) then
+                print("BEEP No slot " .. tostring(equip_slot) .. " in current weapon.")
+                return 0
+            end
+            if Characters[self.char_id].weapon.materia[equip_slot] ~= nil then
+                materia_equip = Characters[self.char_id].weapon.materia[equip_slot]
+            end
+        elseif self.header_position_row == 2 then
+            equip = Inventory.ITEM_TYPE.ARMOR
+            if equip_slot + 1 > #(Game.Items[Characters[self.char_id].armor.id].slots) then
+                print("BEEP No slot " .. tostring(equip_slot) .. " in current armor.")
+                return 0
+            end
+            if Characters[self.char_id].armor.materia[equip_slot] ~= nil then
+                materia_equip = Characters[self.char_id].armor.materia[equip_slot]
+            end
+        end
+        if Materia[list_pos] ~= nil then
+            materia_list = Materia[list_pos]
+        end
+        --- Equipped materia to the list (can be null)
+        Materia[list_pos] = materia_equip
+        if Materia[list_pos].id == nil then
+            Materia[list_pos] = nil
+        end
+        --- Materia from list to weapon or armor
+        if equip == Inventory.ITEM_TYPE.WEAPON then
+            Characters[self.char_id].weapon.materia[equip_slot] = materia_list
+            if Characters[self.char_id].weapon.materia[equip_slot].id == nil then
+                Characters[self.char_id].weapon.materia[equip_slot] = nil
+            end
+        elseif equip == Inventory.ITEM_TYPE.ARMOR then
+            Characters[self.char_id].armor.materia[equip_slot] = materia_list
+            if Characters[self.char_id].armor.materia[equip_slot].id == nil then
+                Characters[self.char_id].armor.materia[equip_slot] = nil
+            end
+        end
+        -- Reload character data and list.
+        self.populate_equip(self)
+        self.populate_list(self)
+        self.submenu_none(self)
+    end,
+
     --- Hides the item menu and goes back to the main menu.
     hide = function(self)
         ui_manager:get_widget("MateriaMenu"):set_visible(false)

+ 16 - 1
data/data/scripts/menu/name_menu.lua

@@ -52,18 +52,21 @@ UiContainer.NameMenu = {
         if UiContainer.current_menu == "name" then
             if UiContainer.current_submenu == "confirm" then
                 if button == "Right" then
+                    audio_manager:play_sound("Cursor")
                     self.confirm_position = self.confirm_position + 1
                     if self.confirm_position > self.confirm_position_total then
                         self.confirm_position = 1
                     end
                     ui_manager:get_widget("NameMenu.Container.Confirm.Cursor"):set_default_animation("Position" .. self.confirm_position)
                 elseif button == "Left" then
+                    audio_manager:play_sound("Cursor")
                     self.confirm_position = self.confirm_position - 1
                     if self.confirm_position < 1 then
                         self.confirm_position = self.confirm_position_total
                     end
                     ui_manager:get_widget("NameMenu.Container.Confirm.Cursor"):set_default_animation("Position" .. self.confirm_position)
                 elseif button == "Enter" then
+                    audio_manager:play_sound("Cursor")
                     if self.confirm_position == 2 then -- Back
                         UiContainer.current_submenu = ""
                         ui_manager:get_widget("NameMenu.Container.Confirm"):set_visible(false)
@@ -83,12 +86,14 @@ UiContainer.NameMenu = {
 
                 -- Handle buttons.
                 if button == "Down" then
+                    audio_manager:play_sound("Cursor")
                     self.options_position = self.options_position + 1
                     if self.options_position > self.options_position_total then
                         self.options_position = 1
                     end
                     ui_manager:get_widget("NameMenu.Container.Options.Cursor"):set_default_animation("Position" .. self.options_position)
                 elseif button == "Up" then
+                    audio_manager:play_sound("Cursor")
                     self.options_position = self.options_position - 1
                     if self.options_position < 1 then
                         self.options_position = self.options_position_total
@@ -98,19 +103,27 @@ UiContainer.NameMenu = {
                     if self.text_cursor > 1 then
                         self.text_cursor = self.text_cursor - 1
                         self.draw_cursor(self)
+                        audio_manager:play_sound("Cursor")
+                    else
+                        audio_manager:play_sound("Error")
                     end
                 elseif button == "Right" then
                     if self.text_cursor < self.max_characters then
+                        audio_manager:play_sound("Cursor")
                         self.text_cursor = self.text_cursor + 1
                         self.draw_cursor(self)
+                    else
+                        audio_manager:play_sound("Error")
                     end
                 elseif button == "Enter" then
                     if self.options_position == 1 then -- Clear name.
+                        audio_manager:play_sound("Cursor")
                         self.name = ""
                         self.text_cursor = 1
                         self.populate_name(self)
                         self.draw_cursor(self)
                     elseif self.options_position == 2 then -- Default name.
+                        audio_manager:play_sound("Cursor")
                         self.name = self.default_name
                         self.text_cursor = math.min(#(self.name) + 1, self.max_characters)
                         self.populate_name(self)
@@ -120,6 +133,7 @@ UiContainer.NameMenu = {
                         self.populate_name(self)
                         self.draw_cursor(self)
                         if #(self.name) > 0 then
+                            audio_manager:play_sound("Cursor")
                             UiContainer.current_submenu = "confirm"
                             local name_display = self.name -- Pad with spaces, to display only.
                             while #(name_display) < self.max_characters do
@@ -130,12 +144,13 @@ UiContainer.NameMenu = {
                             self.confirm_position = 1
                             ui_manager:get_widget("NameMenu.Container.Options.Cursor"):set_default_animation("Position" .. self.options_position)
                         else
-                            print("BEEP no name")
+                            audio_manager:play_sound("Error")
                         end
                     end
                 -- TODO: Implement backspace, but how?
                 -- Letters and spaces
                 elseif character_index ~= nil then
+                    audio_manager:play_sound("Cursor")
                     self.set_char(self, character_index)
                     self.text_cursor = math.min(self.text_cursor + 1, self.max_characters)
                     self.populate_name(self)

+ 5 - 0
data/data/scripts/menu/pause_menu.lua

@@ -19,6 +19,7 @@ UiContainer.PauseMenu = {
             local menu_cursor = ui_manager:get_widget( "PauseMenu.Menu.Cursor" )
 
             if button == "Enter" and event == "Press" then
+                audio_manager:play_sound("Cursor")
                 script:request_end_sync( Script.UI, "PauseMenu", "hide", 0 )
                 if self.position == 1 then
                     script:request_end_sync( Script.UI, "BeginMenu", "show", 0 )
@@ -28,15 +29,18 @@ UiContainer.PauseMenu = {
                     FFVII.MenuSettings.available = true
                 end
             elseif button == "Escape" and event == "Press" then
+                audio_manager:play_sound("Back")
                 script:request_end_sync( Script.UI, "PauseMenu", "hide", 0 )
                 FFVII.MenuSettings.available = true
             elseif button == "Right" then
+                audio_manager:play_sound("Cursor")
                 self.position = self.position + 1
                 if self.position > self.position_total then
                     self.position = 1;
                 end
                 menu_cursor:set_default_animation( "Position" .. self.position )
             elseif button == "Left" then
+                audio_manager:play_sound("Cursor")
                 self.position = self.position - 1
                 if self.position <= 0 then
                     self.position = self.position_total;
@@ -45,6 +49,7 @@ UiContainer.PauseMenu = {
             end
         elseif FFVII.MenuSettings.pause_available == true then
             if button == "Enter" and event == "Press" then
+                audio_manager:play_sound("Cursor")
                 FFVII.MenuSettings.available = false
                 script:request_end_sync( Script.UI, "PauseMenu", "show", 0 )
             end

+ 23 - 2
data/data/scripts/system.lua

@@ -1,4 +1,5 @@
 MAX_INVENTORY_SLOTS = 320
+MAX_MATERIA_SLOTS = 200
 
 --- Export character names to the text manager.
 --
@@ -642,8 +643,28 @@ end
 
 
 Materia.add = function(id, ap)
-    ap = ap or 0
-    -- TODO Implement.
+    local ap = ap or 0
+    if Game.Materia[id] == nil then
+        print("Tried to add invalid materia \"" .. item .. "\".");
+        return
+    end
+    -- TODO Cap AP?
+    for i = 1, MAX_MATERIA_SLOTS do
+        if Materia[i] == nil then
+            Materia[i] = {id = id, ap = ap}
+            if Materia[i].id == Materia.ENEMY_SKILL_ID then
+                Materia[i].ap = 0
+                Materia[i].skills = {}
+                for s = 1, 24 do
+                    Materia[i].skills[s] = false
+                end
+            end
+            return
+        end
+    end
+    -- If reached this point, it means that the materia wasn't added because all slots are full.
+    -- TODO: Do something.
+    return
 end
 
 --- Splits the party in teams.

+ 4 - 0
data/data/sounds.xml

@@ -0,0 +1,4 @@
+<!-- This file is to be overwritten by the installer -->
+<sounds>
+    <sound file_name="audio/sound/example.ogg" name="Example" />
+</sounds>

+ 1 - 0
data/data/system/README.txt

@@ -0,0 +1 @@
+This directory contains a few files required to make the engine work.

+ 2 - 0
data/data/texts/README.txt

@@ -0,0 +1,2 @@
+This folder contains the game static texts. As of the time of this writting, only the english
+language is actively maintained.

+ 6 - 0
doc/INSTALL_DATA.md

@@ -17,6 +17,12 @@ FFVIIPC/
 └── wm
 ```
 
+## Install FFmpeg
+
+The installer uses [FFmpeg](https://ffmpeg.org "FFmpeg") to convert audio files. To do this, it uses the ffmpeg executable in the system, and the sounds files will not be properly installed if there is such executable. In most GNU/Linux distros, ffmpeg is available as a package, so you can install it with your package manager. For the rest of cases, see [the downloads page](https://ffmpeg.org/download.html "FFmpeg download page").
+
+It's important that the ffmpeg executable is located in a globally accessed location. To verify this, try to run this command from a console: `ffmpeg -version`. If ffmpeg version is show, you are all set. If not, you must include the path to the ffmpeg executable to your system PATH environment variable.
+
 ## Launch the Installer
 
 If you have followed the [build](BUILD.md "Building guide") and [installaton](INSTALL.md "Installation guide") guides, you'll have the game data copied to `~/.v-gears/`, and the binaries compilled somewhere (we'll assum they are in `~/V-Gears/build/bin`, change the directories according to your system).