Ver Fonte

Walkability by terrain. Better vehicle functionality. Script extraction in the installer.

Iñigo Valentin há 1 semana atrás
pai
commit
5dd48e07e3
36 ficheiros alterados com 1323 adições e 195 exclusões
  1. 2 0
      data/data/scripts/_scripts.xml
  2. 48 0
      data/data/scripts/field.lua
  3. 85 0
      data/data/scripts/worldmap/triangle_triggers.lua
  4. 299 41
      data/data/scripts/worldmap/vehicles.lua
  5. 3 0
      src/CMakeLists.txt
  6. 25 1
      src/core/DialogsManager.cpp
  7. 8 0
      src/core/DialogsManager.h
  8. 29 3
      src/core/Entity.cpp
  9. 19 0
      src/core/EntityManager.cpp
  10. 4 0
      src/core/EntityManager.h
  11. 11 1
      src/core/ScriptManagerBinds.h
  12. 1 1
      src/data/worldmap/WorldmapFileSerializer.cpp
  13. 3 4
      src/data/worldmap/WorldmapFileSerializer.h
  14. 1 0
      src/installer/CMakeLists.txt
  15. 10 1
      src/installer/DataInstaller.cpp
  16. 35 15
      src/installer/FieldDataInstaller.cpp
  17. 7 1
      src/installer/FieldDataInstaller.h
  18. 1 1
      src/installer/MainWindow.cpp
  19. 40 40
      src/installer/MainWindow.ui
  20. 172 0
      src/installer/WorldInstaller.cpp
  21. 8 0
      src/installer/WorldInstaller.h
  22. 5 0
      src/installer/decompiler/Disassembler.cpp
  23. 2 0
      src/installer/decompiler/Disassembler.h
  24. 18 5
      src/installer/decompiler/field/FieldCodeGenerator.cpp
  25. 91 29
      src/installer/decompiler/field/instruction/FieldModelInstruction.cpp
  26. 36 13
      src/installer/decompiler/field/instruction/FieldWindowInstruction.cpp
  27. 7 2
      src/installer/decompiler/world/WorldCodeGenerator.cpp
  28. 3 0
      src/installer/decompiler/world/WorldCodeGenerator.h
  29. 23 9
      src/installer/decompiler/world/WorldDisassembler.cpp
  30. 3 1
      src/installer/decompiler/world/WorldDisassembler.h
  31. 4 4
      src/installer/decompiler/world/instruction/WorldKernelCallInstruction.cpp
  32. 253 10
      src/modules/worldmap/WorldmapModule.cpp
  33. 20 1
      src/modules/worldmap/WorldmapModule.h
  34. 1 1
      test/data/worldmap/MapFileSerializer.cpp
  35. 11 5
      test/installer/decompiler/field/FieldCodeGenerator.cpp
  36. 35 6
      test/installer/decompiler/world/WorldDisassembler.cpp

+ 2 - 0
data/data/scripts/_scripts.xml

@@ -21,6 +21,7 @@
     <script file="gamedata/initial_savemap.lua" />
 
     <script file="scripts/field.lua" />
+    <script file="scripts/worldmap/triangle_triggers.lua" />
     <script file="scripts/worldmap/vehicles.lua" />
     <script file="scripts/battle_ai.lua" />
     <script file="scripts/menu/menu_helpers.lua" />
@@ -29,4 +30,5 @@
     <script file="scripts/debug_data.lua" />
 
     <system_script name="MapChanger" />
+    <system_script name="WorldMap" />
 </scripts>

+ 48 - 0
data/data/scripts/field.lua

@@ -14,6 +14,44 @@ load_field_map_request = function(map_name, point_name)
     end
 end
 
+dispatch_menu = function(menu_id, parameter)
+    menu_id = tonumber(menu_id)
+    parameter = tonumber(parameter)
+    if not menu_id or not parameter then
+        return
+    end
+
+    if menu_id == 5 then
+        ending_credits()
+    elseif menu_id == 6 then
+        if parameter == 64 then
+            name_chocobo()
+        else
+            name_character(parameter)
+        end
+    elseif menu_id == 7 then
+        if parameter == 1 then
+            Party.split_teams(3)
+        elseif parameter == 2 then
+            Party.split_teams(2)
+        else
+            Party.choose_party()
+        end
+    elseif menu_id == 8 then
+        open_shop(parameter)
+    elseif menu_id == 9 then
+        open_menu("party")
+    elseif menu_id == 14 then
+        open_menu("save")
+    elseif menu_id == 15 then
+        Party.steal_materia()
+    elseif menu_id == 18 then
+        Characters.remove_materia(parameter)
+    elseif menu_id == 19 then
+        Characters.restore_materia(parameter)
+    end
+end
+
 --- Assigns an entity to a playable character.
 --
 -- @param entity_name The entity name.
@@ -34,6 +72,14 @@ set_entity_to_character = function(entity_name, character_id)
         end
 
         entity_manager:set_player_entity(entity_name)
+        local world_map_entry = System.MapChanger.world_map_entry
+        if world_map_entry then
+            entity_manager:place_player_at_field_entry(
+                world_map_entry.x, world_map_entry.y,
+                world_map_entry.triangle, world_map_entry.direction
+            )
+            System.MapChanger.world_map_entry = nil
+        end
     end
     entity_manager:set_entity_to_character(entity_name, character_id)
 end
