Skip to content

Commit be0f014

Browse files
authored
Merge pull request #1342 from melonjs/chore/move-eventemitter-spec
EventEmitter: add native context support, fix removeAllListeners bug, move spec
2 parents a829024 + 580b78c commit be0f014

34 files changed

Lines changed: 656 additions & 350 deletions

packages/melonjs/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,15 @@
77
- Tiled: new public `registerTiledObjectFactory(type, factory)` and `registerTiledObjectClass(name, Constructor)` APIs allowing plugins to register custom Tiled object handlers by class name without modifying engine code
88
- Tiled: `detectObjectType()` now checks `settings.class` and `settings.name` against the factory registry before falling through to structural detection, enabling class-based dispatch for custom types
99

10+
### Changed
11+
- EventEmitter: native context parameter support — `addListener(event, fn, context)` and `addListenerOnce(event, fn, context)` now accept an optional context, eliminating `.bind()` closure overhead and enabling proper `removeListener()` by original function reference
12+
- EventEmitter: `event.on()` and `event.once()` no longer create `.bind()` closures when a context is provided
13+
1014
### Fixed
1115
- WebGLRenderer: `setBlendMode()` now tracks the `premultipliedAlpha` flag — previously only the mode name was checked, causing incorrect GL blend function when mixing PMA and non-PMA textures with the same blend mode
1216
- TMX: fix crash in `getObjects(false)` when a map contains an empty object group (Container.children lazily initialized)
17+
- EventEmitter: `removeAllListeners()` now correctly clears once-listeners (previously only cleared regular listeners)
18+
- Loader: fix undefined `crossOrigin` variable in script parser, unsafe regex match in video parser, missing error parameter in video/fontface error callbacks, `fetchData` Promise constructor antipattern and silent error swallowing
1319

1420
### Chore
1521
- Minimum Node.js version is now 24.0.0 (Node 18/20 EOL, Node 22 in maintenance)

