- Версия(и) Minecraft
- Не важна
Мини-бот, запускаемый как отдельный процесс, для решения в полу-автоматическом и автоматическом режиме чат-игр "Реши пример", "Напиши быстрее", "Вопрос-ответ" с возможностью автоматического составления базы ответов.
ВНИМАНИЕ! Использование данного алгоритма производится на ВАШ страх и риск. За любые последствия, в том числе получение бана, мута и иных мер наказания, отвечаете лично ВЫ.
Ну а теперь приступим:
Православные звуковые файлы прилагаются.
Для администраторов проектов:
Возможные плюсы:
- Алгоритм использует как источник информации лог-файл игры, что исключает необходимость внедряться в процесс игры, следовательно, его возможно использовать на проектах с лаунчером и анти-читом;
- (Не проверено) Используется библиотека
User32.dll
для обходаCREATE_ROBOT_PERMISSION
; - Переключение между полу-автоматическим режимом (ответ копируется в буфер обмена) и автоматическим (бот сам отправляет сообщение с ответом) с помощью комбинации клавиш;
- Звуковая индикация в полу-автоматическом режиме, чтобы точно не пропустить викторину в чате;
- Гибкая подстройка триггеров и границ сообщений;
Возможные минусы:
- Некоторые проекты могут просто не давать сохранять лог-файлы клиентам вообще или иметь мод, который фильтрует запись логов на клиенте;
- Если игра запущена от имени администратора, то и программу тоже необходимо запускать от имени администратора.
- Для автоматического режима необходимы библиотеки JNA и JNA-Platform.
- В особо запущенных случаях администраторы блочат не только использование роботов, но и буфера обмена, так что в данном случае придется эмулировать нажатие каждой клавиши.
Ну а теперь приступим:
Test.java:
@SuppressWarnings({"ResultOfMethodCallIgnored", "InfiniteLoopStatement", "BusyWait"})
public class Test {
static final ScriptEngineManager mgr = new ScriptEngineManager();
//Стандартное решение для решения примеров из строки
static final ScriptEngine engine= mgr.getEngineByName("JavaScript");
//Путь к файлу с ответами
static final File answ = new File("answers.txt");
//Хранение ответов в памяти
static final HashMap<String, String> map = new HashMap<>();
//Доступ к буферу обмена
static final Clipboard clipboard = getSystemClipboard();
static final Random random = new Random();
static WinUser.INPUT winInput;
//Пути к звуковым файлам
static final File s1 = new File("A.wav"),s2 = new File("NA.wav");
static Clip c1, c2;
//Разделитель для хранения пары вопрос-ответ в файле
static final String split = ":=:";
//Объект для хранения двух объектов
static class Vec2Obj<T>{
T a; T b;
Vec2Obj(T a, T b){
this.a=a; this.b=b;
}
}
static String[]
//Строки первичной фильтрации
trigger1 = {": [CHAT] [Чат-Игра]", ": [CHAT] [Чат-игра]"};
@SuppressWarnings("unchecked")
static Vec2Obj<String>[]
//Строки, характерезующие сообщение с правильным ответом
trigger2 = new Vec2Obj[]{
new Vec2Obj<>("Правильным ответом было: ", ""),
new Vec2Obj<>("Ответом было: ", "")
};
static String
mathPre = "[Чат-игра] Реши пример ", mathPost = " быстрее остальных и получи награду!",
copyPastePre = "[Чат-игра] Напишите \"", copyPastePost = "\" быстрее остальных и получи награду!",
quizPre = "[CHAT] [Чат-Игра] ", quizPost = "";
//Ожидание правиильного ответа от чат-игры
static boolean waiting = false, AFKMode = false, useAudio = true;
static volatile boolean delayCheck = false;
//Хранение неизвестного вопроса в ожидании ответа
static String buffered;
public static void main(String[] args) throws IOException, InterruptedException {
//Настройка черной магии для обхода CREATE_ROBOT_PERMISSION
WinUser.INPUT in = new WinUser.INPUT( );
winInput=in;
in.type = new WinDef.DWORD( WinUser.INPUT.INPUT_KEYBOARD );
in.input.setType("ki");
in.input.ki.wScan = new WinDef.WORD( 0 );
in.input.ki.time = new WinDef.DWORD( 0 );
in.input.ki.dwExtraInfo = new BaseTSD.ULONG_PTR( 0 );
if(useAudio){
try {
prepareAudio();
} catch (UnsupportedAudioFileException | LineUnavailableException e) {
e.printStackTrace();
useAudio=false;
}
}
//Путь к лог-файлу
File f = new File("run/logs/latest.log");
if(!f.exists()) System.exit(-1);
try(InputStreamReader isr = new InputStreamReader(new FileInputStream(f));
BufferedReader br = new BufferedReader(isr)){
//Загрузка базы вопрос-ответ из файла
readData();
//Переход в конец файла
br.skip(Long.MAX_VALUE);
//Основной цикл чтения файла
while(true) {
//Простое переключение АФК-режима комбинацией клавиш
if(User32.INSTANCE.GetAsyncKeyState(KeyEvent.VK_TAB)<0){
if(User32.INSTANCE.GetAsyncKeyState(KeyEvent.VK_1)<0){ AFKMode=true; }
else if(User32.INSTANCE.GetAsyncKeyState(KeyEvent.VK_SPACE)<0){ AFKMode=false; }
}
//Чтение всех новых сток
while (br.ready()){
scanLine(br.readLine());
}
Thread.sleep(500L);
}
}
}
//Определение типа чат-игры из сообщения
static void scanLine(String line){
//Если строка содержит любую ключевую фразу - продолжить
if(Arrays.stream(trigger1).anyMatch(line::contains)){
//Если задача - решить пример быстрее всех
if(line.contains(mathPre)){
//Выделение части с примером
line = extract(line, mathPre, mathPost);
//Удаление пробелов
line = line.trim();
try {
output(engine.eval(line).toString());
} catch (ScriptException e) {
e.printStackTrace();
}
} else
//Если задача - написать набор символов быстрее всех
if(line.contains(copyPastePre)){
//Выделение копируемой части
line = extract(line, copyPastePre, copyPastePost);
output(line);
} else {
//Если задача - викторина вопрос-ответ
final String finalLine = line;
//Имеет ли сообщение ответ на вопрос
boolean hasT2 = Arrays.stream(trigger2).anyMatch(s -> finalLine.contains(s.a));
if(!waiting || !hasT2){
if(!waiting && !hasT2){
line = extract(line, quizPre, quizPost);
//Имеется ли ответ на данный вопрос в базе
if(map.containsKey(line)){
output(map.get(line));
} else {
//Переход в режим ожидания ответа
waiting = true;
buffered = line;
if(!AFKMode && useAudio){
c2.setFramePosition(0);
c2.start();
}
}
}
} else {
//Определение типа ответа
for (Vec2Obj<String> get : trigger2) {
if(line.contains(get.a)){
line = extract(line, get.a, get.b);
break;
}
}
//Запись в базу и сохранения на диск
map.put(buffered, line);
writeData();
waiting = false;
}
//Предохранитель на случай, если кто-то ответит быстрее бота
if(AFKMode && delayCheck && hasT2){
delayCheck=false;
}
}
} else
//Простое закрытие бота при закрытии игры
if(line.contains("[net.minecraft.client.Minecraft]: Stopping!")){
System.exit(1);
}
}
static String extract(String line, String pre, String post){
//Раздельный substring необходим для корректной работы indexOf при коротких значениях post
line = line.substring(line.indexOf(pre)+pre.length());
//Проверка на случай, если сообщение чат-игры не имеет завершающей фразы
if(post!=null&&!post.isEmpty()){
line = line.substring(0, line.indexOf(post));
}
return line;
}
static void readData() throws IOException {
if (!answ.exists()) {
PrintWriter writer = new PrintWriter(answ, "UTF-8");
writer.close();
return;
}
try (InputStreamReader isr = new InputStreamReader(new FileInputStream(answ));
BufferedReader br = new BufferedReader(isr)){
for(String line = br.readLine(); line != null; line = br.readLine()) {
String key = line.substring(0, line.indexOf(split));
String value = line.substring(line.indexOf(split)+split.length());
map.put(key, value);
}
}
}
@SuppressWarnings("ResultOfMethodCallIgnored")
//Безопасное сохранение базы ответов
static void writeData(){
File temp = new File(answ+"_tmp");
try(FileWriter fw = new FileWriter(temp)) {
for (String key : map.keySet()) { fw.write(key + ":=:" + map.get(key) + "\n"); }
fw.close();
if(answ.exists()){ answ.delete(); }
temp.renameTo(answ);
} catch (IOException e) {
e.printStackTrace();
}
}
static void prepareAudio() throws IOException, UnsupportedAudioFileException, LineUnavailableException {
AudioInputStream ais1 = AudioSystem.getAudioInputStream(s1);
c1 = AudioSystem.getClip();
c1.open(ais1);
ais1.close();
AudioInputStream ais2 = AudioSystem.getAudioInputStream(s2);
c2 = AudioSystem.getClip();
c2.open(ais2);
ais2.close();
}
static Clipboard getSystemClipboard() {
Toolkit defaultToolkit = Toolkit.getDefaultToolkit();
return defaultToolkit.getSystemClipboard();
}
//Действия с готовым ответом
static void output(String answer){
clipboard.setContents(new StringSelection(answer), null);
if(AFKMode){
delayCheck=true;
Thread t = new Thread(()->{
try {
//Случайная задержка от 2 до 7 секунд
Thread.sleep(random.nextInt(5000)+2000);
//Если до нас ещё никто не ответил
if(delayCheck){
delayCheck=false;
setButtonState('T', true);
Thread.sleep(50);
setButtonState('T', false);
Thread.sleep(50);
setButtonState(0x11, true);
Thread.sleep(50);
setButtonState('V', true);
Thread.sleep(50);
setButtonState('V', false);
Thread.sleep(50);
setButtonState(0x11, false);
Thread.sleep(50);
setButtonState(0x0D, true);
Thread.sleep(50);
setButtonState(0x0D, false);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t.start();
} else if(useAudio){
c1.setFramePosition(0);
c1.start();
}
}
static void setButtonState(long button, boolean pressed){
winInput.input.ki.wVk = new WinDef.WORD(button);
winInput.input.ki.dwFlags = new WinDef.DWORD( pressed?0:2 );
User32.INSTANCE.SendInput( new WinDef.DWORD( 1 ), ( WinUser.INPUT[] ) winInput.toArray( 1 ), winInput.size() );
}
}
}
Православные звуковые файлы прилагаются.
Для администраторов проектов:
Как писалось выше, данный бот не будет работать, если в лог-файле не будут содержаться элементы викторины, а для этого наиболее эффективным решением будет мод на клиентской стороне, который фильтрует сообщения, заносимые в лог-файл. Другим, не менее эффективным, но более трудоёмким решением будет вывод викторины не в чате, а в оверлее клиента, но это совсем другая история...