igEndTabItem и так далее с ig все вызывается только с помощью FFI
```lua
local function beginTabBar(name, flags)
    return igBeginTabBar(name, flags or 0)
end

local function endTabBar()
    igEndTabBar()
end

local function beginTabItem(name)
    return igBeginTabItem(name, nil, 0)
end

local function endTabItem()
    igEndTabItem()
end

-- imports
local imgui = require "imgui"
local encoding = require "encoding"
encoding.default = "CP1251"
u8 = encoding.UTF8

-- Logger модуль
local Logger = {}
Logger.__index = Logger

function Logger:new()
    local o = {}
    setmetatable(o, self)
    o.enabled = true
    return o
end

function Logger:log(message, level)
    level = level or "INFO"
    if self.enabled then
        print(string.format("[%s] %s", level, message))
    end
end

local logger = Logger:new()

local function safeCall(fn, ...)
    local ok, err = pcall(fn, ...)
    if not ok then
        logger:log("Error: " .. tostring(err), "ERROR")
    end
    return ok, err
end

-- Placeholder stubs for external functions / constants from sampfuncs, raknet, etc.
-- Эти функции нужно реализовать/подключить в вашем окружении
local ffi = require "ffi"
local sampfuncs = {
    HIGH_PRIORITY = 1,
    UNRELIABLE_SEQUENCED = 2
}
local raknet = {
    PACKET = {
        PLAYER_SYNC = 0,
        VEHICLE_SYNC = 1,
        PASSENGER_SYNC = 2,
        AIM_SYNC = 3,
        TRAILER_SYNC = 4,
        UNOCCUPIED_SYNC = 5,
        BULLET_SYNC = 6,
        SPECTATOR_SYNC = 7,
    },
    raknetNewBitStream = function() return {} end,
    raknetBitStreamWriteInt8 = function() end,
    raknetBitStreamWriteBuffer = function() end,
    raknetSendBitStreamEx = function() end,
    raknetDeleteBitStream = function() end,
}

local PLAYER_PED = 0

local SyncDataFactory = {}
SyncDataFactory.__index = SyncDataFactory
SyncDataFactory.SYNC_TYPES = {
    player = {ctype = "PlayerSyncData", packet = raknet.PACKET.PLAYER_SYNC, copier = sampStorePlayerOnfootData},
    vehicle = {ctype = "VehicleSyncData", packet = raknet.PACKET.VEHICLE_SYNC, copier = sampStorePlayerIncarData},
    passenger = {ctype = "PassengerSyncData", packet = raknet.PACKET.PASSENGER_SYNC, copier = sampStorePlayerPassengerData},
    aim = {ctype = "AimSyncData", packet = raknet.PACKET.AIM_SYNC, copier = sampStorePlayerAimData},
    trailer = {ctype = "TrailerSyncData", packet = raknet.PACKET.TRAILER_SYNC, copier = sampStorePlayerTrailerData},
    unoccupied = {ctype = "UnoccupiedSyncData", packet = raknet.PACKET.UNOCCUPIED_SYNC, copier = nil},
    bullet = {ctype = "BulletSyncData", packet = raknet.PACKET.BULLET_SYNC, copier = nil},
    spectator = {ctype = "SpectatorSyncData", packet = raknet.PACKET.SPECTATOR_SYNC, copier = nil}
}

function SyncDataFactory:create(sync_type, copy_from_player)
    copy_from_player = copy_from_player == nil and true or copy_from_player
    local info = self.SYNC_TYPES[sync_type]
    if not info then
        error("Unknown sync_type: " .. tostring(sync_type))
    end

    local data = ffi.new("struct " .. info.ctype)
    local raw_ptr = tonumber(ffi.cast("uintptr_t", ffi.new(info.ctype .. "*", data)))

    if copy_from_player and info.copier then
        local _, player_id = sampGetPlayerIdByCharHandle(PLAYER_PED)
        if not player_id or player_id < 0 then
            logger:log("Invalid player_id while copying sync data", "ERROR")
        else
            info.copier(player_id, raw_ptr)
        end
    end

    local function send()
        local bs = raknet.raknetNewBitStream()
        raknet.raknetBitStreamWriteInt8(bs, info.packet)
        raknet.raknetBitStreamWriteBuffer(bs, raw_ptr, ffi.sizeof(data))
        raknet.raknetSendBitStreamEx(bs, sampfuncs.HIGH_PRIORITY, sampfuncs.UNRELIABLE_SEQUENCED, 1)
        raknet.raknetDeleteBitStream(bs)
        logger:log("Sent sync data of type " .. sync_type)
    end

    local mt = {
        __index = function(_, key)
            if key == "send" then return send end
            return data[key]
        end,
        __newindex = function(_, key, value)
            data[key] = value
        end,
    }

    return setmetatable({}, mt)
end

local syncFactory = SyncDataFactory

-- TeleportController
local TeleportController = {}
TeleportController.__index = TeleportController

function TeleportController:new(playerPed)
    local o = {
        playerPed = playerPed,
        stepCount = 5,
        stepDelayMs = 1000,
        logger = logger,
    }
    setmetatable(o, self)
    return o
end

function TeleportController:setStepCount(count)
    if count and count > 0 then
        self.stepCount = count
        self.logger:log("Step count set to " .. tostring(count))
    else
        self.logger:log("Invalid step count: " .. tostring(count), "WARN")
    end
end

function TeleportController:setStepDelay(delayMs)
    if delayMs and delayMs >= 0 then
        self.stepDelayMs = delayMs
        self.logger:log("Step delay set to " .. tostring(delayMs) .. "ms")
    else
        self.logger:log("Invalid step delay: " .. tostring(delayMs), "WARN")
    end
end

function TeleportController:instantTeleport(x, y, z)
    if not (x and y and z) then
        self.logger:log("instantTeleport called with invalid coords", "ERROR")
        return false
    end
    setCharCoordinates(self.playerPed, x, y, z)
    self.logger:log(string.format("Instant teleported player to (%.3f, %.3f, %.3f)", x, y, z))
    return true
end

function TeleportController:stepTeleport(x, y, z)
    if not (x and y and z) then
        self.logger:log("stepTeleport called with invalid coords", "ERROR")
        return false
    end

    lua_thread.create(function()
        local startX, startY, startZ = getCharCoordinates(self.playerPed)
        local groundZ = getGroundZFor3dCoord(x, y, z)

        local deltaX = (x - startX) / self.stepCount
        local deltaY = (y - startY) / self.stepCount
        local deltaZ = (groundZ - startZ) / self.stepCount

        for i = 1, self.stepCount do
            startX = startX + deltaX
            startY = startY + deltaY
            startZ = startZ + deltaZ

            local syncData = syncFactory:create("player")
            syncData.moveSpeed.x = math.random(100, 1000)
            syncData.moveSpeed.y = math.random(100, 1000)
            syncData.moveSpeed.z = math.random(100, 1000)
            syncData:send()
            sampForceOnfootSync()
            setCharCoordinates(self.playerPed, startX, startY, startZ)

            wait(self.stepDelayMs)
        end
        self.logger:log(string.format("Completed step teleport to (%.3f, %.3f, %.3f)", x, y, z))
    end)
    return true
end

-- MarkerFinder
local MarkerFinder = {}
MarkerFinder.__index = MarkerFinder

function MarkerFinder:new()
    local o = {logger = logger}
    setmetatable(o, self)
    return o
end

function MarkerFinder:findNearestMarker(posX, posY, posZ, radius, isRace)
    local closestDistance = radius
    local resultX, resultY, resultZ = nil, nil, nil
    local found = false

    local baseAddress = isRace and 0xC7F168 or 0xC7DD88
    local stride = isRace and 56 or 160

    for id = 0, 31 do
        local markerAddress = baseAddress + id * stride
        local markerPosX = representIntAsFloat(readMemory(markerAddress + 0, 4, false))
        local markerPosY = representIntAsFloat(readMemory(markerAddress + 4, 4, false))
        local markerPosZ = representIntAsFloat(readMemory(markerAddress + 8, 4, false))

        if markerPosX ~= 0.0 or markerPosY ~= 0.0 or markerPosZ ~= 0.0 then
            local dist = getDistanceBetweenCoords3d(markerPosX, markerPosY, markerPosZ, posX, posY, posZ)
            if dist < closestDistance then
                closestDistance = dist
                resultX, resultY, resultZ = markerPosX, markerPosY, markerPosZ
                found = true
            end
        end
    end

    if found then
        self.logger:log(string.format("Found marker at (%.3f, %.3f, %.3f), distance %.2f", resultX, resultY, resultZ, closestDistance))
    else
        self.logger:log("No marker found within radius " .. tostring(radius), "WARN")
    end

    return found, resultX, resultY, resultZ
end

-- BlipFinder
local BlipFinder = {}
BlipFinder.__index = BlipFinder

function BlipFinder:new()
    local o = {logger = logger}
    setmetatable(o, self)
    return o
end

function BlipFinder:getTargetBlipCoordinates()
    local found, x, y, z = getTargetBlipCoordinates()
    if not found then
        self.logger:log("No target blip found", "WARN")
        return false
    end
    requestCollision(x, y)
    loadScene(x, y, z)
    local found2, x2, y2, z2 = getTargetBlipCoordinates()
    if not found2 then
        self.logger:log("No target blip found after loading scene", "WARN")
        return false
    end
    return true, x2, y2, z2
end

-- UIManager
local UIManager = {}
UIManager.__index = UIManager

function UIManager:new(config)
    local o = {
        windowOpen = imgui.ImBool(false), -- <--- ИСПРАВЛЕНИЕ: ImBool вместо bool
        config = config or {},
        posBuffers = {
            x = imgui.ImBuffer(128),
            y = imgui.ImBuffer(128),
            z = imgui.ImBuffer(128),
        },
        checkboxStates = {
            enableTeleportSteps = imgui.ImBool(false),
            enableBlipFinder = imgui.ImBool(false),
            enableMarkerFinder = imgui.ImBool(false),
            enableGovnovoz = imgui.ImBool(false),
        },
        sliderValues = {
            stepCount = imgui.ImInt(5),
            stepDelay = imgui.ImInt(1000),
            markerDistance = imgui.ImInt(100),
            govnovozHeight = imgui.ImInt(100),
        },
        logger = logger,
    }
    setmetatable(o, self)
    return o
end

function UIManager:updatePositionFields(x, y, z)
    self.posBuffers.x.v = string.format("%.3f", x)
    self.posBuffers.y.v = string.format("%.3f", y)
    self.posBuffers.z.v = string.format("%.3f", z)
end

function UIManager:draw()
    if not self.windowOpen.v then return end -- <--- использовать .v во всех местах

    imgui.SetNextWindowSize(imgui.ImVec2(560, 480), imgui.Cond.FirstUseEver)
    imgui.SetNextWindowPos(imgui.ImVec2(320, 180), imgui.Cond.FirstUseEver)

    local opened = imgui.Begin("— OTP By Tectrex —", self.windowOpen,
        imgui.WindowFlags.NoCollapse + imgui.WindowFlags.NoResize + imgui.WindowFlags.NoScrollbar)

    if not opened then
        imgui.End()
        return
    end

    if imgui.BeginTabBar("##main_tabs", 0) then

        if imgui.BeginTabItem("Телепортация") then
            local tpCtrl = self.config.teleportController
            local pf = self.posBuffers
            local cb = self.checkboxStates
            local sv = self.sliderValues

            imgui.Text("Координаты для телепорта:")
            imgui.PushItemWidth(140)
            imgui.InputText("X", pf.x)
            imgui.SameLine(0, 10)
            imgui.InputText("Y", pf.y)
            imgui.SameLine(0, 10)
            imgui.InputText("Z", pf.z)
            imgui.PopItemWidth()

            imgui.Separator()
            imgui.Text("Настройки телепортации:")

            if imgui.Checkbox("Пошаговая телепортация (Coord-Master)", cb.enableTeleportSteps) then
                self.logger:log("Пошаговая телепортация: " .. tostring(cb.enableTeleportSteps.v))
            end

            imgui.PushItemWidth(250)
            if imgui.SliderInt("Количество шагов", sv.stepCount, 1, 50) then
                self.logger:log("Количество шагов установлено: " .. sv.stepCount.v)
                tpCtrl:setStepCount(sv.stepCount.v)
            end

            if imgui.SliderInt("Задержка между шагами (мс)", sv.stepDelay, 0, 2000) then
                self.logger:log("Задержка между шагами установлена: " .. sv.stepDelay.v)
                tpCtrl:setStepDelay(sv.stepDelay.v)
            end
            imgui.PopItemWidth()

            imgui.Separator()

            if imgui.Button("Телепортировать", imgui.ImVec2(-1, 0)) then
                local x = tonumber(pf.x.v)
                local y = tonumber(pf.y.v)
                local z = tonumber(pf.z.v)
                if x and y and z then
                    if cb.enableTeleportSteps.v then
                        tpCtrl:stepTeleport(x, y, z)
                    else
                        tpCtrl:instantTeleport(x, y, z)
                    end
                else
                    self.logger:log("Неверные координаты для телепортации", "ERROR")
                    print("Введите корректные числовые координаты!")
                end
            end

            imgui.Spacing()
            imgui.Text("Быстрые действия:")
            if imgui.Button("Скопировать координаты игрока") then
                local px, py, pz = getCharCoordinates(self.config.playerPed)
                self:updatePositionFields(px, py, pz)
                self.logger:log("Скопированы координаты игрока")
            end
            imgui.SameLine()
            if imgui.Button("Скопировать координаты ближайшего маркера") then
                local px, py, pz = getCharCoordinates(self.config.playerPed)
                local found, mx, my, mz = self.config.markerFinder:findNearestMarker(px, py, pz, sv.markerDistance.v, false)
                if not found then
                    printStringNow("Маркеры не найдены, пробуем гонки...", 1000)
                    found, mx, my, mz = self.config.markerFinder:findNearestMarker(px, py, pz, sv.markerDistance.v, true)
                end
                if found then
                    self:updatePositionFields(mx, my, mz)
                    self.logger:log("Скопированы координаты маркера")
                else
                    printStringNow("Маркеры отсутствуют", 1000)
                    self.logger:log("Не удалось найти маркер рядом с игроком", "WARN")
                end
            end
            imgui.SameLine()
            if imgui.Button("Скопировать координаты блипа") then
                local found, bx, by, bz = self.config.blipFinder:getTargetBlipCoordinates()
                if found then
                    self:updatePositionFields(bx, by, bz)
                    self.logger:log("Скопированы координаты блипа")
                else
                    printStringNow("Блип не найден", 1000)
                    self.logger:log("Не найден блип для копирования", "WARN")
                end
            end

            imgui.EndTabItem()
        end

        if imgui.BeginTabItem("Поиск") then
            local cb = self.checkboxStates
            local sv = self.sliderValues

            if imgui.CollapsingHeader("Поиск Маркеров", imgui.TreeNodeFlags.DefaultOpen) then
                if imgui.Checkbox("Включить поиск маркеров", cb.enableMarkerFinder) then
                    self.logger:log("Поиск маркеров: " .. tostring(cb.enableMarkerFinder.v))
                end

                imgui.PushItemWidth(200)
                if imgui.SliderInt("Макс. расстояние до маркера", sv.markerDistance, 1, 10000) then
                    self.logger:log("Радиус поиска маркера изменён: " .. sv.markerDistance.v)
                end
                imgui.PopItemWidth()

                if cb.enableMarkerFinder.v then
                    local px, py, pz = getCharCoordinates(self.config.playerPed)
                    local found, mx, my, mz = self.config.markerFinder:findNearestMarker(px, py, pz, sv.markerDistance.v, false)
                    if not found then
                        printStringNow("Маркеры не найдены, пробуем гонки...", 1000)
                        found, mx, my, mz = self.config.markerFinder:findNearestMarker(px, py, pz, sv.markerDistance.v, true)
                    end
                    if found then
                        self:updatePositionFields(mx, my, mz)
                    end
                end
            end

            imgui.Separator()

            if imgui.CollapsingHeader("Поиск Блипов", imgui.TreeNodeFlags.DefaultOpen) then
                if imgui.Checkbox("Включить поиск блипов", cb.enableBlipFinder) then
                    self.logger:log("Поиск блипов: " .. tostring(cb.enableBlipFinder.v))
                end

                if cb.enableBlipFinder.v then
                    local found, bx, by, bz = self.config.blipFinder:getTargetBlipCoordinates()
                    if found then
                        self:updatePositionFields(bx, by, bz)
                    else
                        printStringNow("Блипы не найдены", 1000)
                    end
                end
            end

            imgui.EndTabItem()
        end

        if imgui.BeginTabItem("Говновоз") then
            local cb = self.checkboxStates
            local sv = self.sliderValues
            local playerPed = self.config.playerPed

            if isCharInAnyCar(playerPed) then
                imgui.TextColored(imgui.ImVec4(0.2, 1.0, 0.2, 1.0), "Игрок находится в транспорте")
                if imgui.Checkbox("Включить Говновоз", cb.enableGovnovoz) then
                    self.logger:log("Говновоз: " .. tostring(cb.enableGovnovoz.v))
                end

                imgui.PushItemWidth(250)
                if imgui.SliderInt("Высота говновоза", sv.govnovozHeight, 1, 500) then
                    self.logger:log("Высота говновоза изменена: " .. sv.govnovozHeight.v)
                end
                imgui.PopItemWidth()

                if cb.enableGovnovoz.v then
                    local car = getCarCharIsUsing(playerPed)
                    local posX, posY, _ = tonumber(self.posBuffers.x.v) or 0, tonumber(self.posBuffers.y.v) or 0, tonumber(self.posBuffers.z.v) or 0
                    local carX, carY, _ = getCarCoordinates(car)
                    local heading = getHeadingFromVector2d(posX - carX, posY - carY)
                    setCarCoordinates(car, carX, carY, sv.govnovozHeight.v)
                    setCarForwardSpeed(car, 100)
                    setCarHeading(car, heading)
                    setCharCanBeKnockedOffBike(playerPed, true)
                else
                    freezeCarPosition(getCarCharIsUsing(playerPed), false)
                    setCharCanBeKnockedOffBike(playerPed, false)
                end
            else
                imgui.TextColored(imgui.ImVec4(1.0, 0.4, 0.4, 1.0), "Игрок не в транспорте")
            end

            imgui.EndTabItem()
        end

        if imgui.BeginTabItem("Информация") then
            imgui.TextWrapped("OTP — скрипт телепортации и поиска маркеров в SA-MP от Tectrex.\n\n" ..
                              "Используйте вкладки для управления телепортацией, поиска ближайших маркеров и управления говновозом.\n\n" ..
                              "Команды:")

            imgui.BulletText("/otp - открыть/закрыть окно\n/otpr - телепортировать по координатам из полей")
            imgui.BulletText("Телепорт с шагами создаёт более реалистичный эффект перемещения")
            imgui.BulletText("Быстрая смена координат с помощью кнопок копирования")

            imgui.EndTabItem()
        end

        imgui.EndTabBar()
    end

    if imgui.Button("Закрыть окно") then
        self.windowOpen.v = false
        self.logger:log("OTP окно закрыто пользователем")
    end
    imgui.SameLine()
    if imgui.Button("Сбросить координаты") then
        self:updatePositionFields(0, 0, 0)
        self.logger:log("Координаты сброшены в 0")
    end

    imgui.End()
end

-- MainApp
local MainApp = {}
MainApp.__index = MainApp

function MainApp:new()
    local o = {
        playerPed = PLAYER_PED,
        teleportController = nil,
        markerFinder = nil,
        blipFinder = nil,
        uiManager = nil,
        initialized = false,
    }
    setmetatable(o, self)
    return o
end

function MainApp:init()
    if self.initialized then return end

    self.teleportController = TeleportController:new(self.playerPed)
    self.markerFinder = MarkerFinder:new()
    self.blipFinder = BlipFinder:new()
    self.uiManager = UIManager:new({
        playerPed = self.playerPed,
        teleportController = self.teleportController,
        markerFinder = self.markerFinder,
        blipFinder = self.blipFinder,
    })

    sampRegisterChatCommand("otp", function()
        self.uiManager.windowOpen.v = not self.uiManager.windowOpen.v
        logger:log("OTP окно переключено: " .. tostring(self.uiManager.windowOpen.v))
    end)

    sampRegisterChatCommand("otpr", function()
        local x, y, z = tonumber(self.uiManager.posBuffers.x.v), tonumber(self.uiManager.posBuffers.y.v), tonumber(self.uiManager.posBuffers.z.v)
        if x and y and z then
            if self.uiManager.checkboxStates.enableTeleportSteps.v then
                self.teleportController:stepTeleport(x, y, z)
            else
                self.teleportController:instantTeleport(x, y, z)
            end
        else
            logger:log("Неверные координаты в команде /otpr", "ERROR")
            print("Неверные координаты")
        end
    end)

    self.initialized = true
    logger:log("MainApp инициализирован")
end

function MainApp:run()
    self:init()

    while not isSampAvailable() do
        wait(200)
    end

    imgui.Process = false
    self.uiManager.windowOpen.v = false

    while true do
        wait(0)
        imgui.Process = self.uiManager.windowOpen.v
    end
end

function imgui.OnDrawFrame()
    if not _G.mainAppInstance then
        _G.mainAppInstance = MainApp:new()
    end
    safeCall(function()
        _G.mainAppInstance.uiManager:draw()
    end)
end

function main()
    _G.mainAppInstance = MainApp:new()
    safeCall(function()
        _G.mainAppInstance:run()
    end)
end
```
Напиши полный код, добавь FFI