ddbot.js-0374 4.4.4 → 4.5.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/docs/ideas.md ADDED
@@ -0,0 +1,104 @@
1
+ # ddbot.js v5 — идеи и архитектура
2
+
3
+ ## Философия
4
+
5
+ v4 — стабильный клиент поверх teeworlds. Ядро решает проблему нестабильности.
6
+ v5 — фреймворк для ботов. Модули становятся полноценными, общаются между собой и не конфликтуют.
7
+
8
+ ---
9
+
10
+ ## Новая архитектура модулей
11
+
12
+ ### InputModule
13
+
14
+ Базовый класс поверх `BaseModule` для модулей которые отправляют инпут.
15
+
16
+ Модуль декларирует какие каналы инпута он использует:
17
+ ```ts
18
+ channels: ['aim', 'move', 'jump', 'hook', 'fire']
19
+ ```
20
+
21
+ Добавляет метод `pauseInput()` — останавливает только отправку инпута, не останавливая весь модуль. Модуль продолжает слушать события.
22
+
23
+ ---
24
+
25
+ ### InputMixer
26
+
27
+ Менеджер приоритетов инпута. Знает все активные `InputModule` и их каналы.
28
+
29
+ - Если два модуля хотят один канал — побеждает тот у кого выше приоритет
30
+ - Если модули не пересекаются по каналам — работают параллельно
31
+ - При смене задачи сам паузит и возобновляет нужные модули
32
+
33
+ Пример:
34
+ ```
35
+ LookAt → [aim]
36
+ Follow → [move, hook]
37
+ Pathfinding → [aim, move, jump, hook]
38
+ ```
39
+ `LookAt` + `Follow` работают одновременно. `Pathfinding` вытесняет обоих по приоритету.
40
+
41
+ ---
42
+
43
+ ### ModuleContainer
44
+
45
+ Отдельный класс — реестр модулей бота. Живёт в боте но не является ядром.
46
+
47
+ - Хранит все зарегистрированные модули
48
+ - Резолвит зависимости между модулями
49
+ - Отвечает за порядок инициализации
50
+
51
+ ```ts
52
+ bot.modules.register(playerList)
53
+ bot.modules.get(PlayerList) // доступно любому модулю
54
+ ```
55
+
56
+ Ядро (Bot) остаётся чистым. Вся сложность графа зависимостей живёт здесь.
57
+
58
+ ---
59
+
60
+ ### EventBus
61
+
62
+ Шина состояния для общения между модулями. Модули не знают друг о друге напрямую — публикуют и читают через шину.
63
+
64
+ ```ts
65
+ this.publish('target', { client_id: 3, x: 100, y: 200 })
66
+ this.subscribe('target', (data) => { ... })
67
+ ```
68
+
69
+ Решает проблему повторения одной и той же логики в разных модулях. Например `Greeter` подписывается на события `PlayerList` не зная о нём напрямую.
70
+
71
+ ---
72
+
73
+ ### Таймеры в BaseModule
74
+
75
+ `BaseModule` предоставляет обёртки над таймерами:
76
+
77
+ ```ts
78
+ this.setTimeout(() => { ... }, 3000)
79
+ this.setInterval(() => { ... }, 1000)
80
+ ```
81
+
82
+ При `stop()` или `destroy()` все таймеры чистятся автоматически. Решает утечки состояния при реконнекте — старые таймеры не срабатывают в новой сессии.
83
+
84
+ ---
85
+
86
+ ## Итого — слои v5
87
+
88
+ | Слой | Класс | Отвечает за |
89
+ |---|---|---|
90
+ | Стабильность | `Bot` | жизненный цикл, реконнект, идентичность |
91
+ | Зависимости | `ModuleContainer` | граф модулей, резолв зависимостей |
92
+ | Инпут | `InputMixer` | каналы, приоритеты, параллельность |
93
+ | Общение | `EventBus` | состояние между модулями |
94
+ | Время | `BaseModule` | чистые таймеры без утечек |
95
+
96
+ ---
97
+
98
+ ## В Ideas (не срочно)
99
+
100
+ **TickSystem** — таймеры привязанные к тикам снапшота (~25/сек) для высокоточных вещей. Пока не нужно.
101
+
102
+ **Recorder/Replayer** — запись и воспроизведение инпута. Полезно для отладки.
103
+
104
+ **Условный запуск модулей** — модуль активируется по условию а не вручную.
@@ -98,6 +98,7 @@ export declare class Bot extends EventEmitter {
98
98
  * @param input - Partial input object
99
99
  */
100
100
  send_input(input: Partial<Types.SnapshotItemTypes.PlayerInput>): void;
101
+ private snapshotUnpackerRef;
101
102
  /**
102
103
  * Setup all client event listeners and forward them
103
104
  */
package/lib/core/core.js CHANGED
@@ -210,6 +210,7 @@ class Bot extends events_1.EventEmitter {
210
210
  return;
211
211
  this.client.movement.input = { ...this.client.movement.input, ...input };
212
212
  }
213
+ snapshotUnpackerRef = null;
213
214
  /**
214
215
  * Setup all client event listeners and forward them
215
216
  */
@@ -217,36 +218,34 @@ class Bot extends events_1.EventEmitter {
217
218
  this.clean(false);
218
219
  if (!this.client)
219
220
  return;
220
- // Connection events
221
221
  this.client.on('connected', () => {
222
+ this.emit('rawconnect', { addr: this.status.addr, port: this.status.port });
223
+ this.setup_snapshot_events();
222
224
  if (this.status.connect.connected) {
223
225
  this.error('connection received when already connected');
224
226
  }
225
227
  else {
226
228
  this.status.connect.connected = true;
227
229
  this.emit('connect', { addr: this.status.addr, port: this.status.port });
228
- this.setup_snapshot_events();
229
230
  }
230
231
  });
231
232
  this.client.on('disconnect', (reason) => {
233
+ this.snapshotUnpackerRef = null;
234
+ this.emit('rawdisconnect', reason, { addr: this.status.addr, port: this.status.port });
232
235
  this.status.connect.connected = false;
233
236
  this.clean(true);
234
237
  if (!!reason) {
235
238
  this.emit('disconnect', reason, { addr: this.status.addr, port: this.status.port });
236
239
  }
237
240
  });
238
- this.client.on('connected', () => {
239
- this.emit('rawconnect', { addr: this.status.addr, port: this.status.port });
240
- });
241
- this.client.on('disconnect', (reason) => {
242
- this.emit('rawdisconnect', reason, { addr: this.status.addr, port: this.status.port });
243
- });
244
- // Game events
245
241
  this.client.on('broadcast', (msg) => this.emit('broadcast', msg));
246
242
  this.client.on('capabilities', (msg) => this.emit('capabilities', msg));
247
243
  this.client.on('emote', (msg) => this.emit('emote', msg));
248
244
  this.client.on('kill', (msg) => this.emit('kill', msg));
249
- this.client.on('snapshot', (msg) => this.emit('snapshot', msg));
245
+ this.client.on('snapshot', (msg) => {
246
+ this.setup_snapshot_events();
247
+ this.emit('snapshot', msg);
248
+ });
250
249
  this.client.on('map_change', (msg) => this.emit('map_change', msg));
251
250
  this.client.on('map_details', (msg) => this.emit('map_details', msg));
252
251
  this.client.on('motd', (msg) => this.emit('motd', msg));
@@ -258,10 +257,11 @@ class Bot extends events_1.EventEmitter {
258
257
  * Setup snapshot unpacker events
259
258
  */
260
259
  setup_snapshot_events() {
261
- if (!this.client?.SnapshotUnpacker) {
262
- this.error('SnapshotUnpacker not available yet');
260
+ if (!this.client?.SnapshotUnpacker)
263
261
  return;
264
- }
262
+ if (this.snapshotUnpackerRef === this.client.SnapshotUnpacker)
263
+ return;
264
+ this.snapshotUnpackerRef = this.client.SnapshotUnpacker;
265
265
  this.client.SnapshotUnpacker.removeAllListeners();
266
266
  this.client.SnapshotUnpacker.on('spawn', (msg) => this.emit('spawn', msg));
267
267
  this.client.SnapshotUnpacker.on('death', (msg) => this.emit('death', msg));
@@ -30,7 +30,9 @@ declare class PlayerList extends BaseModule<[maxclients?: number]> {
30
30
  private maxclients;
31
31
  private playermap;
32
32
  private previousMap;
33
+ private isFirstSnapshot;
33
34
  private readonly snapshotlistener;
35
+ private readonly resetState;
34
36
  /**
35
37
  * Get list of all players
36
38
  */
@@ -13,6 +13,7 @@ class PlayerList extends module_js_1.default {
13
13
  maxclients = 64;
14
14
  playermap = new Map();
15
15
  previousMap = new Map();
16
+ isFirstSnapshot = true;
16
17
  snapshotlistener = () => {
17
18
  this.previousMap = new Map(this.playermap);
18
19
  this.playermap.clear();
@@ -32,25 +33,29 @@ class PlayerList extends module_js_1.default {
32
33
  DDNetCharacter: DDNetCharacter || null,
33
34
  };
34
35
  this.playermap.set(client_id, playerData);
35
- if (!this.previousMap.has(client_id)) {
36
- this.emit('player_joined', {
37
- client_id,
38
- name: clientInfo.name,
39
- playerData,
40
- });
36
+ if (!this.isFirstSnapshot && !this.previousMap.has(client_id)) {
37
+ this.emit('player_joined', { client_id, name: clientInfo.name, playerData });
41
38
  }
42
39
  }
43
40
  }
44
- for (const [client_id, oldData] of this.previousMap) {
45
- if (!this.playermap.has(client_id)) {
46
- this.emit('player_left', {
47
- client_id,
48
- name: oldData.clientInfo.name,
49
- playerData: oldData,
50
- });
41
+ if (!this.isFirstSnapshot) {
42
+ for (const [client_id, oldData] of this.previousMap) {
43
+ if (!this.playermap.has(client_id)) {
44
+ this.emit('player_left', {
45
+ client_id,
46
+ name: oldData.clientInfo.name,
47
+ playerData: oldData,
48
+ });
49
+ }
51
50
  }
52
51
  }
52
+ this.isFirstSnapshot = false;
53
+ this.previousMap.clear();
54
+ };
55
+ resetState = () => {
56
+ this.playermap.clear();
53
57
  this.previousMap.clear();
58
+ this.isFirstSnapshot = true;
54
59
  };
55
60
  /**
56
61
  * Get list of all players
@@ -72,10 +77,15 @@ class PlayerList extends module_js_1.default {
72
77
  }
73
78
  _start(maxclients = 64) {
74
79
  this.maxclients = maxclients;
80
+ this.isFirstSnapshot = true;
75
81
  this.bot.on('snapshot', this.snapshotlistener);
82
+ this.bot.on('connect', this.resetState);
83
+ this.bot.on('disconnect', this.resetState);
76
84
  }
77
85
  _stop() {
78
86
  this.bot.off('snapshot', this.snapshotlistener);
87
+ this.bot.off('connect', this.resetState);
88
+ this.bot.off('disconnect', this.resetState);
79
89
  this.playermap.clear();
80
90
  this.previousMap.clear();
81
91
  }
@@ -23,7 +23,9 @@ class Reconnect extends module_js_1.default {
23
23
  return;
24
24
  if (this.reconnecting)
25
25
  return;
26
- if (!connectionInfo.addr || !connectionInfo.port) {
26
+ const addr = connectionInfo.addr;
27
+ const port = connectionInfo.port;
28
+ if (!addr || !port) {
27
29
  this.emit('reconnect_failed', 'No connection info');
28
30
  return;
29
31
  }
@@ -42,11 +44,11 @@ class Reconnect extends module_js_1.default {
42
44
  });
43
45
  this.reconnectTimer = setTimeout(async () => {
44
46
  try {
45
- await this.bot.connect(connectionInfo.addr, connectionInfo.port);
47
+ await this.bot.connect(addr, port, 30000);
46
48
  this.currentAttempts = 0;
47
49
  this.emit('reconnected', {
48
- addr: connectionInfo.addr,
49
- port: connectionInfo.port
50
+ addr: addr,
51
+ port: port
50
52
  });
51
53
  }
52
54
  catch (err) {
@@ -22,9 +22,14 @@ interface SnapEvents {
22
22
  frozen: () => void;
23
23
  /** Персонаж разморожен */
24
24
  unfrozen: () => void;
25
+ /** Другой игрок заморожен */
26
+ player_frozen: (client_id: number) => void;
27
+ /** Другой игрок разморожен */
28
+ player_unfrozen: (client_id: number) => void;
25
29
  }
26
30
  declare class Snap extends BaseModule {
27
31
  private _isFrozen;
32
+ private _playerFreezeState;
28
33
  private readonly hammerHitlistener;
29
34
  private readonly firelistener;
30
35
  private readonly snapslistener;
@@ -36,6 +41,7 @@ declare class Snap extends BaseModule {
36
41
  y: number;
37
42
  } | null;
38
43
  get isFrozen(): boolean;
44
+ isPlayerFrozen(client_id: number): boolean;
39
45
  lookatplayer(client_id: number): void;
40
46
  protected _start(): void;
41
47
  protected _stop(): void;
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const module_js_1 = __importDefault(require("../core/module.js"));
7
7
  class Snap extends module_js_1.default {
8
8
  _isFrozen = false;
9
+ _playerFreezeState = new Map();
9
10
  hammerHitlistener = (hit) => {
10
11
  if (this.bot.OwnID === undefined)
11
12
  return;
@@ -35,16 +36,44 @@ class Snap extends module_js_1.default {
35
36
  }
36
37
  }
37
38
  };
38
- ffs(); // in future snapshot can be biger, so we just make new functions. блять идите нахуй со своим англиским, я не знаю но я стараюсь идите нахуй
39
+ const fps = () => {
40
+ if (!this.bot.bot_client?.SnapshotUnpacker)
41
+ return;
42
+ const allChars = this.bot.bot_client.SnapshotUnpacker.AllObjCharacter || [];
43
+ const currentIds = new Set();
44
+ for (const char of allChars) {
45
+ if (char.client_id === this.bot.OwnID)
46
+ continue;
47
+ currentIds.add(char.client_id);
48
+ const ddnetChar = this.bot.bot_client.SnapshotUnpacker.getObjExDDNetCharacter(char.client_id);
49
+ if (!ddnetChar)
50
+ continue;
51
+ const isFrozen = ddnetChar.m_FreezeEnd !== 0;
52
+ const wasFrozen = this._playerFreezeState.get(char.client_id);
53
+ if (wasFrozen === undefined) {
54
+ this._playerFreezeState.set(char.client_id, isFrozen);
55
+ continue;
56
+ }
57
+ if (wasFrozen !== isFrozen) {
58
+ this._playerFreezeState.set(char.client_id, isFrozen);
59
+ this.emit(isFrozen ? 'player_frozen' : 'player_unfrozen', char.client_id);
60
+ }
61
+ }
62
+ for (const id of this._playerFreezeState.keys()) {
63
+ if (!currentIds.has(id)) {
64
+ this._playerFreezeState.delete(id);
65
+ }
66
+ }
67
+ };
68
+ ffs();
69
+ fps();
39
70
  };
40
71
  constructor(bot) {
41
72
  super(bot, { moduleName: 'Snap', offonDisconnect: false });
42
73
  }
43
74
  static areWithinTile(x1, y1, x2, y2) {
44
75
  const TILE = 32 * 1.1;
45
- const distanceX = Math.abs(x1 - x2);
46
- const distanceY = Math.abs(y1 - y2);
47
- return distanceX <= TILE && distanceY <= TILE;
76
+ return Math.abs(x1 - x2) <= TILE && Math.abs(y1 - y2) <= TILE;
48
77
  }
49
78
  static whoareWithinTile(x, y, list = [], ignoreClients = []) {
50
79
  for (const character of list) {
@@ -72,16 +101,17 @@ class Snap extends module_js_1.default {
72
101
  get isFrozen() {
73
102
  return this._isFrozen;
74
103
  }
104
+ isPlayerFrozen(client_id) {
105
+ return this._playerFreezeState.get(client_id) ?? false;
106
+ }
75
107
  lookatplayer(client_id) {
76
108
  try {
77
109
  const pl_character = this.bot.bot_client?.SnapshotUnpacker.getObjCharacter(client_id);
78
110
  const own_character = this.bot.bot_client?.SnapshotUnpacker.getObjCharacter(this.bot.OwnID);
79
- if (!pl_character || !own_character) {
111
+ if (!pl_character || !own_character)
80
112
  return;
81
- }
82
113
  const angle = Math.atan2(pl_character.character_core.y - own_character.character_core.y, pl_character.character_core.x - own_character.character_core.x);
83
114
  this.bot.send_input({ m_TargetX: Math.cos(angle) * 256, m_TargetY: Math.sin(angle) * 256 });
84
- return;
85
115
  }
86
116
  catch (e) {
87
117
  return;
@@ -96,6 +126,7 @@ class Snap extends module_js_1.default {
96
126
  this.bot.off('snapshot', this.snapslistener);
97
127
  this.bot.off('hammerhit', this.hammerHitlistener);
98
128
  this.bot.off('sound_world', this.firelistener);
129
+ this._playerFreezeState.clear();
99
130
  }
100
131
  on(event, listener) {
101
132
  return super.on(event, listener);
package/lib/types.d.ts CHANGED
@@ -250,6 +250,6 @@ export type DeltaItem = {
250
250
  key: number;
251
251
  };
252
252
  export interface ConnectionInfo {
253
- addr: string;
254
- port: number;
253
+ addr: string | null;
254
+ port: number | null;
255
255
  }
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "teeworlds": "^2.5.11"
4
4
  },
5
5
  "name": "ddbot.js-0374",
6
- "version": "4.4.4",
6
+ "version": "4.5.0",
7
7
  "description": "ddbot.js — это Node.js проект для автоматизации и управления ботами.",
8
8
  "main": "./lib/index.js",
9
9
  "scripts": {
@@ -30,6 +30,6 @@
30
30
  },
31
31
  "homepage": "https://github.com/0374flop/ddbot.js#readme",
32
32
  "devDependencies": {
33
- "@types/node": "^25.2.3"
33
+ "@types/node": "^25.5.2"
34
34
  }
35
35
  }