@@ -272,6 +318,8 @@ System["MapChanger"] = {
     --- Point of the map to change to.
     point_name = "",
 
+    world_map_entry = nil,
+
     --- ID of the current field.
     current_map_name = "",
 

+ 85 - 0
data/data/scripts/worldmap/triangle_triggers.lua

@@ -0,0 +1,85 @@
+System.WorldMap = {
+    triangle_triggers = {},
+    triangle_functions = {},
+    field_table = {},
+
+    --[[
+    Registers a callback for a WM0 mesh function.
+    @param mesh_x World-map mesh X coordinate (0-35).
+    @param mesh_y World-map mesh Y coordinate (0-27).
+    @param function_id Triangle mesh function ID (0-4).
+    @param callback Function called when the player enters the trigger.
+    @return True when registered, false for invalid input.
+    ]]
+    register_triangle_trigger = function(self, mesh_x, mesh_y, function_id, callback)
+        if type(mesh_x) ~= "number" or mesh_x < 0 or mesh_x > 35
+          or type(mesh_y) ~= "number" or mesh_y < 0 or mesh_y > 27
+          or type(function_id) ~= "number" or function_id < 0 or function_id > 4
+          or type(callback) ~= "function" then
+            return false
+        end
+
+        local mesh_triggers = self.triangle_triggers[mesh_y]
+        if not mesh_triggers then
+            mesh_triggers = {}
+            self.triangle_triggers[mesh_y] = mesh_triggers
+        end
+        local function_triggers = mesh_triggers[mesh_x]
+        if not function_triggers then
+            function_triggers = {}
+            mesh_triggers[mesh_x] = function_triggers
+        end
+        function_triggers[function_id] = callback
+        return true
+    end,
+
+    --[[
+    Dispatches an entered WM0 mesh function to its registered callback.
+    @param self World-map system script table.
+    @param mesh_index Linear mesh index (mesh_y * 36 + mesh_x).
+    @param function_id Triangle mesh function ID.
+    ]]
+    on_triangle_enter = function(self, mesh_index, function_id)
+        mesh_index = tonumber(mesh_index)
+        function_id = tonumber(function_id)
+        if not mesh_index or not function_id then
+            return
+        end
+
+        local mesh_x = mesh_index % 36
+        local mesh_y = math.floor(mesh_index / 36)
+        local mesh_triggers = self.triangle_triggers[mesh_y]
+        local function_triggers = mesh_triggers and mesh_triggers[mesh_x]
+        local callback = function_triggers and function_triggers[function_id]
+        if callback then
+            callback(mesh_x, mesh_y, function_id)
+            return
+        end
+
+        local mesh_functions = self.triangle_functions[mesh_index]
+        local function_index = mesh_functions and mesh_functions[function_id]
+        local world_function = function_index
+          and self["world_function_" .. tostring(function_index)]
+        if world_function then
+            world_function(self)
+        else
+            print(
+                "No world-map function for mesh " .. mesh_index
+                .. ", function " .. function_id
+            )
+        end
+    end,
+}
+
+EnterFieldScene = function(field_table_id, scenario)
+    local field_table = System.WorldMap.field_table[tonumber(field_table_id)]
+    local entry = field_table and field_table[tonumber(scenario)]
+    if not entry then
+        print("No installed field entry for FIELD.TBL record " .. tostring(field_table_id))
+        return 0
+    end
+
+    System.MapChanger.world_map_entry = entry
+    load_field_map_request(entry.field, "")
+    return 0
+end

+ 299 - 41
data/data/scripts/worldmap/vehicles.lua

@@ -1,61 +1,319 @@
 EntityContainer = EntityContainer or {}
 
+local function show_vehicle_error(message)
+    print(message)
+    if not dialog:show_message(message) then
+        return false
+    end
+    return true
+end
+
+local Vehicle = {}
+Vehicle.__index = Vehicle
+
+Vehicle.terrains = {}
+Vehicle.board = function(self)
+    show_vehicle_error("Vehicle.board must be implemented by the vehicle.")
+    return false
+end
+Vehicle.unboard = function(self)
+    show_vehicle_error("Vehicle.unboard must be implemented by the vehicle.")
+    return false
+end
+Vehicle.on_interact = function(self)
+    self:board()
+end
+
+--[[
+Creates a vehicle that inherits the Vehicle interface.
+@param entity_name Managed world-map entity name.
+@param terrains Walkmap terrain type names this vehicle can travel.
+@param rise_amount Height offset applied when boarding.
+@return The vehicle script table.
+]]
+local function extend_vehicle(entity_name, terrains, rise_amount, unboarding_terrains)
+    return setmetatable({
+        entity_name = entity_name,
+        terrains = terrains,
+        unboarding_terrains = unboarding_terrains or terrains,
+        rise_amount = rise_amount,
+    }, Vehicle)
+end
+
+local function is_unboarding_terrain_allowed(vehicle)
+    local terrain_name = world_map_module:get_terrain_type_under_entity(vehicle.entity_name)
+    for _, allowed_terrain in ipairs(vehicle.unboarding_terrains) do
+        if terrain_name == allowed_terrain then
+            return true
+        end
+    end
+    return false, terrain_name
+end
+
 --[[
-Boards a world-map vehicle and hides the previous player entity.
-@param entity_name Name of the vehicle entity.
-@param rise_amount Height to raise the vehicle while boarding.
+Copies a terrain list so a Chocobo color change does not mutate a shared profile.
+@param terrains Terrain type names to copy.
+@return A new terrain list.
 ]]
-local function board_vehicle(entity_name, rise_amount)
+local function copy_terrain_list(terrains)
+    local copy = {}
+    for _, terrain_name in ipairs(terrains) do
+        table.insert(copy, terrain_name)
+    end
+    return copy
+end
+
+local yellow_chocobo_terrains = {
+    "GRASS", "FOREST", "SWAMP", "DESERT", "WASTELAND", "SNOW", "RIVERSIDE",
+    "COREL_BRIDGE", "WUTAI_BRIDGE", "HILL_SIDE", "BEACH", "CANYON", "MOUNTAIN_PASS",
+    "GOLD_SAUCER_DESERT_BORDER", "BRIDGE_HEAD",
+}
+local green_chocobo_terrains = copy_terrain_list(yellow_chocobo_terrains)
+table.insert(green_chocobo_terrains, "MOUNTAIN")
+table.insert(green_chocobo_terrains, "CLIFF")
+local blue_chocobo_terrains = copy_terrain_list(yellow_chocobo_terrains)
+table.insert(blue_chocobo_terrains, "RIVER_CROSSING")
+table.insert(blue_chocobo_terrains, "RIVER")
+table.insert(blue_chocobo_terrains, "WATER")
+local black_chocobo_terrains = copy_terrain_list(green_chocobo_terrains)
+table.insert(black_chocobo_terrains, "RIVER_CROSSING")
+table.insert(black_chocobo_terrains, "RIVER")
+table.insert(black_chocobo_terrains, "WATER")
+local gold_chocobo_terrains = {
+    "GRASS", "FOREST", "MOUNTAIN", "SEA_DEEP_WATER", "RIVER_CROSSING", "RIVER", "WATER",
+    "SWAMP", "DESERT", "WASTELAND", "SNOW", "RIVERSIDE", "CLIFF", "COREL_BRIDGE",
+    "WUTAI_BRIDGE", "UNUSED_1", "HILL_SIDE", "BEACH", "SUBMARINE_PEN", "CANYON",
+    "MOUNTAIN_PASS", "UNKNOWN_1", "WATERFALL", "UNUSED_2", "GOLD_SAUCER_DESERT", "JUNGLE",
+    "SEA", "NORTHERND_CAVE", "GOLD_SAUCER_DESERT_BORDER", "BRIDGE_HEAD", "BACK_ENTRANCE",
+    "UNUSED_3",
+}
+local chocobo_terrains_by_color = {
+    YELLOW = yellow_chocobo_terrains,
+    GREEN = green_chocobo_terrains,
+    BLUE = blue_chocobo_terrains,
+    BLACK = black_chocobo_terrains,
+    GOLD = gold_chocobo_terrains,
+}
+
+EntityContainer["WorldMapHighwind"] = extend_vehicle(
+    "WorldMapHighwind",
+    {
+        "GRASS", "FOREST", "MOUNTAIN", "SEA_DEEP_WATER", "RIVER_CROSSING", "RIVER",
+        "WATER", "SWAMP", "DESERT", "WASTELAND", "SNOW", "RIVERSIDE", "CLIFF",
+        "COREL_BRIDGE", "WUTAI_BRIDGE", "HILL_SIDE", "BEACH", "SUBMARINE_PEN", "CANYON",
+        "MOUNTAIN_PASS", "UNKNOWN_1", "WATERFALL", "GOLD_SAUCER_DESERT", "JUNGLE", "SEA",
+        "NORTHERND_CAVE", "GOLD_SAUCER_DESERT_BORDER", "BRIDGE_HEAD", "BACK_ENTRANCE",
+    },
+    0.75,
+    {"GRASS", "NORTHERND_CAVE"}
+)
+EntityContainer["WorldMapHighwind"].board = function(self)
     local player_entity = entity_manager:get_player_entity()
-    local vehicle_entity = entity_manager:get_entity(entity_name)
+    local vehicle_entity = entity_manager:get_entity(self.entity_name)
     if not player_entity or not vehicle_entity then
-        return
+        return false
     end
-
     local player_name = player_entity:get_name()
-    local boarding_from_foot = player_name == "WorldMapCloud"
-    local chocobo_to_highwind
-        = player_name == "WorldMapChocobo" and entity_name == "WorldMapHighwind"
-    if not boarding_from_foot and not chocobo_to_highwind then
-        print("Leave your current vehicle before boarding another.")
-        return
+    if player_name ~= "WorldMapCloud" and player_name ~= "WorldMapChocobo" then
+        show_vehicle_error("Leave your current vehicle before boarding another.")
+        return false
+    end
+    vehicle_entity:move_by(0, self.rise_amount, 0)
+    player_entity:set_visible(false)
+    player_entity:set_solid(false)
+    player_entity:set_talkable(false)
+    world_map_module:clear_vehicle_terrains()
+    for _, terrain_name in ipairs(self.terrains) do
+        world_map_module:add_vehicle_terrain(terrain_name)
+    end
+    entity_manager:set_player_entity(self.entity_name)
+    return true
+end
+EntityContainer["WorldMapHighwind"].unboard = function(self)
+    if not is_unboarding_terrain_allowed(self) then
+        show_vehicle_error("The Highwind cannot be unboarded on this terrain.")
+        return false
+    end
+    if not world_map_module:unboard_vehicle(self.entity_name) then
+        return false
     end
 
-    if rise_amount ~= 0 then
-        vehicle_entity:move_by(0, rise_amount, 0)
+    local vehicle_entity = entity_manager:get_entity(self.entity_name)
+    if vehicle_entity then
+        vehicle_entity:move_by(0, -self.rise_amount, 0)
+    end
+    return true
+end
+
+EntityContainer["WorldMapBuggy"] = extend_vehicle(
+    "WorldMapBuggy",
+    {
+        "GRASS", "FOREST", "SWAMP", "DESERT", "WASTELAND", "SNOW", "RIVERSIDE", "CLIFF",
+        "COREL_BRIDGE", "WUTAI_BRIDGE", "HILL_SIDE", "BEACH", "RIVER_CROSSING", "CANYON",
+        "MOUNTAIN_PASS", "GOLD_SAUCER_DESERT", "JUNGLE", "GOLD_SAUCER_DESERT_BORDER",
+        "BRIDGE_HEAD", "BACK_ENTRANCE",
+    },
+    0,
+    {
+        "GRASS", "FOREST", "WASTELAND", "SNOW", "SWAMP", "DESERT", "COREL_BRIDGE",
+        "HILL_SIDE", "BEACH", "CANYON", "MOUNTAIN_PASS", "JUNGLE",
+    }
+)
+EntityContainer["WorldMapBuggy"].board = function(self)
+    local player_entity = entity_manager:get_player_entity()
+    local vehicle_entity = entity_manager:get_entity(self.entity_name)
+    if not player_entity or not vehicle_entity then
+        return false
+    end
+    if player_entity:get_name() ~= "WorldMapCloud" then
+        show_vehicle_error("Leave your current vehicle before boarding another.")
+        return false
     end
     player_entity:set_visible(false)
     player_entity:set_solid(false)
     player_entity:set_talkable(false)
-    entity_manager:set_player_entity(entity_name)
+    world_map_module:clear_vehicle_terrains()
+    for _, terrain_name in ipairs(self.terrains) do
+        world_map_module:add_vehicle_terrain(terrain_name)
+    end
+    entity_manager:set_player_entity(self.entity_name)
+    return true
+end
+EntityContainer["WorldMapBuggy"].unboard = function(self)
+    if not is_unboarding_terrain_allowed(self) then
+        show_vehicle_error("The Buggy cannot be unboarded on this terrain.")
+        return false
+    end
+    return world_map_module:unboard_vehicle(self.entity_name)
 end
 
-EntityContainer["WorldMapHighwind"] = {
-    on_interact = function(self)
-        board_vehicle("WorldMapHighwind", 0.75)
-    end,
-}
+EntityContainer["WorldMapChocobo"] = extend_vehicle(
+    "WorldMapChocobo",
+    copy_terrain_list(chocobo_terrains_by_color.YELLOW),
+    0,
+    {
+        "GRASS", "FOREST", "WASTELAND", "SNOW", "SWAMP", "DESERT", "COREL_BRIDGE",
+        "HILL_SIDE", "BEACH", "CANYON", "MOUNTAIN_PASS", "JUNGLE",
+    }
+)
+EntityContainer["WorldMapChocobo"].color = "YELLOW"
+EntityContainer["WorldMapChocobo"].set_color = function(self, color)
+    if type(color) ~= "string" then
+        return false
+    end
 
-EntityContainer["WorldMapBuggy"] = {
-    on_interact = function(self)
-        board_vehicle("WorldMapBuggy", 0)
-    end,
-}
+    local color_name = string.upper(color)
+    local terrains = chocobo_terrains_by_color[color_name]
+    if not terrains then
+        show_vehicle_error("Unknown Chocobo color: " .. color_name)
+        return false
+    end
 
-EntityContainer["WorldMapChocobo"] = {
-    on_interact = function(self)
-        board_vehicle("WorldMapChocobo", 0)
-    end,
-}
+    self.color = color_name
+    self.terrains = copy_terrain_list(terrains)
+    local player_entity = entity_manager:get_player_entity()
+    if player_entity and player_entity:get_name() == self.entity_name then
+        world_map_module:clear_vehicle_terrains()
+        for _, terrain_name in ipairs(self.terrains) do
+            world_map_module:add_vehicle_terrain(terrain_name)
+        end
+    end
+    return true
+end
+EntityContainer["WorldMapChocobo"].board = function(self)
+    local player_entity = entity_manager:get_player_entity()
+    local vehicle_entity = entity_manager:get_entity(self.entity_name)
+    if not player_entity or not vehicle_entity then
+        return false
+    end
+    if player_entity:get_name() ~= "WorldMapCloud" then
+        show_vehicle_error("Leave your current vehicle before boarding another.")
+        return false
+    end
+    player_entity:set_visible(false)
+    player_entity:set_solid(false)
+    player_entity:set_talkable(false)
+    world_map_module:clear_vehicle_terrains()
+    for _, terrain_name in ipairs(self.terrains) do
+        world_map_module:add_vehicle_terrain(terrain_name)
+    end
+    entity_manager:set_player_entity(self.entity_name)
+    return true
+end
+EntityContainer["WorldMapChocobo"].unboard = function(self)
+    if not is_unboarding_terrain_allowed(self) then
+        show_vehicle_error("The Chocobo cannot be unboarded on this terrain.")
+        return false
+    end
+    return world_map_module:unboard_vehicle(self.entity_name)
+end
 
-EntityContainer["WorldMapSubmarine"] = {
-    on_interact = function(self)
-        board_vehicle("WorldMapSubmarine", 0)
-    end,
-}
+EntityContainer["WorldMapSubmarine"] = extend_vehicle(
+    "WorldMapSubmarine",
+    {"SEA_DEEP_WATER", "SEA", "SUBMARINE_PEN"},
+    0,
+    {"SUBMARINE_PEN"}
+)
+EntityContainer["WorldMapSubmarine"].board = function(self)
+    local player_entity = entity_manager:get_player_entity()
+    local vehicle_entity = entity_manager:get_entity(self.entity_name)
+    if not player_entity or not vehicle_entity then
+        return false
+    end
+    if player_entity:get_name() ~= "WorldMapCloud" then
+        show_vehicle_error("Leave your current vehicle before boarding another.")
+        return false
+    end
+    player_entity:set_visible(false)
+    player_entity:set_solid(false)
+    player_entity:set_talkable(false)
+    world_map_module:clear_vehicle_terrains()
+    for _, terrain_name in ipairs(self.terrains) do
+        world_map_module:add_vehicle_terrain(terrain_name)
+    end
+    entity_manager:set_player_entity(self.entity_name)
+    return true
+end
+EntityContainer["WorldMapSubmarine"].unboard = function(self)
+    if not is_unboarding_terrain_allowed(self) then
+        show_vehicle_error("The Submarine cannot be unboarded on this terrain.")
+        return false
+    end
+    return world_map_module:unboard_vehicle(self.entity_name)
+end
 
-EntityContainer["WorldMapTinyBronco"] = {
-    on_interact = function(self)
-        print("Tiny Bronco interaction is not implemented yet.")
-    end,
-}
+EntityContainer["WorldMapTinyBronco"] = extend_vehicle(
+    "WorldMapTinyBronco",
+    {
+        "RIVERSIDE", "CLIFF", "BEACH", "RIVER_CROSSING", "RIVER", "WATER",
+    },
+    0,
+    {"BEACH", "RIVERSIDE"}
+)
+EntityContainer["WorldMapTinyBronco"].board = function(self)
+    local player_entity = entity_manager:get_player_entity()
+    local vehicle_entity = entity_manager:get_entity(self.entity_name)
+    if not player_entity or not vehicle_entity then
+        return false
+    end
+    if player_entity:get_name() ~= "WorldMapCloud" then
+        show_vehicle_error("Leave your current vehicle before boarding another.")
+        return false
+    end
+    player_entity:set_visible(false)
+    player_entity:set_solid(false)
+    player_entity:set_talkable(false)
+    world_map_module:clear_vehicle_terrains()
+    for _, terrain_name in ipairs(self.terrains) do
+        world_map_module:add_vehicle_terrain(terrain_name)
+    end
+    entity_manager:set_player_entity(self.entity_name)
+    return true
+end
+EntityContainer["WorldMapTinyBronco"].unboard = function(self)
+    if not is_unboarding_terrain_allowed(self) then
+        show_vehicle_error("The Tiny Bronco cannot be unboarded on this terrain.")
+        return false
+    end
+    return world_map_module:unboard_vehicle(self.entity_name)
+end

+ 3 - 0
src/CMakeLists.txt

@@ -866,6 +866,9 @@ target_include_directories(libvgears
         ${VGEARS_LUAJIT_INCLUDE_DIRS}
         ${QT_INCLUDE_DIRS}
 )
+if(DEFINED _GENERATED_UI_TARGET)
+    target_include_directories(libvgears BEFORE PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/installer")
+endif()
     target_link_libraries(libvgears PUBLIC ${VGEARS_LUAJIT_LIBRARIES})
 target_compile_definitions(libvgears PUBLIC TIXML_USE_STL)
 if(NOT MSVC)

+ 25 - 1
src/core/DialogsManager.cpp

@@ -169,6 +169,30 @@ void DialogsManager::SetText(const char* d_name, const char* text){
     }
 }
 
+bool DialogsManager::ShowScriptMessage(const char* text){
+    if (text == nullptr || limit_area_ == nullptr) return false;
+
+    int message_id = -1;
+    for (size_t i = 0; i < messages_.size(); ++i){
+        if (messages_[i]->widget->GetName() == "Timer") continue;
+        if (messages_[i]->state == MS_CLOSED){
+            message_id = static_cast<int>(i);
+            break;
+        }
+    }
+    if (message_id < 0) return false;
+
+    MessageData* data = messages_[message_id];
+    data->clickable = true;
+    data->closeable = true;
+    data->show_cursor = false;
+    data->show_window = true;
+    data->auto_close = false;
+    ShowMessage(message_id, 16, 176, 288, 56);
+    data->text_area->SetText(text);
+    return true;
+}
+
 void DialogsManager::SetNumeric(const char* d_name, const bool numeric, const bool timer){
     int id = GetMessageId(d_name);
     if (id == -1){
@@ -490,4 +514,4 @@ void DialogsManager::UpdateField(){
 
 void DialogsManager::UpdateBattle(){}
 
-void DialogsManager::UpdateWorld(){}
+void DialogsManager::UpdateWorld(){UpdateField();}

+ 8 - 0
src/core/DialogsManager.h

@@ -310,6 +310,14 @@ class DialogsManager : public Manager, public Ogre::Singleton<DialogsManager>{
          */
         void SetText(const char* d_name, const char* text);
 
+        /**
+         * Shows literal script text in the first available dialog slot.
+         *
+         * @param[in] text Message text to display.
+         * @return True if a dialog slot was available and shown.
+         */
+        bool ShowScriptMessage(const char* text);
+
         /**
          * Sets or unsets a dialog window as numeric
          *

+ 29 - 3
src/core/Entity.cpp

@@ -20,6 +20,7 @@
 #include "core/Entity.h"
 #include "core/EntityManager.h"
 #include "core/Logger.h"
+#include "modules/worldmap/WorldmapModule.h"
 
 ConfigVar cv_debug_entity("debug_entity", "Draw entity debug info", "0");
 
@@ -141,10 +142,35 @@ void Entity::UpdateDebug(){
         if (debug > 1){
             Ogre::String move_state_string[] = {"NONE", "WALKMESH", "LINEAR", "JUMP"};
             DEBUG_DRAW.Text(entity_pos, 0, 24, Ogre::StringConverter::toString(entity_pos));
-            DEBUG_DRAW.Text(
-              entity_pos, 0, 36, "Triangle: " + Ogre::StringConverter::toString(move_triangle_id_)
-            );
+            if (EntityManager::getSingleton().IsWorldModule()){
+                const int triangle_index
+                  = VGears::WorldmapModule::getSingleton().GetTerrainTriangleUnderEntity(
+                    name_.c_str()
+                  );
+                DEBUG_DRAW.Text(
+                  entity_pos, 0, 36,
+                  "World map triangle: "
+                    + (triangle_index < 0 ? Ogre::String("UNKNOWN")
+                      : Ogre::StringConverter::toString(triangle_index))
+                );
+            }
+            else{
+                DEBUG_DRAW.Text(
+                  entity_pos, 0, 36,
+                  "Triangle: " + Ogre::StringConverter::toString(move_triangle_id_)
+                );
+            }
             DEBUG_DRAW.Text(entity_pos, 0, 48, "Move state: " + move_state_string[state_]);
+            if (EntityManager::getSingleton().IsWorldModule()){
+                const Ogre::String terrain_name
+                  = VGears::WorldmapModule::getSingleton().GetTerrainTypeUnderEntity(
+                    name_.c_str()
+                  );
+                DEBUG_DRAW.Text(
+                  entity_pos, 0, 60,
+                  "Terrain: " + (terrain_name.empty() ? Ogre::String("UNKNOWN") : terrain_name)
+                );
+            }
             switch(state_){
                 case Entity::WALKMESH:
                     DEBUG_DRAW.Line3d(entity_pos, move_position_);

+ 19 - 0
src/core/EntityManager.cpp

@@ -684,6 +684,25 @@ void EntityManager::ScriptSetPlayerEntity(const char* name){
     }
 }
 
+void EntityManager::ScriptPlacePlayerAtFieldEntry(
+    const float x, const float y, const int triangle_id, const float direction
+){
+        if (player_entity_ == nullptr || walkmesh_.GetNumberOfTriangles() <= 0) return;
+
+        int safe_triangle_id = triangle_id;
+        if (safe_triangle_id < 0 || safe_triangle_id >= walkmesh_.GetNumberOfTriangles()){
+                LOG_WARNING("World-map field entry has an invalid walkmesh triangle; using triangle 0.");
+                safe_triangle_id = 0;
+        }
+
+        player_entity_->SetPosition(Ogre::Vector3(
+            x, y, walkmesh_.GetA(static_cast<unsigned int>(safe_triangle_id)).z
+        ));
+        player_entity_->SetRotation(Ogre::Degree(direction));
+        player_entity_->SetMoveTriangleId(safe_triangle_id);
+        player_entity_->SetState(Entity::WALKMESH);
+}
+
 Entity* EntityManager::ScriptGetPlayerEntity() const{
     if (IsBattleModule()){
         LOG_WARNING("Tried to set a player entity but the EntityManager is in battle mode.");

+ 4 - 0
src/core/EntityManager.h

@@ -281,6 +281,10 @@ class EntityManager : public Manager, public Ogre::Singleton<EntityManager>{
          */
         void ScriptSetPlayerEntity(const char* name);
 
+        void ScriptPlacePlayerAtFieldEntry(
+          const float x, const float y, const int triangle_id, const float direction
+        );
+
         /**
          * Retrieves the playable entity.
          *

+ 11 - 1
src/core/ScriptManagerBinds.h

@@ -52,6 +52,8 @@ void ScriptPrint(const char* text){
  * @param[in] text The new map name.
  */
 void ScriptMap(const char* text){
+  VGears::WorldmapModule::getSingleton().Suspend();
+  EntityManager::getSingleton().SetFieldModule();
     EntityManager::getSingleton().Clear();
     XmlMapsFile xml("./data/fields/_fields.xml");
     Ogre::String file_name = xml.GetMapFileNameByName(text);
@@ -190,6 +192,7 @@ void ScriptManager::InitBinds(){
       "get_entity_from_character_id", &EntityManager::GetEntityFromCharacterId,
       "get_entity_point", &EntityManager::ScriptGetEntityPoint,
       "set_player_entity", &EntityManager::ScriptSetPlayerEntity,
+      "place_player_at_field_entry", &EntityManager::ScriptPlacePlayerAtFieldEntry,
       "get_player_entity", &EntityManager::ScriptGetPlayerEntity,
       "unset_player_entity", &EntityManager::ScriptUnsetPlayerEntity,
       "player_lock", &EntityManager::ScriptPlayerLock,
@@ -391,6 +394,7 @@ void ScriptManager::InitBinds(){
       "update_timer", &DialogsManager::UpdateTimer,
       "set_mode", &DialogsManager::SetMode,
       "dialog_set_text", &DialogsManager::SetText,
+      "show_message", &DialogsManager::ShowScriptMessage,
       "dialog_close", &DialogsManager::Hide,
       "set_variable", &DialogsManager::SetVariable,
       "set_clickable", &DialogsManager::SetClickable,
@@ -470,7 +474,13 @@ void ScriptManager::InitBinds(){
     // Commands to initiate modules
     lua.new_usertype<VGears::WorldmapModule>(
       "world_map_module",
-      "init", &VGears::WorldmapModule::Init
+      "init", &VGears::WorldmapModule::Init,
+      "suspend", &VGears::WorldmapModule::Suspend,
+      "clear_vehicle_terrains", &VGears::WorldmapModule::ClearVehicleTerrains,
+      "add_vehicle_terrain", &VGears::WorldmapModule::AddVehicleTerrain,
+      "get_terrain_type_under_entity", &VGears::WorldmapModule::GetTerrainTypeUnderEntity,
+      "get_terrain_triangle_under_entity", &VGears::WorldmapModule::GetTerrainTriangleUnderEntity,
+      "unboard_vehicle", &VGears::WorldmapModule::UnboardVehicle
     );
 
     // Register all command handlers

+ 1 - 1
src/data/worldmap/WorldmapFileSerializer.cpp

@@ -184,7 +184,7 @@ namespace VGears {
                     triangle.vertex_2_index = decompressed[data_offset++];
                     const uint8 walkmap_and_function = decompressed[data_offset++];
                     triangle.walkability_info_ = walkmap_and_function & 0x1F;
-                    triangle.unknown = (walkmap_and_function >> 5) & 0x07;
+                    triangle.function_id = (walkmap_and_function >> 5) & 0x07;
                     triangle.vertex_0_u = decompressed[data_offset++];
                     triangle.vertex_0_v = decompressed[data_offset++];
                     triangle.vertex_1_u = decompressed[data_offset++];

+ 3 - 4
src/data/worldmap/WorldmapFileSerializer.h

@@ -139,13 +139,12 @@ namespace VGears {
                 uint8 walkability_info_;
 
                 /**
-                 * Mesh function ID.
+                 * Packed mesh function value.
                  *
                  * Only 3 bits are valid.
-                 *
-                 * Values 0-2 mean no script is attached to this triangle.
+                 * Values 0-2 mean no script; values 3-7 map to function IDs 0-4.
                  */
-                uint8 unknown;
+                uint8 function_id;
 
                 /**
                  * First side vector U component.

+ 1 - 0
src/installer/CMakeLists.txt

@@ -108,6 +108,7 @@ include_directories (
 # Generate QT UIs.
 set(v-gears-installer_UIS MainWindow.ui)
 QT5_WRAP_UI(UIS ${v-gears-installer_UIS})
+include_directories(BEFORE ${CMAKE_CURRENT_BINARY_DIR})
 set(v-gears-installer_MOCS MainWindow.h)
 QT5_WRAP_CPP(MOCS ${v-gears-installer_MOCS})
 set(v-gears-installer_RESOURCES installer_resources.qrc)

+ 10 - 1
src/installer/DataInstaller.cpp

@@ -412,7 +412,16 @@ float DataInstaller::Progress(){
         case WM_MAPS:
             if (world_installer_->ProcessMap() == false) cur_substep_ ++;
             else{
-                // TODO: Next step: map scripts, etc
+                std::string script_error;
+                                if (world_installer_->GenerateScripts(
+                                        field_installer_->GetMapList(), field_installer_->GetFieldScaleFactors(),
+                                        field_installer_->GetConvertedMapList(), script_error
+                                    )){
+                    write_output_line_("Generated WM0 world-map script.", 2, true);
+                }
+                else{
+                    write_output_line_("Could not generate WM0 world-map script: " + script_error, 2, true);
+                }
                 installation_state_ = WM_MODELS;
                 cur_substep_ = 0;
             }

+ 35 - 15
src/installer/FieldDataInstaller.cpp

@@ -14,6 +14,7 @@
  */
 
 #include <string>
+#include <algorithm>
 #include <boost/filesystem.hpp>
 #include <QtCore/QDir>
 #include "DiskImage.h"
@@ -156,7 +157,7 @@ void FieldDataInstaller::CollectFieldScaleFactors(
       = FieldDecompiler::ScaleFactor(field->GetRawScript());
 }
 
-void FieldDataInstaller::CollectSpawnPoints(
+bool FieldDataInstaller::CollectSpawnPoints(
   VGears::FLevelFilePtr& field, const std::vector<std::string>& field_id_to_name_lookup,
   FieldSpawnPointsMap& spawn_points
 ){
@@ -176,9 +177,15 @@ void FieldDataInstaller::CollectSpawnPoints(
         );
     }
     catch (const ::DecompilerException& ex){
-        std::cerr << "CollectSpawnPoints: DecompilerException: " << ex.what() << std::endl;
+        std::cerr << "CollectSpawnPoints: decompiler failed for " << field->getName()
+          << ": " << ex.what() << std::endl;
+                return false;
     }
     const VGears::TriggersFilePtr& triggers = field->GetTriggers();
+    if (triggers.isNull()){
+        std::cerr << "CollectSpawnPoints: missing trigger data for " << field->getName() << std::endl;
+        return false;
+    }
     const auto& gateways = triggers->GetGateways();
     for (size_t i = 0; i < gateways.size(); i ++){
         const VGears::TriggersFile::Gateway& gateway = gateways[i];
@@ -206,6 +213,7 @@ void FieldDataInstaller::CollectSpawnPoints(
             }
         }
     }
+    return true;
 }
 
 FieldDataInstaller::FieldDataInstaller(const DiskImage& disk_image, const std::string output_dir):
@@ -235,13 +243,21 @@ void FieldDataInstaller::CollectSpawnAndScaleFactors(int field_index){
     }
     auto resource_name = (*flevel_file_list_)[field_index];
     // Exclude things that are not fields.
-    //if (IsAFieldFile(resource_name) && !WillCrash(resource_name)){
-    // TODO: DEBUG: Only testing fields.
-    if (IsAFieldFile(resource_name) && IsTestField(resource_name) && !WillCrash(resource_name)){
-        VGears::FLevelFilePtr field = VGears::LZSFLevelFileManager::GetSingleton().load(resource_name, "FFVIIFields")
-          .staticCast<VGears::FLevelFile>();
-        CollectSpawnPoints(field, map_list_, spawn_points_);
-        CollectFieldScaleFactors(field, scale_factors_, map_list_);
+    if (IsAFieldFile(resource_name) && !WillCrash(resource_name)){
+                try{
+                        VGears::FLevelFilePtr field = VGears::LZSFLevelFileManager::GetSingleton()
+                            .load(resource_name, "FFVIIFields").staticCast<VGears::FLevelFile>();
+                        if (!CollectSpawnPoints(field, map_list_, spawn_points_)){
+                                failed_fields_.push_back(resource_name);
+                                return;
+                        }
+                        CollectFieldScaleFactors(field, scale_factors_, map_list_);
+                }
+                catch (const std::exception& exception){
+                        failed_fields_.push_back(resource_name);
+                        std::cerr << "CollectSpawnAndScaleFactors: skipped " << resource_name
+                            << " after error: " << exception.what() << std::endl;
+                }
     }
 }
 
@@ -254,9 +270,12 @@ void FieldDataInstaller::Convert(int field_index){
     auto resource_name = (*flevel_file_list_)[field_index];
     // Exclude things that are not fields.
     if (IsAFieldFile(resource_name)){
-        //if (/*IsTestField(resource_name) &&*/ !WillCrash(resource_name)){
-        // TODO: DEBUG: Only test fields
-        if (IsTestField(resource_name) && !WillCrash(resource_name)){
+                if (std::find(failed_fields_.begin(), failed_fields_.end(), resource_name)
+                    != failed_fields_.end()){
+                        std::cerr << "[ERROR] Skip field: " << resource_name
+                            << " because its scripts or scale factor could not be collected." << std::endl;
+                }
+                else if (!WillCrash(resource_name)){
             //write_output_line_("Converting field " + resource_name);
             CreateDir(FIELD_MAPS_DIR + "/" + resource_name);
             VGears::FLevelFilePtr field = VGears::LZSFLevelFileManager::GetSingleton()
@@ -264,8 +283,8 @@ void FieldDataInstaller::Convert(int field_index){
             PcFieldToVGearsField(field);
         }
         else{
-            //write_output_line_("[ERROR] Skip field " + resource_name + " due to crash or hang issue.");
-            std::cerr << "[ERROR] Skip field: " << resource_name << " due to crash or hang issue." << std::endl;
+                        std::cerr << "[ERROR] Skip field: " << resource_name
+                            << " due to a known crash or hang issue." << std::endl;
         }
     }
 }
@@ -630,7 +649,8 @@ void FieldDataInstaller::PcFieldToVGearsField(VGears::FLevelFilePtr& field){
         for (FieldDecompiler::FieldEntity entity : decompiled.entities){
             // If the entity has been added as a line, skip.
             if (line_entities.size() > 0){
-                if (*std::find(line_entities.begin(), line_entities.end(), entity.name) == entity.name) continue;
+                                if (std::find(line_entities.begin(), line_entities.end(), entity.name)
+                                    != line_entities.end()) continue;
             }
             const int char_id = entity.char_id;
             if (char_id != -1){

+ 7 - 1
src/installer/FieldDataInstaller.h

@@ -314,6 +314,10 @@ class FieldDataInstaller{
          */
         ~FieldDataInstaller();
 
+        const MapList& GetMapList() const{return map_list_;}
+        const FieldScaleFactorMap& GetFieldScaleFactors() const{return scale_factors_;}
+        const MapList& GetConvertedMapList() const{return converted_map_list_;}
+
         /**
          * Initializer for {@see CollectionFieldSpawnAndScaleFactors}.
          *
@@ -475,7 +479,7 @@ class FieldDataInstaller{
          * @param[out] spawn_points Spawn points will be added sequentially to
          * the end of this map.
          */
-        static void CollectSpawnPoints(
+        static bool CollectSpawnPoints(
           VGears::FLevelFilePtr& field, const std::vector<std::string>& field_id_to_name_lookup,
           FieldSpawnPointsMap& spawn_points
         );
@@ -597,6 +601,8 @@ class FieldDataInstaller{
          */
         std::vector<std::string> map_list_;
 
+        std::vector<std::string> failed_fields_;
+
         /**
          * Map of the collected spawn points.
          */

+ 1 - 1
src/installer/MainWindow.cpp

@@ -25,7 +25,7 @@
 #include <QtWidgets/QCheckBox>
 #include "DataInstaller.h"
 #include "MainWindow.h"
-#include "ui_MainWindow.h"
+#include <ui_MainWindow.h>
 
 /**
  * Indicates if an installer has already been created.

+ 40 - 40
src/installer/MainWindow.ui

@@ -320,10 +320,10 @@
             </size>
            </property>
            <property name="toolTip">
-            <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:700;&quot;&gt;Don't extract sound.&lt;/span&gt;&lt;br/&gt;&lt;br/&gt;If checked, game sound efects will not be extracted.&lt;br/&gt;&lt;br/&gt;Sound extraction can take a few minutes, and it's ok to skip this step if it has already been installed.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+            <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;b&gt;Extract sound effects.&lt;/b&gt;&lt;br/&gt;&lt;br/&gt;Checked extracts sound effects; uncheck to reuse an existing installation. This can take a few minutes.&lt;/body&gt;&lt;/html&gt;</string>
            </property>
-           <property name="text">
-            <string>Don't extract sounds</string>
+            <property name="text">
+             <string>Extract sound effects</string>
            </property>
            <property name="checked">
             <bool>true</bool>
@@ -351,10 +351,10 @@
             </size>
            </property>
            <property name="toolTip">
-            <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:700;&quot;&gt;Don't extract music.&lt;/span&gt;&lt;br/&gt;&lt;br/&gt;If checked, game music tracks will not be extracted.&lt;br/&gt;&lt;br/&gt;Music extraction can take a few minutes, and it's ok to skip this step if it has already been installed.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+            <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;b&gt;Extract music tracks.&lt;/b&gt;&lt;br/&gt;&lt;br/&gt;Checked extracts music; uncheck to reuse an existing installation. This can take a few minutes.&lt;/body&gt;&lt;/html&gt;</string>
            </property>
-           <property name="text">
-            <string>Don't extract music</string>
+            <property name="text">
+             <string>Extract music tracks</string>
            </property>
            <property name="checked">
             <bool>true</bool>
@@ -479,10 +479,10 @@
             </size>
            </property>
            <property name="toolTip">
-            <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;b&gt;Don't extract world map data.&lt;/b&gt;&lt;br&gt;&lt;br&gt;If checked, world map models and data will not be extracted. Data from previous installations will not be deleted.&lt;br&gt;&lt;br&gt;This installation step is not usually very long, so there is no reason to skip it unless you have manually edited the world map data. Skipping world map data installation will also skip the installation of the world map 3D models.&lt;/body&gt;&lt;/html&gt;</string>
+            <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;b&gt;Extract world map data.&lt;/b&gt;&lt;br&gt;&lt;br&gt;Checked installs world map terrain and scripts. Uncheck to keep the existing world map data.&lt;/body&gt;&lt;/html&gt;</string>
            </property>
-           <property name="text">
-            <string>Don't extract world map data</string>
+            <property name="text">
+             <string>Extract world map data</string>
            </property>
            <property name="checked">
             <bool>true</bool>
@@ -513,10 +513,10 @@
             </size>
            </property>
            <property name="toolTip">
-            <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;b&gt;Don't use the ffmpeg command.&lt;/b&gt;&lt;br&gt;&lt;br&gt;If checked, sound effects and background music will not be available to V-Gears.&lt;br&gt;&lt;br&gt;Sound effects and music indexes will still be built, but you will need to provide your own ogg files.&lt;/body&gt;&lt;/html&gt;</string>
+            <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;b&gt;Use FFmpeg for media conversion.&lt;/b&gt;&lt;br&gt;&lt;br&gt;Checked enables FFmpeg to convert audio and video assets.&lt;/body&gt;&lt;/html&gt;</string>
            </property>
-           <property name="text">
-            <string>Prevent FFmpeg calls</string>
+            <property name="text">
+             <string>Use FFmpeg for media conversion</string>
            </property>
            <property name="checked">
             <bool>true</bool>
@@ -544,10 +544,10 @@
             </size>
            </property>
            <property name="toolTip">
-            <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;b&gt;Don't extract images.&lt;/b&gt;&lt;br&gt;&lt;br&gt;If checked, images will not be extracted. This includes window decorations, menus icons, character portraits... It doesn' include backgrounds or textures. Images from previous installations will not be deleted.&lt;br&gt;&lt;br&gt;This installation step is usually quite fast, so there is no reason to skip it unless you have manually edited the images.&lt;/body&gt;&lt;/html&gt;</string>
+            <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;b&gt;Extract images.&lt;/b&gt;&lt;br&gt;&lt;br&gt;Checked installs UI images such as window decorations, menu icons, and character portraits.&lt;/body&gt;&lt;/html&gt;</string>
            </property>
-           <property name="text">
-            <string>Don't extract images</string>
+            <property name="text">
+             <string>Extract images</string>
            </property>
            <property name="checked">
             <bool>true</bool>
@@ -575,10 +575,10 @@
             </size>
            </property>
            <property name="toolTip">
-            <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;b&gt;Don't extract battle data.&lt;/b&gt;&lt;br&gt;&lt;br&gt;If checked, battle data such as enemies, attacks or enemy formations will not be extracted. Data from previous installations will not be deleted.&lt;br&gt;&lt;br&gt;This installation step is usually quite fast, so there is no reason to skip it unless you have manually changed data.&lt;/body&gt;&lt;/html&gt;</string>
+            <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;b&gt;Extract battle data.&lt;/b&gt;&lt;br&gt;&lt;br&gt;Checked installs battle data such as enemies, attacks, and enemy formations.&lt;/body&gt;&lt;/html&gt;</string>
            </property>
-           <property name="text">
-            <string>Don't extract battle data</string>
+            <property name="text">
+             <string>Extract battle data</string>
            </property>
            <property name="checked">
             <bool>true</bool>
@@ -606,10 +606,10 @@
             </size>
            </property>
            <property name="toolTip">
-            <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;b&gt;Don't extract world map 3D models.&lt;/b&gt;&lt;br&gt;&lt;br&gt;If checked, world map 3D models and textures will not be extracted. Models from previous installations will not be deleted.&lt;br&gt;&lt;br&gt;This installation step is not usually very long, so there is no reason to skip it unless you have manually edited the models. Skipping world map data installation will also skip the installation of world map 3D models.&lt;/body&gt;&lt;/html&gt;</string>
+            <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;b&gt;Extract world map 3D models.&lt;/b&gt;&lt;br&gt;&lt;br&gt;Checked converts and installs world map models and textures. Requires world map data.&lt;/body&gt;&lt;/html&gt;</string>
            </property>
-           <property name="text">
-            <string>Dont't extract world 3D models</string>
+            <property name="text">
+             <string>Extract world map 3D models</string>
            </property>
            <property name="checked">
             <bool>true</bool>
@@ -637,10 +637,10 @@
             </size>
            </property>
            <property name="toolTip">
-            <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;b&gt;Don't use the timidity command.&lt;/b&gt;&lt;br&gt;&lt;br&gt;If checked, background music will not be available to V-Gears.&lt;br&gt;&lt;br&gt;Music indexes will still be built, but you will need to provide your own ogg tracks.&lt;/body&gt;&lt;/html&gt;</string>
+            <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;b&gt;Use TiMidity for MIDI conversion.&lt;/b&gt;&lt;br&gt;&lt;br&gt;Checked enables TiMidity when converting MIDI music.&lt;/body&gt;&lt;/html&gt;</string>
            </property>
-           <property name="text">
-            <string>Prevent TiMidity calls</string>
+            <property name="text">
+             <string>Use TiMidity for MIDI conversion</string>
            </property>
            <property name="checked">
             <bool>true</bool>
@@ -674,7 +674,7 @@
             <string>Preserve original data</string>
            </property>
            <property name="checked">
-            <bool>false</bool>
+            <bool>true</bool>
            </property>
           </widget>
           <widget class="QCheckBox" name="chk_fields">
@@ -699,10 +699,10 @@
             </size>
            </property>
            <property name="toolTip">
-            <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;b&gt;Don't extract field maps.&lt;/b&gt;&lt;br&gt;&lt;br&gt;If checked, map fields will not be extracted. Field maps from previous installations will not be deleted.&lt;br&gt;&lt;br&gt;This installation step is not usually very long, so there is no reason to skip it unless you have manually edited the fields. Skipping field map installation will also skip the installation of field 3D models.&lt;/body&gt;&lt;/html&gt;</string>
+            <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;b&gt;Extract field maps.&lt;/b&gt;&lt;br&gt;&lt;br&gt;Checked converts and installs field maps and scripts. Required for world-map city entrances.&lt;/body&gt;&lt;/html&gt;</string>
            </property>
-           <property name="text">
-            <string>Don't extract field maps</string>
+            <property name="text">
+             <string>Extract field maps</string>
            </property>
            <property name="checked">
             <bool>true</bool>
@@ -730,10 +730,10 @@
             </size>
            </property>
            <property name="toolTip">
-            <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;b&gt;Don't extract kernel data.&lt;/b&gt;&lt;br&gt;&lt;br&gt;If checked, game data such as items, materia, character information or initial savemap will not be extracted. Data from previous installations will not be deleted.&lt;br&gt;&lt;br&gt;This installation step is usually quite fast, so there is no reason to skip it unless you have manually changed data.&lt;/body&gt;&lt;/html&gt;</string>
+            <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;b&gt;Extract game data.&lt;/b&gt;&lt;br&gt;&lt;br&gt;Checked installs kernel data such as items, materia, and character information.&lt;/body&gt;&lt;/html&gt;</string>
            </property>
-           <property name="text">
-            <string>Don't extract kernel data</string>
+            <property name="text">
+             <string>Extract game data</string>
            </property>
            <property name="checked">
             <bool>true</bool>
@@ -761,10 +761,10 @@
             </size>
            </property>
            <property name="toolTip">
-            <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;b&gt;Don't extract field 3D models.&lt;/b&gt;&lt;br&gt;&lt;br&gt;If checked, field 3D models and textures will not be extracted. Models from previous installations will not be deleted.&lt;br&gt;&lt;br&gt;This installation step is not usually very long, so there is no reason to skip it unless you have manually edited the models. Skipping field map installation will also skip the installation of field 3D models.&lt;/body&gt;&lt;/html&gt;</string>
+            <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;b&gt;Extract field 3D models.&lt;/b&gt;&lt;br&gt;&lt;br&gt;Checked converts and installs field models and textures. Requires field maps.&lt;/body&gt;&lt;/html&gt;</string>
            </property>
-           <property name="text">
-            <string>Dont't extract field 3D models</string>
+            <property name="text">
+             <string>Extract field 3D models</string>
            </property>
            <property name="checked">
             <bool>true</bool>
@@ -792,10 +792,10 @@
             </size>
            </property>
            <property name="toolTip">
-            <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;b&gt;Don't extract videos.&lt;/b&gt;&lt;br&gt;&lt;br&gt;If checked, FMV videos will not be extracted. Videos from previous installations will not be deleted.&lt;br&gt;&lt;br&gt;Installing videos can take a few minutes, and it's OK to skip this step if they are already installed.&lt;/body&gt;&lt;/html&gt;</string>
+            <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;b&gt;Extract videos.&lt;/b&gt;&lt;br&gt;&lt;br&gt;Checked installs FMV videos. This can take a few minutes.&lt;/body&gt;&lt;/html&gt;</string>
            </property>
-           <property name="text">
-            <string>Don't extract videos</string>
+            <property name="text">
+             <string>Extract videos</string>
            </property>
            <property name="checked">
             <bool>true</bool>
@@ -829,10 +829,10 @@
             </size>
            </property>
            <property name="toolTip">
-            <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;b&gt;Don't extract battle models.&lt;/b&gt;&lt;br&gt;&lt;br&gt;If checked, battle 3D models will not be extracted. This includes models for players, enemies, attacks, battle scenarios... Models from previous installations will not be deleted.&lt;br&gt;&lt;br&gt;This installation step is usually quite fast, so there is no reason to skip it unless you have manually edited the models.&lt;/body&gt;&lt;/html&gt;</string>
+            <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;b&gt;Extract battle models.&lt;/b&gt;&lt;br&gt;&lt;br&gt;Checked converts and installs player, enemy, attack, and battle scene models.&lt;/body&gt;&lt;/html&gt;</string>
            </property>
-           <property name="text">
-            <string>Don't extract battle models</string>
+            <property name="text">
+             <string>Extract battle models</string>
            </property>
            <property name="checked">
             <bool>true</bool>

+ 172 - 0
src/installer/WorldInstaller.cpp

@@ -14,7 +14,9 @@
  */
 
 #include <iostream>
+#include <algorithm>
 #include <fstream>
+#include <sstream>
 #include <tinyxml.h>
 #include <OgreRoot.h>
 #include <OgreMesh.h>
@@ -29,6 +31,9 @@
 #include "data/WorldMapWalkmesh.h"
 #include "data/VGearsHRCFileManager.h"
 #include "data/FF7Data.h"
+#include "decompiler/ControlFlow.h"
+#include "decompiler/DecompilerException.h"
+#include "decompiler/world/WorldEngine.h"
 
 std::string WorldInstaller::ELEMENT_MODELS_DIR("models/world/element");
 
@@ -644,6 +649,173 @@ bool WorldInstaller::ProcessMap(){
     else return false;
 }
 
+bool WorldInstaller::GenerateScripts(
+    const std::vector<std::string>& field_names,
+    const std::map<u16, float>& field_scale_factors,
+    const std::vector<std::string>& converted_fields,
+    std::string& error_message
+){
+    error_message.clear();
+        if (field_names.empty() || converted_fields.empty()){
+                error_message = "Field maps were not installed; enable field map extraction.";
+                return false;
+        }
+    std::unique_ptr<std::string> archive_path
+      = disk_image_.filePath("data/wm/world_us.lgp");
+    if (!archive_path){
+        error_message = "WORLD_US.LGP was not found on the game disc.";
+        return false;
+    }
+
+    File archive_file(*archive_path);
+    VGears::LGPArchive archive(*archive_path, "LGP");
+    archive.open(*archive_path, true);
+    archive.load();
+    const VGears::LGPArchive::FileList& archive_entries = archive.GetFiles();
+    const VGears::LGPArchive::FileEntry* event_entry = nullptr;
+    const VGears::LGPArchive::FileEntry* field_table_entry = nullptr;
+    for (const VGears::LGPArchive::FileEntry& entry : archive_entries){
+        if (entry.file_name == "wm0.ev" || entry.file_name == "WM0.EV"){
+            event_entry = &entry;
+        }
+        else if (entry.file_name == "field.tbl" || entry.file_name == "FIELD.TBL")
+            field_table_entry = &entry;
+    }
+    if (event_entry == nullptr || field_table_entry == nullptr){
+        error_message = "WM0.EV or FIELD.TBL was not found inside WORLD_US.LGP.";
+        return false;
+    }
+
+    File event_file(&archive_file, event_entry->data_offset, event_entry->data_size);
+    std::vector<u8> event_data(event_entry->data_size);
+    if (event_data.empty()){
+        error_message = "WM0.EV inside WORLD_US.LGP is empty.";
+        return false;
+    }
+    event_file.GetFileBuffer(event_data.data(), 0, event_entry->data_size);
+
+        File field_table_file(
+            &archive_file, field_table_entry->data_offset, field_table_entry->data_size
+        );
+        std::vector<u8> field_table_data(field_table_entry->data_size);
+        if (field_table_data.size() != 64 * 24){
+                error_message = "FIELD.TBL has an unexpected size.";
+                return false;
+        }
+        field_table_file.GetFileBuffer(
+            field_table_data.data(), 0, field_table_entry->data_size
+        );
+
+    const std::string script_directory = output_dir_ + "/scripts/worldmap";
+    boost::filesystem::create_directories(script_directory);
+    std::ofstream script_file(script_directory + "/wm0.lua");
+    if (!script_file){
+        error_message = "Cannot create script output in " + script_directory + ".";
+        return false;
+    }
+    script_file << "-- Decompiled from the user's WM0.EV data.\n\n";
+
+    std::vector<bool> generated_functions(256, false);
+    unsigned int generated_function_count = 0;
+    for (int script_index = 0; script_index < 256; ++script_index){
+        try{
+            WorldEngine engine(script_index);
+            InstVec instructions;
+            std::unique_ptr<Disassembler> disassembler = engine.GetDisassembler(instructions);
+            disassembler->Open(event_data);
+            disassembler->Disassemble();
+            if (instructions.empty()) continue;
+
+            ControlFlow control_flow(instructions, engine);
+            control_flow.CreateGroups();
+            std::ostringstream generated_script;
+            std::unique_ptr<CodeGenerator> code_generator
+              = engine.GetCodeGenerator(instructions, generated_script);
+            code_generator->Generate(instructions, control_flow.GetGraph());
+            script_file << generated_script.str() << "\n";
+            generated_functions[script_index] = true;
+            ++generated_function_count;
+        }
+        catch (const DecompilerException&){
+            script_file << "-- world function " << script_index
+              << " could not be decompiled.\n";
+        }
+        catch (const std::exception& exception){
+            script_file << "-- world function " << script_index
+              << " failed: " << exception.what() << ".\n";
+        }
+    }
+
+        script_file << "-- WM0 mesh-function call-table mapping.\n";
+        for (size_t function_index = 0; function_index < generated_functions.size(); ++function_index){
+                if (!generated_functions[function_index]) continue;
+
+                const size_t type_offset = function_index * 4;
+                const uint16 function_type = static_cast<uint16>(event_data[type_offset])
+                    | static_cast<uint16>(event_data[type_offset + 1] << 8);
+                if ((function_type >> 14) != 2) continue;
+
+                const uint16 mesh_index = (function_type >> 4) & 0x03FF;
+                const uint8 mesh_function_id = function_type & 0x0F;
+                if (mesh_index >= 36 * 28 || mesh_function_id > 4) continue;
+
+                script_file << "System.WorldMap.triangle_functions[" << mesh_index << "]"
+                    << " = System.WorldMap.triangle_functions[" << mesh_index << "] or {}\n"
+                    << "System.WorldMap.triangle_functions[" << mesh_index << "]["
+                    << static_cast<unsigned int>(mesh_function_id) << "] = " << function_index << "\n";
+        }
+
+        script_file << "System.WorldMap.field_table = {}\n";
+        for (size_t record_index = 0; record_index < 64; ++record_index){
+                for (size_t scenario = 0; scenario < 2; ++scenario){
+                        const size_t offset = record_index * 24 + scenario * 12;
+                        const uint16 raw_x = static_cast<uint16>(field_table_data[offset])
+                            | (static_cast<uint16>(field_table_data[offset + 1]) << 8);
+                        const uint16 raw_y = static_cast<uint16>(field_table_data[offset + 2])
+                            | (static_cast<uint16>(field_table_data[offset + 3]) << 8);
+                        const uint16 raw_triangle = static_cast<uint16>(field_table_data[offset + 4])
+                            | (static_cast<uint16>(field_table_data[offset + 5]) << 8);
+                        const uint16 field_id = static_cast<uint16>(field_table_data[offset + 6])
+                            | (static_cast<uint16>(field_table_data[offset + 7]) << 8);
+                        if (field_id >= field_names.size()) continue;
+                        const std::string& field_name = field_names[field_id];
+                        if (std::find(converted_fields.begin(), converted_fields.end(), field_name)
+                            == converted_fields.end()){
+                                continue;
+                        }
+                        const auto scale_factor = field_scale_factors.find(field_id);
+                        if (scale_factor == field_scale_factors.end() || scale_factor->second <= 0){
+                                continue;
+                        }
+
+                        const float downscale = 128.0f * scale_factor->second;
+                        const float x = static_cast<int16>(raw_x) / downscale;
+                        const float y = static_cast<int16>(raw_y) / downscale;
+                        const int16 triangle = static_cast<int16>(raw_triangle);
+                        const uint8 direction = field_table_data[offset + 8];
+                        const float direction_degrees
+                            = 360.0f * static_cast<float>(direction) / 255.0f;
+                        const size_t table_id = record_index + 1;
+                        script_file << "System.WorldMap.field_table[" << table_id << "]"
+                            << " = System.WorldMap.field_table[" << table_id << "] or {}\n"
+                            << "System.WorldMap.field_table[" << table_id << "][" << scenario << "] = {"
+                            << "field = \"" << field_name << "\", x = " << x << ", y = " << y
+                            << ", triangle = " << triangle << ", direction = "
+                            << direction_degrees << "}\n";
+                }
+        }
+    script_file.flush();
+    if (!script_file){
+        error_message = "Failed while writing " + script_directory + "/wm0.lua.";
+        return false;
+    }
+    if (generated_function_count == 0){
+        error_message = "No WM0.EV functions could be decompiled.";
+        return false;
+    }
+    return true;
+}
+
 void WorldInstaller::ExportMesh(const std::string outdir, const Ogre::MeshPtr &mesh){
     if (mesh.isNull()){
         std::cerr << "[ERROR] ExportMesh called with null mesh." << std::endl;

+ 8 - 0
src/installer/WorldInstaller.h

@@ -16,6 +16,7 @@
 #pragma once
 
 #include <string>
+#include <map>
 #include <vector>
 #include <common/TypeDefine.h>
 #include "DiskImage.h"
@@ -75,6 +76,13 @@ class WorldInstaller{
          */
         bool ProcessMap();
 
+                bool GenerateScripts(
+                    const std::vector<std::string>& field_names,
+                    const std::map<u16, float>& field_scale_factors,
+                    const std::vector<std::string>& converted_fields,
+                    std::string& error_message
+                );
+
         //void ReadMapFile(std::string file_name);
 
         //void GenerateMap(std::string file_name, std::string output);

+ 5 - 0
src/installer/decompiler/Disassembler.cpp

@@ -14,6 +14,7 @@
  */
 
 #include "decompiler/Disassembler.h"
+#include <utility>
 
 Disassembler::Disassembler(InstVec &insts) : insts_(insts), address_base_(0){}
 
@@ -21,6 +22,10 @@ void Disassembler::Open(const char *filename){
     stream_ = std::make_unique<BinaryReader>(BinaryReader::ReadAll(filename));
 }
 
+void Disassembler::Open(std::vector<unsigned char> data){
+    stream_ = std::make_unique<BinaryReader>(std::move(data));
+}
+
 void Disassembler::DoDumpDisassembly(std::ostream &output){
     InstIterator inst;
     for (inst = insts_.begin(); inst != insts_.end(); ++ inst) output << *inst << "\n";

+ 2 - 0
src/installer/decompiler/Disassembler.h

@@ -48,6 +48,8 @@ class Disassembler{
          */
         virtual void Open(const char *filename);
 
+        void Open(std::vector<unsigned char> data);
+
         /**
          * Request disassembled instructions.
          */

+ 18 - 5
src/installer/decompiler/field/FieldCodeGenerator.cpp

@@ -14,6 +14,7 @@
  */
 
 #include <boost/algorithm/string/predicate.hpp>
+#include <stdexcept>
 #include "FieldCodeGenerator.h"
 #include "FieldEngine.h"
 
@@ -68,12 +69,24 @@ void FunctionMetaData::ParseEnd(const std::string& item, std::deque<std::string>
 }
 
 void FunctionMetaData::ParseCharId(const std::string& item, std::deque<std::string>& strs){
-    if (!item.empty()) character_id_ = std::stoi(item);
-    if (!strs.empty()){
-        auto tmp = strs.front();
-        strs.pop_front();
-        ParseEntity(tmp, strs);
+    size_t parsed_length = 0;
+    try{
+        const int parsed_character_id = std::stoi(item, &parsed_length);
+        if (parsed_length == item.size()){
+            character_id_ = parsed_character_id;
+            if (!strs.empty()){
+                auto tmp = strs.front();
+                strs.pop_front();
+                ParseEntity(tmp, strs);
+            }
+            return;
+        }
+    }
+    catch (const std::invalid_argument&){
+    }
+    catch (const std::out_of_range&){
     }
+    ParseEntity(item, strs);
 }
 
 void FunctionMetaData::ParseEntity(const std::string& item, std::deque<std::string>& strs){

+ 91 - 29
src/installer/decompiler/field/instruction/FieldModelInstruction.cpp

@@ -18,12 +18,27 @@
 
 #include <iostream>
 #include <sstream>
+#include <stdexcept>
 #include <boost/format.hpp>
 #include "decompiler/field/instruction/FieldModelInstruction.h"
 #include "decompiler/field/FieldEngine.h"
 #include "decompiler/field/FieldCodeGenerator.h"
 #include "decompiler/field/FieldDisassembler.h"
 
+static bool TryParseFloat(const std::string& value, float& parsed_value){
+  size_t parsed_length = 0;
+  try{
+    parsed_value = std::stof(value, &parsed_length);
+    return parsed_length == value.size();
+  }
+  catch (const std::invalid_argument&){
+    return false;
+  }
+  catch (const std::out_of_range&){
+    return false;
+  }
+}
+
 void FieldModelInstruction::ProcessInst(
   Function& func, ValueStack&, Engine* engine, CodeGenerator *code_gen
 ){
@@ -153,6 +168,18 @@ void FieldModelInstruction::ProcessJOIN(CodeGenerator* code_gen){
 }
 
 void FieldModelInstruction::ProcessSPLIT(CodeGenerator* code_gen){
+  const std::vector<ValuePtr> parameters = GetParams();
+  if (parameters.size() != 13){
+    code_gen->WriteTodo("FieldModelInstruction", "SPLIT with malformed parameters");
+    return;
+  }
+  for (const ValuePtr& parameter : parameters){
+    if (parameter == nullptr || !parameter->IsInteger()){
+      code_gen->WriteTodo("FieldModelInstruction", "SPLIT with invalid parameter");
+      return;
+    }
+  }
+
     FieldCodeGenerator* cg = static_cast<FieldCodeGenerator*>(code_gen);
     const float scale = 128.0f * cg->GetScaleFactor();
     const auto& ax = FieldCodeGenerator::FormatValueOrVariable(
@@ -434,14 +461,18 @@ void FieldModelInstruction::ProcessCC(CodeGenerator* code_gen, const FieldEngine
 void FieldModelInstruction::ProcessJUMP(CodeGenerator* code_gen, const std::string& entity){
     FieldCodeGenerator* cg = static_cast<FieldCodeGenerator*>(code_gen);
     const float scale = 128.0f * cg->GetScaleFactor();
-    float x = std::stof(FieldCodeGenerator::FormatValueOrVariable(
+    const std::string x_value = FieldCodeGenerator::FormatValueOrVariable(
       cg->GetFormatter(), params_[0]->GetUnsigned(), params_[4]->GetSigned(),
       FieldCodeGenerator::ValueType::Float, scale
-    ));
-    float y = std::stof(FieldCodeGenerator::FormatValueOrVariable(
+    );
+    const std::string y_value = FieldCodeGenerator::FormatValueOrVariable(
       cg->GetFormatter(), params_[1]->GetUnsigned(), params_[5]->GetSigned(),
       FieldCodeGenerator::ValueType::Float, scale
-    ));
+    );
+    float x = 0;
+    float y = 0;
+    const bool literal_coordinates
+      = TryParseFloat(x_value, x) && TryParseFloat(y_value, y);
     int i = atoi(FieldCodeGenerator::FormatValueOrVariable(
       cg->GetFormatter(), params_[2]->GetUnsigned(), params_[6]->GetSigned()
     ).c_str());
@@ -456,10 +487,18 @@ void FieldModelInstruction::ProcessJUMP(CodeGenerator* code_gen, const std::stri
     if (steps > 5) steps = 5;
     // TODO: Z hardcoded as -1, handled in Entity::ScriptJumpToPosition.
     // TODO: Hardcoded 0.5 in seconds. Calculate using distance.
-    code_gen->AddOutputLine((
-      boost::format("self.%1%:jump_to_position(%2%, %3%, -1, 0.5, %4%) -- %5% steps.")
-      % entity % x % y % i % steps
-    ).str());
+    if (literal_coordinates){
+        code_gen->AddOutputLine((
+          boost::format("self.%1%:jump_to_position(%2%, %3%, -1, 0.5, %4%) -- %5% steps.")
+          % entity % x % y % i % steps
+        ).str());
+    }
+    else{
+        code_gen->AddOutputLine(
+          "self." + entity + ":jump_to_position(" + x_value + ", " + y_value
+            + ", -1, 0.5, " + std::to_string(i) + ")"
+        );
+    }
     code_gen->AddOutputLine((boost::format("self.%1%:jump_sync()") % entity).str());
 }
 
@@ -516,14 +555,22 @@ void FieldModelInstruction::ProcessSLIDR(CodeGenerator* code_gen, const std::str
     FieldCodeGenerator* cg = static_cast<FieldCodeGenerator*>(code_gen);
     const float scale = 128.0f * cg->GetScaleFactor();
     // The documentation says the value should be in param_[1], but it's actually on param_[2]
-    float radius = std::stof(FieldCodeGenerator::FormatValueOrVariable(
+    const std::string radius_value = FieldCodeGenerator::FormatValueOrVariable(
       cg->GetFormatter(), params_[0]->GetUnsigned(), params_[2]->GetUnsigned(),
       FieldCodeGenerator::ValueType::Float, scale
-    ));
-    code_gen->AddOutputLine((
-      boost::format("self.%1%:set_solid_radius(%2%)")
-      % entity % radius
-    ).str());
+    );
+    float radius = 0;
+    if (TryParseFloat(radius_value, radius)){
+        code_gen->AddOutputLine((
+          boost::format("self.%1%:set_solid_radius(%2%)")
+          % entity % radius
+        ).str());
+    }
+    else{
+        code_gen->AddOutputLine(
+          "self." + entity + ":set_solid_radius(" + radius_value + ")"
+        );
+    }
 }
 
 void FieldModelInstruction::ProcessSOLID(CodeGenerator* code_gen, const std::string& entity){
@@ -536,26 +583,41 @@ void FieldModelInstruction::ProcessSOLID(CodeGenerator* code_gen, const std::str
 void FieldModelInstruction::ProcessOFST(CodeGenerator* code_gen, const std::string& entity){
     FieldCodeGenerator* cg = static_cast<FieldCodeGenerator*>(code_gen);
     const float scale = 128.0f * cg->GetScaleFactor();
-    float x = std::stof(FieldCodeGenerator::FormatValueOrVariable(
+    const std::string x_value = FieldCodeGenerator::FormatValueOrVariable(
       cg->GetFormatter(), params_[0]->GetUnsigned(), params_[5]->GetSigned(),
       FieldCodeGenerator::ValueType::Float, scale
-    ));
-    float y = std::stof(FieldCodeGenerator::FormatValueOrVariable(
+    );
+    const std::string y_value = FieldCodeGenerator::FormatValueOrVariable(
       cg->GetFormatter(), params_[1]->GetUnsigned(),params_[6]->GetSigned(),
       FieldCodeGenerator::ValueType::Float, scale
-    ));
-    float z = std::stof(FieldCodeGenerator::FormatValueOrVariable(
+    );
+    const std::string z_value = FieldCodeGenerator::FormatValueOrVariable(
       cg->GetFormatter(), params_[2]->GetUnsigned(), params_[7]->GetSigned(),
       FieldCodeGenerator::ValueType::Float, scale
-    ));
-    float speed = std::stof(FieldCodeGenerator::FormatValueOrVariable(
+    );
+    const std::string speed_value = FieldCodeGenerator::FormatValueOrVariable(
       cg->GetFormatter(), params_[3]->GetUnsigned(), params_[8]->GetUnsigned()
-    ));
-    // Speed needs to be scaled down by the frame rate.
-    speed /= 30.0f;
-    code_gen->AddOutputLine((
-      boost::format("self.%1%:offset_to_position(%2%, %3%, %4%, %5%, %6%)")
-      % entity % x % y % z
-      % (params_[4]->GetUnsigned() ? "Entity.SMOOTH" : "Entity.LINEAR")% speed
-    ).str());
+    );
+    float x = 0;
+    float y = 0;
+    float z = 0;
+    float speed = 0;
+    const bool literal_values
+      = TryParseFloat(x_value, x) && TryParseFloat(y_value, y)
+        && TryParseFloat(z_value, z) && TryParseFloat(speed_value, speed);
+    const std::string move_type
+      = params_[4]->GetUnsigned() ? "Entity.SMOOTH" : "Entity.LINEAR";
+    if (literal_values){
+        speed /= 30.0f;
+        code_gen->AddOutputLine((
+          boost::format("self.%1%:offset_to_position(%2%, %3%, %4%, %5%, %6%)")
+          % entity % x % y % z % move_type % speed
+        ).str());
+    }
+    else{
+        code_gen->AddOutputLine(
+          "self." + entity + ":offset_to_position(" + x_value + ", " + y_value
+            + ", " + z_value + ", " + move_type + ", (" + speed_value + ") / 30.0)"
+        );
+    }
 }

+ 36 - 13
src/installer/decompiler/field/instruction/FieldWindowInstruction.cpp

@@ -18,12 +18,27 @@
 
 #include <iostream>
 #include <sstream>
+#include <stdexcept>
 #include <boost/format.hpp>
 #include "decompiler/field/instruction/FieldWindowInstruction.h"
 #include "decompiler/field/FieldEngine.h"
 #include "decompiler/field/FieldCodeGenerator.h"
 #include "decompiler/field/FieldDisassembler.h"
 
+static bool TryParseInteger(const std::string& value, int& parsed_value){
+  size_t parsed_length = 0;
+  try{
+    parsed_value = std::stoi(value, &parsed_length);
+    return parsed_length == value.size();
+  }
+  catch (const std::invalid_argument&){
+    return false;
+  }
+  catch (const std::out_of_range&){
+    return false;
+  }
+}
+
 void FieldWindowInstruction::ProcessInst(
   Function& func, ValueStack&, Engine* engine, CodeGenerator *code_gen
 ){
@@ -115,14 +130,22 @@ void FieldWindowInstruction::ProcessWINDOW(CodeGenerator* code_gen){
 
 void FieldWindowInstruction::ProcessMENU(CodeGenerator* code_gen){
     FieldCodeGenerator* cg = static_cast<FieldCodeGenerator*>(code_gen);
-    int menu_id = std::stoi(FieldCodeGenerator::FormatValueOrVariable(
+    const std::string menu_id_value = FieldCodeGenerator::FormatValueOrVariable(
         cg->GetFormatter(), params_[1]->GetUnsigned(), params_[2]->GetSigned(),
         FieldCodeGenerator::ValueType::Integer
-      ));
-    int param = std::stoi(FieldCodeGenerator::FormatValueOrVariable(
+      );
+    const std::string param_value = FieldCodeGenerator::FormatValueOrVariable(
       cg->GetFormatter(), params_[1]->GetUnsigned(), params_[3]->GetSigned(),
       FieldCodeGenerator::ValueType::Integer
-    ));
+    );
+    int menu_id = 0;
+    int param = 0;
+    if (!TryParseInteger(menu_id_value, menu_id) || !TryParseInteger(param_value, param)){
+        code_gen->AddOutputLine(
+          "dispatch_menu(" + menu_id_value + ", " + param_value + ")"
+        );
+        return;
+    }
     switch(menu_id){
         case 0x05: // Ending credits.
             code_gen->AddOutputLine("ending_credits()");
@@ -176,21 +199,21 @@ void FieldWindowInstruction::ProcessMENU(CodeGenerator* code_gen){
 
 void FieldWindowInstruction::ProcessSTTIM(CodeGenerator* code_gen){
     FieldCodeGenerator* cg = static_cast<FieldCodeGenerator*>(code_gen);
-    int h = std::stoi(FieldCodeGenerator::FormatValueOrVariable(
+    const std::string hours = FieldCodeGenerator::FormatValueOrVariable(
       cg->GetFormatter(), params_[0]->GetUnsigned(), params_[4]->GetSigned(),
       FieldCodeGenerator::ValueType::Integer
-    ));
-    int m = std::stoi(FieldCodeGenerator::FormatValueOrVariable(
+    );
+    const std::string minutes = FieldCodeGenerator::FormatValueOrVariable(
       cg->GetFormatter(), params_[1]->GetUnsigned(), params_[5]->GetSigned(),
       FieldCodeGenerator::ValueType::Integer
-    ));
-    int s = std::stoi(FieldCodeGenerator::FormatValueOrVariable(
+    );
+    const std::string seconds = FieldCodeGenerator::FormatValueOrVariable(
       cg->GetFormatter(), params_[3]->GetUnsigned(), params_[6]->GetSigned(),
       FieldCodeGenerator::ValueType::Integer
-    ));
-    code_gen->AddOutputLine((
-      boost::format("Timer.set(%1%) -- %2%:%3%:%4%") % (3600 * h + 60 * m + s) % h % m % s
-    ).str());
+    );
+    code_gen->AddOutputLine(
+      "Timer.set(3600 * (" + hours + ") + 60 * (" + minutes + ") + (" + seconds + "))"
+    );
 }
 
 void FieldWindowInstruction::ProcessMESSAGE(

+ 7 - 2
src/installer/decompiler/world/WorldCodeGenerator.cpp

@@ -20,8 +20,13 @@ WorldCodeGenerator::WorldCodeGenerator(Engine *engine, std::ostream &output)
 : CodeGenerator(engine, output, FIFO_ARGUMENT_ORDER, LIFO_ARGUMENT_ORDER){}
 
 std::string WorldCodeGenerator::ConstructFuncSignature(const Function& function){
-    // TODO: Implement.
-    return function.name + " = function(self)";
+    return "System.WorldMap." + function.name + " = function(self)";
+}
+
+bool WorldCodeGenerator::OutputOnlyRequiredLabels() const{return true;}
+
+void WorldCodeGenerator::OnEndFunction(const Function&){
+    AddOutputLine("end", true, false);
 }
 
 const InstPtr WorldCodeGenerator::FindFirstCall(){

+ 3 - 0
src/installer/decompiler/world/WorldCodeGenerator.h

@@ -58,6 +58,9 @@ class WorldCodeGenerator : public CodeGenerator{
 
     protected:
 
+        bool OutputOnlyRequiredLabels() const override;
+        void OnEndFunction(const Function& function) override;
+
         /**
          * Constructs the function signature.
          *

+ 23 - 9
src/installer/decompiler/world/WorldDisassembler.cpp

@@ -14,6 +14,7 @@
  */
 
 #include <boost/format.hpp>
+#include <stdexcept>
 #include "decompiler/world/WorldDisassembler.h"
 #include "decompiler/world/WorldEngine.h"
 #include "decompiler/world/instruction/WorldBinaryEqualStackInstruction.h"
@@ -27,8 +28,8 @@
 #include "decompiler/world/instruction/WorldSubStackInstruction.h"
 #include "decompiler/world/instruction/WorldUncondJumpInstruction.h"
 
-WorldDisassembler::WorldDisassembler(WorldEngine*, InstVec &insts, int script_number):
-  Disassembler(insts), script_number_(script_number){}
+WorldDisassembler::WorldDisassembler(WorldEngine* engine, InstVec &insts, int script_number):
+    Disassembler(insts), engine_(engine), script_number_(script_number){}
 
 WorldDisassembler::~WorldDisassembler(){}
 
@@ -76,17 +77,24 @@ void WorldDisassembler::DoDisassemble(){
         tmp.instruction_pointer = this->stream_->ReadU16();
         world_data.emplace_back(tmp);
     }
-    int i = this->script_number_;
-    for (int i = 1; i < 2; i ++){ //for (int i = 1; i < 2; i ++){
-        WorldScriptData data = world_data[i];
-        this->stream_->Seek((256 * sizeof(int)) + (data.instruction_pointer*sizeof(uint16)));
+    if (script_number_ < 0 || script_number_ >= static_cast<int>(world_data.size())){
+        throw std::out_of_range("World-map script index is outside the call table.");
+    }
+    const WorldScriptData& data = world_data[script_number_];
+    if (data.type == 0 && data.instruction_pointer == 0) return;
+
+        const unsigned int code_offset = static_cast<unsigned int>(
+          256 * sizeof(uint32) + data.instruction_pointer * sizeof(uint16)
+        );
+        if (code_offset >= stream_->GetSize()) return;
+        this->stream_->Seek(code_offset);
         this->address_ = this->stream_->GetPosition();
         bool end = false;
-        for (;;){ //    while (this->_f.pos() != (int)this->_f.size())
+        for (;;){
+            if (end) break;
             uint32 full_opcode = 0;
             uint16 opcode = this->stream_->ReadU16();
             std::string opcode_prefix;
-            if (end) break;
             if (opcode == 0x203) end = true;
             if (opcode >= 0x204 && opcode < 0x300)
                 ParseOpcode(opcode, "reset stack", new WorldKernelCallInstruction(), 0, "");
@@ -406,6 +414,12 @@ void WorldDisassembler::DoDisassemble(){
             this->address_ += 2;
         }
         for (auto& instruction : this->insts_) instruction->SetAddress(instruction->GetAddress() / 2);
-    }
+        if (!insts_.empty()){
+            const uint32 start_address = insts_.front()->GetAddress();
+            const uint32 end_address = insts_.back()->GetAddress();
+            Function function(start_address, end_address);
+            function.name = "world_function_" + std::to_string(script_number_);
+            engine_->functions[start_address] = function;
+        }
 }
 

+ 3 - 1
src/installer/decompiler/world/WorldDisassembler.h

@@ -44,6 +44,8 @@ class WorldDisassembler : public Disassembler{
 
     private:
 
+        WorldEngine* engine_;
+
         /**
          * Parses an opcode.
          *
@@ -61,7 +63,7 @@ class WorldDisassembler : public Disassembler{
         template<typename T> void ParseOpcode(
           int opcode, std::string name, T instruction, int stack_change, const char* argument_format
         ){
-            uint32 full_opcode = (full_opcode << 8) + opcode;
+                        const uint32 full_opcode = static_cast<uint32>(opcode);
             this->insts_.push_back(instruction);
             this->insts_.back()->SetOpcode(full_opcode);
             this->insts_.back()->SetAddress(this->address_);

+ 4 - 4
src/installer/decompiler/world/instruction/WorldKernelCallInstruction.cpp

@@ -104,7 +104,7 @@ void WorldKernelCallInstruction::ProcessInst(
                     case 3: type = "black"; break;
                     case 4: type = "gold"; break;
                 }
-                func = "SetChocoboType(" + type + ");";
+                func = "SetChocoboType(\"" + type + "\");";
             }
             break;
         case 0x34c:
@@ -117,13 +117,13 @@ void WorldKernelCallInstruction::ProcessInst(
                     case -1: type = "gold"; break;
                     case -2: type = "black"; break;
                 }
-                func = "SetSubmarineColor(" + type + ");";
+                func = "SetSubmarineColor(\"" + type + "\");";
             }
             break;
         case 0x349:
             {
                 std::string type = stack.Pop()->GetString();
-                std::string comment = "// ";
+                std::string comment = "-- ";
                 switch (std::stoi(type)){
                     case 0: comment += "before temple of the ancients,"; break;
                     case 1: comment += "after temple of the ancients"; break;
@@ -138,7 +138,7 @@ void WorldKernelCallInstruction::ProcessInst(
         case 0x300:
             {
                 std::string type = stack.Pop()->GetString();
-                std::string comment = "// ";
+                std::string comment = "-- ";
                 try{
                     switch (std::stoi(type)){
                         case 0: comment += "Cloud"; break;

+ 253 - 10
src/modules/worldmap/WorldmapModule.cpp

@@ -43,6 +43,8 @@
 #include "core/InputManager.h"
 #include "core/EntityManager.h"
 #include "core/EntityModel.h"
+#include "core/DialogsManager.h"
+#include "core/ScriptManager.h"
 #include "data/worldmap/WorldmapFile.h"
 #include "data/worldmap/WorldmapFileManager.h"
 #include "data/worldmap/WorldmapFileSerializer.h"
@@ -412,6 +414,52 @@ namespace VGears {
       }
     }
 
+    static const std::map<Ogre::String, uint8>& GetWorldMapTerrainTypes(){
+      using WalkType = VGears::WorldmapFileSerializer::TriangleWalkMapTypes;
+      static const std::map<Ogre::String, uint8> terrain_types = {
+        {"GRASS", static_cast<uint8>(WalkType::GRASS)},
+        {"FOREST", static_cast<uint8>(WalkType::FOREST)},
+        {"MOUNTAIN", static_cast<uint8>(WalkType::MOUNTAIN)},
+        {"SEA_DEEP_WATER", static_cast<uint8>(WalkType::SEA_DEEP_WATER)},
+        {"RIVER_CROSSING", static_cast<uint8>(WalkType::RIVER_CROSSING)},
+        {"RIVER", static_cast<uint8>(WalkType::RIVER)},
+        {"WATER", static_cast<uint8>(WalkType::WATER)},
+        {"SWAMP", static_cast<uint8>(WalkType::SWAMP)},
+        {"DESERT", static_cast<uint8>(WalkType::DESERT)},
+        {"WASTELAND", static_cast<uint8>(WalkType::WASTELAND)},
+        {"SNOW", static_cast<uint8>(WalkType::SNOW)},
+        {"RIVERSIDE", static_cast<uint8>(WalkType::RIVERSIDE)},
+        {"CLIFF", static_cast<uint8>(WalkType::CLIFF)},
+        {"COREL_BRIDGE", static_cast<uint8>(WalkType::COREL_BRIDGE)},
+        {"WUTAI_BRIDGE", static_cast<uint8>(WalkType::WUTAI_BRIDGE)},
+        {"UNUSED_1", static_cast<uint8>(WalkType::UNUSED_1)},
+        {"HILL_SIDE", static_cast<uint8>(WalkType::HILL_SIDE)},
+        {"BEACH", static_cast<uint8>(WalkType::BEACH)},
+        {"SUBMARINE_PEN", static_cast<uint8>(WalkType::SUBMARINE_PEN)},
+        {"CANYON", static_cast<uint8>(WalkType::CANYON)},
+        {"MOUNTAIN_PASS", static_cast<uint8>(WalkType::MOUNTAIN_PASS)},
+        {"UNKNOWN_1", static_cast<uint8>(WalkType::UNKNOWN_1)},
+        {"WATERFALL", static_cast<uint8>(WalkType::WATERFALL)},
+        {"UNUSED_2", static_cast<uint8>(WalkType::UNUSED_2)},
+        {"GOLD_SAUCER_DESERT", static_cast<uint8>(WalkType::GOLD_SAUCER_DESERT)},
+        {"JUNGLE", static_cast<uint8>(WalkType::JUNGLE)},
+        {"SEA", static_cast<uint8>(WalkType::SEA)},
+        {"NORTHERND_CAVE", static_cast<uint8>(WalkType::NORTHERND_CAVE)},
+        {"GOLD_SAUCER_DESERT_BORDER", static_cast<uint8>(WalkType::GOLD_SAUCER_DESERT_BORDER)},
+        {"BRIDGE_HEAD", static_cast<uint8>(WalkType::BRIDGE_HEAD)},
+        {"BACK_ENTRANCE", static_cast<uint8>(WalkType::BACK_ENTRANCE)},
+        {"UNUSED_3", static_cast<uint8>(WalkType::UNUSED_3)}
+      };
+      return terrain_types;
+    }
+
+      static Ogre::String GetWorldMapTerrainName(const uint8 walkability_type){
+        for (const auto& terrain : GetWorldMapTerrainTypes()){
+          if (terrain.second == walkability_type) return terrain.first;
+        }
+        return "";
+      }
+
     /**
      * Loads and parses the installed WM0 map.
      *
@@ -540,13 +588,18 @@ namespace VGears {
       buggy_entity_(nullptr),
       chocobo_entity_(nullptr),
       submarine_entity_(nullptr),
+      tiny_bronco_entity_(nullptr),
       current_vehicle_(VehicleType::ON_FOOT),
       player_height_offset_(0),
-      initialized_(false)
+      active_function_mesh_index_(-1),
+      active_function_id_(-1),
+      initialized_(false),
+      frame_listener_attached_(false),
+      cancel_was_pressed_(false)
     {}
 
     WorldmapModule::~WorldmapModule(){
-      if (initialized_) Root::getSingleton().removeFrameListener(this);
+      if (frame_listener_attached_) Root::getSingleton().removeFrameListener(this);
       if (scene_manager_ != nullptr && world_map_node_ != nullptr){
         scene_manager_->getRootSceneNode()->removeAndDestroyChild("WorldMapTerrain");
       }
@@ -602,6 +655,9 @@ namespace VGears {
                       From16BitFixedPoint(vertex_2.z) + pos_z
                     );
                     terrain_triangle.walkability_type = triangle.walkability_info_;
+                    terrain_triangle.function_id = triangle.function_id;
+                    terrain_triangle.mesh_x = static_cast<int>(map_x * 4 + mesh_x);
+                    terrain_triangle.mesh_y = static_cast<int>(map_y * 4 + mesh_y);
                     terrain_triangles_.push_back(terrain_triangle);
 
                     if (unavailable_texture_ids.find(triangle.TextureInfo)
@@ -864,15 +920,19 @@ namespace VGears {
         submarine_entity_ = static_cast<EntityModel*>(
           EntityManager::getSingleton().GetEntity("WorldMapSubmarine")
         );
+        tiny_bronco_entity_ = static_cast<EntityModel*>(
+          EntityManager::getSingleton().GetEntity("WorldMapTinyBronco")
+        );
     }
 
     bool WorldmapModule::FindWalkableSurface(
       const Ogre::Vector3& position, Ogre::Vector3& surface, bool grass_only,
-      bool require_foot_walkable
+      bool require_walkable, uint8* walkability_type, size_t* triangle_index
     ) const{
         bool found = false;
         Ogre::Real closest_height_distance = std::numeric_limits<Ogre::Real>::max();
-        for (const TerrainTriangle& triangle : terrain_triangles_){
+        for (size_t i = 0; i < terrain_triangles_.size(); ++i){
+            const TerrainTriangle& triangle = terrain_triangles_[i];
             if (grass_only
               && triangle.walkability_type != VGears::WorldmapFileSerializer::GRASS){
                 continue;
@@ -900,9 +960,10 @@ namespace VGears {
             const Ogre::Real height_distance = Ogre::Math::Abs(height - position.y);
             if (height_distance >= closest_height_distance) continue;
             closest_height_distance = height_distance;
-            found = !require_foot_walkable
-              || IsFootWalkable(triangle.walkability_type);
+            found = !require_walkable || IsActiveTerrainAllowed(triangle.walkability_type);
             surface = Ogre::Vector3(position.x, height, position.z);
+            if (walkability_type != nullptr) *walkability_type = triangle.walkability_type;
+            if (triangle_index != nullptr) *triangle_index = i;
         }
         return found;
     }
@@ -911,6 +972,125 @@ namespace VGears {
       return current_vehicle_;
     }
 
+    void WorldmapModule::ClearVehicleTerrains(){
+        active_vehicle_terrain_types_.clear();
+    }
+
+    void WorldmapModule::AddVehicleTerrain(const char* terrain_name){
+        if (terrain_name == nullptr) return;
+        const auto terrain = GetWorldMapTerrainTypes().find(terrain_name);
+        if (terrain == GetWorldMapTerrainTypes().end()){
+            LogManager::getSingleton().logMessage(
+              "Unknown world-map vehicle terrain '" + Ogre::String(terrain_name) + "'."
+            );
+            return;
+        }
+        if (std::find(
+              active_vehicle_terrain_types_.begin(), active_vehicle_terrain_types_.end(),
+              terrain->second
+            ) == active_vehicle_terrain_types_.end()){
+            active_vehicle_terrain_types_.push_back(terrain->second);
+        }
+    }
+
+    bool WorldmapModule::IsActiveTerrainAllowed(uint8 walkability_type) const{
+        if (current_vehicle_ == VehicleType::ON_FOOT){
+            return IsFootWalkable(walkability_type);
+        }
+        return std::find(
+          active_vehicle_terrain_types_.begin(), active_vehicle_terrain_types_.end(),
+          walkability_type
+        ) != active_vehicle_terrain_types_.end();
+    }
+
+    void WorldmapModule::TriggerTriangleFunction(const TerrainTriangle& triangle){
+        ScriptEntity* world_map_script
+          = ScriptManager::getSingleton().GetScriptEntityByName(
+              ScriptManager::SYSTEM, "WorldMap"
+            );
+        if (world_map_script == nullptr) return;
+
+        const int mesh_index = triangle.mesh_y * 36 + triangle.mesh_x;
+        ScriptManager::getSingleton().ScriptRequest(
+          world_map_script, "on_triangle_enter", 1,
+          std::to_string(mesh_index), std::to_string(triangle.function_id - 3), false, false
+        );
+    }
+
+      Ogre::String WorldmapModule::GetTerrainTypeUnderEntity(const char* entity_name) const{
+        if (entity_name == nullptr) return "";
+        const ::Entity* entity = EntityManager::getSingleton().GetEntity(entity_name);
+        if (entity == nullptr) return "";
+
+        Ogre::Vector3 surface;
+        uint8 walkability_type = 0;
+        if (!FindWalkableSurface(entity->GetPosition(), surface, false, false, &walkability_type)){
+          return "";
+        }
+        return GetWorldMapTerrainName(walkability_type);
+      }
+
+      int WorldmapModule::GetTerrainTriangleUnderEntity(const char* entity_name) const{
+        if (entity_name == nullptr) return -1;
+        const ::Entity* entity = EntityManager::getSingleton().GetEntity(entity_name);
+        if (entity == nullptr) return -1;
+
+        Ogre::Vector3 surface;
+        size_t triangle_index = 0;
+        if (!FindWalkableSurface(entity->GetPosition(), surface, false, false, nullptr, &triangle_index)){
+          return -1;
+        }
+        return static_cast<int>(triangle_index);
+      }
+
+      bool WorldmapModule::UnboardVehicle(const char* vehicle_name){
+        if (vehicle_name == nullptr) return false;
+
+        EntityManager& entity_manager = EntityManager::getSingleton();
+        ::Entity* player_entity = entity_manager.ScriptGetPlayerEntity();
+        ::Entity* vehicle_entity = entity_manager.GetEntity(vehicle_name);
+        if (
+          player_entity == nullptr || vehicle_entity == nullptr
+          || player_entity != vehicle_entity || player_entity == player_entity_
+        ){
+          return false;
+        }
+
+        const Ogre::Vector3 vehicle_position = vehicle_entity->GetPosition();
+        const Ogre::Real unboard_distance
+          = vehicle_entity->GetSolidRadius() + player_entity_->GetSolidRadius() + 0.1f;
+        const Ogre::Real diagonal_distance = unboard_distance * 0.70710678f;
+        const Ogre::Vector3 directions[] = {
+          Ogre::Vector3(1, 0, 0), Ogre::Vector3(-1, 0, 0),
+          Ogre::Vector3(0, 0, 1), Ogre::Vector3(0, 0, -1),
+          Ogre::Vector3(1, 0, 1), Ogre::Vector3(-1, 0, 1),
+          Ogre::Vector3(1, 0, -1), Ogre::Vector3(-1, 0, -1)
+        };
+
+        for (size_t i = 0; i < sizeof(directions) / sizeof(directions[0]); ++i){
+          const Ogre::Real distance = i < 4 ? unboard_distance : diagonal_distance;
+          const Ogre::Vector3 requested_position
+            = vehicle_position + directions[i] * distance;
+          Ogre::Vector3 surface;
+          uint8 walkability_type = 0;
+          if (!FindWalkableSurface(
+              requested_position, surface, false, false, &walkability_type
+            ) || !IsFootWalkable(walkability_type)){
+            continue;
+          }
+          surface.y += player_height_offset_;
+          if (entity_manager.IsWorldPositionBlocked(player_entity_, surface)) continue;
+
+          player_entity_->SetPosition(surface);
+          vehicle_entity->SetTalkable(true);
+          entity_manager.ScriptSetPlayerEntity("WorldMapCloud");
+          ClearVehicleTerrains();
+          current_vehicle_ = VehicleType::ON_FOOT;
+          return true;
+        }
+        return false;
+      }
+
     bool WorldmapModule::frameStarted(const Ogre::FrameEvent& event){
       if (!initialized_ || player_entity_ == nullptr) return true;
 
@@ -930,16 +1110,29 @@ namespace VGears {
       else if (active_entity == submarine_entity_){
         current_vehicle_ = VehicleType::SUBMARINE;
       }
+      else if (active_entity == tiny_bronco_entity_){
+        current_vehicle_ = VehicleType::TINY_BRONCO;
+      }
       else{
         return true;
       }
 
         const bool highwind_active = active_entity == highwind_entity_;
       const bool vehicle_active = current_vehicle_ != VehicleType::ON_FOOT;
+        const bool cancel_pressed
+          = InputManager::getSingleton().IsButtonPressed(OIS::KC_ESCAPE);
+        if (vehicle_active && cancel_pressed && !cancel_was_pressed_){
+            ScriptManager::getSingleton().ScriptRequest(
+              ScriptManager::ENTITY, active_entity->GetName().c_str(), "unboard", 0
+            );
+        }
+        cancel_was_pressed_ = cancel_pressed;
       EntityModel* active_model = active_entity == player_entity_ ? player_entity_
         : (current_vehicle_ == VehicleType::CHOCOBO ? chocobo_entity_
         : (current_vehicle_ == VehicleType::BUGGY ? buggy_entity_
-          : (highwind_active ? highwind_entity_ : submarine_entity_)));
+          : (highwind_active ? highwind_entity_
+            : (current_vehicle_ == VehicleType::SUBMARINE
+              ? submarine_entity_ : tiny_bronco_entity_))));
 
         Ogre::Vector3 movement = Ogre::Vector3::ZERO;
         if (InputManager::getSingleton().IsButtonPressed(OIS::KC_LEFT)) movement.x -= 1.0f;
@@ -955,7 +1148,9 @@ namespace VGears {
           Ogre::Vector3 requested_position = current_position
             + movement * speed * event.timeSinceLastFrame;
           Ogre::Vector3 surface;
-          if (FindWalkableSurface(requested_position, surface, false, !highwind_active)){
+          size_t triangle_index = 0;
+          if (FindWalkableSurface(requested_position, surface, false, true, nullptr,
+              &triangle_index)){
                 if (highwind_active){
                     Ogre::Vector3 current_surface;
                     if (FindWalkableSurface(current_position, current_surface, false, false)){
@@ -969,6 +1164,20 @@ namespace VGears {
                   active_entity, surface
                 )){
                     active_entity->SetPosition(surface);
+                    const TerrainTriangle& triangle = terrain_triangles_[triangle_index];
+                    const int mesh_index = triangle.mesh_y * 36 + triangle.mesh_x;
+                    if (triangle.function_id >= 3){
+                        if (mesh_index != active_function_mesh_index_
+                          || triangle.function_id != active_function_id_){
+                            TriggerTriangleFunction(triangle);
+                        }
+                        active_function_mesh_index_ = mesh_index;
+                        active_function_id_ = triangle.function_id;
+                    }
+                    else{
+                        active_function_mesh_index_ = -1;
+                        active_function_id_ = -1;
+                    }
                     const Ogre::Vector3 model_forward(0.0f, 0.0f, 1.0f);
                     const Ogre::Vector3 move_direction(movement.x, 0.0f, movement.z);
                     const Ogre::Quaternion heading
@@ -1010,12 +1219,46 @@ namespace VGears {
       }
 
     void WorldmapModule::Init(){
-        if (initialized_) return;
-      EntityManager::getSingleton().SetWorldModule(); // Ensure the world module is set
+      if (initialized_){
+        if (frame_listener_attached_) return;
+        EntityManager::getSingleton().SetWorldModule();
+        DialogsManager::getSingleton().SetWorldModule();
+        world_map_node_->setVisible(true);
+        CreatePlayer();
+        Root::getSingleton().addFrameListener(this);
+        frame_listener_attached_ = true;
+        return;
+      }
+      EntityManager::getSingleton().SetWorldModule();
+        DialogsManager::getSingleton().SetWorldModule();
         CreateWorldMap(Root::getSingleton().getSceneManager("Scene"));
+        if (ResourceGroupManager::getSingleton().resourceExists(
+              "SCRIPTS", "worldmap/wm0.lua"
+            )){
+            ScriptManager::getSingleton().RunFile("scripts/worldmap/wm0.lua");
+        }
         CreatePlayer();
         initialized_ = true;
         Root::getSingleton().addFrameListener(this);
+      frame_listener_attached_ = true;
+    }
+
+    void WorldmapModule::Suspend(){
+      if (!frame_listener_attached_) return;
+      Root::getSingleton().removeFrameListener(this);
+      frame_listener_attached_ = false;
+      if (world_map_node_ != nullptr) world_map_node_->setVisible(false);
+      player_entity_ = nullptr;
+      highwind_entity_ = nullptr;
+      buggy_entity_ = nullptr;
+      chocobo_entity_ = nullptr;
+      submarine_entity_ = nullptr;
+      tiny_bronco_entity_ = nullptr;
+      current_vehicle_ = VehicleType::ON_FOOT;
+      active_vehicle_terrain_types_.clear();
+      active_function_mesh_index_ = -1;
+      active_function_id_ = -1;
+      cancel_was_pressed_ = false;
     }
 
 } // namespace VGears

+ 20 - 1
src/modules/worldmap/WorldmapModule.h

@@ -49,16 +49,25 @@ namespace VGears {
              * Initializes the world map module
              */
             void Init();
+            void Suspend();
 
             bool frameStarted(const Ogre::FrameEvent& event) override;
 
             VehicleType GetCurrentVehicle() const;
+            void ClearVehicleTerrains();
+            void AddVehicleTerrain(const char* terrain_name);
+            Ogre::String GetTerrainTypeUnderEntity(const char* entity_name) const;
+            int GetTerrainTriangleUnderEntity(const char* entity_name) const;
+            bool UnboardVehicle(const char* vehicle_name);
 
         private:
 
             struct TerrainTriangle{
                 Ogre::Vector3 vertices[3];
                 uint8 walkability_type;
+                uint8 function_id;
+                int mesh_x;
+                int mesh_y;
             };
 
             void CreateWorldMap(Ogre::SceneManager* scene_manager);
@@ -74,8 +83,12 @@ namespace VGears {
             void UpdateCamera(Ogre::Real delta_time, bool snap_to_player);
             bool FindWalkableSurface(
                             const Ogre::Vector3& position, Ogre::Vector3& surface,
-                            bool grass_only = false, bool require_foot_walkable = true
+                            bool grass_only = false, bool require_walkable = true,
+                            uint8* walkability_type = nullptr,
+                            size_t* triangle_index = nullptr
             ) const;
+            bool IsActiveTerrainAllowed(uint8 walkability_type) const;
+            void TriggerTriangleFunction(const TerrainTriangle& triangle);
 
             std::vector<TerrainTriangle> terrain_triangles_;
             Ogre::SceneManager* scene_manager_;
@@ -85,9 +98,15 @@ namespace VGears {
             EntityModel* buggy_entity_;
             EntityModel* chocobo_entity_;
             EntityModel* submarine_entity_;
+            EntityModel* tiny_bronco_entity_;
             VehicleType current_vehicle_;
+            std::vector<uint8> active_vehicle_terrain_types_;
             Ogre::Real player_height_offset_;
+            int active_function_mesh_index_;
+            int active_function_id_;
             bool initialized_;
+            bool frame_listener_attached_;
+            bool cancel_was_pressed_;
     };
 
 }

+ 1 - 1
test/data/worldmap/MapFileSerializer.cpp

@@ -168,7 +168,7 @@ BOOST_AUTO_TEST_CASE(TestMapFileSerializerReadsPackedTriangleFields){
         const VGears::WorldmapFileSerializer::BlockTriangle& triangle
             = part.triangles[0];
         BOOST_CHECK_EQUAL(triangle.walkability_info_, 5);
-        BOOST_CHECK_EQUAL(triangle.unknown, 5);
+                BOOST_CHECK_EQUAL(triangle.function_id, 5);
         BOOST_CHECK_EQUAL(triangle.vertex_0_u, 10);
         BOOST_CHECK_EQUAL(triangle.vertex_0_v, 20);
         BOOST_CHECK_EQUAL(triangle.vertex_1_u, 30);

+ 11 - 5
test/installer/decompiler/field/FieldCodeGenerator.cpp

@@ -16,11 +16,17 @@
 #include <boost/test/unit_test.hpp>
 #include "installer/decompiler/field/FieldCodeGenerator.h"
 
-#pragma message("Unimplemented test file: " __FILE__)
-
 BOOST_AUTO_TEST_CASE(TestFieldCodeGenerator){
-    // TODO
-    BOOST_WARN_MESSAGE(false, "Unimplemented test file: FieldCodeGenerator");
-    BOOST_CHECK(0 == 0);
+    FunctionMetaData entity_only("start_end_Cloud");
+    BOOST_CHECK(entity_only.IsStart());
+    BOOST_CHECK(entity_only.IsEnd());
+    BOOST_CHECK_EQUAL(entity_only.GetEntityName(), "Cloud");
+    BOOST_CHECK_EQUAL(entity_only.GetCharacterId(), -1);
+
+    FunctionMetaData with_character("start_end_0_Cloud");
+    BOOST_CHECK(with_character.IsStart());
+    BOOST_CHECK(with_character.IsEnd());
+    BOOST_CHECK_EQUAL(with_character.GetEntityName(), "Cloud");
+    BOOST_CHECK_EQUAL(with_character.GetCharacterId(), 0);
 }
 

+ 35 - 6
test/installer/decompiler/world/WorldDisassembler.cpp

@@ -14,13 +14,42 @@
  */
 
 #include <boost/test/unit_test.hpp>
-#include "installer/decompiler/world/WorldDisassembler.h"
-
-#pragma message("Unimplemented test file: " __FILE__)
+#include <sstream>
+#include <utility>
+#include <vector>
+#include "installer/decompiler/ControlFlow.h"
+#include "installer/decompiler/world/WorldEngine.h"
 
 BOOST_AUTO_TEST_CASE(TestWorldDisassembler){
-    // TODO
-    BOOST_WARN_MESSAGE(false, "Unimplemented test file: WorldDisassembler");
-    BOOST_CHECK(0 == 0);
+    std::vector<unsigned char> data(1026, 0);
+    data[4] = 1;
+    data[1024] = 0x03;
+    data[1025] = 0x02;
+
+    WorldEngine engine(1);
+    InstVec instructions;
+    std::unique_ptr<Disassembler> disassembler = engine.GetDisassembler(instructions);
+        disassembler->Open(std::move(data));
+    disassembler->Disassemble();
+
+    BOOST_REQUIRE_EQUAL(instructions.size(), 1u);
+    BOOST_REQUIRE_EQUAL(engine.functions.size(), 1u);
+    BOOST_CHECK_EQUAL(engine.functions.begin()->second.name, "world_function_1");
+
+        ControlFlow control_flow(instructions, engine);
+        control_flow.CreateGroups();
+        std::ostringstream generated_script;
+        std::unique_ptr<CodeGenerator> code_generator
+            = engine.GetCodeGenerator(instructions, generated_script);
+        code_generator->Generate(instructions, control_flow.GetGraph());
+        BOOST_CHECK_NE(
+            generated_script.str().find(
+                "System.WorldMap.world_function_1 = function(self)"
+            ),
+            std::string::npos
+        );
+        BOOST_CHECK_NE(generated_script.str().find("return;"), std::string::npos);
+        BOOST_CHECK_NE(generated_script.str().find("end"), std::string::npos);
+        BOOST_CHECK_EQUAL(generated_script.str().find("}\n"), std::string::npos);
 }