1botWindow = nil
2botButton = nil
3contentsPanel = nil
4editWindow = nil
5
6local checkEvent = nil
7
8local botStorage = {}
9local botStorageFile = nil
10local botWebSockets = {}
11local botMessages = nil
12local botTabs = nil
13local botExecutor = nil
14
15local configList = nil
16local enableButton = nil
17local executeEvent = nil
18local statusLabel = nil
19
20local configManagerUrl = "http://otclient.ovh/configs.php"
21
22function init()
23 dofile("executor")
24
25 g_ui.importStyle("ui/basic.otui")
26 g_ui.importStyle("ui/panels.otui")
27 g_ui.importStyle("ui/config.otui")
28 g_ui.importStyle("ui/icons.otui")
29 g_ui.importStyle("ui/container.otui")
30
31 connect(g_game, {
32 onGameStart = online,
33 onGameEnd = offline,
34 })
35
36 initCallbacks()
37
38 botButton = modules.client_topmenu.addRightGameToggleButton('botButton', tr('Bot'), '/images/topbuttons/bot', toggle, false, 99999)
39
40 botWindow = g_ui.loadUI('bot', modules.game_interface.getLeftPanel())
41 botWindow:setup()
42
43 contentsPanel = botWindow.contentsPanel
44 configList = contentsPanel.config
45 enableButton = contentsPanel.enableButton
46 statusLabel = contentsPanel.statusLabel
47 botMessages = contentsPanel.messages
48 botTabs = contentsPanel.botTabs
49 botTabs:setContentWidget(contentsPanel.botPanel)
50
51 editWindow = g_ui.displayUI('edit')
52 editWindow:hide()
53
54 if g_game.isOnline() then
55 clear()
56 online()
57 end
58end
59
60function terminate()
61 save()
62 clear()
63
64 disconnect(g_game, {
65 onGameStart = online,
66 onGameEnd = offline,
67 })
68
69 terminateCallbacks()
70 editWindow:destroy()
71
72 botWindow:destroy()
73 --botButton:destroy()
74end
75
76function clear()
77 botExecutor = nil
78 removeEvent(checkEvent)
79
80 -- optimization, callback is not used when not needed
81 g_game.enableTileThingLuaCallback(false)
82
83 botTabs:clearTabs()
84 botTabs:setOn(false)
85
86 botMessages:destroyChildren()
87 botMessages:updateLayout()
88
89 for i, socket in pairs(botWebSockets) do
90 g_http.cancel(socket)
91 botWebSockets[i] = nil
92 end
93
94 for i, widget in pairs(g_ui.getRootWidget():getChildren()) do
95 if widget.botWidget then
96 widget:destroy()
97 end
98 end
99 for i, widget in pairs(modules.game_interface.gameMapPanel:getChildren()) do
100 if widget.botWidget then
101 widget:destroy()
102 end
103 end
104
105 local gameMapPanel = modules.game_interface.getMapPanel()
106 if gameMapPanel then
107 gameMapPanel:unlockVisibleFloor()
108 end
109
110 if g_sounds then
111 g_sounds.getChannel(SoundChannels.Bot):stop()
112 end
113end
114
115
116function refresh()
117 if not g_game.isOnline() then return end
118 save()
119 clear()
120
121 -- create bot dir
122 if not g_resources.directoryExists("/bot") then
123 g_resources.makeDir("/bot")
124 if not g_resources.directoryExists("/bot") then
125 return onError("Can't create bot directory in " .. g_resources.getWriteDir())
126 end
127 end
128
129 -- get list of configs
130 createDefaultConfigs()
131 local configs = g_resources.listDirectoryFiles("/bot", false, false)
132
133 -- clean
134 configList.onOptionChange = nil
135 enableButton.onClick = nil
136 configList:clearOptions()
137
138 -- select active config based on settings
139 local settings = g_settings.getNode('bot') or {}
140 local index = g_game.getCharacterName() .. "_" .. g_game.getClientVersion()
141 if settings[index] == nil then
142 settings[index] = {
143 enabled=false,
144 config=""
145 }
146 end
147
148 -- init list and buttons
149 for i=1,#configs do
150 configList:addOption(configs[i])
151 end
152 configList:setCurrentOption(settings[index].config)
153 if configList:getCurrentOption().text ~= settings[index].config then
154 settings[index].config = configList:getCurrentOption().text
155 settings[index].enabled = false
156 end
157
158 enableButton:setOn(settings[index].enabled)
159
160 configList.onOptionChange = function(widget)
161 settings[index].config = widget:getCurrentOption().text
162 g_settings.setNode('bot', settings)
163 g_settings.save()
164 refresh()
165 end
166
167 enableButton.onClick = function(widget)
168 settings[index].enabled = not settings[index].enabled
169 g_settings.setNode('bot', settings)
170 g_settings.save()
171 refresh()
172 end
173
174 if not g_game.isOnline() or not settings[index].enabled then
175 statusLabel:setOn(true)
176 statusLabel:setText("Status: disabled\nPress off button to enable")
177 return
178 end
179
180 local configName = settings[index].config
181
182 -- storage
183 botStorage = {}
184 botStorageFile = "/bot/" .. configName .. "/storage.json"
185 if g_resources.fileExists(botStorageFile) then
186 local status, result = pcall(function()
187 return json.decode(g_resources.readFileContents(botStorageFile))
188 end)
189 if not status then
190 return onError("Error while reading storage (" .. botStorageFile .. "). To fix this problem you can delete storage.json. Details: " .. result)
191 end
192 botStorage = result
193 end
194
195 -- run script
196 local status, result = pcall(function()
197 return executeBot(configName, botStorage, botTabs, message, save, refresh, botWebSockets) end
198 )
199 if not status then
200 return onError(result)
201 end
202
203 statusLabel:setOn(false)
204 botExecutor = result
205 check()
206end
207
208function save()
209 if not botExecutor then
210 return
211 end
212
213 local settings = g_settings.getNode('bot') or {}
214 local index = g_game.getCharacterName() .. "_" .. g_game.getClientVersion()
215 if settings[index] == nil then
216 return
217 end
218
219 local status, result = pcall(function()
220 return json.encode(botStorage, 2)
221 end)
222 if not status then
223 return onError("Error while saving bot storage. Storage won't be saved. Details: " .. result)
224 end
225
226 if result:len() > 100 * 1024 * 1024 then
227 return onError("Storage file is too big, above 100MB, it won't be saved")
228 end
229
230 g_resources.writeFileContents(botStorageFile, result)
231end
232
233function onMiniWindowClose()
234 --botButton:setOn(false)
235end
236
237function toggle()
238 if botButton:isOn() then
239 botWindow:close()
240 botButton:setOn(false)
241 else
242 botWindow:open()
243 botButton:setOn(true)
244 end
245end
246
247function online()
248 --botButton:show()
249 scheduleEvent(refresh, 20)
250end
251
252function offline()
253 save()
254 clear()
255 --botButton:hide()
256 editWindow:hide()
257end
258
259function onError(message)
260 statusLabel:setOn(true)
261 statusLabel:setText("Error:\n" .. message)
262 g_logger.error("[BOT] " .. message)
263end
264
265function edit()
266 local configs = g_resources.listDirectoryFiles("/bot", false, false)
267 editWindow.manager.upload.config:clearOptions()
268 for i=1,#configs do
269 editWindow.manager.upload.config:addOption(configs[i])
270 end
271 editWindow.manager.download.config:setText("")
272
273 editWindow:show()
274 editWindow:focus()
275 editWindow:raise()
276end
277
278function createDefaultConfigs()
279 local defaultConfigFiles = g_resources.listDirectoryFiles("default_configs", false, false)
280 for i, config_name in ipairs(defaultConfigFiles) do
281 if not g_resources.directoryExists("/bot/" .. config_name) then
282 g_resources.makeDir("/bot/" .. config_name)
283 if not g_resources.directoryExists("/bot/" .. config_name) then
284 return onError("Can't create /bot/" .. config_name .. " directory in " .. g_resources.getWriteDir())
285 end
286 end
287
288 local defaultConfigFiles = g_resources.listDirectoryFiles("default_configs/" .. config_name, true, false)
289 for i, file in ipairs(defaultConfigFiles) do
290 local baseName = file:split("/")
291 baseName = baseName[#baseName]
292 if g_resources.directoryExists(file) then
293 g_resources.makeDir("/bot/" .. config_name .. "/" .. baseName)
294 if not g_resources.directoryExists("/bot/" .. config_name .. "/" .. baseName) then
295 return onError("Can't create /bot/" .. config_name .. "/" .. baseName .. " directory in " .. g_resources.getWriteDir())
296 end
297 local defaultConfigFiles2 = g_resources.listDirectoryFiles("default_configs/" .. config_name .. "/" .. baseName, true, false)
298 for i, file in ipairs(defaultConfigFiles2) do
299 local baseName2 = file:split("/")
300 baseName2 = baseName2[#baseName2]
301 local contents = g_resources.fileExists(file) and g_resources.readFileContents(file) or ""
302 if contents:len() > 0 then
303 g_resources.writeFileContents("/bot/" .. config_name .. "/" .. baseName .. "/" .. baseName2, contents)
304 end
305 end
306 else
307 local contents = g_resources.fileExists(file) and g_resources.readFileContents(file) or ""
308 if contents:len() > 0 then
309 g_resources.writeFileContents("/bot/" .. config_name .. "/" .. baseName, contents)
310 end
311 end
312 end
313 end
314end
315
316function uploadConfig()
317 local config = editWindow.manager.upload.config:getCurrentOption().text
318 local archive = compressConfig(config)
319 if not archive then
320 return displayErrorBox(tr("Config upload failed"), tr("Config %s is invalid (can't be compressed)", config))
321 end
322 if archive:len() > 64 * 1024 then
323 return displayErrorBox(tr("Config upload failed"), tr("Config %s is too big, maximum size is 64KB. Now it has %s KB.", config, math.floor(archive / 1024)))
324 end
325
326 local infoBox = displayInfoBox(tr("Uploading config"), tr("Uploading config %s. Please wait.", config))
327
328 HTTP.postJSON(configManagerUrl .. "?config=" .. config:gsub("%s+", "_"), archive, function(data, err)
329 if infoBox then
330 infoBox:destroy()
331 end
332 if err or data["error"] then
333 return displayErrorBox(tr("Config upload failed"), tr("Error while upload config %s:\n%s", config, err or data["error"]))
334 end
335 displayInfoBox(tr("Succesful config upload"), tr("Config %s has been uploaded.\n%s", config, data["message"]))
336 end)
337end
338
339function downloadConfig()
340 local hash = editWindow.manager.download.config:getText()
341 if hash:len() == 0 then
342 return displayErrorBox(tr("Config download error"), tr("Enter correct config hash"))
343 end
344 local infoBox = displayInfoBox(tr("Downloading config"), tr("Downloading config with hash %s. Please wait.", hash))
345 HTTP.download(configManagerUrl .. "?hash=" .. hash, hash .. ".zip", function(path, checksum, err)
346 if infoBox then
347 infoBox:destroy()
348 end
349 if err then
350 return displayErrorBox(tr("Config download error"), tr("Config with hash %s cannot be downloaded", hash))
351 end
352 modules.client_textedit.show("", {
353 title="Enter name for downloaded config",
354 description="Config with hash " .. hash .. " has been downloaded. Enter name for new config.\nWarning: if config with same name already exist, it will be overwritten!",
355 width=500
356 }, function(configName)
357 decompressConfig(configName, "/downloads/" .. path)
358 refresh()
359 edit()
360 end)
361 end)
362end
363
364function compressConfig(configName)
365 if not g_resources.directoryExists("/bot/" .. configName) then
366 return onError("Config " .. configName .. " doesn't exist")
367 end
368 local forArchive = {}
369 for _, file in ipairs(g_resources.listDirectoryFiles("/bot/" .. configName)) do
370 local fullPath = "/bot/" .. configName .. "/" .. file
371 if g_resources.fileExists(fullPath) then -- regular file
372 forArchive[file] = g_resources.readFileContents(fullPath)
373 else -- dir
374 for __, file2 in ipairs(g_resources.listDirectoryFiles(fullPath)) do
375 local fullPath2 = fullPath .. "/" .. file2
376 if g_resources.fileExists(fullPath2) then -- regular file
377 forArchive[file .. "/" .. file2] = g_resources.readFileContents(fullPath2)
378 end
379 end
380 end
381 end
382 return g_resources.createArchive(forArchive)
383end
384
385function decompressConfig(configName, archive)
386 if g_resources.directoryExists("/bot/" .. configName) then
387 g_resources.deleteFile("/bot/" .. configName) -- also delete dirs
388 end
389 local files = g_resources.decompressArchive(archive)
390 g_resources.makeDir("/bot/" .. configName)
391 if not g_resources.directoryExists("/bot/" .. configName) then
392 return onError("Can't create /bot/" .. configName .. " directory in " .. g_resources.getWriteDir())
393 end
394
395 for file, contents in pairs(files) do
396 local split = file:split("/")
397 split[#split] = nil -- remove file name
398 local dirPath = "/bot/" .. configName
399 for _, s in ipairs(split) do
400 dirPath = dirPath .. "/" .. s
401 if not g_resources.directoryExists(dirPath) then
402 g_resources.makeDir(dirPath)
403 if not g_resources.directoryExists(dirPath) then
404 return onError("Can't create " .. dirPath .. " directory in " .. g_resources.getWriteDir())
405 end
406 end
407 end
408 g_resources.writeFileContents("/bot/" .. configName .. file, contents)
409 end
410end
411
412-- Executor
413function message(category, msg)
414 local widget = g_ui.createWidget('BotLabel', botMessages)
415 widget.added = g_clock.millis()
416 if category == 'error' then
417 widget:setText(msg)
418 widget:setColor("red")
419 g_logger.error("[BOT] " .. msg)
420 elseif category == 'warn' then
421 widget:setText(msg)
422 widget:setColor("yellow")
423 g_logger.warning("[BOT] " .. msg)
424 elseif category == 'info' then
425 widget:setText(msg)
426 widget:setColor("white")
427 g_logger.info("[BOT] " .. msg)
428 end
429
430 if botMessages:getChildCount() > 5 then
431 botMessages:getFirstChild():destroy()
432 end
433end
434
435function check()
436 removeEvent(checkEvent)
437 if not botExecutor then
438 return
439 end
440
441 checkEvent = scheduleEvent(check, 10)
442
443 local status, result = pcall(function()
444 return botExecutor.script()
445 end)
446 if not status then
447 botExecutor = nil -- critical
448 return onError(result)
449 end
450
451 -- remove old messages
452 local widget = botMessages:getFirstChild()
453 if widget and widget.added + 5000 < g_clock.millis() then
454 widget:destroy()
455 end
456end
457
458-- Callbacks
459function initCallbacks()
460 connect(rootWidget, {
461 onKeyDown = botKeyDown,
462 onKeyUp = botKeyUp,
463 onKeyPress = botKeyPress
464 })
465
466 connect(g_game, {
467 onTalk = botOnTalk,
468 onTextMessage = botOnTextMessage,
469 onUse = botOnUse,
470 onUseWith = botOnUseWith,
471 onChannelList = botChannelList,
472 onOpenChannel = botOpenChannel,
473 onCloseChannel = botCloseChannel,
474 onChannelEvent = botChannelEvent
475 })
476
477 connect(Tile, {
478 onAddThing = botAddThing,
479 onRemoveThing = botRemoveThing
480 })
481
482 connect(Creature, {
483 onAppear = botCreatureAppear,
484 onDisappear = botCreatureDisappear,
485 onPositionChange = botCreaturePositionChange,
486 onHealthPercentChange = botCraetureHealthPercentChange,
487 onTurn = botCreatureTurn
488 })
489
490 connect(LocalPlayer, {
491 onPositionChange = botCreaturePositionChange,
492 onHealthPercentChange = botCraetureHealthPercentChange
493 })
494
495 connect(Container, {
496 onOpen = botContainerOpen,
497 onClose = botContainerClose,
498 onUpdateItem = botContainerUpdateItem
499 })
500
501 connect(g_map, {
502 onMissle = botOnMissle,
503 onAnimatedText = botOnAnimatedText,
504 onStaticText = botOnStaticText
505 })
506end
507
508function terminateCallbacks()
509 disconnect(rootWidget, {
510 onKeyDown = botKeyDown,
511 onKeyUp = botKeyUp,
512 onKeyPress = botKeyPress
513 })
514
515 disconnect(g_game, {
516 onTalk = botOnTalk,
517 onTextMessage = botOnTextMessage,
518 onUse = botOnUse,
519 onUseWith = botOnUseWith,
520 onChannelList = botChannelList,
521 onOpenChannel = botOpenChannel,
522 onCloseChannel = botCloseChannel,
523 onChannelEvent = botChannelEvent
524 })
525
526 disconnect(Tile, {
527 onAddThing = botAddThing,
528 onRemoveThing = botRemoveThing
529 })
530
531 disconnect(Creature, {
532 onAppear = botCreatureAppear,
533 onDisappear = botCreatureDisappear,
534 onPositionChange = botCreaturePositionChange,
535 onHealthPercentChange = botCraetureHealthPercentChange,
536 onTurn = botCreatureTurn
537 })
538
539 disconnect(LocalPlayer, {
540 onPositionChange = botCreaturePositionChange,
541 onHealthPercentChange = botCraetureHealthPercentChange
542 })
543
544 disconnect(Container, {
545 onOpen = botContainerOpen,
546 onClose = botContainerClose,
547 onUpdateItem = botContainerUpdateItem
548 })
549
550 disconnect(g_map, {
551 onMissle = botOnMissle,
552 onAnimatedText = botOnAnimatedText,
553 onStaticText = botOnStaticText
554 })
555end
556
557function safeBotCall(func)
558 local status, result = pcall(func)
559 if not status then
560 onError(result)
561 end
562end
563
564function botKeyDown(widget, keyCode, keyboardModifiers)
565 if botExecutor == nil then return false end
566 if keyCode == KeyUnknown then return end
567 safeBotCall(function() botExecutor.callbacks.onKeyDown(keyCode, keyboardModifiers) end)
568end
569
570function botKeyUp(widget, keyCode, keyboardModifiers)
571 if botExecutor == nil then return false end
572 if keyCode == KeyUnknown then return end
573 safeBotCall(function() botExecutor.callbacks.onKeyUp(keyCode, keyboardModifiers) end)
574end
575
576function botKeyPress(widget, keyCode, keyboardModifiers, autoRepeatTicks)
577 if botExecutor == nil then return false end
578 if keyCode == KeyUnknown then return end
579 safeBotCall(function() botExecutor.callbacks.onKeyPress(keyCode, keyboardModifiers, autoRepeatTicks) end)
580end
581
582function botOnTalk(name, level, mode, text, channelId, pos)
583 if botExecutor == nil then return false end
584 safeBotCall(function() botExecutor.callbacks.onTalk(name, level, mode, text, channelId, pos) end)
585end
586
587function botOnTextMessage(mode, text)
588 if botExecutor == nil then return false end
589 safeBotCall(function() botExecutor.callbacks.onTextMessage(mode, text) end)
590end
591
592function botAddThing(tile, thing)
593 if botExecutor == nil then return false end
594 safeBotCall(function() botExecutor.callbacks.onAddThing(tile, thing) end)
595end
596
597function botRemoveThing(tile, thing)
598 if botExecutor == nil then return false end
599 safeBotCall(function() botExecutor.callbacks.onRemoveThing(tile, thing) end)
600end
601
602function botCreatureAppear(creature)
603 if botExecutor == nil then return false end
604 safeBotCall(function() botExecutor.callbacks.onCreatureAppear(creature) end)
605end
606
607function botCreatureDisappear(creature)
608 if botExecutor == nil then return false end
609 safeBotCall(function() botExecutor.callbacks.onCreatureDisappear(creature) end)
610end
611
612function botCreaturePositionChange(creature, newPos, oldPos)
613 if botExecutor == nil then return false end
614 safeBotCall(function() botExecutor.callbacks.onCreaturePositionChange(creature, newPos, oldPos) end)
615end
616
617function botCraetureHealthPercentChange(creature, healthPercent)
618 if botExecutor == nil then return false end
619 safeBotCall(function() botExecutor.callbacks.onCreatureHealthPercentChange(creature, healthPercent) end)
620end
621
622function botOnUse(pos, itemId, stackPos, subType)
623 if botExecutor == nil then return false end
624 safeBotCall(function() botExecutor.callbacks.onUse(pos, itemId, stackPos, subType) end)
625end
626
627function botOnUseWith(pos, itemId, target, subType)
628 if botExecutor == nil then return false end
629 safeBotCall(function() botExecutor.callbacks.onUseWith(pos, itemId, target, subType) end)
630end
631
632function botContainerOpen(container, previousContainer)
633 if botExecutor == nil then return false end
634 safeBotCall(function() botExecutor.callbacks.onContainerOpen(container, previousContainer) end)
635end
636
637function botContainerClose(container)
638 if botExecutor == nil then return false end
639 safeBotCall(function() botExecutor.callbacks.onContainerClose(container) end)
640end
641
642function botContainerUpdateItem(container, slot, item)
643 if botExecutor == nil then return false end
644 safeBotCall(function() botExecutor.callbacks.onContainerUpdateItem(container, slot, item) end)
645end
646
647function botOnMissle(missle)
648 if botExecutor == nil then return false end
649 safeBotCall(function() botExecutor.callbacks.onMissle(missle) end)
650end
651
652function botOnAnimatedText(thing, text)
653 if botExecutor == nil then return false end
654 safeBotCall(function() botExecutor.callbacks.onAnimatedText(thing, text) end)
655end
656
657function botOnStaticText(thing, text)
658 if botExecutor == nil then return false end
659 safeBotCall(function() botExecutor.callbacks.onStaticText(thing, text) end)
660end
661
662function botChannelList(channels)
663 if botExecutor == nil then return false end
664 safeBotCall(function() botExecutor.callbacks.onChannelList(channels) end)
665end
666
667function botOpenChannel(channelId, name)
668 if botExecutor == nil then return false end
669 safeBotCall(function() botExecutor.callbacks.onOpenChannel(channelId, name) end)
670end
671
672function botCloseChannel(channelId)
673 if botExecutor == nil then return false end
674 safeBotCall(function() botExecutor.callbacks.onCloseChannel(channelId) end)
675end
676
677function botChannelEvent(channelId, name, event)
678 if botExecutor == nil then return false end
679 safeBotCall(function() botExecutor.callbacks.onChannelEvent(channelId, name, event) end)
680end
681
682function botCreatureTurn(creature, direction)
683 if botExecutor == nil then return false end
684 safeBotCall(function() botExecutor.callbacks.onTurn(creature, direction) end)
685end