ddbot.js-0374 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +44 -0
- package/docs/documentation.md +108 -0
- package/docs/examples/echo-bot.js +61 -0
- package/docs/examples/main.js +139 -0
- package/index.js +17 -0
- package/package.json +35 -0
- package/src/bot/bot.js +462 -0
- package/src/bot/index.js +5 -0
- package/src/debug.js +108 -0
- package/src/map/automaploader.js +60 -0
- package/src/map/maploader.js +135 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 0374flop
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
Смотрите за руками
|
|
2
|
+
|
|
3
|
+
Скажу прямо.
|
|
4
|
+
Коментари с нецензурной лексикой и тут и в коде только потому что я за то чтобы цензура стала слабее. Мне не нравиться что везде слишком сильная цензура.
|
|
5
|
+
Так что да, не плакайте там.
|
|
6
|
+
Простите за орфографические ошибки, писал на скорую руку. (за другие ошибки простите тоже)
|
|
7
|
+
|
|
8
|
+
Лицензия находиться в файле "LICENSE" там большими буквами для идиотов написано
|
|
9
|
+
|
|
10
|
+
У нас есть много всякого, например:
|
|
11
|
+
1. bot.js
|
|
12
|
+
Начнем с него.
|
|
13
|
+
Ето класс, с методами для работы с большим количеством ботов.
|
|
14
|
+
Работает с дебагером.
|
|
15
|
+
Емитит кастомные емиты
|
|
16
|
+
и там всякое которое мне лень разказывать
|
|
17
|
+
2. debug.js
|
|
18
|
+
Думаю, нужно было начинать с него.
|
|
19
|
+
Ета хуйня дает кучу возможностей для дебагинга
|
|
20
|
+
например можно менять режим дебажинга, типа консольный лог или дебаг
|
|
21
|
+
зделан в виде класса самый лутший по моему мнению ЖСДок из всех остальных файлов тут.
|
|
22
|
+
зделан даже тест если запущен как node debug.js
|
|
23
|
+
3. maploader.js
|
|
24
|
+
Первая вещь которая позволяет загружать карты дднет напрямую с дднет сайта. а точнее гита на котором лежат карты, как я выяснил.
|
|
25
|
+
можно получать тип карты, загружать и автоматически понимать какой тип у карты и по нему загружать.
|
|
26
|
+
есть возможность получать сырые функции в advanced. ето просто чтобы можно было напрямую все делать.
|
|
27
|
+
в нем же есть и объект дебагера.
|
|
28
|
+
4. automaploader.js
|
|
29
|
+
изпользует bot.js чтобы автоматически загружать карту через maploader.js когда он подключиться к серверу.
|
|
30
|
+
Выгружает свой дебаг логер.
|
|
31
|
+
также с кастомным емитером.
|
|
32
|
+
5. index'ы.js
|
|
33
|
+
просто вещи чтобы проще было.
|
|
34
|
+
что про них сказать?
|
|
35
|
+
подключают из других файлов все, и выгружают все в один объект.
|
|
36
|
+
|
|
37
|
+
## Установка.
|
|
38
|
+
Кароче, клонируем `git clone https://github.com/0374flop/ddbot.js/tree/new-bot`, да новый бот потому что старый он на то и старый что кривой и не работает.
|
|
39
|
+
После того как клонировали, и все прошло хорошо, ставим зависимости `npm i` или `npm install` и все.
|
|
40
|
+
После етого вы можете спокойно подключать либу черер `require('./index')` если вы конечно в пвпке проекта подключаете. Иначе другой путь.
|
|
41
|
+
И все после етого вам доступно все ето : { bot, mapLoader, Automaploader, BotManager, DebugLogger }.
|
|
42
|
+
Наслаждайтесь етом дерьмом.
|
|
43
|
+
## Техническая документация.
|
|
44
|
+
смотрите теперь в docs/
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
|
|
2
|
+
# bot/BotManager
|
|
3
|
+
Окей...
|
|
4
|
+
Сейчас я вам разкажу как работает bot/BotManager.
|
|
5
|
+
|
|
6
|
+
neiky-ddracebot.js
|
|
7
|
+
## Базовые методы
|
|
8
|
+
У нас есть :
|
|
9
|
+
1. bot.createBot()
|
|
10
|
+
1. fulladdress - Полный адрес сервера (IP:порт)
|
|
11
|
+
2. botName - Имя бота
|
|
12
|
+
3. parameter - Параметры бота
|
|
13
|
+
1. identity со скином и другими косметическими деталями.
|
|
14
|
+
2. reconnect переподключаеться бот или нет
|
|
15
|
+
3. reconnectAttempts количество возможных реконектов (-1 для безконечного)
|
|
16
|
+
4. randreconnect будет ли бот переключаться с чуть разной задержкой или нет (в районе 0 - 1 сек)
|
|
17
|
+
- На выходе у нас имя бота по которому можно обращаться. (Уникальное имя бота или null в случае ошибки)
|
|
18
|
+
2. bot.connectBot()
|
|
19
|
+
1. Принимает уникальное имя бота и подключает нужного.
|
|
20
|
+
- На выхоте булевое значение (нет точных данных подключился ли бот или нет)
|
|
21
|
+
3. bot.disconnectBot()
|
|
22
|
+
1. Принимает уникальное имя бота и отключает нужного.
|
|
23
|
+
- Работает также как и connectBot().
|
|
24
|
+
4. bot.disconnectAllBots()
|
|
25
|
+
Отключает всех ботов, не принимает аргументов.
|
|
26
|
+
Под капотом disconnectBot().
|
|
27
|
+
5. bot.getBotInfo()
|
|
28
|
+
1. Принимает уникальное имя бота и возвращает информацию про него.
|
|
29
|
+
- Информация о боте или null, если бот не найден
|
|
30
|
+
6. bot.isBotConnected()
|
|
31
|
+
1. Принимает уникальное имя бота.
|
|
32
|
+
- Возвращает булевое значение.
|
|
33
|
+
7. bot.isFreezeBot()
|
|
34
|
+
1. Принимает уникальное имя бота.
|
|
35
|
+
- Возвращает булевое значение.
|
|
36
|
+
8. bot.setFreezeBot()
|
|
37
|
+
Просто меняет значение того заморожен ли бот.
|
|
38
|
+
9. bot.getAllActiveBots()
|
|
39
|
+
Получение всех активных ботов
|
|
40
|
+
- Массив имен всех активных ботов
|
|
41
|
+
10. bot.getBotClient()
|
|
42
|
+
1. Принимает уникальное имя бота.
|
|
43
|
+
- Возвращает клиент neiky-ddracebot.js
|
|
44
|
+
11. bot.removeBot()
|
|
45
|
+
Удаляет бота полностью.
|
|
46
|
+
1. Принимает уникальное имя бота.
|
|
47
|
+
- Возвращает булевое значение.
|
|
48
|
+
12. bot.getBot()
|
|
49
|
+
1. Принимает уникальное имя бота.
|
|
50
|
+
- Возвращает прокси-объект бота.
|
|
51
|
+
13. bot.(Тут нижнее подчеркивание, я не могу поставить из-за обсидиана, он ломается)setupBotEvents()
|
|
52
|
+
Заставляет ивенты работать. Изпользуеться в bot.createBot() чтобы вы могли делать все проще и вам не нужно было получать оригинальный клиент neiky-ddracebot.js
|
|
53
|
+
1. Принимает уникальное имя бота.
|
|
54
|
+
2. Принимает клиент neiky-ddracebot.js
|
|
55
|
+
Ничего не возвращает.
|
|
56
|
+
14. bot.getPlayerList()
|
|
57
|
+
1. Принимает уникальное имя бота.
|
|
58
|
+
- Array список игроков.
|
|
59
|
+
15. bot.getPlayerName()
|
|
60
|
+
1. Принимает уникальное имя бота/Array список игроков.
|
|
61
|
+
2. clientid нужного игрока.
|
|
62
|
+
- Имя нужного игрока.
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# maploader
|
|
66
|
+
Возможно первая вещь которая может загружать карты по официальным названиям ДДНета.
|
|
67
|
+
|
|
68
|
+
## Самые нужные
|
|
69
|
+
fetchMapType и loadMap.
|
|
70
|
+
|
|
71
|
+
1. fetchMapType
|
|
72
|
+
Получает тип карты с ddnet.org
|
|
73
|
+
1. mapName - Имя карты.
|
|
74
|
+
- Тип карты (novice тд.).
|
|
75
|
+
2. loadMap
|
|
76
|
+
Загружает карту по нужному имени в нужную директорию.
|
|
77
|
+
1. mapName - Имя карты.
|
|
78
|
+
2. MAP_DIR - Директория куда будет загружена карта.
|
|
79
|
+
- Возвращает булевое значение.
|
|
80
|
+
|
|
81
|
+
## Дополнитильные или сырые функции
|
|
82
|
+
Находяться в advanced
|
|
83
|
+
Тут есть сырой downloadMap, tryDownloadMap, logDebuger.
|
|
84
|
+
|
|
85
|
+
1. downloadMap
|
|
86
|
+
Загрузка карты по имени и типу.
|
|
87
|
+
Нет никакого протекшена почти.
|
|
88
|
+
1. mapName - Имя карты
|
|
89
|
+
2. type - Тип карты
|
|
90
|
+
3. MAP_DIR_DM - Папка для загрузки карты
|
|
91
|
+
- Путь к загруженной карте
|
|
92
|
+
2. tryDownloadMap
|
|
93
|
+
Принимает все тоже что и downloadMap, только оборачивает его в try.
|
|
94
|
+
- Возвращает болевое значение.
|
|
95
|
+
|
|
96
|
+
logDebuger из debug.js
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# automaploader.js
|
|
100
|
+
Хуйня.
|
|
101
|
+
Не работает не с официальными ДДНет серверами, потому что они уже дают нам mapDetails.map_url а automaploader загружает по имени.
|
|
102
|
+
Мне лень переделывать его. пусть будет.
|
|
103
|
+
|
|
104
|
+
Если хотите понять как им пользоваться, лутше сами посмотрите его. он очень простой и маленький.
|
|
105
|
+
|
|
106
|
+
# debug.js
|
|
107
|
+
Главный логер.
|
|
108
|
+
Тоже достаточно простой, проще самому посмотреть.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Пример писался под комит c453b96f9fd717375f5ff70525603b7e1491c290.
|
|
2
|
+
|
|
3
|
+
const { bot, botClassAndLoger, DebugLogger } = require('../../index');
|
|
4
|
+
const botdebug = botClassAndLoger.logDebuger;
|
|
5
|
+
botdebug.setDebugMode(true, true, true);
|
|
6
|
+
|
|
7
|
+
const logDebuger = new DebugLogger('example', true, true, null, true);
|
|
8
|
+
|
|
9
|
+
async function main() {
|
|
10
|
+
logDebuger.logDebug('Main started');
|
|
11
|
+
|
|
12
|
+
const identitybot = {
|
|
13
|
+
name: "Towa",
|
|
14
|
+
clan: "Towa Team",
|
|
15
|
+
skin: "Astolfofinho",
|
|
16
|
+
use_custom_color: 1,
|
|
17
|
+
color_body: 16711680,
|
|
18
|
+
color_feet: 16711680,
|
|
19
|
+
country: -1
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const botName = await bot.createBot('45.141.57.22:8375', 'Towa', {
|
|
23
|
+
identity: identitybot,
|
|
24
|
+
reconnect: true,
|
|
25
|
+
reconnectAttempts: -1,
|
|
26
|
+
randreconnect: true
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
bot.connectBot(botName); // подкюлчаем
|
|
30
|
+
|
|
31
|
+
const botClient = bot.getBotClient(botName); // получаем оригинальный клиент neiky-ddracebot.js
|
|
32
|
+
|
|
33
|
+
// Подписка на событие подключения
|
|
34
|
+
bot.on(`${botName}:connect`, () => {
|
|
35
|
+
let timemsg = 0; // время
|
|
36
|
+
|
|
37
|
+
// подписка на чат
|
|
38
|
+
bot.on(`${botName}:ChatNoSystem`, (msgraw, autormsg, text, team, client_id) => {
|
|
39
|
+
logDebuger.logDebug(`${client_id} ${team} '${autormsg}' : ${text}`); // вывод чата в консоль
|
|
40
|
+
if (text == 'exit') exit(); // выход
|
|
41
|
+
|
|
42
|
+
// Эхо-логика
|
|
43
|
+
if (Date.now() - timemsg > 6000) {
|
|
44
|
+
timemsg = Date.now(); // устанавливаем текущее время
|
|
45
|
+
if (text && autormsg) {
|
|
46
|
+
botClient.game.Say(`${autormsg}: ${text}`); // отправка сообения (neiky-ddracebot.js)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Выход через Ctrl+C
|
|
53
|
+
async function exit() {
|
|
54
|
+
logDebuger.logDebug('Shutting down...');
|
|
55
|
+
await bot.disconnectAllBots(); // отключаем всех ботов
|
|
56
|
+
process.exit(0); // завершаем процес
|
|
57
|
+
}
|
|
58
|
+
process.on('SIGINT', exit); // Ctrl+C
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (require.main === module) main();
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
const { bot, botClassAndLoger } = require('../../index');
|
|
2
|
+
botClassAndLoger.logDebuger.setDebugMode(true, true);
|
|
3
|
+
|
|
4
|
+
async function main() {
|
|
5
|
+
console.log('Main started');
|
|
6
|
+
|
|
7
|
+
const identitybot = {
|
|
8
|
+
name: "Towa",
|
|
9
|
+
clan: "Towa Team",
|
|
10
|
+
skin: "Astolfofinho",
|
|
11
|
+
use_custom_color: 1,
|
|
12
|
+
color_body: 16711680,
|
|
13
|
+
color_feet: 16711680,
|
|
14
|
+
country: -1
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const botName = await bot.createBot('45.141.57.22:8375', 'Towa', {
|
|
18
|
+
identity: identitybot,
|
|
19
|
+
reconnect: true,
|
|
20
|
+
reconnectAttempts: -1
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
bot.connectBot(botName);
|
|
24
|
+
|
|
25
|
+
const botClient = bot.getBotClient(botName);
|
|
26
|
+
|
|
27
|
+
bot.on(`${botName}:connect`, () => {
|
|
28
|
+
function startemote(botClient, Emotenumber) {
|
|
29
|
+
const intervalemote = setInterval(() => {
|
|
30
|
+
botClient.game.Emote(Emotenumber);
|
|
31
|
+
}, 5000);
|
|
32
|
+
return intervalemote;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function startnameset(botClient, bot, identitybot) {
|
|
36
|
+
const intervalnameset = setInterval(() => {
|
|
37
|
+
const myclientid = botClient.SnapshotUnpacker.OwnID;
|
|
38
|
+
const me = bot.getPlayerList(botName).find(p => p.client_id === myclientid);
|
|
39
|
+
|
|
40
|
+
if (!me) return;
|
|
41
|
+
|
|
42
|
+
if (me.name !== identitybot.name) {
|
|
43
|
+
botClient.game.ChangePlayerInfo({ ...identitybot, name: identitybot.name });
|
|
44
|
+
}
|
|
45
|
+
}, 10000);
|
|
46
|
+
return intervalnameset;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let sync = false;
|
|
50
|
+
let intervalemote = startemote(botClient, 2);
|
|
51
|
+
let intervalnameset = startnameset(botClient, bot, identitybot);
|
|
52
|
+
|
|
53
|
+
async function startchatlistener(bot, botName) {
|
|
54
|
+
bot.on(`${botName}:ChatNoSystem`, (msg, autormsg, text, team, client_id) => {
|
|
55
|
+
console.log(`${client_id} ${team} '${autormsg}' : ${text}`);
|
|
56
|
+
|
|
57
|
+
if (text.includes(identitybot.name) && text.includes('%syncE')) {
|
|
58
|
+
sync = true;
|
|
59
|
+
if (intervalemote) clearInterval(intervalemote);
|
|
60
|
+
intervalemote = startemote(botClient, 2);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
startchatlistener(bot, botName);
|
|
66
|
+
|
|
67
|
+
bot.on(`${botName}:disconnect`, (reason) => {
|
|
68
|
+
clearInterval(intervalemote);
|
|
69
|
+
clearInterval(intervalMove);
|
|
70
|
+
clearInterval(intervalnameset);
|
|
71
|
+
console.log(reason)
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
function findbot2(botName, identitybot) {
|
|
75
|
+
const players = bot.getPlayerList(botName);
|
|
76
|
+
|
|
77
|
+
return players.some(player =>
|
|
78
|
+
player.clan === identitybot.clan &&
|
|
79
|
+
player.skin === identitybot.skin &&
|
|
80
|
+
player.name.includes(identitybot.name) &&
|
|
81
|
+
player.name !== identitybot.name
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
setInterval(() => {
|
|
86
|
+
if (findbot2(botName, identitybot) && !sync) {
|
|
87
|
+
botClient.game.Say(botName+'%syncE');
|
|
88
|
+
} if (!findbot2(botName, identitybot)) {
|
|
89
|
+
sync = false;
|
|
90
|
+
}
|
|
91
|
+
}, 60000);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
let x = 100;
|
|
95
|
+
let direction = -1;
|
|
96
|
+
const intervalMove = setInterval(() => {
|
|
97
|
+
x += direction * 10;
|
|
98
|
+
if (x <= -100) {
|
|
99
|
+
direction = 1;
|
|
100
|
+
} else if (x >= 100) {
|
|
101
|
+
direction = -1;
|
|
102
|
+
}
|
|
103
|
+
if (bot.isBotConnected(botName) && bot.isFreezeBot(botName)) {
|
|
104
|
+
if (botClient && botClient.movement) {
|
|
105
|
+
botClient.movement.FlagHookline(true);
|
|
106
|
+
setTimeout(() => {
|
|
107
|
+
botClient.movement.FlagHookline(false);
|
|
108
|
+
}, Math.random() * 50);
|
|
109
|
+
botClient.movement.SetAim(x, -100);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}, Math.random() * 100);
|
|
113
|
+
|
|
114
|
+
async function SayChat(message) {
|
|
115
|
+
botClient.game.Say(message);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function exit() {
|
|
119
|
+
console.log('Shutting down...');
|
|
120
|
+
console.log('disconnecting...');
|
|
121
|
+
await bot.disconnectAllBots();
|
|
122
|
+
console.log('Main stopped');
|
|
123
|
+
process.exit(0);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
process.on('SIGINT', () => {
|
|
127
|
+
exit();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
module.exports.exit = exit;
|
|
131
|
+
module.exports.SayChat = SayChat;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (require.main === module) main();
|
|
135
|
+
|
|
136
|
+
module.exports.main = main;
|
|
137
|
+
|
|
138
|
+
// можете щитать ето примером, ето просто как тест
|
|
139
|
+
// тут нет коментариев но если хотите то сами поймите как работает
|
package/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const { bot, BotManager, logDebuger } = require('./src/bot'); // Импортируем bot и BotManager из модуля bot/index.js
|
|
3
|
+
const mapLoader = require('./src/map/maploader'); // Импортируем mapLoader из модуля map/maploader.js
|
|
4
|
+
const Automaploader = require('./src/map/Automaploader'); // Импортируем Automaploader из модуля map/Automaploader.js
|
|
5
|
+
const DebugLogger = require('./src/debug'); // Импортируем DebugLogger из модуля debug.js
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Зделал так чтобы было меньше експорта.
|
|
9
|
+
* Тут у нас не активированый BotManager.
|
|
10
|
+
* И logDebuger который изпользуеться в BotManager, а точнее в bot.
|
|
11
|
+
*/
|
|
12
|
+
const botClassAndLoger = {
|
|
13
|
+
BotManager,
|
|
14
|
+
logDebuger
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
module.exports = { bot, mapLoader, Automaploader, botClassAndLoger, DebugLogger }; // Экспортируем bot, mapLoader, Automaploader, botClassAndLoger и DebugLogger
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"dependencies": {
|
|
3
|
+
"chalk": "^4.1.2",
|
|
4
|
+
"neiky-ddracebot.js": "^2.4.0"
|
|
5
|
+
},
|
|
6
|
+
"name": "ddbot.js-0374",
|
|
7
|
+
"version": "1.0.0",
|
|
8
|
+
"description": "ddbot.js — это Node.js проект для автоматизации и управления ботами, а также работы с картами и логами.",
|
|
9
|
+
"main": "./src/bot/index.js",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"electron": "npx electron ./main-electron.js",
|
|
12
|
+
"start": "node main.js",
|
|
13
|
+
"test": "node main.js"
|
|
14
|
+
},
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/0374flop/ddbot.js.git"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"ddracebot",
|
|
21
|
+
"DDNet",
|
|
22
|
+
"DDraceNetwork",
|
|
23
|
+
"ddbot",
|
|
24
|
+
"bot",
|
|
25
|
+
"automation",
|
|
26
|
+
"maps"
|
|
27
|
+
],
|
|
28
|
+
"author": "0374flop",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"type": "commonjs",
|
|
31
|
+
"bugs": {
|
|
32
|
+
"url": "https://github.com/0374flop/ddbot.js/issues"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://github.com/0374flop/ddbot.js#readme"
|
|
35
|
+
}
|
package/src/bot/bot.js
ADDED
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const DDRaceBot = require('neiky-ddracebot.js');
|
|
3
|
+
const EventEmitter = require('events');
|
|
4
|
+
const DebugLogger = require('../debug');
|
|
5
|
+
const logDebuger = new DebugLogger('BotManager', false, true, null, true);
|
|
6
|
+
const logDebug = ( ...args) => {
|
|
7
|
+
logDebuger.logDebug(...args);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function random(min, max) {
|
|
11
|
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
class BotManager extends EventEmitter {
|
|
15
|
+
/**
|
|
16
|
+
* Конструктор класса BotManager
|
|
17
|
+
*/
|
|
18
|
+
constructor() {
|
|
19
|
+
super();
|
|
20
|
+
this.activeBots = new Map();
|
|
21
|
+
this.botCounter = 0;
|
|
22
|
+
this.botFreezeStates = new Map(); // Хранит состояние заморозки для каждого бота
|
|
23
|
+
this.playerLists = new Map(); // Хранит список игроков по каждому боту
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Извлечение IP и порта из полного адреса
|
|
28
|
+
* @param {string} address - Полный адрес в формате "IP:порт"
|
|
29
|
+
* @returns {object} IP адрес и порт { address: string, port: number }
|
|
30
|
+
*/
|
|
31
|
+
getIPsplitPort(address) {
|
|
32
|
+
const parts = address.split(':');
|
|
33
|
+
const ip = parts[0]; // IPv4 остаётся строкой
|
|
34
|
+
const port = parseInt(parts[1], 10); // порт парсим в число
|
|
35
|
+
return { address: ip, port: port };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Генерация уникального имени бота
|
|
40
|
+
* @param {string} baseName - Базовое имя бота
|
|
41
|
+
* @returns {string} - Уникальное имя бота
|
|
42
|
+
*/
|
|
43
|
+
generateUniqueBotName(baseName) {
|
|
44
|
+
logDebug('generateUniqueBotName called with baseName:', baseName);
|
|
45
|
+
this.botCounter++;
|
|
46
|
+
const name = `${baseName}${this.botCounter}`;
|
|
47
|
+
logDebug('Generated unique bot name:', name);
|
|
48
|
+
return name;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Создание бота
|
|
53
|
+
* @param {string} fulladdress - Полный адрес сервера (IP:порт)
|
|
54
|
+
* @param {string} botName - Имя бота
|
|
55
|
+
* @param {Object} parameter - Параметры бота
|
|
56
|
+
* parameter может иметь в себе: identity со скином и другими косметическими деталями,
|
|
57
|
+
* reconnect переподключаеться бот или нет,
|
|
58
|
+
* reconnectAttempts количество возможных реконектов (-1 для безконечного),
|
|
59
|
+
* randreconnect будет ли бот переключаться с чуть разной задержкой или нет (в районе 0 - 1 сек)
|
|
60
|
+
* @returns {Promise<string|null>} - Уникальное имя бота или null в случае ошибки
|
|
61
|
+
*/
|
|
62
|
+
async createBot(fulladdress, botName, parameter = {}) {
|
|
63
|
+
logDebug('createBot called with:', fulladdress, botName, parameter); // логируем вызов функции
|
|
64
|
+
const { address, port } = this.getIPsplitPort(fulladdress); // разбираем адрес
|
|
65
|
+
const serverIp = address; // айпи
|
|
66
|
+
const serverPort = port; // порт
|
|
67
|
+
|
|
68
|
+
if (!serverIp || !serverPort) {
|
|
69
|
+
return new Error('и где мы возьмём айпи или порт? ты можешь нормально ввести адрес?');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const uniqueBotName = this.generateUniqueBotName(botName);
|
|
73
|
+
try {
|
|
74
|
+
const identity = parameter.identity || { // если нет индентити, создаём дефолтное
|
|
75
|
+
clan: "",
|
|
76
|
+
skin: "default",
|
|
77
|
+
use_custom_color: 0,
|
|
78
|
+
color_body: 0,
|
|
79
|
+
color_feet: 0,
|
|
80
|
+
country: 0
|
|
81
|
+
};
|
|
82
|
+
logDebug('creating bot client');
|
|
83
|
+
// то самое место откуда создаеться клинт бота
|
|
84
|
+
const client = new DDRaceBot.Client(serverIp, serverPort, botName, {
|
|
85
|
+
identity: identity // то самое индентити для скина и тд
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Настраиваем события для бота или пиздеи будет
|
|
89
|
+
this._setupBotEvents(uniqueBotName, client);
|
|
90
|
+
|
|
91
|
+
// Сохраняем информацию о боте
|
|
92
|
+
logDebug('storing bot info');
|
|
93
|
+
this.activeBots.set(uniqueBotName, {
|
|
94
|
+
client,
|
|
95
|
+
fulladdress,
|
|
96
|
+
originalName: botName,
|
|
97
|
+
parameter,
|
|
98
|
+
isConnected: false,
|
|
99
|
+
createdAt: Date.now()
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Инициализируем состояние заморозки
|
|
103
|
+
logDebug('initializing freeze state');
|
|
104
|
+
this.botFreezeStates.set(uniqueBotName, false);
|
|
105
|
+
|
|
106
|
+
return uniqueBotName;
|
|
107
|
+
} catch (error) {
|
|
108
|
+
return null; // пиздеи
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Подключение бота к серверу
|
|
114
|
+
* @param {string} botName
|
|
115
|
+
* @returns {boolean} (Да если ошибок не возникло)
|
|
116
|
+
*/
|
|
117
|
+
async connectBot(botName) {
|
|
118
|
+
logDebug('connectBot called with botName:', botName); // логируем вызов функции
|
|
119
|
+
const botInfo = this.activeBots.get(botName); // получаем инфу о боте
|
|
120
|
+
if (!botInfo) {
|
|
121
|
+
return false; // бот не найден
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
botInfo.client.joinDDRaceServer(); // то самое место подключения
|
|
125
|
+
return true; // да
|
|
126
|
+
} catch (error) { // фак
|
|
127
|
+
logDebug(JSON.stringify(error, null, 2));
|
|
128
|
+
return false; // нет
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Отключение бота от сервера
|
|
134
|
+
* @param {string} botName
|
|
135
|
+
* @returns
|
|
136
|
+
*/
|
|
137
|
+
async disconnectBot(botName) {
|
|
138
|
+
logDebug('disconnectBot called with botName:', botName); // логируем вызов функции
|
|
139
|
+
const botInfo = this.activeBots.get(botName); // получаем инфу о боте
|
|
140
|
+
if (!botInfo) {
|
|
141
|
+
return false; // бот не найден
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
botInfo.client.Disconnect(); // отключаемся
|
|
146
|
+
botInfo.isConnected = false; // обновляем статус
|
|
147
|
+
logDebug('disconnectedBot', botName)
|
|
148
|
+
return true; // да
|
|
149
|
+
} catch (error) { // фак
|
|
150
|
+
logDebug(error)
|
|
151
|
+
return false; // нет
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Отключение всех ботов
|
|
157
|
+
* @return {Promise<Array>} - Результаты отключения каждого бота
|
|
158
|
+
*/
|
|
159
|
+
async disconnectAllBots() {
|
|
160
|
+
logDebug('disconnectAllBots called'); // логируем вызов функции
|
|
161
|
+
const botNames = Array.from(this.activeBots.keys()); // получаем имена всех ботов
|
|
162
|
+
const results = await Promise.allSettled( // Проходимся по каждому и даем по жопе и отключаем
|
|
163
|
+
botNames.map(botName => this.disconnectBot(botName)) // отключаем каждого бота
|
|
164
|
+
);
|
|
165
|
+
this.botFreezeStates.clear(); // Очищаем все состояния заморозки
|
|
166
|
+
this.playerLists.clear(); // Очищаем списки игроков
|
|
167
|
+
return results; // возвращаем результаты
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Получение информации о боте
|
|
172
|
+
* @param {string} botName
|
|
173
|
+
* @returns {Object|null} - Информация о боте или null, если бот не найден
|
|
174
|
+
*/
|
|
175
|
+
getBotInfo(botName) {
|
|
176
|
+
return this.activeBots.get(botName);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Проверка подключения бота
|
|
181
|
+
* @param {string} botName
|
|
182
|
+
* @returns {boolean} - true если бот подключен, иначе false
|
|
183
|
+
*/
|
|
184
|
+
isBotConnected(botName) {
|
|
185
|
+
const botInfo = this.activeBots.get(botName); // получаем инфу о боте
|
|
186
|
+
return botInfo ? botInfo.isConnected : false; // подключен? да или нет
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Проверка состояния заморозки бота
|
|
191
|
+
* @param {string} botName
|
|
192
|
+
* @returns {boolean} - true если бот заморожен, иначе false
|
|
193
|
+
*/
|
|
194
|
+
isFreezeBot(botName) {
|
|
195
|
+
return this.botFreezeStates.get(botName) || false; // возвращаем состояние заморозки
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Установка состояния заморозки бота (для внутреннего использования)
|
|
200
|
+
* @param {string} botName - Имя бота
|
|
201
|
+
* @param {boolean} isFrozen - true для заморозки, false для разморозки
|
|
202
|
+
*/
|
|
203
|
+
setFreezeBot(botName, isFrozen) {
|
|
204
|
+
this.botFreezeStates.set(botName, isFrozen);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Получение всех активных ботов
|
|
209
|
+
* @returns {object[]} - Массив имен всех активных ботов
|
|
210
|
+
*/
|
|
211
|
+
getAllActiveBots() {
|
|
212
|
+
return Array.from(this.activeBots.keys()); // возвращаем имена всех активных ботов
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Получение клиента бота по имени
|
|
217
|
+
* @param {string} botName - Имя бота
|
|
218
|
+
* @returns {object|null} - Объект клиента бота или null, если бот не найден
|
|
219
|
+
*/
|
|
220
|
+
getBotClient(botName) {
|
|
221
|
+
const botInfo = this.activeBots.get(botName); // получаем инфу о боте
|
|
222
|
+
return botInfo ? botInfo.client : null; // возвращаем клиент или null
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Удаление бота из менеджера
|
|
227
|
+
* @param {string} botName - Имя бота
|
|
228
|
+
* @returns {boolean} - true если бот был удален, иначе false
|
|
229
|
+
*/
|
|
230
|
+
removeBot(botName) {
|
|
231
|
+
logDebug('removeBot called with botName:', botName); // логируем вызов функции
|
|
232
|
+
const botInfo = this.activeBots.get(botName); // получаем инфу о боте
|
|
233
|
+
if (botInfo) {
|
|
234
|
+
// Отключаем если подключен
|
|
235
|
+
if (botInfo.isConnected) {
|
|
236
|
+
botInfo.client.disconnect(); // отключаемся
|
|
237
|
+
}
|
|
238
|
+
this.activeBots.delete(botName); // удаляем бота
|
|
239
|
+
this.botFreezeStates.delete(botName); // удаляем состояние заморозки
|
|
240
|
+
this.playerLists.delete(botName); // удаляем список игроков
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Получение объекта бота с событиями
|
|
248
|
+
* @param {string} botName - Имя бота
|
|
249
|
+
* @returns {object|null} - Объект бота с событиями или null, если бот не найден
|
|
250
|
+
*/
|
|
251
|
+
getBot(botName) {
|
|
252
|
+
const botInfo = this.activeBots.get(botName); // получаем инфу о боте
|
|
253
|
+
if (!botInfo) {
|
|
254
|
+
return null; // бот не найден
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Создаем прокси-объект для бота с событиями
|
|
258
|
+
const bot = {
|
|
259
|
+
name: botName, // скажи мое имя.
|
|
260
|
+
client: botInfo.client, // сам клиент бота
|
|
261
|
+
info: botInfo, // инфа о боте
|
|
262
|
+
|
|
263
|
+
// Методы для работы с событиями
|
|
264
|
+
on: (event, callback) => {
|
|
265
|
+
this.on(`${botName}:${event}`, callback);
|
|
266
|
+
},
|
|
267
|
+
|
|
268
|
+
off: (event, callback) => {
|
|
269
|
+
this.off(`${botName}:${event}`, callback);
|
|
270
|
+
},
|
|
271
|
+
|
|
272
|
+
once: (event, callback) => {
|
|
273
|
+
this.once(`${botName}:${event}`, callback);
|
|
274
|
+
},
|
|
275
|
+
|
|
276
|
+
// Методы управления
|
|
277
|
+
connect: () => this.connectBot(botName),
|
|
278
|
+
disconnect: () => this.disconnectBot(botName),
|
|
279
|
+
|
|
280
|
+
// Проверка статуса
|
|
281
|
+
isConnected: () => this.isBotConnected(botName)
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
return bot; // ботик <3
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Настройка событий бота (для внутреннего использования)
|
|
289
|
+
* @param {string} botName - Имя бота
|
|
290
|
+
* @param {object} client - Объект клиента бота
|
|
291
|
+
*/
|
|
292
|
+
_setupBotEvents(botName, client) {
|
|
293
|
+
logDebug('_setupBotEvents called for botName:', botName); // логируем вызов функции
|
|
294
|
+
let chatinterval = null; // интервал для чата
|
|
295
|
+
|
|
296
|
+
client.on('connection_au_serveur_ddrace', () => { // Шок контент бот зашел на сервер
|
|
297
|
+
const botInfo = this.activeBots.get(botName); // получаем инфу о боте
|
|
298
|
+
if (!botInfo) {
|
|
299
|
+
return; // бот не найден фак
|
|
300
|
+
} else {
|
|
301
|
+
botInfo.isConnected = true; // обновляем статус
|
|
302
|
+
}
|
|
303
|
+
this.emit(`${botName}:connect`); // емитим событие коннекта1
|
|
304
|
+
this.emit(`${botName}:connected`); // емитим событие коннекта2
|
|
305
|
+
logDebug(`${botName} connected to server`); // логируем подключение
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
client.on('disconnect', (reason) => { // бот отключился от сервера
|
|
309
|
+
let botInfo = this.activeBots.get(botName); // получаем инфу о боте
|
|
310
|
+
if (!botInfo) {
|
|
311
|
+
return; // бот не найден фак
|
|
312
|
+
} else {
|
|
313
|
+
botInfo.isConnected = false; // обновляем статус
|
|
314
|
+
clearInterval(chatinterval); // очищаем интервал чата
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (botInfo.parameter.reconnect && (botInfo.parameter.reconnectAttempts>0 || botInfo.parameter.reconnectAttempts===-1)) {
|
|
318
|
+
if (botInfo.parameter.reconnectAttempts!==-1)botInfo.parameter.reconnectAttempts--;
|
|
319
|
+
|
|
320
|
+
let reconnectTime = 10000;
|
|
321
|
+
logDebug('base reconnect time is '+reconnectTime+'ms');
|
|
322
|
+
logDebug('Calculating reconnect time for botName: ', botName);
|
|
323
|
+
if (reason.startsWith('You have been banned')) { // бота забанили, фааааааак
|
|
324
|
+
if (reason.startsWith('You have been banned for 5 minutes (Banned by vote)')) { // похуй, это всего лишь 5 минут и воутом а не админ
|
|
325
|
+
reconnectTime = 300000; // 5 минут
|
|
326
|
+
} else {
|
|
327
|
+
reconnectTime = 1000000; // ФАК админ забанил, надеемся что не долго
|
|
328
|
+
}
|
|
329
|
+
} else if (reason.startsWith('Too many connections in a short time')) { // ладненько
|
|
330
|
+
reconnectTime = 20000; // 20 секунд
|
|
331
|
+
} else if (reason.startsWith('Timed Out. (no packets received for ')) { // таймаут, если инет есть, просто нужно еще пару раз поконектиться и пройдет
|
|
332
|
+
reconnectTime = 500; // 0.5 секунд
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
logDebug('Base reconnect time set to:', reconnectTime);
|
|
336
|
+
if (botInfo.parameter.randreconnect) { // рандомайзер времени риконнекта
|
|
337
|
+
const randomtime = random(reconnectTime, reconnectTime+random(100, 1000)); // рандом от базового времени до базового + 1 секунда
|
|
338
|
+
reconnectTime = randomtime; // устанавливаем рандомное время
|
|
339
|
+
logDebug('Randomized reconnect time to: ', reconnectTime); // логируем рандомное время
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
this.emit(`${botName}:disconnect`, reason, reconnectTime);
|
|
343
|
+
this.emit(`${botName}:disconnected`, reason, reconnectTime);
|
|
344
|
+
logDebug(`${botName} disconnected due to: `, reason, '\nand reconnecting in ', reconnectTime, 'ms');
|
|
345
|
+
setTimeout(() => {
|
|
346
|
+
client.joinDDRaceServer();
|
|
347
|
+
this.emit(`${botName}:reconnect`, reconnectTime);
|
|
348
|
+
logDebug(`${botName} reconnect now`);
|
|
349
|
+
}, reconnectTime);
|
|
350
|
+
} else {
|
|
351
|
+
this.emit(`${botName}:disconnect`, reason);
|
|
352
|
+
this.emit(`${botName}:disconnected`, reason);
|
|
353
|
+
logDebug(`${botName} disconnected due to: `, reason);
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
client.on('snapshot', (snapshot) => {
|
|
358
|
+
try {
|
|
359
|
+
const myDDNetChar = client.SnapshotUnpacker.getObjExDDNetCharacter(client.SnapshotUnpacker.OwnID);
|
|
360
|
+
if (myDDNetChar) {
|
|
361
|
+
const isFrozen = myDDNetChar.m_FreezeEnd !== 0;
|
|
362
|
+
this.botFreezeStates.set(botName, isFrozen);
|
|
363
|
+
}
|
|
364
|
+
} catch (error) {
|
|
365
|
+
logDebug('Error processing snapshot for botName:', botName, error);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
try {
|
|
369
|
+
const oldList = this.playerLists.get(botName) || [];
|
|
370
|
+
const playerMap = new Map(oldList.map(p => [p.client_id, p]));
|
|
371
|
+
|
|
372
|
+
for (let client_id = 0; client_id < 64; client_id++) {
|
|
373
|
+
const clientInfo = client.SnapshotUnpacker.getObjClientInfo(client_id);
|
|
374
|
+
const playerInfo = client.SnapshotUnpacker.getObjPlayerInfo(client_id);
|
|
375
|
+
const ddnetChar = client.SnapshotUnpacker.getObjExDDNetCharacter
|
|
376
|
+
? client.SnapshotUnpacker.getObjExDDNetCharacter(client_id)
|
|
377
|
+
: null;
|
|
378
|
+
|
|
379
|
+
if (clientInfo && clientInfo.name && playerInfo && playerInfo.m_Team !== -1) {
|
|
380
|
+
playerMap.set(client_id, {
|
|
381
|
+
client_id,
|
|
382
|
+
name: clientInfo.name,
|
|
383
|
+
clan: clientInfo.clan || '',
|
|
384
|
+
country: clientInfo.country || -1,
|
|
385
|
+
team: playerInfo.m_Team,
|
|
386
|
+
skin: clientInfo.skin || 'default',
|
|
387
|
+
x: ddnetChar ? ddnetChar.m_X : null,
|
|
388
|
+
y: ddnetChar ? ddnetChar.m_Y : null
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
this.playerLists.set(botName, Array.from(playerMap.values()));
|
|
393
|
+
} catch (error) {
|
|
394
|
+
logDebug('Error updating player list for botName:', botName, error);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
this.emit(`${botName}:snapshot`, snapshot);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
let s = new Set(); // сет
|
|
401
|
+
chatinterval = setInterval(() => {
|
|
402
|
+
s.clear();
|
|
403
|
+
}, 1000); // чистка
|
|
404
|
+
client.on('message_au_serveur', (msg) => {
|
|
405
|
+
this.emit(`${botName}:message`, msg); // Сырое сообщение, без фильтрации
|
|
406
|
+
|
|
407
|
+
const msgraw = msg; // ориг для чата на всякий
|
|
408
|
+
const text = msg.message; // само сообщение
|
|
409
|
+
const client_id = msg.client_id; // айди отправителя
|
|
410
|
+
const autormsg = msg.client_id === -1 ? "system" : this.getPlayerName(botName, client_id) || (msg.utilisateur?.InformationDuBot?.name || null) // имя отправителя
|
|
411
|
+
const team = msg.team; // команда отправителя
|
|
412
|
+
|
|
413
|
+
// фильтрация дубликатов сообщений
|
|
414
|
+
const key = `${client_id}:${text}:${team}`;
|
|
415
|
+
if (s.has(key)) return;
|
|
416
|
+
s.add(key);
|
|
417
|
+
|
|
418
|
+
// емитим.
|
|
419
|
+
if (client_id !== -1) this.emit(`${botName}:ChatNoSystem`, msgraw, autormsg, text, team, client_id); // все только без системы или сервера
|
|
420
|
+
this.emit(`${botName}:ChatRaw`, msgraw, autormsg, text, team, client_id); // все сообщения
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
client.on('error', (error) => {
|
|
424
|
+
logDebug('error event for botName:', botName, error);
|
|
425
|
+
this.emit(`${botName}:error`, error);
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
client.on('map_details', (mapDetails) => {
|
|
429
|
+
logDebug('map_details event for botName:', botName, mapDetails.map_url, mapDetails.map_name);
|
|
430
|
+
this.emit(`${botName}:map_details`, mapDetails);
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Получение списка игроков подключенных к тому же серверу что и бот
|
|
435
|
+
* @param {string} botName
|
|
436
|
+
* @returns {Array} - Массив объектов игроков
|
|
437
|
+
*/
|
|
438
|
+
getPlayerList(botName) {
|
|
439
|
+
return this.playerLists.get(botName) || [];
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Получение имени игрока по его clientId
|
|
444
|
+
* @param {string|Array} botName - Имя бота. Или, если конечно нужно, массив игроков.
|
|
445
|
+
* @param {number} clientId - ID клиента игрока
|
|
446
|
+
* @returns {string|null} - Имя игрока или null если не найден
|
|
447
|
+
*/
|
|
448
|
+
getPlayerName(botName, clientId) {
|
|
449
|
+
if (clientId === -1) return null;
|
|
450
|
+
if (botName === null || botName === undefined) return null;
|
|
451
|
+
if (Array.isArray(botName)) {
|
|
452
|
+
const name = botName.find(bot => bot.client_id === clientId)?.name || null;
|
|
453
|
+
return name;
|
|
454
|
+
} else {
|
|
455
|
+
const playerList = this.getPlayerList(botName);
|
|
456
|
+
const player = playerList.find(p => p.client_id === clientId);
|
|
457
|
+
return player ? player.name : null;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
module.exports = { BotManager, logDebuger }; // экспортируем BotManager и дебагер
|
package/src/bot/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const { BotManager, logDebuger } = require('./bot'); // Импортируем BotManager из модуля bot.js
|
|
3
|
+
const bot = new BotManager(); // Создаем экземпляр BotManager
|
|
4
|
+
|
|
5
|
+
module.exports = { bot, BotManager, logDebuger }; // Экспортируем bot, BotManager, logDebuger
|
package/src/debug.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
let hasChalk = false;
|
|
4
|
+
let chalklib;
|
|
5
|
+
try {
|
|
6
|
+
require.resolve('chalk');
|
|
7
|
+
chalklib = require('chalk');
|
|
8
|
+
hasChalk = true;
|
|
9
|
+
} catch {
|
|
10
|
+
hasChalk = false;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
class DebugLogger {
|
|
14
|
+
/**
|
|
15
|
+
* Класс DebugLogger.
|
|
16
|
+
* @param {string} prefix - Префикс для сообщений отладки.
|
|
17
|
+
* @param {boolean} isDebug - Флаг для включения/выключения режима отладки.
|
|
18
|
+
* @param {boolean} islog - Флаг для использования console.log вместо console.debug.
|
|
19
|
+
* @param {object} prefixforprefix - Префикс для префикса. Аррей, Array где 1 айтем для начала, и 2 для конца оригинальноо префикса.
|
|
20
|
+
* @param {boolean} chalk - Изпользовать красивый цветной текст или нет.
|
|
21
|
+
*/
|
|
22
|
+
constructor(prefix, isDebug = false, islog = true, prefixforprefix = ['[',']'], chalk = true) {
|
|
23
|
+
/**
|
|
24
|
+
* @var {boolean} isDebug - Флаг для включения/выключения режима отладки
|
|
25
|
+
*/
|
|
26
|
+
if (typeof isDebug === "boolean") this.isDebug = isDebug; else this.isDebug = false;
|
|
27
|
+
/**
|
|
28
|
+
* @var {boolean} islog - Флаг для использования console.log вместо console.debug
|
|
29
|
+
*/
|
|
30
|
+
if (typeof islog === "boolean") this.islog = islog; else this.islog = true;
|
|
31
|
+
/**
|
|
32
|
+
* @var {string} prefix - Префикс для сообщений отладки
|
|
33
|
+
*/
|
|
34
|
+
if (typeof prefix === "string") this.prefix = prefix; else this.prefix = "null";
|
|
35
|
+
/**
|
|
36
|
+
* @var {object} prefixforprefix - Array Массив из двух строк, которые будут по бокам обычного префикса.
|
|
37
|
+
*/
|
|
38
|
+
if (Array.isArray(prefixforprefix)) this.prefixforprefix = prefixforprefix; else this.prefixforprefix = ['[',']'];
|
|
39
|
+
/**
|
|
40
|
+
* @var {boolean} chalk - Изпользовать ли цветные тексты.
|
|
41
|
+
*/
|
|
42
|
+
if (typeof chalk === "boolean") this.chalk = chalk; else this.chalk = hasChalk;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* (Для внутреннего изпользования)
|
|
47
|
+
* Формат, ПфП1, префикс, ПфП2, Аргументы.
|
|
48
|
+
* @param {string} prefix - Префикс
|
|
49
|
+
* @param {object} prefixforprefix - ПфП, то что по бокам префикса.
|
|
50
|
+
* @param {...any} args - Данные.
|
|
51
|
+
* @returns {string} Цветовой текст в формате.
|
|
52
|
+
*/
|
|
53
|
+
_format(prefix, prefixforprefix, ...args) {
|
|
54
|
+
if (this.chalk && hasChalk)
|
|
55
|
+
return [chalklib.grey(prefixforprefix[0]), chalklib.green(prefix), chalklib.grey(prefixforprefix[1]), chalklib.blue(...args)];
|
|
56
|
+
return [prefixforprefix[0], prefix, prefixforprefix[1], ...args];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Функция для логирования отладочной информации
|
|
61
|
+
* @param {...*} args - Аргументы для логирования
|
|
62
|
+
*/
|
|
63
|
+
logDebug(...args) {
|
|
64
|
+
const prefix = this.prefix; // Используем префикс из свойства класса
|
|
65
|
+
const prefixforprefix = this.prefixforprefix; // префикс для префикса
|
|
66
|
+
if (this.isDebug) { // Проверяем, включен ли режим отладки
|
|
67
|
+
if (this.islog) { // Проверяем, использовать ли console.log
|
|
68
|
+
console.log(...this._format(prefix, prefixforprefix, ...args));
|
|
69
|
+
} else { // Иначе
|
|
70
|
+
console.debug(...this._format(prefix, prefixforprefix, ...args));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Функция для установки режима отладки и использования console.log
|
|
77
|
+
* @param {boolean} debugMode - Включить или выключить режим отладки
|
|
78
|
+
* @param {boolean} useLog - Использовать console.log вместо console.debug
|
|
79
|
+
* @param {boolean} chalk - Изпользовать ли цветной текст или нет
|
|
80
|
+
*/
|
|
81
|
+
setDebugMode(debugMode, useLog = true, chalk = this.chalk) { // лог по умолчанию, потому что мне так удобнее
|
|
82
|
+
this.chalk = chalk
|
|
83
|
+
this.isDebug = debugMode; // Устанавливаем режим отладки
|
|
84
|
+
this.islog = useLog; // Устанавливаем использование console.log
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Экспортируем класс DebugLogger
|
|
89
|
+
module.exports = DebugLogger;
|
|
90
|
+
module.exports.DebugLogger = DebugLogger; // ну чтобы можно было класно короче
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
// Пример использования класса DebugLogger
|
|
94
|
+
if (require.main === module) { // Только если запускаеться напрямую
|
|
95
|
+
let test = new DebugLogger('test', true, true, ["[","]"], true); // Создаем экземпляр класса с префиксом 'test', режим отладки включен и используем console.log
|
|
96
|
+
function log() {
|
|
97
|
+
test.logDebug('cum.'); // Логируем сообщение
|
|
98
|
+
test.setDebugMode(false); // Выключаем режим отладки
|
|
99
|
+
test.logDebug('ето не выведется.'); // Это сообщение не будет выведено
|
|
100
|
+
test.setDebugMode(true, false); // Включаем режим отладки и используем console.debug
|
|
101
|
+
test.logDebug('ето выведется в console.debug.'); // Логируем сообщение
|
|
102
|
+
}
|
|
103
|
+
log(); // тестовая функция
|
|
104
|
+
test = new DebugLogger('говно', true, true, ['1','Какашечка'], false); // создаем новый екземпляр с другими настройками
|
|
105
|
+
log(); // тестовая функция 2
|
|
106
|
+
test = new DebugLogger('кал', true, true, ['Абоба','2'], true); // снова создаем новый екземпляр с другими настройками
|
|
107
|
+
log(); // тестовая функция 3
|
|
108
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const { bot } = require('../bot');
|
|
3
|
+
const mapLoader = require('./maploader');
|
|
4
|
+
const DebugLogger = require('../debug');
|
|
5
|
+
const logDebuger = new DebugLogger('Automaploader', false, true);
|
|
6
|
+
const logdebug = logDebuger.logDebug;
|
|
7
|
+
|
|
8
|
+
const EventEmitter = require('events');
|
|
9
|
+
const eventEmitter = new EventEmitter();
|
|
10
|
+
|
|
11
|
+
const registeredHandlers = new Set(); // чтобы не срать обработчиками
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
/** * Функция для работы с Automaploader
|
|
15
|
+
* @param {string} botName - Имя бота, для которого будет работать Automaploader
|
|
16
|
+
* @param {string} dir - Папка, в которую будут загружаться карты
|
|
17
|
+
* @returns {Promise<void>} Пустой промис после регистрации обработчика
|
|
18
|
+
*/
|
|
19
|
+
async function work(botName, dir) {
|
|
20
|
+
// Проверяем, не зарегистрирован ли уже обработчик для этого бота
|
|
21
|
+
const handlerKey = `${botName}:map_details`;
|
|
22
|
+
if (registeredHandlers.has(handlerKey)) {
|
|
23
|
+
return; // Обработчик уже зарегистрирован
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
registeredHandlers.add(handlerKey); // Добавляем обработчик в множество
|
|
27
|
+
|
|
28
|
+
// Регистрация обработчика события map_details для данного бота
|
|
29
|
+
bot.on(`${botName}:map_details`, (mapDetails) => {
|
|
30
|
+
const mapName = mapDetails.map_name; // Извлекаем имя карты из объекта mapDetails
|
|
31
|
+
logdebug(`Automaploader`, `Received map details for bot "${botName}":`, mapDetails);
|
|
32
|
+
// Вызываем загрузку карты с помощью mapLoader
|
|
33
|
+
mapLoader.loadMap(mapName, dir)
|
|
34
|
+
.then((loaded) => {
|
|
35
|
+
eventEmitter.emit(`automaploader:map_loaded`, { loaded, mapDetails, botName, dir});
|
|
36
|
+
logdebug(`Automaploader`, `Map "${mapName}" loaded: ${loaded}, for bot "${botName}"`);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Automaploader — объект для автоматической загрузки карт
|
|
43
|
+
*/
|
|
44
|
+
const Automaploader = {
|
|
45
|
+
/**
|
|
46
|
+
* Запускает автомаплоадер для бота
|
|
47
|
+
* @param {string} botName - Имя бота
|
|
48
|
+
* @param {string} dir - Папка для загрузки карт
|
|
49
|
+
*/
|
|
50
|
+
work,
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Подписка на события Automaploader
|
|
54
|
+
* @param {string} event - Имя события (Пока что только одно, "automaploader:map_loaded")
|
|
55
|
+
* @param {function(Object): void} callback - Колбэк для события с объектом { loaded, mapDetails, botName, dir }
|
|
56
|
+
*/
|
|
57
|
+
on: eventEmitter.on.bind(eventEmitter)
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
module.exports = { Automaploader, logDebuger };
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const fs = require("fs"); // фс
|
|
3
|
+
const path = require("path"); // патх
|
|
4
|
+
const https = require("https"); // хттпс
|
|
5
|
+
const DebugLogger = require("../debug"); // дебаглоггер
|
|
6
|
+
const logDebuger = new DebugLogger("MapLoader", false, true); // создаем дебагер
|
|
7
|
+
const logdebug = logDebuger.logDebug.bind(logDebuger); // закидываем сюда чтобы писать короче
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Получает тип карты с ddnet.org
|
|
11
|
+
* @param {string} mapName - Имя карты
|
|
12
|
+
* @returns {Promise<string>} - Тип карты
|
|
13
|
+
*/
|
|
14
|
+
function fetchMapType(mapName) {
|
|
15
|
+
logdebug("Fetching map type for:", mapName);
|
|
16
|
+
const url = `https://ddnet.org/maps/?json=${encodeURIComponent(mapName)}`; // сылОчка1
|
|
17
|
+
return new Promise((resolve, reject) => {
|
|
18
|
+
logdebug("Requesting URL:", url);
|
|
19
|
+
https.get(url, (res) => { // Получаем ответ
|
|
20
|
+
logdebug("Received response with status code:", res.statusCode);
|
|
21
|
+
if (res.statusCode !== 200) {
|
|
22
|
+
reject(new Error(`Failed to fetch map type: ${res.statusCode}`)); // пропердоливаемся1
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let rawData = ""; // не приготовненная дата
|
|
27
|
+
res.on("data", (chunk) => (rawData += chunk)); // плюсуем чанки
|
|
28
|
+
res.on("end", () => { // КОНЕЦ
|
|
29
|
+
logdebug("end of data received");
|
|
30
|
+
try { // пытаемся, пытаемся...
|
|
31
|
+
const json = JSON.parse(rawData); // парсим жсон
|
|
32
|
+
const type = json.type.toLowerCase(); // типа
|
|
33
|
+
if (json && type) { // если есть жсон и тип
|
|
34
|
+
logdebug("Fetched map type:", type);
|
|
35
|
+
resolve(type); // резолвим тип
|
|
36
|
+
} else { // иначе
|
|
37
|
+
logdebug("Map type not found in response");
|
|
38
|
+
reject(new Error("Map type not found in response")); // пропердоливаемся2
|
|
39
|
+
}
|
|
40
|
+
} catch (e) { // е
|
|
41
|
+
reject(new Error("Failed to parse map type JSON")); // пропердоливаемся3
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
}).on("error", reject); // еще раз пропердоливаемся4
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Загрузка карты по имени и типу
|
|
49
|
+
* @param {string} mapName - Имя карты
|
|
50
|
+
* @param {string} type - Тип карты
|
|
51
|
+
* @param {string} MAP_DIR_DM - Папка для загрузки карты
|
|
52
|
+
* @returns {Promise<string>} - Путь к загруженной карте
|
|
53
|
+
*/
|
|
54
|
+
function downloadMap(mapName, type, MAP_DIR_DM) {
|
|
55
|
+
const filePath = path.join(MAP_DIR_DM, `${mapName}.map`); // прикидываем путь
|
|
56
|
+
logdebug("Downloading map:", mapName, "of type:", type, "to:", filePath);
|
|
57
|
+
if (fs.existsSync(filePath)) {
|
|
58
|
+
logdebug("Map already exists at:", filePath);
|
|
59
|
+
return Promise.resolve(filePath); // если карта уже есть, резолвим путь а точнее отдыхаем и не напрягаемся1
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const url = `https://raw.githubusercontent.com/ddnet/ddnet-maps/master/types/${type}/maps/${mapName}.map`; // сылОчка2
|
|
63
|
+
logdebug("Map download URL:", url);
|
|
64
|
+
return new Promise((resolve, reject) => {
|
|
65
|
+
if (!fs.existsSync(MAP_DIR_DM)) {
|
|
66
|
+
reject(new Error(`Directory does not exist: ${MAP_DIR_DM}`)); // Директории нет, отдыхаем и не напрягаемся2
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const fileStream = fs.createWriteStream(filePath); // начинаем стримить на твич
|
|
70
|
+
https.get(url, (res) => { // Получаем ответ
|
|
71
|
+
logdebug("Received response with status code:", res.statusCode);
|
|
72
|
+
if (res.statusCode !== 200) {
|
|
73
|
+
reject(new Error(`Failed to download map: ${res.statusCode}`)); // пропердоливаемся5
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
res.pipe(fileStream); // пипе
|
|
78
|
+
fileStream.on("finish", () => { // финиш
|
|
79
|
+
logdebug("Map downloaded to:", filePath);
|
|
80
|
+
fileStream.close(); // завершаем стрим и отдыхаем и не напрягаемся3
|
|
81
|
+
resolve(filePath); // резолвим путь
|
|
82
|
+
});
|
|
83
|
+
}).on("error", (err) => {
|
|
84
|
+
logdebug("Error downloading map:", err.message);
|
|
85
|
+
if (fs.existsSync(filePath)) fs.unlinkSync(filePath);// пропердоливаемся6 и удаляем файл, если он есть конечно
|
|
86
|
+
reject(err);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Пытается загрузить карту и возвращает успех или неудачу
|
|
93
|
+
* @param {string} mapName - Имя карты
|
|
94
|
+
* @param {string} type - Тип карты
|
|
95
|
+
* @param {string} MAP_DIR - Папка для загрузки карты
|
|
96
|
+
* @returns {Promise<boolean>} - Успех или неудача загрузки
|
|
97
|
+
*/
|
|
98
|
+
async function tryDownloadMap(mapName, type, MAP_DIR) {
|
|
99
|
+
logdebug("Trying to download map:", mapName, "of type:", type);
|
|
100
|
+
try { // пытаемся
|
|
101
|
+
await downloadMap(mapName, type, MAP_DIR); // ждемс
|
|
102
|
+
return true; // да
|
|
103
|
+
} catch (e) { // фак, провалились
|
|
104
|
+
logdebug("Failed to download map:", JSON.stringify(e.message, null, 2));
|
|
105
|
+
return false; // нет
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Загружает карту по имени в указанную папку
|
|
111
|
+
* @param {string} mapName - Имя карты
|
|
112
|
+
* @param {string} MAP_DIR - Папка для загрузки карты
|
|
113
|
+
* @returns {Promise<boolean>} - Успех или неудача загрузки
|
|
114
|
+
*/
|
|
115
|
+
async function loadMap(mapName, MAP_DIR) {
|
|
116
|
+
logdebug("Loading map:", mapName, "into directory:", MAP_DIR);
|
|
117
|
+
const type = await fetchMapType(mapName); // получаем тип карты
|
|
118
|
+
return await tryDownloadMap(mapName, type, MAP_DIR); // пытаемся загрузить карту и возвращаем результат
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Расширенные функции для работы с картами
|
|
123
|
+
*/
|
|
124
|
+
const advanced = {
|
|
125
|
+
downloadMap,
|
|
126
|
+
tryDownloadMap,
|
|
127
|
+
logDebuger // экспортируем дебаглоггер
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Экспортируем функции
|
|
131
|
+
module.exports = {
|
|
132
|
+
fetchMapType, // получитьТипКарты
|
|
133
|
+
loadMap, // загрузитьКарту
|
|
134
|
+
advanced // продвинутые или расширенные, или пашёл ты
|
|
135
|
+
};
|