packages/melonjs/src/application/application.ts

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import state from "../state/state.ts";
55
import * as device from "../system/device.js";
66
import {
77
BLUR,
8-
eventEmitter,
8+
emit,
99
FOCUS,
1010
GAME_AFTER_DRAW,
1111
GAME_AFTER_UPDATE,
@@ -14,6 +14,7 @@ import {
1414
GAME_INIT,
1515
GAME_RESET,
1616
GAME_UPDATE,
17+
on,
1718
STAGE_RESET,
1819
STATE_CHANGE,
1920
STATE_RESTART,
@@ -253,10 +254,10 @@ export default class Application {
253254
}
254255

255256
// register to the channel
256-
eventEmitter.addListener(WINDOW_ONRESIZE, () => {
257+
on(WINDOW_ONRESIZE, () => {
257258
onresize(this);
258259
});
259-
eventEmitter.addListener(WINDOW_ONORIENTATION_CHANGE, () => {
260+
on(WINDOW_ONORIENTATION_CHANGE, () => {
260261
onresize(this);
261262
});
262263

@@ -308,17 +309,19 @@ export default class Application {
308309

309310
this.isInitialized = true;
310311

311-
eventEmitter.emit(GAME_INIT);
312-
eventEmitter.addListener(STATE_CHANGE, this.repaint.bind(this));
313-
eventEmitter.addListener(STATE_RESTART, this.repaint.bind(this));
314-
eventEmitter.addListener(STATE_RESUME, this.repaint.bind(this));
315-
eventEmitter.addListener(STAGE_RESET, this.reset.bind(this));
316-
eventEmitter.addListener(TICK, (time: number) => {
312+
emit(GAME_INIT);
313+
/* eslint-disable @typescript-eslint/unbound-method */
314+
on(STATE_CHANGE, this.repaint, this);
315+
on(STATE_RESTART, this.repaint, this);
316+
on(STATE_RESUME, this.repaint, this);
317+
on(STAGE_RESET, this.reset, this);
318+
/* eslint-enable @typescript-eslint/unbound-method */
319+
on(TICK, (time: number) => {
317320
this.update(time);
318321
this.draw();
319322
});
320323

321-
eventEmitter.addListener(BLUR, () => {
324+
on(BLUR, () => {
322325
if (this.stopOnBlur) {
323326
state.stop(true);
324327
}
@@ -327,7 +330,7 @@ export default class Application {
327330
}
328331
});
329332

330-
eventEmitter.addListener(FOCUS, () => {
333+
on(FOCUS, () => {
331334
if (this.stopOnBlur) {
332335
state.restart(true);
333336
}
@@ -351,7 +354,7 @@ export default class Application {
351354
}
352355

353356
// publish reset notification
354-
eventEmitter.emit(GAME_RESET);
357+
emit(GAME_RESET);
355358

356359
// Refresh internal variables for framerate limiting
357360
this.updateFrameRate();
@@ -424,7 +427,7 @@ export default class Application {
424427
this.frameCounter = 0;
425428

426429
// publish notification
427-
eventEmitter.emit(GAME_BEFORE_UPDATE, time);
430+
emit(GAME_BEFORE_UPDATE, time);
428431
this.accumulator += timer.getDelta();
429432
this.accumulator = Math.min(this.accumulator, this.accumulatorMax);
430433

@@ -441,7 +444,7 @@ export default class Application {
441444

442445
// game update event
443446
if (!state.isPaused()) {
444-
eventEmitter.emit(GAME_UPDATE, time);
447+
emit(GAME_UPDATE, time);
445448
}
446449

447450
// update all objects (and pass the elapsed time since last frame)
@@ -460,7 +463,7 @@ export default class Application {
460463
}
461464

462465
// publish notification
463-
eventEmitter.emit(GAME_AFTER_UPDATE, this.lastUpdate);
466+
emit(GAME_AFTER_UPDATE, this.lastUpdate);
464467
}
465468
}
466469

@@ -470,7 +473,7 @@ export default class Application {
470473
draw(): void {
471474
if (this.renderer.isContextValid && (this.isDirty || this.isAlwaysDirty)) {
472475
// publish notification
473-
eventEmitter.emit(GAME_BEFORE_DRAW, globalThis.performance.now());
476+
emit(GAME_BEFORE_DRAW, globalThis.performance.now());
474477

475478
// prepare renderer to draw a new frame
476479
this.renderer.clear();
@@ -485,7 +488,7 @@ export default class Application {
485488
this.renderer.flush();
486489

487490
// publish notification
488-
eventEmitter.emit(GAME_AFTER_DRAW, globalThis.performance.now());
491+
emit(GAME_AFTER_DRAW, globalThis.performance.now());
489492
}
490493
}
491494
}

packages/melonjs/src/camera/camera2d.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@ import type Container from "./../renderable/container.js";
1212
import Renderable from "./../renderable/renderable.js";
1313
import {
1414
CANVAS_ONRESIZE,
15-
eventEmitter,
15+
emit,
1616
GAME_RESET,
17+
on,
1718
VIEWPORT_ONCHANGE,
1819
VIEWPORT_ONRESIZE,
1920
} from "../system/event.ts";
@@ -274,9 +275,11 @@ export default class Camera2d extends Renderable {
274275
this._updateProjectionMatrix();
275276

276277
// subscribe to the game reset event
277-
eventEmitter.addListener(GAME_RESET, this.reset.bind(this));
278+
// eslint-disable-next-line @typescript-eslint/unbound-method
279+
on(GAME_RESET, this.reset, this);
278280
// subscribe to the canvas resize event
279-
eventEmitter.addListener(CANVAS_ONRESIZE, this.resize.bind(this));
281+
// eslint-disable-next-line @typescript-eslint/unbound-method
282+
on(CANVAS_ONRESIZE, this.resize, this);
280283
}
281284

282285
// -- some private function ---
@@ -468,7 +471,7 @@ export default class Camera2d extends Renderable {
468471
this._updateProjectionMatrix();
469472

470473
// publish the viewport resize event
471-
eventEmitter.emit(VIEWPORT_ONRESIZE, this.width, this.height);
474+
emit(VIEWPORT_ONRESIZE, this.width, this.height);
472475

473476
return this;
474477
}
@@ -650,7 +653,7 @@ export default class Camera2d extends Renderable {
650653

651654
if (this.isDirty) {
652655
//publish the corresponding message
653-
eventEmitter.emit(VIEWPORT_ONCHANGE, this.pos);
656+
emit(VIEWPORT_ONCHANGE, this.pos);
654657
}
655658

656659
// check for fade/flash effect

packages/melonjs/src/index.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ import Stage from "./state/stage.js";
4848
import state from "./state/state.ts";
4949
import { initVisibilityEvents } from "./system/device.js";
5050
import { DOMContentLoaded } from "./system/dom.ts";
51-
import { BOOT, DOM_READY, eventEmitter } from "./system/event.ts";
51+
import { BOOT, DOM_READY, emit } from "./system/event.ts";
5252
import pool from "./system/legacy_pool.js";
5353
import save from "./system/save.ts";
5454
import timer from "./system/timer.ts";
@@ -239,7 +239,7 @@ export function boot() {
239239
registerBuiltinTiledClass("ColorLayer", ColorLayer);
240240

241241
// publish Boot notification
242-
eventEmitter.emit(BOOT);
242+
emit(BOOT);
243243

244244
// enable/disable the cache
245245
setNocache(getUriFragment().nocache || false);
@@ -254,7 +254,7 @@ export function boot() {
254254
initialized = true;
255255

256256
// notify that the engine is ready
257-
eventEmitter.emit(DOM_READY);
257+
emit(DOM_READY);
258258
}
259259

260260
// call the library init function when ready

packages/melonjs/src/input/gamepad.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import {
2-
eventEmitter,
2+
emit,
33
GAME_BEFORE_UPDATE,
44
GAMEPAD_CONNECTED,
55
GAMEPAD_DISCONNECTED,
66
GAMEPAD_UPDATE,
7+
has,
8+
on,
79
} from "../system/event.ts";
810
import { getBindingKey, triggerKeyEvent } from "./keyboard.ts";
911

@@ -242,7 +244,7 @@ const updateGamepads = function (): void {
242244
}
243245
}
244246

245-
eventEmitter.emit(GAMEPAD_UPDATE, index, "buttons", +button, current);
247+
emit(GAMEPAD_UPDATE, index, "buttons", +button, current);
246248

247249
// Edge detection
248250
if (!last.pressed && current.pressed) {
@@ -286,7 +288,7 @@ const updateGamepads = function (): void {
286288
const pressed =
287289
Math.abs(value) >= deadzone + Math.abs(last[range].threshold || 0);
288290

289-
eventEmitter.emit(GAMEPAD_UPDATE, index, "axes", +axis, value as any);
291+
emit(GAMEPAD_UPDATE, index, "axes", +axis, value as any);
290292

291293
// Edge detection
292294
if (!last[range].pressed && pressed) {
@@ -322,7 +324,7 @@ if (
322324
globalThis.addEventListener(
323325
"gamepadconnected",
324326
(e: GamepadEvent) => {
325-
eventEmitter.emit(GAMEPAD_CONNECTED, e.gamepad);
327+
emit(GAMEPAD_CONNECTED, e.gamepad);
326328
},
327329
false,
328330
);
@@ -333,7 +335,7 @@ if (
333335
globalThis.addEventListener(
334336
"gamepaddisconnected",
335337
(e: GamepadEvent) => {
336-
eventEmitter.emit(GAMEPAD_DISCONNECTED, e.gamepad);
338+
emit(GAMEPAD_DISCONNECTED, e.gamepad);
337339
},
338340
false,
339341
);
@@ -437,10 +439,10 @@ export function bindGamepad(
437439
// register to the the update event if not yet done and supported by the browser
438440
// if not supported, the function will fail silently (-> update loop won't be called)
439441
if (
440-
!eventEmitter.hasListener(GAME_BEFORE_UPDATE, updateGamepads) &&
442+
!has(GAME_BEFORE_UPDATE, updateGamepads) &&
441443
typeof navigator.getGamepads === "function"
442444
) {
443-
eventEmitter.addListener(GAME_BEFORE_UPDATE, updateGamepads);
445+
on(GAME_BEFORE_UPDATE, updateGamepads);
444446
}
445447

446448
// Allocate bindings if not defined

packages/melonjs/src/input/keyboard.ts

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { eventEmitter, KEYDOWN, KEYUP } from "../system/event.ts";
1+
import { emit, KEYDOWN, KEYUP } from "../system/event.ts";
22
import { isMobile } from "../system/platform.ts";
33
import { preventDefault as preventDefaultAction } from "./input.ts";
44

@@ -31,12 +31,7 @@ const keyDownEvent: KeyEventHandler = (options) => {
3131
const { keyCode } = options;
3232
const action = _keyBindings[keyCode];
3333

34-
eventEmitter.emit(
35-
KEYDOWN,
36-
action,
37-
keyCode,
38-
action ? !_keyLocked[action] : true,
39-
);
34+
emit(KEYDOWN, action, keyCode, action ? !_keyLocked[action] : true);
4035

4136
if (action) {
4237
if (!_keyLocked[action]) {
@@ -58,7 +53,7 @@ const keyUpEvent: KeyEventHandler = (options) => {
5853
const { keyCode } = options;
5954
const action = _keyBindings[keyCode];
6055

61-
eventEmitter.emit(KEYUP, action, keyCode);
56+
emit(KEYUP, action, keyCode);
6257

6358
if (action) {
6459
const trigger =

packages/melonjs/src/input/pointerevent.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,7 @@ import { game } from "../index.js";
33
import type { Vector2d } from "../math/vector2d.ts";
44
import { vector2dPool } from "../math/vector2d.ts";
55
import * as device from "./../system/device.js";
6-
import {
7-
eventEmitter,
8-
POINTERLOCKCHANGE,
9-
POINTERMOVE,
10-
} from "../system/event.ts";
6+
import { emit, POINTERLOCKCHANGE, POINTERMOVE } from "../system/event.ts";
117
import timer from "./../system/timer.ts";
128
import { remove } from "./../utils/array.ts";
139
import { throttle } from "./../utils/function.ts";
@@ -209,7 +205,7 @@ function enablePointerEvent(): void {
209205
locked =
210206
globalThis.document.pointerLockElement === game.getParentElement();
211207
// emit the corresponding internal event
212-
eventEmitter.emit(POINTERLOCKCHANGE, locked);
208+
emit(POINTERLOCKCHANGE, locked);
213209
},
214210
true,
215211
);
@@ -310,7 +306,7 @@ function dispatchEvent(normalizedEvents: Pointer[]): boolean {
310306
if (POINTER_MOVE.includes(pointer.type!)) {
311307
pointer.gameX = pointer.gameLocalX = pointer.gameScreenX;
312308
pointer.gameY = pointer.gameLocalY = pointer.gameScreenY;
313-
eventEmitter.emit(POINTERMOVE, pointer);
309+
emit(POINTERMOVE, pointer);
314310
}
315311

316312
// fetch valid candiates from the game world container

packages/melonjs/src/level/level.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { game } from "../index.js";
22
import { getTMX } from "./../loader/loader.js";
33
import state from "./../state/state.ts";
4-
import { eventEmitter, LEVEL_LOADED } from "../system/event.ts";
4+
import { emit, LEVEL_LOADED } from "../system/event.ts";
55
import { resetGUID } from "./../utils/utils.ts";
66
import TMXTileMap from "./tiled/TMXTileMap.js";
77

@@ -39,7 +39,7 @@ function safeLoadLevel(levelId, options, restart) {
3939
);
4040

4141
// publish the corresponding message
42-
eventEmitter.emit(LEVEL_LOADED, levelId);
42+
emit(LEVEL_LOADED, levelId);
4343

4444
// fire the callback
4545
options.onLoaded(levelId);

packages/melonjs/src/level/tiled/TMXTileMap.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { warning } from "../../lang/console.js";
33
import { vector2dPool } from "../../math/vector2d.ts";
44
import { collision } from "../../physics/collision.js";
55
import Container from "../../renderable/container.js";
6-
import { eventEmitter, VIEWPORT_ONRESIZE } from "../../system/event.ts";
6+
import { off, on, VIEWPORT_ONRESIZE } from "../../system/event.ts";
77
import pool from "../../system/legacy_pool.js";
88
import { checkVersion } from "../../utils/utils.ts";
99
import { COLLISION_GROUP, TILED_SUPPORTED_VERSION } from "./constants.js";
@@ -432,11 +432,11 @@ export default class TMXTileMap {
432432
}
433433

434434
if (setViewportBounds === true) {
435-
eventEmitter.removeListener(VIEWPORT_ONRESIZE, _setBounds);
435+
off(VIEWPORT_ONRESIZE, _setBounds);
436436
// force viewport bounds update
437437
_setBounds(game.viewport.width, game.viewport.height);
438438
// Replace the resize handler
439-
eventEmitter.addListener(VIEWPORT_ONRESIZE, _setBounds);
439+
on(VIEWPORT_ONRESIZE, _setBounds);
440440
}
441441

442442
// set back auto-sort and auto-depth

0 commit comments

Comments
 (0)