Forráskód Böngészése

Merge branch 'feature/world_installer' into develop

Iñigo Valentin 1 hete
szülő
commit
3ed80e0885
44 módosított fájl, 2723 hozzáadás és 594 törlés
  1. 3 0
      data/data/scripts/_scripts.xml
  2. 48 0
      data/data/scripts/field.lua
  3. 109 107
      data/data/scripts/menu/begin_menu.lua
  4. 85 0
      data/data/scripts/worldmap/triangle_triggers.lua
  5. 319 0
      data/data/scripts/worldmap/vehicles.lua
  6. 3 0
      src/CMakeLists.txt
  7. 25 1
      src/core/DialogsManager.cpp
  8. 8 0
      src/core/DialogsManager.h
  9. 46 4
      src/core/Entity.cpp
  10. 23 0
      src/core/Entity.h
  11. 116 2
      src/core/EntityManager.cpp
  12. 34 0
      src/core/EntityManager.h
  13. 14 11
      src/core/EntityModel.cpp
  14. 1 1
      src/core/EntityModel.h
  15. 2 0
      src/core/ScriptManager.cpp
  16. 13 1
      src/core/ScriptManagerBinds.h
  17. 199 88
      src/data/worldmap/WorldmapFileSerializer.cpp
  18. 6 7
      src/data/worldmap/WorldmapFileSerializer.h
  19. 1 0
      src/installer/CMakeLists.txt
  20. 15 2
      src/installer/DataInstaller.cpp
  21. 41 17
      src/installer/FieldDataInstaller.cpp
  22. 11 2
      src/installer/FieldDataInstaller.h
  23. 1 1
      src/installer/MainWindow.cpp
  24. 40 40
      src/installer/MainWindow.ui
  25. 194 0
      src/installer/WorldInstaller.cpp
  26. 8 0
      src/installer/WorldInstaller.h
  27. 5 0
      src/installer/decompiler/Disassembler.cpp
  28. 2 0
      src/installer/decompiler/Disassembler.h
  29. 18 5
      src/installer/decompiler/field/FieldCodeGenerator.cpp
  30. 91 29
      src/installer/decompiler/field/instruction/FieldModelInstruction.cpp
  31. 36 13
      src/installer/decompiler/field/instruction/FieldWindowInstruction.cpp
  32. 7 2
      src/installer/decompiler/world/WorldCodeGenerator.cpp
  33. 3 0
      src/installer/decompiler/world/WorldCodeGenerator.h
  34. 23 9
      src/installer/decompiler/world/WorldDisassembler.cpp
  35. 3 1
      src/installer/decompiler/world/WorldDisassembler.h
  36. 4 4
      src/installer/decompiler/world/instruction/WorldKernelCallInstruction.cpp
  37. 9 1
      src/installer/decompiler/world/instruction/WorldSubStackInstruction.cpp
  38. 6 0
      src/main.cpp
  39. 810 224
      src/modules/worldmap/WorldmapModule.cpp
  40. 77 1
      src/modules/worldmap/WorldmapModule.h
  41. 206 5
      test/data/worldmap/MapFileSerializer.cpp
  42. 11 5
      test/installer/decompiler/field/FieldCodeGenerator.cpp
  43. 35 6
      test/installer/decompiler/world/WorldDisassembler.cpp
  44. 12 5
      test/installer/decompiler/world/instruction/WorldSubStackInstruction.cpp

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

@@ -21,6 +21,8 @@
     <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" />
 
@@ -28,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 = "",
 

+ 109 - 107
data/data/scripts/menu/begin_menu.lua

@@ -1,31 +1,31 @@
 if UiContainer == nil then UiContainer = {} end
 
