function executeBot(config, storage, tabs, msgCallback, saveConfigCallback, reloadCallback, websockets)
  -- load lua and otui files
  local configFiles = g_resources.listDirectoryFiles("/bot/" .. config, true, false)  
  local luaFiles = {}
  local uiFiles = {}
  for i, file in ipairs(configFiles) do
    local ext = file:split(".")
    if ext[#ext]:lower() == "lua" then
      table.insert(luaFiles, file)
    end
    if ext[#ext]:lower() == "ui" or ext[#ext]:lower() == "otui" then
      table.insert(uiFiles, file)
    end
  end
  
  if #luaFiles == 0 then
    return error("Config (/bot/" .. config .. ") doesn't have lua files")
  end
  
  -- init bot variables
  local context = {}
  context.configDir = "/bot/".. config
  context.tabs = tabs
  context.mainTab = context.tabs:addTab("Main", g_ui.createWidget('BotPanel')).tabPanel.content
  context.panel = context.mainTab
  context.saveConfig = saveConfigCallback
  context.reload = reloadCallback
  
  context.storage = storage
  if context.storage._macros == nil then
    context.storage._macros = {} -- active macros
  end

  -- websockets, macros, hotkeys, scheduler, icons, callbacks
  context._websockets = websockets
  context._macros = {}
  context._hotkeys = {}
  context._scheduler = {}
  context._callbacks = {
    onKeyDown = {},
    onKeyUp = {},
    onKeyPress = {},
    onTalk = {},
    onTextMessage = {},
    onLoginAdvice = {},
    onAddThing = {},
    onRemoveThing = {},
    onCreatureAppear = {},
    onCreatureDisappear = {},
    onCreaturePositionChange = {},
    onCreatureHealthPercentChange = {},
    onUse = {},
    onUseWith = {},
    onContainerOpen = {},
    onContainerClose = {},
    onContainerUpdateItem = {},
    onMissle = {},
    onAnimatedText = {},
    onStaticText = {},
    onChannelList = {},
    onOpenChannel = {},
    onCloseChannel = {},
    onChannelEvent = {},
    onTurn = {},
    onWalk = {},
    onImbuementWindow = {},
    onModalDialog = {},
    onAttackingCreatureChange = {},
    onManaChange = {},
    onStatesChange = {},
    onAddItem = {},
    onGameEditText = {},
    onGroupSpellCooldown = {},
    onSpellCooldown = {},
    onRemoveItem = {},
    onInventoryChange = {},
    onGameQuestLog = {},
    onGameQuestLine = {}
  }
  
  -- basic functions & classes
  context.print = print
  context.bit32 = bit32
  context.bit = bit
  context.pairs = pairs
  context.ipairs = ipairs
  context.tostring = tostring
  context.math = math
  context.table = table
  context.setmetatable = setmetatable
  context.string = string
  context.tonumber = tonumber
  context.type = type
  context.pcall = pcall
  context.os = {
    time = os.time,
    difftime = os.difftime,
    date = os.date,
    clock = os.clock
  }
  context.load = function(str) return assert(load(str, nil, nil, context)) end
  context.loadstring = context.load
  context.assert = assert
  context.dofile = function(file) assert(load(g_resources.readFileContents("/bot/" .. config .. "/" .. file), file, nil, context))() end
  context.gcinfo = gcinfo
  context.tr = tr
  context.json = json
  context.base64 = base64
  context.regexMatch = regexMatch
  context.getDistanceBetween = function(p1, p2)
    return math.max(math.abs(p1.x - p2.x), math.abs(p1.y - p2.y))
  end
  context.isMobile = g_app.isMobile
  context.getVersion = g_app.getVersion
  
  -- classes
  context.g_resources = g_resources
  context.g_game = g_game
  context.g_map = g_map
  context.g_ui = g_ui
  context.g_sounds = g_sounds
  context.g_window = g_window
  context.g_mouse = g_mouse
  context.g_keyboard = g_keyboard
  context.g_things = g_things
  context.g_settings = g_settings
  context.g_platform = {
    openUrl = g_platform.openUrl,
    openDir = g_platform.openDir,
  }

  context.Item = Item
  context.Creature = Creature
  context.ThingType = ThingType
  context.Effect = Effect
  context.Missile = Missile
  context.Player = Player
  context.Monster = Monster
  context.StaticText = StaticText
  context.HTTP = HTTP
  context.OutputMessage = OutputMessage
  context.modules = modules

  -- log functions
  context.info = function(text) return msgCallback("info", tostring(text)) end
  context.warn = function(text) return msgCallback("warn", tostring(text)) end
  context.error = function(text) return msgCallback("error", tostring(text)) end
  context.warning = context.warn      

  -- init context
  context.now = g_clock.millis()
  context.time = g_clock.millis()
  context.player = g_game.getLocalPlayer()

  -- init functions
  G.botContext = context
  dofiles("functions")
  context.Panels = {}
  dofiles("panels")
  G.botContext = nil

  -- run ui scripts
  for i, file in ipairs(uiFiles) do
    g_ui.importStyle(file)
  end

  -- run lua script
  for i, file in ipairs(luaFiles) do
    assert(load(g_resources.readFileContents(file), file, nil, context))()
    context.panel = context.mainTab -- reset default tab
  end

  return {
    script = function()      
      context.now = g_clock.millis()
      context.time = g_clock.millis()
      
      for i, macro in ipairs(context._macros) do
        if macro.lastExecution + macro.timeout <= context.now and macro.enabled then
          local status, result = pcall(function()
            if macro.callback(macro) then
                macro.lastExecution = context.now
            end
          end)
          if not status then
            context.error("Macro: " .. macro.name .. " execution error: " .. result)
          end
        end
      end
      
      while #context._scheduler > 0 and context._scheduler[1].execution <= g_clock.millis() do
        local status, result = pcall(function()
          context._scheduler[1].callback()
        end)
        if not status then
          context.error("Schedule execution error: " .. result)
        end
        table.remove(context._scheduler, 1)
      end
    end,
    callbacks = {
      onKeyDown = function(keyCode, keyboardModifiers)
        local keyDesc = determineKeyComboDesc(keyCode, keyboardModifiers)
        for i, macro in ipairs(context._macros) do
          if macro.switch and macro.hotkey == keyDesc then
            macro.switch:onClick()
          end
        end
        local hotkey = context._hotkeys[keyDesc]
        if hotkey then
          if hotkey.single then
            if hotkey.callback() then
              hotkey.lastExecution = context.now            
            end
          end
          if hotkey.switch then
            hotkey.switch:setOn(true)
          end
        end
        for i, callback in ipairs(context._callbacks.onKeyDown) do
          callback(keyDesc)
        end
      end,
      onKeyUp = function(keyCode, keyboardModifiers)
        local keyDesc = determineKeyComboDesc(keyCode, keyboardModifiers)
        local hotkey = context._hotkeys[keyDesc]
        if hotkey then        
          if hotkey.switch then
            hotkey.switch:setOn(false)
          end
        end
        for i, callback in ipairs(context._callbacks.onKeyUp) do
          callback(keyDesc)
        end
      end,
      onKeyPress = function(keyCode, keyboardModifiers, autoRepeatTicks)
        local keyDesc = determineKeyComboDesc(keyCode, keyboardModifiers)
        local hotkey = context._hotkeys[keyDesc]
        if hotkey and not hotkey.single then
          if hotkey.callback() then
            hotkey.lastExecution = context.now          
          end
        end
        for i, callback in ipairs(context._callbacks.onKeyPress) do
          callback(keyDesc, autoRepeatTicks)
        end
      end,
      onTalk = function(name, level, mode, text, channelId, pos)
        for i, callback in ipairs(context._callbacks.onTalk) do
          callback(name, level, mode, text, channelId, pos)
        end
      end,
      onImbuementWindow = function(itemId, slots, activeSlots, imbuements, needItems)
        for i, callback in ipairs(context._callbacks.onImbuementWindow) do
          callback(itemId, slots, activeSlots, imbuements, needItems)
        end
      end,
      onTextMessage = function(mode, text)
        for i, callback in ipairs(context._callbacks.onTextMessage) do
          callback(mode, text)
        end
      end,
      onLoginAdvice = function(message)
        for i, callback in ipairs(context._callbacks.onLoginAdvice) do
          callback(message)
        end
      end,      
      onAddThing = function(tile, thing)
        for i, callback in ipairs(context._callbacks.onAddThing) do
          callback(tile, thing)
        end      
      end,
      onRemoveThing = function(tile, thing)
        for i, callback in ipairs(context._callbacks.onRemoveThing) do
          callback(tile, thing)
        end      
      end,
      onCreatureAppear = function(creature)
        for i, callback in ipairs(context._callbacks.onCreatureAppear) do
          callback(creature)
        end      
      end,
      onCreatureDisappear = function(creature)
        for i, callback in ipairs(context._callbacks.onCreatureDisappear) do
          callback(creature)
        end
      end,
      onCreaturePositionChange = function(creature, newPos, oldPos)
        for i, callback in ipairs(context._callbacks.onCreaturePositionChange) do
          callback(creature, newPos, oldPos)
        end      
      end,
      onCreatureHealthPercentChange = function(creature, healthPercent)
        for i, callback in ipairs(context._callbacks.onCreatureHealthPercentChange) do
          callback(creature, healthPercent)
        end      
      end,
      onUse = function(pos, itemId, stackPos, subType)
        for i, callback in ipairs(context._callbacks.onUse) do
          callback(pos, itemId, stackPos, subType)
        end      
      end,
      onUseWith = function(pos, itemId, target, subType)
        for i, callback in ipairs(context._callbacks.onUseWith) do
          callback(pos, itemId, target, subType)
        end
      end,
      onContainerOpen = function(container, previousContainer)
        for i, callback in ipairs(context._callbacks.onContainerOpen) do
          callback(container, previousContainer)
        end
      end,
      onContainerClose = function(container)
        for i, callback in ipairs(context._callbacks.onContainerClose) do
          callback(container)
        end
      end,
      onContainerUpdateItem = function(container, slot, item, oldItem)
        for i, callback in ipairs(context._callbacks.onContainerUpdateItem) do
          callback(container, slot, item, oldItem)
        end
      end,
      onMissle = function(missle)
        for i, callback in ipairs(context._callbacks.onMissle) do
          callback(missle)
        end
      end,
      onAnimatedText = function(thing, text)
        for i, callback in ipairs(context._callbacks.onAnimatedText) do
          callback(thing, text)
        end
      end,
      onStaticText = function(thing, text)
        for i, callback in ipairs(context._callbacks.onStaticText) do
          callback(thing, text)
        end
      end,
      onChannelList = function(channels)
        for i, callback in ipairs(context._callbacks.onChannelList) do
          callback(channels)
        end      
      end,
      onOpenChannel = function(channelId, channelName)
        for i, callback in ipairs(context._callbacks.onOpenChannel) do
          callback(channels)
        end      
      end,
      onCloseChannel = function(channelId)
        for i, callback in ipairs(context._callbacks.onCloseChannel) do
          callback(channelId)
        end      
      end,
      onChannelEvent = function(channelId, name, event)
        for i, callback in ipairs(context._callbacks.onChannelEvent) do
          callback(channelId, name, event)
        end      
      end,
      onTurn = function(creature, direction)
        for i, callback in ipairs(context._callbacks.onTurn) do
          callback(creature, direction)
        end      
      end,
      onWalk = function(creature, oldPos, newPos)
        for i, callback in ipairs(context._callbacks.onWalk) do
          callback(creature, oldPos, newPos)
        end      
      end,
      onModalDialog = function(id, title, message, buttons, enterButton, escapeButton, choices, priority)
        for i, callback in ipairs(context._callbacks.onModalDialog) do
          callback(id, title, message, buttons, enterButton, escapeButton, choices, priority)
        end
      end,
      onGameEditText = function(id, itemId, maxLength, text, writer, time)
        for i, callback in ipairs(context._callbacks.onGameEditText) do
          callback(id, itemId, maxLength, text, writer, time)
        end
      end,
      onAttackingCreatureChange = function(creature, oldCreature)
        for i, callback in ipairs(context._callbacks.onAttackingCreatureChange) do
          callback(creature, oldCreature)
        end
      end,
      onManaChange = function(player, mana, maxMana, oldMana, oldMaxMana)
        for i, callback in ipairs(context._callbacks.onManaChange) do
          callback(player, mana, maxMana, oldMana, oldMaxMana)
        end
      end,
      onAddItem = function(container, slot, item)
        for i, callback in ipairs(context._callbacks.onAddItem) do
          callback(container, slot, item)
        end
      end,
      onRemoveItem = function(container, slot, item)
        for i, callback in ipairs(context._callbacks.onRemoveItem) do
          callback(container, slot, item)
        end
      end,
      onStatesChange = function(player, states, oldStates)
        for i, callback in ipairs(context._callbacks.onStatesChange) do
          callback(player, states, oldStates)
        end
      end,
      onGroupSpellCooldown = function(iconId, duration)
        for i, callback in ipairs(context._callbacks.onGroupSpellCooldown) do
          callback(iconId, duration)
        end
      end,
      onSpellCooldown = function(iconId, duration)
        for i, callback in ipairs(context._callbacks.onSpellCooldown) do
          callback(iconId, duration)
        end
      end,
      onSpellCooldown = function(iconId, duration)
        for i, callback in ipairs(context._callbacks.onSpellCooldown) do
          callback(iconId, duration)
        end
      end,
      onInventoryChange = function(player, slot, item, oldItem)
        for i, callback in ipairs(context._callbacks.onInventoryChange) do
          callback(player, slot, item, oldItem)
        end
      end,
      onGameQuestLog = function(quests)
        local tmp = {}
        for j,questEntry in pairs(quests) do
          local id, name, completed = unpack(questEntry)
          table.insert(tmp, { id = id, name = name, completed = completed })
        end
        for i, callback in ipairs(context._callbacks.onGameQuestLog) do
          callback(tmp)
        end
      end,
      onGameQuestLine = function(questId, questMissions)
        local tmp = {}
        for i,questMission in pairs(questMissions) do
          local name, description = unpack(questMission)
          table.insert(tmp, { name = name, description = description })
        end
        for i, callback in ipairs(context._callbacks.onGameQuestLine) do
          callback(questId, tmp)
        end
      end,
    }    
  }
end