---- The begin menu
-UiContainer.BeginMenu = {
+    --- The begin menu
+    UiContainer.BeginMenu = {
 
-    --- Current cursor position
-    position = 1,
+        --- Current cursor position
+        position = 1,
 
-    -- Max cursor position
-    position_total = 7,
+        -- Max cursor position
+        position_total = 7,
 
-    --- Run when the menu is created.
-    --
-    -- Sets this menu as the one beind displayed.
-    on_start = function(self)
+        --- Run when the menu is created.
+        --
+        -- Sets this menu as the one beind displayed.
+        on_start = function(self)
         UiContainer.current_menu = "begin"
         local cursor = ui_manager:get_widget("BeginMenu.Container.Cursor")
         cursor:set_default_animation("Position" .. self.position)
         return 0
-    end,
+        end,
 
 
-    --- Handles button events.
-    --
-    -- Handles directional Up, Down and Enter keys.
-    -- @param button Pressed button string key. Config.CONTROLS.UP, Config.CONTROLS.LEFT, Config.CONTROLS.ACTION and Config.CONTROLS.CANCEL are handled.
-    -- @param event Trigger event. Normally, "Press".
-    on_button = function(self, button, event)
+        --- Handles button events.
+        --
+        -- Handles directional Up, Down and Enter keys.
+        -- @param button Pressed button string key. Config.CONTROLS.UP, Config.CONTROLS.LEFT, Config.CONTROLS.ACTION and Config.CONTROLS.CANCEL are handled.
+        -- @param event Trigger event. Normally, "Press".
+        on_button = function(self, button, event)
         if UiContainer.current_menu == "begin" then
             local cursor = ui_manager:get_widget("BeginMenu.Container.Cursor")
             if button == Config.CONTROLS.ACTION and event == "Press" then
@@ -37,103 +37,105 @@ UiContainer.BeginMenu = {
                     --console("debug_walkmesh true")
                     script:request_end_sync(Script.UI, "BeginMenu", "hide", 0)
                     MenuSettings.pause_available = true
-                elseif self.position == 2 then -- Load, show save menu.
-                    audio_manager:play_sound("Cursor")
-                    UiContainer.SaveMenu.operation = "load"
-                    script:request_end_sync(Script.UI, "SaveMenu", "show", 0)
-                elseif self.position == 3 then
-                    -- NMKIN_3 START
-                    Data.progress_game = 27
-                    load_field_map_request("nmkin_3", "")
-                    console("camera_free false")
-                    console("debug_walkmesh true")
-                    script:wait(1)
-                    script:request_end_sync(Script.UI, "BeginMenu", "hide", 0)
-                    entity_manager:get_entity("Cloud"):set_position(-1.090750, 15.310307, 17.281244)
-                    entity_manager:set_player_entity("Cloud")
-                    script:wait(1)
-                    entity_manager:get_player_entity():set_position(-1.093750, 15.320307, 17.281244)
-                    entity_manager:get_entity("Cloud"):jump_to_position(0.46875, 14.9922, -1, 1, 42) -- Triangle 42, 2 steps.
-                    entity_manager:get_entity("Cloud"):jump_sync()
-                    entity_manager:player_lock(false)
-                    MenuSettings.pause_available = true
-                    -- NMKIN_3 END
-                    -- NMKIN_4 START
-                    --[[Data.progress_game = 14
-                    --load_field_map_request("nmkin_4", "")
-                    map("nmkin_4")
-                    console("camera_free false")
-                    console("debug_walkmesh true")
-                    script:wait(1.5)
-                    script:request_end_sync(Script.UI, "BeginMenu", "hide", 0)
-                    entity_manager:get_entity("Cloud"):set_position(-0.820312, 0.500000, 17.281244)
-                    entity_manager:set_player_entity("Cloud")
-                    set_party(0, 1, 4)
-                    script:wait(0.5)
-                    entity_manager:get_player_entity():set_position(-0.820312, 0.500000, 17.281244)
-                    entity_manager:get_entity("Cloud"):jump_to_position(-0.800312, 0.500000, -1, 1) -- Triangle 42, 2 steps.
-                    entity_manager:get_entity("Cloud"):jump_sync()
-                    entity_manager:player_lock(false)
-                    print("TRIANGLE " .. entity_manager:get_entity("Cloud"):get_move_triangle_id())
-                    MenuSettings.pause_available = true]]
-                    -- NMKIN_4 END
-                elseif self.position == 4 then
-                    script:request_end_sync(Script.UI, "BeginMenu", "hide", 0)
-                    console("camera_free true")
-                    console("debug_walkmesh true")
-                    map("test_2")
-                    MenuSettings.pause_available = true
-                elseif self.position == 5 then
-                    script:request_end_sync(Script.UI, "BeginMenu", "hide", 0)
-                    console("camera_free true")
-                    world_map_module:init()
-                elseif self.position == 6 then
-                    load_field_map_request("startmap", "")
-                    console("camera_free false")
-                    console("debug_walkmesh false")
-                    script:request_end_sync(Script.UI, "BeginMenu", "hide", 0)
-                    MenuSettings.pause_available = true
-                elseif self.position == 7 then
-                    --load_field_map_request("startmap", "")
-                    console("camera_free false")
-                    console("debug_walkmesh false")
-                    script:request_end_sync(Script.UI, "BeginMenu", "hide", 0)
-                    set_party(0, 2, 3)
-                    Battle.start(220)
-                    MenuSettings.pause_available = true
-                end
-            elseif button == Config.CONTROLS.DOWN then
-                audio_manager:play_sound("Cursor")
-                self.position = self.position + 1
-                if self.position > self.position_total then
-                    self.position = 1;
-                end
-                cursor:set_default_animation("Position" .. self.position)
-            elseif button == Config.CONTROLS.UP then
-                audio_manager:play_sound("Cursor")
-                self.position = self.position - 1
-                if self.position <= 0 then
-                    self.position = self.position_total;
-                end
-                cursor:set_default_animation("Position" .. self.position)
-            end
+                    elseif self.position == 2 then -- Load, show save menu.
+                        audio_manager:play_sound("Cursor")
+                        UiContainer.SaveMenu.operation = "load"
+                        script:request_end_sync(Script.UI, "SaveMenu", "show", 0)
+                        elseif self.position == 3 then
+                            -- NMKIN_3 START
+                            Data.progress_game = 27
+                            load_field_map_request("nmkin_3", "")
+                            console("camera_free false")
+                            console("debug_walkmesh true")
+                            script:wait(1)
+                            script:request_end_sync(Script.UI, "BeginMenu", "hide", 0)
+                            entity_manager:get_entity("Cloud"):set_position(-1.090750, 15.310307, 17.281244)
+                            entity_manager:set_player_entity("Cloud")
+                            script:wait(1)
+                            entity_manager:get_player_entity():set_position(-1.093750, 15.320307, 17.281244)
+                            entity_manager:get_entity("Cloud"):jump_to_position(0.46875, 14.9922, -1, 1, 42) -- Triangle 42, 2 steps.
+                            entity_manager:get_entity("Cloud"):jump_sync()
+                            entity_manager:player_lock(false)
+                            MenuSettings.pause_available = true
+                            -- NMKIN_3 END
+                            -- NMKIN_4 START
+                            --[[Data.progress_game = 14
+                            --load_field_map_request("nmkin_4", "")
+                            map("nmkin_4")
+                            console("camera_free false")
+                            console("debug_walkmesh true")
+                            script:wait(1.5)
+                            script:request_end_sync(Script.UI, "BeginMenu", "hide", 0)
+                            entity_manager:get_entity("Cloud"):set_position(-0.820312, 0.500000, 17.281244)
+                            entity_manager:set_player_entity("Cloud")
+                            set_party(0, 1, 4)
+                            script:wait(0.5)
+                            entity_manager:get_player_entity():set_position(-0.820312, 0.500000, 17.281244)
+                            entity_manager:get_entity("Cloud"):jump_to_position(-0.800312, 0.500000, -1, 1) -- Triangle 42, 2 steps.
+                            entity_manager:get_entity("Cloud"):jump_sync()
+                            entity_manager:player_lock(false)
+                            print("TRIANGLE " .. entity_manager:get_entity("Cloud"):get_move_triangle_id())
+                            MenuSettings.pause_available = true]]
+                            -- NMKIN_4 END
+                            elseif self.position == 4 then
+                                script:request_end_sync(Script.UI, "BeginMenu", "hide", 0)
+                                console("camera_free true")
+                                console("debug_walkmesh true")
+                                map("test_2")
+                                MenuSettings.pause_available = true
+                                elseif self.position == 5 then
+                                    script:request_end_sync(Script.UI, "BeginMenu", "hide", 0)
+                                    console("camera_free true")
+                                    console("debug_entity 2")
+                                    MenuSettings.pause_available = true
+                                    world_map_module:init()
+                                    elseif self.position == 6 then
+                                        load_field_map_request("startmap", "")
+                                        console("camera_free false")
+                                        console("debug_walkmesh false")
+                                        script:request_end_sync(Script.UI, "BeginMenu", "hide", 0)
+                                        MenuSettings.pause_available = true
+                                        elseif self.position == 7 then
+                                            --load_field_map_request("startmap", "")
+                                            console("camera_free false")
+                                            console("debug_walkmesh false")
+                                            script:request_end_sync(Script.UI, "BeginMenu", "hide", 0)
+                                            set_party(0, 2, 3)
+                                            Battle.start(220)
+                                            MenuSettings.pause_available = true
+                                            end
+                                            elseif button == Config.CONTROLS.DOWN then
+                                                audio_manager:play_sound("Cursor")
+                                                self.position = self.position + 1
+                                                if self.position > self.position_total then
+                                                    self.position = 1;
+        end
+        cursor:set_default_animation("Position" .. self.position)
+        elseif button == Config.CONTROLS.UP then
+            audio_manager:play_sound("Cursor")
+            self.position = self.position - 1
+            if self.position <= 0 then
+                self.position = self.position_total;
+        end
+        cursor:set_default_animation("Position" .. self.position)
+        end
         end
         return 0
-    end,
+        end,
 
-    --- Opens the save menu.
-    show = function(self)
+        --- Opens the save menu.
+        show = function(self)
         UiContainer.current_menu = "begin"
         ui_manager:get_widget("BeginMenu"):set_visible(true)
         return 0;
-    end,
+        end,
 
-    --- Hiddens the begin menu.
-    --
-    -- It doesn't do anything else.
-    hide = function(self)
+        --- Hiddens the begin menu.
+        --
+        -- It doesn't do anything else.
+        hide = function(self)
         UiContainer.current_menu = ""
         ui_manager:get_widget("BeginMenu"):set_visible(false)
         return 0;
-    end,
-}
+        end,
+    }

+ 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

+ 319 - 0
data/data/scripts/worldmap/vehicles.lua

@@ -0,0 +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
+
+--[[
+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 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(self.entity_name)
+    if not player_entity or not vehicle_entity then
+        return false
+    end
+    local player_name = player_entity:get_name()
+    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
+
+    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",
+        "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)
+    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["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
+
+    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
+
+    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"] = 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"] = extend_vehicle(
+    "WorldMapTinyBronco",
+    {
+        "RIVERSIDE", "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
          *

+ 46 - 4
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");
 
@@ -94,7 +95,7 @@ Entity::Entity(const Ogre::String& name, Ogre::SceneNode* node):
     //talk_collision_->setMaterial("entity/talk_collision");
     Ogre::MaterialPtr mat_talk_collision
       = Ogre::MaterialManager::getSingleton().getByName("entity/talk_collision");
-    solid_collision_->setMaterial(mat_talk_collision);
+    talk_collision_->setMaterial(mat_talk_collision);
     talk_collision_node_ = scene_node_->createChildSceneNode();
     talk_collision_node_->setScale(talk_radius_, talk_radius_, height_);
     talk_collision_node_->attachObject(talk_collision_);
@@ -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_);
@@ -213,6 +239,11 @@ void Entity::ScriptSetPosition(const float x, const float y, const float z){
     move_triangle_id_ = -1;
 }
 
+  void Entity::ScriptMoveBy(const float x, const float y, const float z){
+    SetPosition(GetPosition() + Ogre::Vector3(x, y, z));
+    move_triangle_id_ = -1;
+  }
+
 const Ogre::Vector3 Entity::GetPosition() const{return scene_node_->getPosition();}
 
 void Entity::ScriptGetPosition() const{
@@ -280,6 +311,17 @@ void Entity::setRootOrientation(const Ogre::Quaternion &root_orientation){
 
 float Entity::GetHeight() const{return height_;}
 
+void Entity::SetHeight(const float height){
+  height_ = height;
+  solid_collision_node_->setScale(solid_radius_, solid_radius_, height_);
+  talk_collision_node_->setScale(talk_radius_, talk_radius_, height_);
+}
+
+void Entity::SetCollisionDebugOrientation(const Ogre::Quaternion& orientation){
+  solid_collision_node_->setOrientation(orientation);
+  talk_collision_node_->setOrientation(orientation);
+}
+
 void Entity::SetSolidRadius(const float radius){
     solid_radius_ = radius;
     solid_collision_node_->setScale(solid_radius_, solid_radius_, height_);

+ 23 - 0
src/core/Entity.h

@@ -224,6 +224,15 @@ class Entity{
          */
         void ScriptSetPosition(const float x, const float y, const float z);
 
+        /**
+         * Moves the entity by the specified position offset.
+         *
+         * @param[in] x X coordinate offset.
+         * @param[in] y Y coordinate offset.
+         * @param[in] z Z coordinate offset.
+         */
+        void ScriptMoveBy(const float x, const float y, const float z);
+
         /**
          * Retrieves the entity position.
          *
@@ -320,6 +329,20 @@ class Entity{
          */
         float GetHeight() const;
 
+        /**
+         * Sets the entity collision height and updates its debug proxies.
+         *
+         * @param[in] height Height in map units.
+         */
+        void SetHeight(const float height);
+
+        /**
+         * Sets the orientation of the debug collision proxies.
+         *
+         * @param[in] orientation Proxy orientation relative to the entity.
+         */
+        void SetCollisionDebugOrientation(const Ogre::Quaternion& orientation);
+
         /**
          * Sets the entity's solid radius.
          *

+ 116 - 2
src/core/EntityManager.cpp

@@ -101,6 +101,7 @@ Ogre::Degree EntityManager::GetDirectionToPoint(
 }
 
 EntityManager::EntityManager():
+    background_3d_(nullptr),
   player_entity_(nullptr),
   player_move_(Ogre::Vector3::ZERO),
   player_move_rotation_(0),
@@ -305,7 +306,12 @@ void EntityManager::UpdateBattle(){
 }
 
 void EntityManager::UpdateWorld(){
-    // TODO: Implement
+    ScriptManager::getSingleton().Update(ScriptManager::ENTITY);
+    for (Entity* entity : entity_){
+        entity->Update();
+    }
+    player_move_ = Ogre::Vector3::ZERO;
+    player_run_ = false;
 }
 
 void EntityManager::AddFieldOrWorldMapEntity(
@@ -328,6 +334,31 @@ void EntityManager::AddFieldOrWorldMapEntity(
     ScriptManager::getSingleton().AddEntity(ScriptManager::ENTITY, entity->GetName(), entity);
 }
 
+EntityModel* EntityManager::AddWorldMapEntity(
+  const Ogre::String& name, const Ogre::String& file_name,
+  const Ogre::Vector3& position, const Ogre::Degree& rotation,
+  const Ogre::Vector3& scale, const Ogre::String& resource_group
+){
+    if (!IsWorldModule()){
+        LOG_ERROR("Tried to add a world-map entity outside world-map mode.");
+        return nullptr;
+    }
+    if (GetEntity(name) != nullptr){
+        LOG_ERROR("Tried to add duplicate world-map entity '" + name + "'.");
+        return nullptr;
+    }
+
+    Ogre::SceneNode* node = scene_node_->createChildSceneNode("Model_" + name);
+    EntityModel* entity = new EntityModel(name, file_name, node, resource_group);
+    entity->SetPosition(position);
+    entity->SetRotation(rotation);
+    entity->setScale(scale);
+    entity->SetVisible(true);
+    entity_.push_back(entity);
+    ScriptManager::getSingleton().AddEntity(ScriptManager::ENTITY, entity->GetName(), entity);
+    return entity;
+}
+
 void EntityManager::AddBattleEntity(
   const Ogre::String& name, const Ogre::String& file_name, const Ogre::Vector3& position,
   const Ogre::Degree& rotation, const Ogre::Vector3& scale,
@@ -474,11 +505,54 @@ void EntityManager::ClearBattle(){
 }
 
 void EntityManager::ClearWorld(){
-    // TODO implement
+    walkmesh_.Clear();
+    for (Entity* entity : entity_){
+        ScriptManager::getSingleton().RemoveEntity(ScriptManager::ENTITY, entity->GetName());
+        delete entity;
+    }
+    entity_.clear();
+    player_entity_ = nullptr;
+    player_move_ = Ogre::Vector3::ZERO;
+    player_move_rotation_ = 0;
+    player_lock_ = false;
+    player_run_ = false;
+    for (EntityTrigger* trigger : entity_triggers_){
+        ScriptManager::getSingleton().RemoveEntity(ScriptManager::ENTITY, trigger->GetName());
+        delete trigger;
+    }
+    entity_triggers_.clear();
+    for (EntityPoint* point : entity_points_) delete point;
+    entity_points_.clear();
+    for (const Ogre::String& script : entity_scripts_){
+        ScriptManager::getSingleton().RemoveEntity(ScriptManager::ENTITY, script);
+    }
+    entity_scripts_.clear();
+    scene_node_->removeAndDestroyAllChildren();
 }
 
 Walkmesh* EntityManager::GetWalkmesh(){return &walkmesh_;}
 
+bool EntityManager::IsWorldPositionBlocked(
+    const Entity* moving_entity, const Ogre::Vector3& position
+) const{
+        if (!IsWorldModule() || moving_entity == nullptr) return false;
+
+        for (const Entity* other : entity_){
+                if (other == moving_entity || !other->IsSolid()) continue;
+
+                const Ogre::Vector3 other_position = other->GetPosition();
+                const Ogre::Real dx = position.x - other_position.x;
+                const Ogre::Real dz = position.z - other_position.z;
+                const Ogre::Real radius = moving_entity->GetSolidRadius() + other->GetSolidRadius();
+                if (dx * dx + dz * dz >= radius * radius) continue;
+
+                const Ogre::Real moving_top = position.y + moving_entity->GetHeight();
+                const Ogre::Real other_top = other_position.y + other->GetHeight();
+                if (position.y < other_top && other_position.y < moving_top) return true;
+        }
+        return false;
+}
+
 Background2D* EntityManager::GetBackground2D(){return &background_2d_;}
 
 void EntityManager::AddEntity(
@@ -610,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.");
@@ -1219,6 +1312,7 @@ void EntityManager::CheckEntityInteract(){
     Ogre::Degree angle_pc = player_entity_->GetRotation();
     Entity* entity_to_interact = NULL;
     Ogre::Degree less_angle(90);
+    Ogre::Real nearest_worldmap_distance = -1.0f;
     for (unsigned int i = 0; i < entity_.size(); ++ i){
         if (entity_[i]->IsTalkable() == false) continue;
         if (entity_[i] == player_entity_) continue;
@@ -1226,6 +1320,26 @@ void EntityManager::CheckEntityInteract(){
         float interact_range =  entity_[i]->GetTalkRadius();
         Ogre::Vector3 pos2 = player_entity_->GetPosition();
         float solid_range = player_entity_->GetSolidRadius();
+                if (IsWorldModule()){
+                        const Ogre::Real target_bottom = pos1.y;
+                        const Ogre::Real player_bottom = pos2.y;
+                                                const Ogre::Real talk_range
+                                                    = interact_range + solid_range + 0.05f;
+                        const Ogre::Real distance
+                            = (pos1.x - pos2.x) * (pos1.x - pos2.x)
+                                + (pos1.z - pos2.z) * (pos1.z - pos2.z);
+                        const bool height_overlap
+                            = player_bottom <= target_bottom + entity_[i]->GetHeight()
+                                && player_bottom + player_entity_->GetHeight() >= target_bottom;
+                        if (
+                                height_overlap && distance <= talk_range * talk_range
+                                && (nearest_worldmap_distance < 0 || distance < nearest_worldmap_distance)
+                        ){
+                                entity_to_interact = entity_[i];
+                                nearest_worldmap_distance = distance;
+                        }
+                        continue;
+                }
         int height = (pos1.z < pos2.z) ? entity_[i]->GetHeight() : player_entity_->GetHeight();
         if (((pos1.z - pos2.z + height) < (height * 2)) && ((pos1.z - pos2.z + height) >= 0)){
             interact_range = (interact_range + solid_range) * (interact_range + solid_range);

+ 34 - 0
src/core/EntityManager.h

@@ -24,6 +24,8 @@
 #include "Manager.h"
 #include "Walkmesh.h"
 
+class EntityModel;
+
 /**
  * The entity manager.
  */
@@ -88,6 +90,17 @@ class EntityManager : public Manager, public Ogre::Singleton<EntityManager>{
          */
         Walkmesh* GetWalkmesh();
 
+        /**
+         * Checks a proposed world-map position against managed solid entities.
+         *
+         * @param[in] moving_entity Entity being moved.
+         * @param[in] position Proposed base position in world-map coordinates.
+         * @return True if the entity would overlap another solid entity.
+         */
+        bool IsWorldPositionBlocked(
+          const Entity* moving_entity, const Ogre::Vector3& position
+        ) const;
+
         /**
          * Retrieves the 2D background.
          *
@@ -130,6 +143,23 @@ class EntityManager : public Manager, public Ogre::Singleton<EntityManager>{
           const Ogre::Vector3& scale, const Ogre::Quaternion& root_orientation, const int index
         );
 
+        /**
+         * Adds a managed world-map entity using an explicit resource group.
+         *
+         * @param[in] name Entity name.
+         * @param[in] file_name Mesh resource name.
+         * @param[in] position Initial position.
+         * @param[in] rotation Initial rotation.
+         * @param[in] scale Initial scale.
+         * @param[in] resource_group Ogre resource group containing the mesh.
+         * @return The managed entity, or nullptr if the manager is not in world mode.
+         */
+        EntityModel* AddWorldMapEntity(
+          const Ogre::String& name, const Ogre::String& file_name,
+          const Ogre::Vector3& position, const Ogre::Degree& rotation,
+          const Ogre::Vector3& scale, const Ogre::String& resource_group
+        );
+
         /**
          * Adds an entity to the manager.
          *
@@ -251,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.
          *

+ 14 - 11
src/core/EntityModel.cpp

@@ -22,21 +22,24 @@
 #include "core/Timer.h"
 
 EntityModel::EntityModel(
-  const Ogre::String& name, const Ogre::String file_name, Ogre::SceneNode* node
+  const Ogre::String& name, const Ogre::String file_name, Ogre::SceneNode* node,
+  const Ogre::String& resource_group
 ): Entity(name, node), animation_current_(nullptr)
 {
     Ogre::SceneManager* scene_manager;
-    Ogre::String res_group;
+  Ogre::String res_group = resource_group;
+  if (res_group.empty()){
     switch(EntityManager::getSingleton().GetModule()){
-        case Manager::BATTLE:
-            res_group = "BATTLE_MODELS";
-            break;
-        case Manager::WORLD:
-            res_group = "WORLD_MODELS";
-            break;
-        case Manager::FIELD:
-        default:
-            res_group = "FIELD_MODELS";
+      case Manager::BATTLE:
+        res_group = "BATTLE_MODELS";
+        break;
+      case Manager::WORLD:
+        res_group = "WORLD_MODELS";
+        break;
+      case Manager::FIELD:
+      default:
+        res_group = "FIELD_MODELS";
+    }
     }
     scene_manager = Ogre::Root::getSingleton().getSceneManager("Scene");
     model_ = scene_manager->createEntity(name_, file_name, res_group);

+ 1 - 1
src/core/EntityModel.h

@@ -36,7 +36,7 @@ class EntityModel : public Entity{
          */
         EntityModel(
           const Ogre::String& name, const Ogre::String file_name,
-          Ogre::SceneNode* node
+          Ogre::SceneNode* node, const Ogre::String& resource_group = ""
         );
         virtual ~EntityModel();
 

+ 2 - 0
src/core/ScriptManager.cpp

@@ -630,6 +630,8 @@ bool ScriptManager::ScriptRequest(
   ScriptEntity* script_entity, const Ogre::String& function, const int priority,
   const Ogre::String& argument1, const Ogre::String& argument2, bool start_sync, bool end_sync
 ){
+    if (script_entity == nullptr) return false;
+
     sol::object table
       = GetTableByEntityName(script_entity->type, script_entity->name, lua_state_);
     sol::object table_function;

+ 13 - 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);
@@ -110,7 +112,9 @@ void ScriptManager::InitBinds(){
     lua.new_usertype<Entity>(
       "Entity",
       "set_position", &Entity::ScriptSetPosition,
+      "move_by", &Entity::ScriptMoveBy,
       "get_position", &Entity::ScriptGetPosition,
+      "get_name", &Entity::GetName,
       "set_rotation", &Entity::ScriptSetRotation,
       "get_rotation", &Entity::ScriptGetRotation,
       "set_solid_radius", &Entity::SetSolidRadius,
@@ -188,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,
@@ -389,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,
@@ -468,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

+ 199 - 88
src/data/worldmap/WorldmapFileSerializer.cpp

@@ -15,14 +15,88 @@
 
 #include "WorldmapFileSerializer.h"
 
+#include <OgreException.h>
+#include <string>
 #include "common/LzsFile.h"
 
 namespace VGears {
 
-    /**
-     * Size of each block in a world map file.
-     */
-    const int WORLD_MAP_BLOCK_SIZE = 0xB800;
+    namespace {
+
+        const size_t WORLD_MAP_BLOCK_SIZE = 0xB800;
+        const size_t WORLD_MAP_BLOCK_HEADER_SIZE = sizeof(uint32) * 16;
+        const size_t WORLD_MAP_TRIANGLE_SIZE = 12;
+        const size_t WORLD_MAP_VERTEX_SIZE = 8;
+
+        uint16 ReadLittleEndian16(const std::vector<uint8>& data, size_t offset){
+            return static_cast<uint16>(data[offset])
+              | (static_cast<uint16>(data[offset + 1]) << 8);
+        }
+
+        uint32 ReadLittleEndian32(const std::vector<uint8>& data, size_t offset){
+            return static_cast<uint32>(data[offset])
+              | (static_cast<uint32>(data[offset + 1]) << 8)
+              | (static_cast<uint32>(data[offset + 2]) << 16)
+              | (static_cast<uint32>(data[offset + 3]) << 24);
+        }
+
+        sint16 ReadLittleEndianSigned16(
+          const std::vector<uint8>& data, size_t offset
+        ){
+            const uint16 value = ReadLittleEndian16(data, offset);
+            const sint32 signed_value = value <= 0x7FFF
+              ? static_cast<sint32>(value)
+              : static_cast<sint32>(value) - 0x10000;
+            return static_cast<sint16>(signed_value);
+        }
+
+        void ThrowInvalidWorldmap(const std::string& reason){
+            OGRE_EXCEPT(
+              Ogre::Exception::ERR_INVALIDPARAMS,
+              "Invalid world map file: " + reason,
+              "WorldmapFileSerializer::ImportWorldmapFile"
+            );
+        }
+
+        void ValidateLzsData(
+          const std::vector<uint8>& data, size_t max_output_size
+        ){
+            if (data.size() <= 4
+              || ReadLittleEndian32(data, 0) != data.size() - 4){
+                ThrowInvalidWorldmap("invalid LZS payload size");
+            }
+
+            size_t input_offset = 4;
+            size_t output_size = 0;
+            while (input_offset < data.size()){
+                const uint8 control = data[input_offset++];
+                bool has_token = false;
+                for (unsigned int bit = 0;
+                  bit < 8 && input_offset < data.size(); ++bit){
+                    has_token = true;
+                    size_t token_output_size = 1;
+                    if ((control & (1u << bit)) == 0){
+                        if (data.size() - input_offset < 2){
+                            ThrowInvalidWorldmap("truncated LZS back-reference");
+                        }
+                        token_output_size
+                          = static_cast<size_t>(data[input_offset + 1] & 0x0F) + 3;
+                        input_offset += 2;
+                    }
+                    else ++input_offset;
+
+                    if (token_output_size > max_output_size - output_size){
+                        ThrowInvalidWorldmap("LZS output exceeds a MAP block");
+                    }
+                    output_size += token_output_size;
+                }
+                if (!has_token){
+                    ThrowInvalidWorldmap("truncated LZS control byte");
+                  }
+            }
+        }
+
+    }
 
     WorldmapFileSerializer::WorldmapFileSerializer(){}
 
@@ -31,101 +105,138 @@ namespace VGears {
     void WorldmapFileSerializer::ImportWorldmapFile(
       Ogre::DataStreamPtr& stream, WorldmapFile& dest
     ){
-        const auto fileSize = stream->size();
-        auto nublocks = fileSize / WORLD_MAP_BLOCK_SIZE;
-        for (unsigned int j = 0; j < nublocks; j ++){
+        static_cast<void>(dest);
+        if (stream.isNull()){
+            ThrowInvalidWorldmap("input stream is null");
+        }
+
+        const size_t file_size = stream->size();
+        if (file_size == 0 || file_size % WORLD_MAP_BLOCK_SIZE != 0){
+            ThrowInvalidWorldmap("file size is not a whole number of MAP blocks");
+        }
+
+        std::vector<uint8> file_data(file_size);
+        stream->seek(0);
+        if (stream->read(file_data.data(), file_data.size()) != file_data.size()){
+            ThrowInvalidWorldmap("could not read the complete file");
+        }
+
+        std::vector<Block> parsed_blocks;
+        const size_t block_count = file_size / WORLD_MAP_BLOCK_SIZE;
+        parsed_blocks.reserve(block_count);
+        for (size_t block_index = 0; block_index < block_count; ++block_index){
             Block block;
-            const size_t basePos = WORLD_MAP_BLOCK_SIZE * j;
-            stream->seek(basePos);
-            // Read the offset to compressed data in this block.
-            BlockHeader header = {};
-            for (auto i = 0u; i < 16; i ++)
-                ReadUInt32(stream, header.compressed_data_offsets[i]);
-            for (auto i = 0u; i < 16; i ++){
-                BlockPart block_part;
-                // Go to the offset.
-                stream->seek(basePos + header.compressed_data_offsets[i]);
-                // Read the size of the compressed data.
-                uint32 compressed_data_size = 0;
-                ReadUInt32(stream, compressed_data_size);
-                // Go back to before the compressed data size.
-                stream->seek(basePos + header.compressed_data_offsets[i]);
-                // Read the compressed data into a temp buffer, including the
-                // compressed data size.
-                std::vector<uint8> buffer(compressed_data_size + 4);
-                stream->read(buffer.data(), buffer.size());
-                // Decompress the data.
-                auto decompressed = LzsBuffer::Decompress(buffer);
-                Ogre::MemoryDataStream decompressed_stream(
-                  decompressed.data(), decompressed.size(), false, true
+            const size_t block_start = block_index * WORLD_MAP_BLOCK_SIZE;
+            block.meshes.reserve(16);
+            for (size_t mesh_index = 0; mesh_index < 16; ++mesh_index){
+                const uint32 mesh_offset = ReadLittleEndian32(
+                  file_data, block_start + mesh_index * sizeof(uint32)
                 );
-                ReadUInt16(
-                  decompressed_stream, block_part.header.triengle_count
+                if (mesh_offset < WORLD_MAP_BLOCK_HEADER_SIZE
+                  || mesh_offset % 4 != 0
+                  || mesh_offset > WORLD_MAP_BLOCK_SIZE - sizeof(uint32)){
+                    ThrowInvalidWorldmap("mesh offset is invalid");
+                }
+
+                const size_t mesh_start = block_start + mesh_offset;
+                const uint32 compressed_data_size
+                  = ReadLittleEndian32(file_data, mesh_start);
+                const size_t max_compressed_size
+                  = WORLD_MAP_BLOCK_SIZE - mesh_offset - sizeof(uint32);
+                if (compressed_data_size > max_compressed_size){
+                    ThrowInvalidWorldmap("compressed mesh extends past its MAP block");
+                }
+
+                const size_t compressed_size
+                  = static_cast<size_t>(compressed_data_size) + sizeof(uint32);
+                std::vector<uint8> compressed_data(
+                  file_data.begin() + mesh_start,
+                  file_data.begin() + mesh_start + compressed_size
                 );
-                ReadUInt16(decompressed_stream, block_part.header.vertex_count);
-
-                /*
-                std::cout << "block: " << j
-                  << " from offset " << header.compressed_data_offsets[i]
-                  << " old size: " << buffer.size()
-                  << " decompressed size is " << decompressed.size()
-                  << " header is tris: " <<  block_part.header.triengle_count
-                  << " verts " << block_part.header.vertex_count
-                  << std::endl;
-                */
+                ValidateLzsData(compressed_data, WORLD_MAP_BLOCK_SIZE);
+                const std::vector<uint8> decompressed
+                  = LzsBuffer::Decompress(compressed_data);
+                if (decompressed.size() < 4){
+                    ThrowInvalidWorldmap("decompressed mesh header is truncated");
+                }
 
+                BlockPart block_part;
+                block_part.header.triengle_count
+                  = ReadLittleEndian16(decompressed, 0);
+                block_part.header.vertex_count
+                  = ReadLittleEndian16(decompressed, 2);
+                const size_t expected_size = 4
+                  + static_cast<size_t>(block_part.header.triengle_count)
+                    * WORLD_MAP_TRIANGLE_SIZE
+                  + static_cast<size_t>(block_part.header.vertex_count)
+                    * WORLD_MAP_VERTEX_SIZE * 2;
+                if (expected_size > decompressed.size()){
+                    ThrowInvalidWorldmap("decompressed mesh data is truncated");
+                }
+
+                size_t data_offset = 4;
                 block_part.triangles.resize(block_part.header.triengle_count);
-                for (int k = 0; k < block_part.header.triengle_count; k ++){
-                    BlockTriangle& s = block_part.triangles[k];
-                    ReadUInt8(decompressed_stream, s.vertex_0_index);
-                    ReadUInt8(decompressed_stream, s.vertex_1_index);
-                    ReadUInt8(decompressed_stream, s.vertex_2_index);
-                    ReadUInt8(decompressed_stream, s.walkability_info_);
-                    //ReadUInt8(decompressed_stream, s.unknown);
-                    ReadUInt8(decompressed_stream, s.vertex_0_u);
-                    ReadUInt8(decompressed_stream, s.vertex_0_v);
-                    ReadUInt8(decompressed_stream, s.vertex_1_u);
-                    ReadUInt8(decompressed_stream, s.vertex_1_v);
-                    ReadUInt8(decompressed_stream, s.vertex_2_u);
-                    ReadUInt8(decompressed_stream, s.vertex_2_v);
-                    ReadUInt16(decompressed_stream, s.TextureInfo);
-                    s.TextureInfo = s.TextureInfo & 0x1FF;
-                    //ReadUInt16(decompressed_stream, s.Location);
-                    /*
-                    std::cout << "v0: " << int(s.vertex_0_index)
-                      << " v1 " << int(s.vertex_1_index)
-                      << " v2 " << int(s.vertex_2_index)
-                      << " walk " << int(s.walkability_info_)
-                      << " u1 " << int(s.vertex_1_u)
-                      << " v1 " << int(s.vertex_1_v)
-                      << " v2 " << int(s.vertex_2_u)
-                      << " u2 " << int(s.vertex_2_v)
-                      << " texture " << s.TextureInfo
-                      << " locId " << s.Location
-                      << std::endl;
-                    */
+                for (size_t triangle_index = 0;
+                  triangle_index < block_part.triangles.size(); ++triangle_index){
+                    BlockTriangle& triangle = block_part.triangles[triangle_index];
+                    triangle.vertex_0_index = decompressed[data_offset++];
+                    triangle.vertex_1_index = decompressed[data_offset++];
+                    triangle.vertex_2_index = decompressed[data_offset++];
+                    const uint8 walkmap_and_function = decompressed[data_offset++];
+                    triangle.walkability_info_ = walkmap_and_function & 0x1F;
+                    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++];
+                    triangle.vertex_1_v = decompressed[data_offset++];
+                    triangle.vertex_2_u = decompressed[data_offset++];
+                    triangle.vertex_2_v = decompressed[data_offset++];
+                    const uint16 texture_and_location
+                      = ReadLittleEndian16(decompressed, data_offset);
+                    data_offset += sizeof(uint16);
+                    triangle.TextureInfo = texture_and_location & 0x1FF;
+                    triangle.Location = (texture_and_location >> 9) & 0x7F;
+
+                    if (triangle.vertex_0_index >= block_part.header.vertex_count
+                      || triangle.vertex_1_index >= block_part.header.vertex_count
+                      || triangle.vertex_2_index >= block_part.header.vertex_count){
+                        ThrowInvalidWorldmap("triangle references a missing vertex");
+                    }
                 }
-                block_part.normals.resize(block_part.header.vertex_count);
+
                 block_part.vertices.resize(block_part.header.vertex_count);
-                // All vertices.
-                for (int k = 0; k < block_part.header.vertex_count; k ++){
-                    Vertex& v = block_part.vertices[k];
-                    ReadInt16(decompressed_stream, v.x);
-                    ReadInt16(decompressed_stream, v.y);
-                    ReadInt16(decompressed_stream, v.z);
-                    ReadUInt16(decompressed_stream, v.unused);
+                for (size_t vertex_index = 0;
+                  vertex_index < block_part.vertices.size(); ++vertex_index){
+                    Vertex& vertex = block_part.vertices[vertex_index];
+                    vertex.x = ReadLittleEndianSigned16(decompressed, data_offset);
+                    data_offset += sizeof(uint16);
+                    vertex.y = ReadLittleEndianSigned16(decompressed, data_offset);
+                    data_offset += sizeof(uint16);
+                    vertex.z = ReadLittleEndianSigned16(decompressed, data_offset);
+                    data_offset += sizeof(uint16);
+                    vertex.unused = ReadLittleEndian16(decompressed, data_offset);
+                    data_offset += sizeof(uint16);
                 }
-                // Then all normals.
-                for (int k = 0; k < block_part.header.vertex_count; k ++){
-                    Normal& n = block_part.normals[k];
-                    ReadInt16(decompressed_stream, n.x);
-                    ReadInt16(decompressed_stream, n.y);
-                    ReadInt16(decompressed_stream, n.z);
-                    ReadUInt16(decompressed_stream, n.unused);
+
+                block_part.normals.resize(block_part.header.vertex_count);
+                for (size_t normal_index = 0;
+                  normal_index < block_part.normals.size(); ++normal_index){
+                    Normal& normal = block_part.normals[normal_index];
+                    normal.x = ReadLittleEndianSigned16(decompressed, data_offset);
+                    data_offset += sizeof(uint16);
+                    normal.y = ReadLittleEndianSigned16(decompressed, data_offset);
+                    data_offset += sizeof(uint16);
+                    normal.z = ReadLittleEndianSigned16(decompressed, data_offset);
+                    data_offset += sizeof(uint16);
+                    normal.unused = ReadLittleEndian16(decompressed, data_offset);
+                    data_offset += sizeof(uint16);
                 }
+
                 block.meshes.push_back(block_part);
             }
-            blocks.push_back(block);
+            parsed_blocks.push_back(block);
         }
+
+        blocks.swap(parsed_blocks);
     }
 }

+ 6 - 7
src/data/worldmap/WorldmapFileSerializer.h

@@ -36,8 +36,8 @@ namespace VGears {
                 /**
                  * Offset in this block of the compressed data.
                  *
-                 * At the compressed data offset we have another uint32 which
-                 * is the size of the uncompressed data.
+                 * At the compressed data offset there is a uint32 containing
+                 * the size of the compressed LZS payload.
                  */
                 uint32 compressed_data_offsets[16];
             };
@@ -139,13 +139,12 @@ namespace VGears {
                 uint8 walkability_info_;
 
                 /**
-                 * Unknown data.
+                 * Packed mesh function value.
                  *
                  * Only 3 bits are valid.
-                 *
-                 * @todo Find out what this is for.
+                 * 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.
@@ -445,7 +444,7 @@ namespace VGears {
              * Imports and parses a map file.
              *
              * @param[in] stream Map file contents.
-             * @param[in] dest Unused
+             * @param[in] dest Resource parameter retained for API compatibility.
              */
             void ImportWorldmapFile(Ogre::DataStreamPtr& stream, WorldmapFile& dest);
 

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

+ 15 - 2
src/installer/DataInstaller.cpp

@@ -72,7 +72,11 @@ float DataInstaller::Progress(){
             media_installer_ = std::make_unique<MediaDataInstaller>(
               disk_image_, output_dir_, options_.keep_originals, options_.ffmpeg, options_.timidity
             );
-            field_installer_ = std::make_unique<FieldDataInstaller>(disk_image_, output_dir_);
+                        field_installer_ = std::make_unique<FieldDataInstaller>(
+                            disk_image_, output_dir_, [this](std::string log_message){
+                                    write_output_line_(log_message, 2, true);
+                            }
+                        );
             battle_installer_ = std::make_unique<BattleDataInstaller>(disk_image_, output_dir_, application_.ResMgr());
             world_installer_ = std::make_unique<WorldInstaller>(
               disk_image_, output_dir_, options_.keep_originals, application_.ResMgr()
@@ -412,7 +416,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 world-map scripts.", 2, true);
+                }
+                else{
+                    write_output_line_("Could not generate world-map scripts: " + script_error, 2, true);
+                }
                 installation_state_ = WM_MODELS;
                 cur_substep_ = 0;
             }

+ 41 - 17
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,10 +213,15 @@ void FieldDataInstaller::CollectSpawnPoints(
             }
         }
     }
+    return true;
 }
 
-FieldDataInstaller::FieldDataInstaller(const DiskImage& disk_image, const std::string output_dir):
-  disk_image_(disk_image), output_dir_(output_dir)
+FieldDataInstaller::FieldDataInstaller(
+    const DiskImage& disk_image, const std::string output_dir,
+    std::function<void(std::string)> log_output_line
+):
+    disk_image_(disk_image), output_dir_(output_dir),
+    write_output_line_(log_output_line)
 {}
 
 FieldDataInstaller::~FieldDataInstaller(){}
@@ -235,13 +247,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 +274,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 +287,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 +653,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){

+ 11 - 2
src/installer/FieldDataInstaller.h

@@ -307,13 +307,20 @@ class FieldDataInstaller{
          * @param[in] output_dir Path to the directory to write generated data
          * to.
          */
-        FieldDataInstaller(const DiskImage& disk_image, const std::string output_dir);
+        FieldDataInstaller(
+          const DiskImage& disk_image, const std::string output_dir,
+          std::function<void(std::string)> log_output_line
+        );
 
         /**
          * Installer destructor.
          */
         ~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 +482,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 +604,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>

+ 194 - 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,195 @@ 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();
+    std::vector<const VGears::LGPArchive::FileEntry*> event_entries;
+    const VGears::LGPArchive::FileEntry* field_table_entry = nullptr;
+    bool has_wm0 = false;
+    for (const VGears::LGPArchive::FileEntry& entry : archive_entries){
+        std::string lower_name = entry.file_name;
+        VGears::StringUtil::toLowerCase(lower_name);
+        if (
+          lower_name.size() > 5 && lower_name.compare(0, 2, "wm") == 0
+          && lower_name.compare(lower_name.size() - 3, 3, ".ev") == 0
+        ){
+            const std::string map_number
+              = lower_name.substr(2, lower_name.size() - 5);
+            if (!map_number.empty() && std::all_of(
+                  map_number.begin(), map_number.end(), [](char character){
+                      return character >= '0' && character <= '9';
+                  }
+                )){
+                event_entries.push_back(&entry);
+                if (lower_name == "wm0.ev") has_wm0 = true;
+            }
+        }
+        else if (lower_name == "field.tbl"){
+            field_table_entry = &entry;
+        }
+    }
+    if (event_entries.empty() || !has_wm0 || field_table_entry == nullptr){
+        error_message = "WM0.EV, other world event files, or FIELD.TBL were not found inside WORLD_US.LGP.";
+        return false;
+    }
+
+    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);
+    for (const VGears::LGPArchive::FileEntry* event_entry : event_entries){
+        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 = event_entry->file_name + " inside WORLD_US.LGP is empty.";
+            return false;
+        }
+        event_file.GetFileBuffer(event_data.data(), 0, event_entry->data_size);
+
+        std::string map_file_name = event_entry->file_name;
+        VGears::StringUtil::toLowerCase(map_file_name);
+        const std::string map_name = map_file_name.substr(0, map_file_name.size() - 3);
+        std::ofstream script_file(script_directory + "/" + map_name + ".lua");
+        if (!script_file){
+            error_message = "Cannot create script output in " + script_directory + ".";
+            return false;
+        }
+        script_file << "-- Decompiled from the user's " << event_entry->file_name << " 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 << "-- " << map_name << " 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;
+            if (type_offset + 1 >= event_data.size()) continue;
+                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 + "/" + map_name + ".lua.";
+            return false;
+        }
+        if (generated_function_count == 0){
+            error_message = "No functions in " + event_entry->file_name + " 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;

+ 9 - 1
src/installer/decompiler/world/instruction/WorldSubStackInstruction.cpp

@@ -48,6 +48,14 @@ void WorldSubStackInstruction::ProcessInst(
         case 0x51: op = "<<"; break;
         default:op = "unknown_operation";
     }
-    std::string value = stack.Pop()->GetString() + " " + op + " " + stack.Pop()->GetString();
+    std::string value;
+    if (opcode_ == 0x51){
+        const std::string left = stack.Pop()->GetString();
+        const std::string right = stack.Pop()->GetString();
+        value = "((" + left + ") * (2 ^ " + right + "))";
+    }
+    else{
+        value = stack.Pop()->GetString() + " " + op + " " + stack.Pop()->GetString();
+    }
     stack.Push(new VarValue(value));
 }

+ 6 - 0
src/main.cpp

@@ -104,6 +104,9 @@ int main(int argc, char *argv[]){
         Ogre::ResourceGroupManager::getSingleton().addResourceLocation(
           "./data/models/fields/", "FileSystem", "FIELD_MODELS"
         );
+        Ogre::ResourceGroupManager::getSingleton().addResourceLocation(
+          "./data/models/world/", "FileSystem", "WORLD_MODELS", true
+        );
         Ogre::ResourceGroupManager::getSingleton().addResourceLocation(
           "./data/models/battle/", "FileSystem", "BATTLE_MODELS", true
         );
@@ -122,6 +125,9 @@ int main(int argc, char *argv[]){
         Ogre::ResourceGroupManager::getSingleton().addResourceLocation(
           "./data/wm", "FileSystem", "WORLD_MAP"
         );
+        Ogre::ResourceGroupManager::getSingleton().addResourceLocation(
+          "./temp/world_models", "FileSystem", "WORLD_MAP"
+        );
         Ogre::ResourceGroupManager::getSingleton().initialiseAllResourceGroups();
 
         // Initialize it before console because it may use it

+ 810 - 224
src/modules/worldmap/WorldmapModule.cpp

@@ -2,6 +2,13 @@
  * Copyright (C) 2022 The V-Gears Team
  *
  * This file is part of V-Gears
+  static bool IsJunonTexture(uint16 texture_id){
+    const size_t texture_count = sizeof(textures) / sizeof(textures[0]);
+    if (texture_id >= texture_count) return false;
+    const std::string texture_name = textures[texture_id].name;
+    return texture_name.compare(0, 3, "jun") == 0
+      || texture_name == "jutmpl01";
+  }
  *
  * V-Gears is free software: you can redistribute it and/or modify it under
  * terms of the GNU General Public License as published by the Free Software
@@ -15,21 +22,33 @@
 
 #include "WorldmapModule.h"
 
+#include <algorithm>
+#include <map>
+#include <memory>
+#include <limits>
+#include <set>
+#include <string>
+#include <vector>
+#include <OgreEntity.h>
 #include <OgreSceneManager.h>
 #include <OgreManualObject.h>
 #include <OgreRoot.h>
+#include <OgreException.h>
 #include <OgreHardwarePixelBuffer.h>
-#include <OgreEntity.h>
 #include <OgreLogManager.h>
 #include <OgreTextureManager.h>
 #include <OgreMaterialManager.h>
-#include <OgreTechnique.h>
+#include <OgreResourceGroupManager.h>
+#include "data/VGearsTexFile.h"
+#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"
 #include "core/CameraManager.h"
-#include "data/worldmap/TxzFile.h"
-#include "data/worldmap/TxzFileSerializer.h"
 
 /**
  * World map module singleton.
@@ -364,80 +383,113 @@ namespace VGears {
         {281, "wtrk", 32, 64, 64, 96}
     };
 
-    static const char* WORLD_RESOURCE_GROUP = "FFVII_World";
-
-    /**
-     * Creates a reference texture.
-     *
-     * @param[out] s The texture will be loaded here.
-     * @todo Understand and document properly.
-     */
-    static void CreateReferenceTextureFileInstance(VGears::TxzFileSerializer& s){
-
-        /**
-         * A test file.
-         */
-        class TestFile : public VGears::TxzFile{
-
-            public:
+    static const char* WORLD_MAP_RESOURCE_GROUP = "WORLD_MAP";
 
-                /**
-                 * Constructor.
-                 *
-                 * Generates a null TXZ file.
-                 */
-                TestFile() : VGears::TxzFile(nullptr, "", 0, "") {}
-        };
+    static bool IsFootWalkable(uint8 walkability_type){
+      using WalkType = VGears::WorldmapFileSerializer::TriangleWalkMapTypes;
+      switch (walkability_type){
+        case WalkType::GRASS:
+        case WalkType::FOREST:
+        case WalkType::SWAMP:
+        case WalkType::DESERT:
+        case WalkType::WASTELAND:
+        case WalkType::SNOW:
+        case WalkType::RIVERSIDE:
+        case WalkType::CLIFF:
+        case WalkType::COREL_BRIDGE:
+        case WalkType::WUTAI_BRIDGE:
+        case WalkType::HILL_SIDE:
+        case WalkType::BEACH:
+        case WalkType::CANYON:
+        case WalkType::MOUNTAIN_PASS:
+        case WalkType::GOLD_SAUCER_DESERT:
+        case WalkType::JUNGLE:
+        case WalkType::NORTHERND_CAVE:
+        case WalkType::GOLD_SAUCER_DESERT_BORDER:
+        case WalkType::BRIDGE_HEAD:
+        case WalkType::BACK_ENTRANCE:
+          return true;
+        default:
+          return false;
+      }
+    }
 
-        // TODO: Get correct path
-        // TODO: What is this file? Where does it come from?
-        const char* file_name = "data/wm/wm0.txz";
-        std::ifstream ifs(file_name, std::ifstream::binary);
-        Ogre::DataStreamPtr stream(OGRE_NEW Ogre::FileStreamDataStream(&ifs, false));
-        // Ogre::LogManager log;
-        TestFile f;
-        s.Import(stream, f);
+    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 "";
+      }
+
     /**
-     * Creates a reference file.
+     * Loads and parses the installed WM0 map.
      *
-     * @param[in,out] callBack The callBack function.
-     * @todo Understand and document properly.
+     * @return The map blocks.
      */
-    static void CreateReferenceFileInstance(
-      std::function<void(Ogre::DataStreamPtr& stream, VGears::WorldmapFile& file)> callBack
-    ){
+    static std::vector<VGears::WorldmapFileSerializer::Block> LoadWorldMap(){
         /**
-         * A test file.
+       * The serializer retains its parsed blocks; the resource argument is
+       * currently unused by the MAP serializer.
          */
         class TestFile : public VGears::WorldmapFile{
 
             public:
 
-                /**
-                 * Constructor.
-                 *
-                 * Generates a null WM file.
-                 */
                 TestFile() : VGears::WorldmapFile(nullptr, "", 0, "") {}
         };
-        const char* file_name = "data/wm/WM0.MAP";
-        std::ifstream ifs(file_name, std::ifstream::binary);
-        if (!ifs.is_open()){
-            LogManager::getSingleton().logMessage("ERROR: Failed to open data/wm/WM0.MAP");
-            return;
-        }
-        {
-            // BOOST_REQUIRE(ifs.is_open());
-            Ogre::DataStreamPtr stream(OGRE_NEW Ogre::FileStreamDataStream(&ifs, false));
-            // BOOST_REQUIRE(stream->isReadable());
-            //Ogre::LogManager log;
-            TestFile f;
-            // log.createLog("Default Log", true, true, true);
-            callBack(stream, f);
-            // log.destroyLog("Default Log");
-        }
+
+        Ogre::DataStreamPtr stream
+          = Ogre::ResourceGroupManager::getSingleton().openResource(
+              "WM0.MAP", WORLD_MAP_RESOURCE_GROUP, nullptr, true
+            );
+        TestFile file;
+        VGears::WorldmapFileSerializer serializer;
+        serializer.ImportWorldmapFile(stream, file);
+
+        Ogre::LogManager::getSingleton().logMessage(
+          "Loaded WM0.MAP with " + std::to_string(serializer.blocks.size())
+            + " blocks."
+        );
+        return serializer.blocks;
     }
 
     /**
@@ -451,43 +503,6 @@ namespace VGears {
         return (float(coord) / 4096.0f);
     }
 
-    /**
-     * Tests something.
-     *
-     * @param[in] data The test data.
-     * @todo Understand and document properly.
-     */
-    void Test(const std::vector<std::vector<TxzFileSerializer::Rgba>>& data){
-        //Ogre::TexturePtr texture = Ogre::TextureManager::getSingleton().load(
-        //  "clf_l.png.png",
-        //  Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME
-        //);
-
-        // World resource group
-        Ogre::TexturePtr texture = Ogre::TextureManager::getSingleton().createManual(
-          "BackgroundTex", Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME,
-          Ogre::TEX_TYPE_2D, 640, 256, 0, Ogre::PF_B8G8R8, Ogre:: TU_DYNAMIC
-        );
-        Ogre::MaterialPtr material = Ogre::MaterialManager::getSingleton().create(
-          "BackgroundMat", Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME
-        );
-        material->getTechnique(0)->getPass(0)->createTextureUnitState("BackgroundTex");
-        //material->getTechnique(0)->getPass(0)->setSceneBlending(Ogre::SBT_TRANSPARENT_COLOUR);
-        Ogre::HardwarePixelBufferSharedPtr pixel_buffer = texture->getBuffer();
-        pixel_buffer->lock(Ogre::HardwareBuffer::HBL_DISCARD);
-        const Ogre::PixelBox& pixelBox = pixel_buffer->getCurrentLock();
-        Ogre::uint8* dest = static_cast<Ogre::uint8*>(pixelBox.data);
-        for (size_t i = 0; i < 256; i ++){
-           for(size_t j = 0; j < 640; j ++){
-              *dest ++ = data[i][j].b;
-              *dest ++ = data[i][j].g;
-              *dest ++ = data[i][j].r;
-              *dest ++ = 0;
-           }
-        }
-        pixel_buffer->unlock();
-    }
-
     /**
      * Converts a coordinate to a texture U component coordinate.
      *
@@ -496,7 +511,7 @@ namespace VGears {
      * @return Texture coordinate U component.
      * @todo Understand and document properly.
      */
-    float ToTextureCoordU(uint8 coord, uint16 id){
+    static float ToTextureCoordU(uint8 coord, uint16 id){
         return (float(coord) - float(textures[id].u)) / float(textures[id].width);
         //return (float(textures[id].u) + float(coord)) / 256.0f;
     }
@@ -509,166 +524,737 @@ namespace VGears {
      * @return Texture coordinate V component.
      * @todo Understand and document properly.
      */
-    float ToTextureCoordV(uint8 coord, uint16 id){
+    static float ToTextureCoordV(uint8 coord, uint16 id){
         return (float(coord) - float(textures[id].v)) / float(textures[id].height);
         //return (float(textures[id].v) + float(coord)) / 640.0f;
     }
 
-    /**
-     * The list of loaded textures.
-     */
-    std::vector<TexturePtr> loaded_textures;
+    struct ImageDataDeleter{
 
-    /**
-     * The list of materials.
-     */
-    std::vector<MaterialPtr> materials;
+      void operator()(Ogre::ImageCodec::ImageData* image_data) const{
+        OGRE_DELETE image_data;
+      }
+    };
 
-    /**
-     * Map of texture IDs.
-     */
-    std::map<int, ManualObject*> texture_ids;
+    static TexturePtr LoadWorldMapTexture(uint16 texture_id){
+        const std::string texture_name
+          = std::string(textures[texture_id].name) + ".tex";
+        Ogre::DataStreamPtr stream
+          = ResourceGroupManager::getSingleton().openResource(
+              texture_name, WORLD_MAP_RESOURCE_GROUP, nullptr, true
+            );
+        VGears::TexFile tex_file;
+        tex_file.Read(stream);
 
-    /**
-     * Loads the map textures.
-     */
-    static void LoadTextures(){
-        if (loaded_textures.empty()){
-            for (int i = 0; i < sizeof(textures) / sizeof(textures[0]); i ++){
-                std::string texture_name = std::string(textures[i].name) + ".tex";
-                ResourceGroupManager::getSingleton().declareResource(
-                  texture_name.c_str(), "Texture", "TEST"
-                );
-                TexturePtr tmp = Ogre::TextureManager::getSingleton().load(
-                  texture_name.c_str(), "TEST", TEX_TYPE_2D, 1, 1.0f
-                );
-                loaded_textures.emplace_back(tmp);
-                Ogre::MaterialPtr material = Ogre::MaterialManager::getSingleton().create(
-                  textures[i].name, Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME
-                );
-                material->getTechnique(0)->getPass(0)->createTextureUnitState(texture_name.c_str());
-                material->getTechnique(0)->getPass(0)->setTextureFiltering(TFO_NONE);
-                materials.emplace_back(material);
-            }
+        Ogre::MemoryDataStreamPtr pixel_data(tex_file.GetPixelData());
+        std::unique_ptr<Ogre::ImageCodec::ImageData, ImageDataDeleter> image_data(
+          tex_file.GetImageData()
+        );
+        TexturePtr texture = Ogre::TextureManager::getSingleton().createManual(
+          std::string("WorldMapTex_") + textures[texture_id].name,
+          ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME, TEX_TYPE_2D,
+          image_data->width, image_data->height, 0, image_data->format, TU_STATIC
+        );
+        texture->getBuffer()->blitFromMemory(Ogre::PixelBox(
+          image_data->width, image_data->height, 1, image_data->format,
+          pixel_data->getPtr()
+        ));
+        return texture;
+    }
+
+    static MaterialPtr LoadTextureMaterial(uint16 texture_id){
+        TexturePtr texture = LoadWorldMapTexture(texture_id);
+        const std::string material_name
+          = std::string("WorldMap_") + textures[texture_id].name;
+        MaterialPtr material
+          = Ogre::MaterialManager::getSingleton().getByName(material_name);
+        if (material.isNull()){
+            material = Ogre::MaterialManager::getSingleton().create(
+              material_name, ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME
+            );
+            Pass* pass = material->getTechnique(0)->getPass(0);
+            pass->createTextureUnitState(texture->getName());
+            pass->setTextureFiltering(TFO_NONE);
+            pass->setLightingEnabled(false);
         }
+        return material;
     }
 
-    /**
-     * Creates a test map.
-     *
-     * @param[in] scene_manager The scene manager.
-     */
-    void CreateTestMap(SceneManager* scene_manager){
-        std::vector<VGears::WorldmapFileSerializer::Block> blocks;
-        CreateReferenceFileInstance([&](Ogre::DataStreamPtr& stream, VGears::WorldmapFile& file){
-            VGears::WorldmapFileSerializer s;
-            s.ImportWorldmapFile(stream, file);
-            blocks = s.blocks;
-        });
-
-        if (blocks.empty()) return; // Load failed
-        /*
-        // TODO: Get PSX textures working
-        VGears::TxzFileSerializer s;
-        createReferenceTextureFileInstance(s);
-
-        std::vector<std::vector<TxzFileSerializer::rgba>> data
-          = s.GetWorldmapTexture(19);
-        test(data);
-        */
-        LoadTextures();
-        int c = 0;
-        const auto SPACE = From16BitFixedPoint(8182);
-        size_t block_count = blocks.size();
-        int map_x = 0;
-        int map_y = 0;
-        for (unsigned int k = 0; k < block_count; k ++){
-            // 7x9 size.
-            VGears::WorldmapFileSerializer::Block& block = blocks[k];
-            int block_x = 0;
-            int block_y = 0;
-            for (size_t i = 0; i < block.meshes.size(); i ++){
-                VGears::WorldmapFileSerializer::BlockPart& part = block.meshes[i];
-                for (size_t j = 0; j < part.triangles.size(); j ++){
-                    VGears::WorldmapFileSerializer::BlockTriangle& tri
-                      = part.triangles.at(j);
+    WorldmapModule::WorldmapModule():
+      scene_manager_(nullptr),
+      world_map_node_(nullptr),
+      player_entity_(nullptr),
+      highwind_entity_(nullptr),
+      buggy_entity_(nullptr),
+      chocobo_entity_(nullptr),
+      submarine_entity_(nullptr),
+      tiny_bronco_entity_(nullptr),
+      current_vehicle_(VehicleType::ON_FOOT),
+      player_height_offset_(0),
+      active_function_mesh_index_(-1),
+      active_function_id_(-1),
+      initialized_(false),
+      frame_listener_attached_(false),
+      cancel_was_pressed_(false)
+    {}
+
+    WorldmapModule::~WorldmapModule(){
+      if (frame_listener_attached_) Root::getSingleton().removeFrameListener(this);
+      if (scene_manager_ != nullptr && world_map_node_ != nullptr){
+        scene_manager_->getRootSceneNode()->removeAndDestroyChild("WorldMapTerrain");
+      }
+    }
+
+    void WorldmapModule::CreateWorldMap(SceneManager* scene_manager){
+      scene_manager_ = scene_manager;
+      world_map_node_ = scene_manager_->getRootSceneNode()
+        ->createChildSceneNode("WorldMapTerrain");
+        const std::vector<VGears::WorldmapFileSerializer::Block> blocks
+          = LoadWorldMap();
+        const size_t texture_count = sizeof(textures) / sizeof(textures[0]);
+        const size_t base_block_count = std::min(blocks.size(), size_t(63));
+        const float space = From16BitFixedPoint(8182);
+        std::map<uint16, ManualObject*> texture_objects;
+        std::set<uint16> unavailable_texture_ids;
+        unsigned int manual_index = 0;
+
+        for (size_t block_index = 0; block_index < base_block_count; ++block_index){
+            const VGears::WorldmapFileSerializer::Block& block = blocks[block_index];
+            const size_t map_x = block_index % 9;
+            const size_t map_y = block_index / 9;
+            for (size_t mesh_index = 0; mesh_index < block.meshes.size(); ++mesh_index){
+                const VGears::WorldmapFileSerializer::BlockPart& part
+                  = block.meshes[mesh_index];
+                const size_t mesh_x = mesh_index % 4;
+                const size_t mesh_y = mesh_index / 4;
+                const float pos_x = (mesh_x * space) + (map_x * space * 4);
+                const float pos_z = (mesh_y * space) + (map_y * space * 4);
+
+                for (const VGears::WorldmapFileSerializer::BlockTriangle& triangle
+                  : part.triangles){
+                    const VGears::WorldmapFileSerializer::Vertex& vertex_0
+                      = part.vertices.at(triangle.vertex_0_index);
+                    const VGears::WorldmapFileSerializer::Vertex& vertex_1
+                      = part.vertices.at(triangle.vertex_1_index);
+                    const VGears::WorldmapFileSerializer::Vertex& vertex_2
+                      = part.vertices.at(triangle.vertex_2_index);
+                    TerrainTriangle terrain_triangle;
+                    terrain_triangle.vertices[0] = Ogre::Vector3(
+                      From16BitFixedPoint(vertex_0.x) + pos_x,
+                      From16BitFixedPoint(vertex_0.y),
+                      From16BitFixedPoint(vertex_0.z) + pos_z
+                    );
+                    terrain_triangle.vertices[1] = Ogre::Vector3(
+                      From16BitFixedPoint(vertex_1.x) + pos_x,
+                      From16BitFixedPoint(vertex_1.y),
+                      From16BitFixedPoint(vertex_1.z) + pos_z
+                    );
+                    terrain_triangle.vertices[2] = Ogre::Vector3(
+                      From16BitFixedPoint(vertex_2.x) + pos_x,
+                      From16BitFixedPoint(vertex_2.y),
+                      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)
+                      != unavailable_texture_ids.end()){
+                        continue;
+                    }
+                    if (triangle.TextureInfo >= texture_count){
+                        if (unavailable_texture_ids.insert(triangle.TextureInfo).second){
+                            LogManager::getSingleton().logMessage(
+                              "Skipping world-map texture ID "
+                                + std::to_string(triangle.TextureInfo)
+                                + ": no PC texture mapping is available."
+                            );
+                        }
+                        continue;
+                    }
+
                     ManualObject* manual = nullptr;
-                    auto it = texture_ids.find(tri.TextureInfo);
-                    if (it == texture_ids.end()){
+                    auto object = texture_objects.find(triangle.TextureInfo);
+                    if (object == texture_objects.end()){
+                        MaterialPtr material;
+                        try{
+                            material = LoadTextureMaterial(triangle.TextureInfo);
+                        }
+                        catch (const Ogre::Exception& exception){
+                            unavailable_texture_ids.insert(triangle.TextureInfo);
+                            LogManager::getSingleton().logMessage(
+                              "Failed to load world-map texture "
+                                + std::string(textures[triangle.TextureInfo].name)
+                                + ": " + exception.getDescription()
+                            );
+                            continue;
+                        }
+
                         manual = scene_manager->createManualObject(
-                          ("zzz_manual" + std::to_string(c++)).c_str()
-                        );
-                        manual->begin(
-                          textures[tri.TextureInfo].name, RenderOperation::OT_TRIANGLE_LIST
+                          "world_map_surface_" + std::to_string(++manual_index)
                         );
-                        manual->setDebugDisplayEnabled(true);
+                        manual->begin(material->getName(), RenderOperation::OT_TRIANGLE_LIST);
+                        manual->setDebugDisplayEnabled(false);
                         manual->setDynamic(false);
-                        texture_ids[tri.TextureInfo] = manual;
+                        texture_objects[triangle.TextureInfo] = manual;
                     }
-                    else manual = it->second;
-                    const float pos_x = (block_x * SPACE) + (map_x * SPACE * 4);
-                    const float pos_y = (block_y * SPACE) + (map_y * SPACE * 4);
+                    else manual = object->second;
 
-                    // v1
-                    VGears::WorldmapFileSerializer::Vertex v1
-                      = part.vertices.at(tri.vertex_2_index);
                     manual->position(
-                      From16BitFixedPoint(v1.x) + pos_x,
-                      From16BitFixedPoint(v1.y),
-                      From16BitFixedPoint(v1.z) + pos_y);
+                      terrain_triangle.vertices[2].x,
+                      terrain_triangle.vertices[2].y,
+                      terrain_triangle.vertices[2].z
+                    );
                     manual->textureCoord(
-                      ToTextureCoordU(tri.vertex_2_u, tri.TextureInfo),
-                      ToTextureCoordV(tri.vertex_2_v, tri.TextureInfo)
+                      ToTextureCoordU(triangle.vertex_2_u, triangle.TextureInfo),
+                      ToTextureCoordV(triangle.vertex_2_v, triangle.TextureInfo)
                     );
 
-                    // v2
-                    v1 = part.vertices.at(tri.vertex_1_index);
                     manual->position(
-                      From16BitFixedPoint(v1.x) + pos_x,
-                      From16BitFixedPoint(v1.y),
-                      From16BitFixedPoint(v1.z) + pos_y
+                      terrain_triangle.vertices[1].x,
+                      terrain_triangle.vertices[1].y,
+                      terrain_triangle.vertices[1].z
                     );
                     manual->textureCoord(
-                      ToTextureCoordU(tri.vertex_1_u, tri.TextureInfo),
-                      ToTextureCoordV(tri.vertex_1_v, tri.TextureInfo)
+                      ToTextureCoordU(triangle.vertex_1_u, triangle.TextureInfo),
+                      ToTextureCoordV(triangle.vertex_1_v, triangle.TextureInfo)
                     );
 
-                    // v3
-                    v1 = part.vertices.at(tri.vertex_0_index);
                     manual->position(
-                      From16BitFixedPoint(v1.x) + pos_x,
-                      From16BitFixedPoint(v1.y),
-                      From16BitFixedPoint(v1.z) + pos_y
+                      terrain_triangle.vertices[0].x,
+                      terrain_triangle.vertices[0].y,
+                      terrain_triangle.vertices[0].z
                     );
                     manual->textureCoord(
-                      ToTextureCoordU(tri.vertex_0_u, tri.TextureInfo),
-                      ToTextureCoordV(tri.vertex_0_v, tri.TextureInfo)
+                      ToTextureCoordU(triangle.vertex_0_u, triangle.TextureInfo),
+                      ToTextureCoordV(triangle.vertex_0_v, triangle.TextureInfo)
                     );
-
-                }
-                block_x ++;
-                if (block_x >= 4){
-                    block_x = 0;
-                    block_y++;
                 }
             }
-            map_x ++;
-            if (map_x >= 9){
-                map_x = 0;
-                map_y++;
+        }
+
+        for (const auto& texture_object : texture_objects){
+            texture_object.second->end();
+            world_map_node_->attachObject(texture_object.second);
+        }
+
+        const float map_center_x = space * 18.0f;
+        const float map_center_z = space * 14.0f;
+        Ogre::Camera* camera = CameraManager::getSingleton().GetCurrentCamera();
+        camera->setPosition(Ogre::Vector3(map_center_x, 80.0f, map_center_z + 90.0f));
+        camera->lookAt(Ogre::Vector3(map_center_x, 0.0f, map_center_z));
+    }
+
+    void WorldmapModule::CreatePlayer(){
+        const Ogre::Vector3 requested_spawn(42.0f, 0.0f, 36.5f);
+        Ogre::Vector3 spawn_position;
+        if (!FindWalkableSurface(requested_spawn, spawn_position, true)){
+            LogManager::getSingleton().logMessage(
+              "WM0 has no GRASS surface at Cloud's requested spawn (42, 36.5)."
+            );
+            return;
+        }
+
+        LogManager::getSingleton().logMessage(
+          "Spawning Cloud at "
+            + Ogre::StringConverter::toString(spawn_position.x) + ", "
+            + Ogre::StringConverter::toString(spawn_position.z)
+        );
+
+        player_entity_ = EntityManager::getSingleton().AddWorldMapEntity(
+          "WorldMapCloud", "cloud.mesh", spawn_position, Ogre::Degree(0),
+          Ogre::Vector3::UNIT_SCALE, "FIELD_MODELS"
+        );
+        if (player_entity_ == nullptr) return;
+
+        const Ogre::AxisAlignedBox& bounds = player_entity_->GetModel()->getBoundingBox();
+        const Ogre::Vector3 model_size = bounds.getSize();
+        const Ogre::Real model_height = model_size.y;
+        if (model_height > 0){
+            const Ogre::Real scale = From16BitFixedPoint(8182) * 0.45f * 0.25f * 0.25f
+              / model_height;
+            player_entity_->setScale(Ogre::Vector3::UNIT_SCALE * scale);
+            player_entity_->SetHeight(model_height * scale);
+            player_entity_->SetSolidRadius(
+              std::max(model_size.x, model_size.z) * scale * 0.5f
+            );
+            player_entity_->SetCollisionDebugOrientation(
+              Ogre::Quaternion(Ogre::Degree(-90), Ogre::Vector3::UNIT_X)
+            );
+            player_height_offset_ = -bounds.getMinimum().y * scale;
+            spawn_position.y += player_height_offset_;
+            player_entity_->SetPosition(spawn_position);
+            Ogre::AnimationStateSet* animations
+              = player_entity_->GetModel()->getAllAnimationStates();
+            if (animations == nullptr || !animations->hasAnimationState("Walk")){
+                LogManager::getSingleton().logMessage(
+                  "Cloud model has no Walk animation state."
+                );
+            }
+            EntityManager::getSingleton().ScriptSetPlayerEntity("WorldMapCloud");
+            player_entity_->PlayAnimationContinue("Idle");
+            CreateCompanionModels();
+            UpdateCamera(0, true);
+        }
+    }
+
+    void WorldmapModule::AddWorldModel(
+      const Ogre::String& name, const Ogre::String& mesh_name,
+      const Ogre::Vector3& desired_position, Ogre::Real target_size,
+      const Ogre::Quaternion& orientation, Ogre::Real collision_radius_scale,
+      bool talkable
+    ){
+        Ogre::Vector3 surface_position;
+        if (!FindWalkableSurface(desired_position, surface_position, false, false)){
+            LogManager::getSingleton().logMessage(
+              "No terrain surface found for world-map model '" + name + "'."
+            );
+            return;
+        }
+
+        EntityModel* entity = nullptr;
+        try{
+            entity = EntityManager::getSingleton().AddWorldMapEntity(
+              "WorldMap" + name, mesh_name, surface_position,
+              Ogre::Degree(0), Ogre::Vector3::UNIT_SCALE, "WORLD_MODELS"
+            );
+        }
+        catch (const Ogre::Exception& exception){
+            LogManager::getSingleton().logMessage(
+              "Failed to load world-map model '" + mesh_name + "': "
+                + exception.getDescription()
+            );
+            return;
+        }
+        if (entity == nullptr) return;
+
+        const Ogre::Vector3 bounds_size = entity->GetModel()->getBoundingBox().getSize();
+        const Ogre::Real largest_dimension = std::max(
+          bounds_size.x, std::max(bounds_size.y, bounds_size.z)
+        );
+        if (largest_dimension <= 0){
+            LogManager::getSingleton().logMessage(
+              "World-map model '" + mesh_name + "' has empty bounds."
+            );
+            return;
+        }
+
+        const Ogre::Real model_scale = target_size / largest_dimension;
+        const Ogre::Vector3 oriented_x = orientation * Ogre::Vector3::UNIT_X;
+        const Ogre::Vector3 oriented_y = orientation * Ogre::Vector3::UNIT_Y;
+        const Ogre::Vector3 oriented_z = orientation * Ogre::Vector3::UNIT_Z;
+        const Ogre::Real world_width =
+          Ogre::Math::Abs(oriented_x.x) * bounds_size.x
+          + Ogre::Math::Abs(oriented_y.x) * bounds_size.y
+          + Ogre::Math::Abs(oriented_z.x) * bounds_size.z;
+        const Ogre::Real world_height =
+          Ogre::Math::Abs(oriented_x.y) * bounds_size.x
+          + Ogre::Math::Abs(oriented_y.y) * bounds_size.y
+          + Ogre::Math::Abs(oriented_z.y) * bounds_size.z;
+        const Ogre::Real world_depth =
+          Ogre::Math::Abs(oriented_x.z) * bounds_size.x
+          + Ogre::Math::Abs(oriented_y.z) * bounds_size.y
+          + Ogre::Math::Abs(oriented_z.z) * bounds_size.z;
+        entity->setRootOrientation(orientation);
+        entity->setScale(Ogre::Vector3::UNIT_SCALE * model_scale);
+        entity->SetSolid(true);
+        entity->SetHeight(world_height * model_scale);
+        entity->SetSolidRadius(
+          std::max(world_width, world_depth) * model_scale * 0.5f
+            * collision_radius_scale
+        );
+        entity->SetTalkable(talkable);
+        if (talkable) entity->SetTalkRadius(entity->GetSolidRadius());
+        entity->SetCollisionDebugOrientation(
+          Ogre::Quaternion(Ogre::Degree(-90), Ogre::Vector3::UNIT_X)
+        );
+        LogManager::getSingleton().logMessage(
+          "Placed world-map model '" + name + "' at "
+            + Ogre::StringConverter::toString(surface_position.x) + ", "
+            + Ogre::StringConverter::toString(surface_position.z)
+        );
+    }
+
+    void WorldmapModule::CreateCompanionModels(){
+        // These models and their spawn points are now hardcoded, they mmust be
+        // loaded according to the story point and save game data.
+        const Ogre::Real space = From16BitFixedPoint(8182);
+        AddWorldModel(
+          "Chocobo", "aja.mesh",
+          Ogre::Vector3(42.5f, 0.0f, 37.5f),
+          space * 0.14f,
+          Ogre::Quaternion(Ogre::Degree(-90), Ogre::Vector3::UNIT_X), 1.0f, true
+        );
+        AddWorldModel(
+          "Highwind", "cgd.mesh",
+          Ogre::Vector3(43.0f, 0.0f, 36.5f),
+          space * 0.25f,
+          Ogre::Quaternion(Ogre::Degree(-90), Ogre::Vector3::UNIT_X), 0.5f, true
+        );
+        AddWorldModel(
+          "Buggy", "aba.mesh",
+          Ogre::Vector3(42.0f, 0.0f, 36.0f),
+          space * 0.3f, Ogre::Quaternion::IDENTITY, 0.5f, true
+        );
+        AddWorldModel(
+          "Submarine", "ddd.mesh",
+          Ogre::Vector3(41.5f, 0.0f, 36.5f),
+          space * 0.1125f, Ogre::Quaternion::IDENTITY, 1.0f, true
+        );
+        AddWorldModel(
+          "TinyBronco", "dva.mesh",
+          Ogre::Vector3(42.0f, 0.0f, 37.37f),
+          space * 0.15f, Ogre::Quaternion::IDENTITY, 0.75f, true
+        );
+        highwind_entity_ = static_cast<EntityModel*>(
+          EntityManager::getSingleton().GetEntity("WorldMapHighwind")
+        );
+        buggy_entity_ = static_cast<EntityModel*>(
+          EntityManager::getSingleton().GetEntity("WorldMapBuggy")
+        );
+        chocobo_entity_ = static_cast<EntityModel*>(
+          EntityManager::getSingleton().GetEntity("WorldMapChocobo")
+        );
+        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_walkable, uint8* walkability_type, size_t* triangle_index
+    ) const{
+        bool found = false;
+        Ogre::Real closest_height_distance = std::numeric_limits<Ogre::Real>::max();
+        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;
             }
+            const Ogre::Vector3& a = triangle.vertices[0];
+            const Ogre::Vector3& b = triangle.vertices[1];
+            const Ogre::Vector3& c = triangle.vertices[2];
+            const Ogre::Real denominator
+              = (b.z - c.z) * (a.x - c.x) + (c.x - b.x) * (a.z - c.z);
+            if (Ogre::Math::Abs(denominator) < 1e-6f) continue;
+
+            const Ogre::Real weight_a
+              = ((b.z - c.z) * (position.x - c.x)
+                + (c.x - b.x) * (position.z - c.z)) / denominator;
+            const Ogre::Real weight_b
+              = ((c.z - a.z) * (position.x - c.x)
+                + (a.x - c.x) * (position.z - c.z)) / denominator;
+            const Ogre::Real weight_c = 1.0f - weight_a - weight_b;
+            if (weight_a < -1e-4f || weight_b < -1e-4f || weight_c < -1e-4f){
+                continue;
+            }
+
+            const Ogre::Real height
+              = weight_a * a.y + weight_b * b.y + weight_c * c.y;
+            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_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;
+    }
+
+    WorldmapModule::VehicleType WorldmapModule::GetCurrentVehicle() const{
+      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;
         }
-        for (auto& it : texture_ids){
-            it.second->end();
-            SceneNode* node = scene_manager->getRootSceneNode()->createChildSceneNode();
-            node->attachObject(it.second);
-            node->setPosition(0, 0, 0);
+        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;
+
+        ::Entity* active_entity = EntityManager::getSingleton().ScriptGetPlayerEntity();
+      if (active_entity == player_entity_){
+        current_vehicle_ = VehicleType::ON_FOOT;
+      }
+      else if (active_entity == chocobo_entity_){
+        current_vehicle_ = VehicleType::CHOCOBO;
+      }
+      else if (active_entity == buggy_entity_){
+        current_vehicle_ = VehicleType::BUGGY;
+      }
+      else if (active_entity == highwind_entity_){
+        current_vehicle_ = VehicleType::HIGHWIND;
+      }
+      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_
+            : (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;
+        if (InputManager::getSingleton().IsButtonPressed(OIS::KC_RIGHT)) movement.x += 1.0f;
+        if (InputManager::getSingleton().IsButtonPressed(OIS::KC_UP)) movement.z -= 1.0f;
+        if (InputManager::getSingleton().IsButtonPressed(OIS::KC_DOWN)) movement.z += 1.0f;
+        bool is_walking = false;
+        if (!movement.isZeroLength()){
+          movement.normalise();
+            const Ogre::Vector3 current_position = active_entity->GetPosition();
+          const Ogre::Real speed = From16BitFixedPoint(8182) * 0.45f
+            * (vehicle_active ? 2.0f : 1.0f);
+          Ogre::Vector3 requested_position = current_position
+            + movement * speed * event.timeSinceLastFrame;
+          Ogre::Vector3 surface;
+          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)){
+                        surface.y += current_position.y - current_surface.y;
+                    }
+                }
+                else if (active_entity == player_entity_){
+                  surface.y += player_height_offset_;
+                }
+                if (!EntityManager::getSingleton().IsWorldPositionBlocked(
+                  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
+                      = model_forward.getRotationTo(move_direction, Ogre::Vector3::UNIT_Y);
+                    active_model->GetModel()->getParentSceneNode()
+                      ->getParentSceneNode()->setOrientation(heading);
+                    is_walking = true;
+                }
+          }
+        }
+
+        if (!vehicle_active){
+            player_entity_->PlayAnimationContinue(is_walking ? "Walk" : "Idle");
+        }
+
+        UpdateCamera(event.timeSinceLastFrame, false);
+        return true;
+    }
+
+      void WorldmapModule::UpdateCamera(Ogre::Real delta_time, bool snap_to_player){
+        ::Entity* active_entity = EntityManager::getSingleton().ScriptGetPlayerEntity();
+        if (active_entity == nullptr) return;
+        const Ogre::Vector3 target
+          = active_entity->GetPosition() + Ogre::Vector3(0, active_entity->GetHeight() * 0.5f, 0);
+        const Ogre::Vector3 camera_offset(0.0f, 1.5f, 2.25f);
+        Ogre::Camera* camera = CameraManager::getSingleton().GetCurrentCamera();
+        const Ogre::Vector3 desired_position = target + camera_offset;
+        if (snap_to_player){
+          camera->setPosition(desired_position);
+        }
+        else{
+          const Ogre::Real follow_amount = std::min(1.0f, delta_time * 8.0f);
+          camera->setPosition(
+            camera->getPosition()
+            + (desired_position - camera->getPosition()) * follow_amount
+          );
+        }
+        camera->lookAt(target);
+      }
+
     void WorldmapModule::Init(){
-        CreateTestMap(Root::getSingleton().getSceneManager("Scene"));
-        CameraManager::getSingleton().EnableWireFrame(true);
+      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"));
+        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

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

@@ -15,22 +15,98 @@
 
 #pragma once
 
+#include <OgreFrameListener.h>
 #include <OgreSingleton.h>
+#include <OgreVector3.h>
+#include <vector>
 #include "common/TypeDefine.h"
 
+class EntityModel;
+
 namespace VGears {
 
     /**
      * The world map module.
      */
-    class WorldmapModule : public Ogre::Singleton<WorldmapModule>{
+    class WorldmapModule : public Ogre::Singleton<WorldmapModule>, public Ogre::FrameListener{
 
         public:
 
+            enum class VehicleType{
+                ON_FOOT,
+                CHOCOBO,
+                BUGGY,
+                HIGHWIND,
+                SUBMARINE,
+                TINY_BRONCO
+            };
+
+            WorldmapModule();
+
+            virtual ~WorldmapModule();
+
             /**
              * 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);
+            void CreatePlayer();
+                        void CreateCompanionModels();
+                        void AddWorldModel(
+                            const Ogre::String& name, const Ogre::String& mesh_name,
+                            const Ogre::Vector3& desired_position, Ogre::Real target_size,
+                            const Ogre::Quaternion& orientation,
+                            Ogre::Real collision_radius_scale = 1.0f,
+                            bool talkable = false
+                        );
+            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_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_;
+            Ogre::SceneNode* world_map_node_;
+            EntityModel* player_entity_;
+            EntityModel* highwind_entity_;
+            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_;
     };
 
 }

+ 206 - 5
test/data/worldmap/MapFileSerializer.cpp

@@ -15,13 +15,214 @@
 
 #include <boost/test/unit_test.hpp>
 
+#include <algorithm>
+#include <vector>
+#include "data/worldmap/WorldmapFile.h"
 #include "../../../src/data/worldmap/WorldmapFileSerializer.h"
 
-#pragma message("Unimplemented test file: " __FILE__)
+namespace {
 
-BOOST_AUTO_TEST_CASE(TestMapFileSerializer){
-    // TODO
-    BOOST_WARN_MESSAGE(false, "Unimplemented test file: MapFileSerializer");
-    BOOST_CHECK(0 == 0);
+        const size_t WORLD_MAP_BLOCK_SIZE = 0xB800;
+
+        class TestWorldmapFile : public VGears::WorldmapFile{
+
+                public:
+
+                        TestWorldmapFile() : VGears::WorldmapFile(nullptr, "", 0, "") {}
+        };
+
+        void AppendUInt16(std::vector<VGears::uint8>& data, VGears::uint16 value){
+                data.push_back(static_cast<VGears::uint8>(value & 0xFF));
+                data.push_back(static_cast<VGears::uint8>(value >> 8));
+        }
+
+        void AppendUInt32(std::vector<VGears::uint8>& data, VGears::uint32 value){
+                data.push_back(static_cast<VGears::uint8>(value & 0xFF));
+                data.push_back(static_cast<VGears::uint8>((value >> 8) & 0xFF));
+                data.push_back(static_cast<VGears::uint8>((value >> 16) & 0xFF));
+                data.push_back(static_cast<VGears::uint8>((value >> 24) & 0xFF));
+        }
+
+        void WriteUInt32(
+            std::vector<VGears::uint8>& data, size_t offset, VGears::uint32 value
+        ){
+                data[offset] = static_cast<VGears::uint8>(value & 0xFF);
+                data[offset + 1] = static_cast<VGears::uint8>((value >> 8) & 0xFF);
+                data[offset + 2] = static_cast<VGears::uint8>((value >> 16) & 0xFF);
+                data[offset + 3] = static_cast<VGears::uint8>((value >> 24) & 0xFF);
+        }
+
+        VGears::uint32 ReadUInt32(
+            const std::vector<VGears::uint8>& data, size_t offset
+        ){
+                return static_cast<VGears::uint32>(data[offset])
+                    | (static_cast<VGears::uint32>(data[offset + 1]) << 8)
+                    | (static_cast<VGears::uint32>(data[offset + 2]) << 16)
+                    | (static_cast<VGears::uint32>(data[offset + 3]) << 24);
+        }
+
+        std::vector<VGears::uint8> CreateMeshData(
+            bool truncated, bool invalid_vertex
+        ){
+                std::vector<VGears::uint8> data;
+                AppendUInt16(data, 1);
+                AppendUInt16(data, 3);
+                if (truncated) return data;
+
+                data.push_back(invalid_vertex ? 3 : 0);
+                data.push_back(1);
+                data.push_back(2);
+                data.push_back(0xA5);
+                data.push_back(10);
+                data.push_back(20);
+                data.push_back(30);
+                data.push_back(40);
+                data.push_back(50);
+                data.push_back(60);
+                AppendUInt16(data, 0xAB55);
+
+                for (int vertex_index = 0; vertex_index < 3; ++vertex_index){
+                        AppendUInt16(data, static_cast<VGears::uint16>(-100 + vertex_index));
+                        AppendUInt16(data, static_cast<VGears::uint16>(200 + vertex_index));
+                        AppendUInt16(data, static_cast<VGears::uint16>(-300 + vertex_index));
+                        AppendUInt16(data, 0);
+                }
+                for (int normal_index = 0; normal_index < 3; ++normal_index){
+                        AppendUInt16(data, static_cast<VGears::uint16>(normal_index + 1));
+                        AppendUInt16(data, static_cast<VGears::uint16>(normal_index + 2));
+                        AppendUInt16(data, static_cast<VGears::uint16>(normal_index + 3));
+                        AppendUInt16(data, 0);
+                }
+                return data;
+        }
+
+        std::vector<VGears::uint8> EncodeLzs(
+            const std::vector<VGears::uint8>& data
+        ){
+                std::vector<VGears::uint8> payload;
+                size_t data_offset = 0;
+                while (data_offset < data.size()){
+                        const size_t control_offset = payload.size();
+                        payload.push_back(0);
+                        VGears::uint8 control = 0;
+                        for (unsigned int bit = 0; bit < 8 && data_offset < data.size(); ++bit){
+                                control |= static_cast<VGears::uint8>(1u << bit);
+                                payload.push_back(data[data_offset++]);
+                        }
+                        payload[control_offset] = control;
+                }
+
+                std::vector<VGears::uint8> compressed;
+                AppendUInt32(compressed, static_cast<VGears::uint32>(payload.size()));
+                compressed.insert(compressed.end(), payload.begin(), payload.end());
+                return compressed;
+        }
+
+        std::vector<VGears::uint8> CreateMapFixture(
+            bool truncated_mesh = false, bool invalid_vertex = false
+        ){
+                std::vector<VGears::uint8> data(WORLD_MAP_BLOCK_SIZE, 0);
+                size_t write_offset = sizeof(VGears::uint32) * 16;
+                for (size_t mesh_index = 0; mesh_index < 16; ++mesh_index){
+                        const std::vector<VGears::uint8> mesh_data = mesh_index == 0
+                            ? CreateMeshData(truncated_mesh, invalid_vertex)
+                            : std::vector<VGears::uint8>{0, 0, 0, 0};
+                        const std::vector<VGears::uint8> compressed = EncodeLzs(mesh_data);
+                        write_offset = (write_offset + 3) & ~static_cast<size_t>(3);
+                        WriteUInt32(data, mesh_index * sizeof(VGears::uint32),
+                            static_cast<VGears::uint32>(write_offset));
+                        std::copy(compressed.begin(), compressed.end(), data.begin() + write_offset);
+                        write_offset += compressed.size();
+                }
+                return data;
+        }
+
+        void ImportFixture(std::vector<VGears::uint8>& data){
+                VGears::WorldmapFileSerializer serializer;
+                TestWorldmapFile file;
+                Ogre::DataStreamPtr stream(OGRE_NEW Ogre::MemoryDataStream(
+                    data.data(), data.size(), false, true
+                ));
+                serializer.ImportWorldmapFile(stream, file);
+        }
+
+}
+
+BOOST_AUTO_TEST_CASE(TestMapFileSerializerReadsPackedTriangleFields){
+        std::vector<VGears::uint8> data = CreateMapFixture();
+        VGears::WorldmapFileSerializer serializer;
+        TestWorldmapFile file;
+        Ogre::DataStreamPtr stream(OGRE_NEW Ogre::MemoryDataStream(
+            data.data(), data.size(), false, true
+        ));
+
+        serializer.ImportWorldmapFile(stream, file);
+
+        BOOST_REQUIRE_EQUAL(serializer.blocks.size(), 1u);
+        BOOST_REQUIRE_EQUAL(serializer.blocks[0].meshes.size(), 16u);
+        const VGears::WorldmapFileSerializer::BlockPart& part
+            = serializer.blocks[0].meshes[0];
+        BOOST_CHECK_EQUAL(part.header.triengle_count, 1);
+        BOOST_CHECK_EQUAL(part.header.vertex_count, 3);
+        BOOST_REQUIRE_EQUAL(part.triangles.size(), 1u);
+        const VGears::WorldmapFileSerializer::BlockTriangle& triangle
+            = part.triangles[0];
+        BOOST_CHECK_EQUAL(triangle.walkability_info_, 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);
+        BOOST_CHECK_EQUAL(triangle.vertex_1_v, 40);
+        BOOST_CHECK_EQUAL(triangle.vertex_2_u, 50);
+        BOOST_CHECK_EQUAL(triangle.vertex_2_v, 60);
+        BOOST_CHECK_EQUAL(triangle.TextureInfo, 0x155);
+        BOOST_CHECK_EQUAL(triangle.Location, 0x55);
+        BOOST_REQUIRE_EQUAL(part.vertices.size(), 3u);
+        BOOST_CHECK_EQUAL(part.vertices[0].x, -100);
+        BOOST_CHECK_EQUAL(part.vertices[0].y, 200);
+        BOOST_CHECK_EQUAL(part.vertices[0].z, -300);
+        BOOST_REQUIRE_EQUAL(part.normals.size(), 3u);
+        BOOST_CHECK_EQUAL(part.normals[0].x, 1);
+        BOOST_CHECK_EQUAL(part.normals[0].y, 2);
+        BOOST_CHECK_EQUAL(part.normals[0].z, 3);
+}
+
+BOOST_AUTO_TEST_CASE(TestMapFileSerializerRejectsPartialBlocks){
+        std::vector<VGears::uint8> data = CreateMapFixture();
+        data.pop_back();
+        BOOST_CHECK_THROW(ImportFixture(data), Ogre::Exception);
+}
+
+BOOST_AUTO_TEST_CASE(TestMapFileSerializerRejectsInvalidMeshOffsets){
+        std::vector<VGears::uint8> data = CreateMapFixture();
+        WriteUInt32(data, 0, static_cast<VGears::uint32>(WORLD_MAP_BLOCK_SIZE - 2));
+        BOOST_CHECK_THROW(ImportFixture(data), Ogre::Exception);
+}
+
+BOOST_AUTO_TEST_CASE(TestMapFileSerializerRejectsMeshesOutsideBlock){
+        std::vector<VGears::uint8> data = CreateMapFixture();
+        const size_t mesh_offset = WORLD_MAP_BLOCK_SIZE - sizeof(VGears::uint32);
+        WriteUInt32(data, 0, static_cast<VGears::uint32>(mesh_offset));
+        WriteUInt32(data, mesh_offset, 1);
+        BOOST_CHECK_THROW(ImportFixture(data), Ogre::Exception);
+}
+
+BOOST_AUTO_TEST_CASE(TestMapFileSerializerRejectsTruncatedLzsTokens){
+        std::vector<VGears::uint8> data = CreateMapFixture();
+        const size_t mesh_offset = ReadUInt32(data, 0);
+        WriteUInt32(data, mesh_offset, 2);
+        data[mesh_offset + sizeof(VGears::uint32)] = 0;
+        data[mesh_offset + sizeof(VGears::uint32) + 1] = 0x10;
+        BOOST_CHECK_THROW(ImportFixture(data), Ogre::Exception);
+}
+
+BOOST_AUTO_TEST_CASE(TestMapFileSerializerRejectsTruncatedMeshes){
+        std::vector<VGears::uint8> data = CreateMapFixture(true);
+        BOOST_CHECK_THROW(ImportFixture(data), Ogre::Exception);
+}
+
+BOOST_AUTO_TEST_CASE(TestMapFileSerializerRejectsInvalidVertexIndices){
+        std::vector<VGears::uint8> data = CreateMapFixture(false, true);
+        BOOST_CHECK_THROW(ImportFixture(data), Ogre::Exception);
 }
 

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

+ 12 - 5
test/installer/decompiler/world/instruction/WorldSubStackInstruction.cpp

@@ -16,11 +16,18 @@
 #include <boost/test/unit_test.hpp>
 #include "installer/decompiler/world/instruction/WorldSubStackInstruction.h"
 
-#pragma message("Unimplemented test file: " __FILE__)
+BOOST_AUTO_TEST_CASE(TestWorldSubStackInstructionLeftShiftIsValidLua){
+    WorldSubStackInstruction instruction;
+    instruction.SetOpcode(0x51);
 
-BOOST_AUTO_TEST_CASE(TestWorldSubStackInstruction){
-    // TODO
-    BOOST_WARN_MESSAGE(false, "Unimplemented test file: WorldSubStackInstruction");
-    BOOST_CHECK(0 == 0);
+    ValueStack stack;
+    stack.Push(new VarValue("2"));
+    stack.Push(new VarValue("2928 - Read(912)"));
+    Function function;
+    instruction.ProcessInst(function, stack, nullptr, nullptr);
+
+    BOOST_CHECK_EQUAL(
+        stack.Pop()->GetString(), "((2928 - Read(912)) * (2 ^ 2))"
+    );
 }