• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

project-slippi / slippi-js / 15010447932

14 May 2025 01:48AM UTC coverage: 83.333% (-0.1%) from 83.446%
15010447932

push

github

web-flow
refactor: migrate yarn -> npm and update ci to node 20 (#141)

* migrate yarn -> npm and update ci to node 20

* add eslint-plugin-flowtype to dev deps

* fix coverage, add jest and ts-jest explicitly to dev deps

still need to fix tests on CI

647 of 822 branches covered (78.71%)

Branch coverage included in aggregate %.

1818 of 2136 relevant lines covered (85.11%)

221835.12 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

10.48
/src/console/dolphinConnection.ts
1
import { EventEmitter } from "events";
22✔
2

3
import type { Connection, ConnectionDetails, ConnectionSettings } from "./types";
4
import { ConnectionEvent, ConnectionStatus, Ports } from "./types";
22✔
5

6
const MAX_PEERS = 32;
22✔
7

8
export enum DolphinMessageType {
22✔
9
  CONNECT_REPLY = "connect_reply",
22✔
10
  GAME_EVENT = "game_event",
22✔
11
  START_GAME = "start_game",
22✔
12
  END_GAME = "end_game",
22✔
13
}
14

15
export class DolphinConnection extends EventEmitter implements Connection {
22✔
16
  private ipAddress: string;
17
  private port: number;
18
  private connectionStatus = ConnectionStatus.DISCONNECTED;
×
19
  private gameCursor = 0;
×
20
  private nickname = "unknown";
×
21
  private version = "";
×
22
  private peer: any | null = null;
×
23
  private client: any | null;
24

25
  public constructor() {
26
    super();
×
27
    this.ipAddress = "0.0.0.0";
×
28
    this.port = Ports.DEFAULT;
×
29
  }
30

31
  /**
32
   * @returns The current connection status.
33
   */
34
  public getStatus(): ConnectionStatus {
35
    return this.connectionStatus;
×
36
  }
37

38
  /**
39
   * @returns The IP address and port of the current connection.
40
   */
41
  public getSettings(): ConnectionSettings {
42
    return {
×
43
      ipAddress: this.ipAddress,
44
      port: this.port,
45
    };
46
  }
47

48
  public getDetails(): ConnectionDetails {
49
    return {
×
50
      consoleNick: this.nickname,
51
      gameDataCursor: this.gameCursor,
52
      version: this.version,
53
    };
54
  }
55

56
  public async connect(ip: string, port: number): Promise<void> {
57
    console.log(`Connecting to: ${ip}:${port}`);
×
58
    this.ipAddress = ip;
×
59
    this.port = port;
×
60

61
    const enet = await import("enet");
×
62
    // Create the enet client
63
    this.client = enet.createClient({ peers: MAX_PEERS, channels: 3, down: 0, up: 0 }, (err) => {
×
64
      if (err) {
×
65
        console.error(err);
×
66
        return;
×
67
      }
68
    });
69

70
    this.peer = this.client.connect(
×
71
      {
72
        address: this.ipAddress,
73
        port: this.port,
74
      },
75
      3,
76
      1337, // Data to send, not sure what this is or what this represents
77
      (err: any, newPeer: any) => {
78
        if (err) {
×
79
          console.error(err);
×
80
          return;
×
81
        }
82

83
        newPeer.ping();
×
84
        this.emit(ConnectionEvent.CONNECT);
×
85
        this._setStatus(ConnectionStatus.CONNECTED);
×
86
      },
87
    );
88

89
    this.peer.on("connect", () => {
×
90
      // Reset the game cursor to the beginning of the game. Do we need to do this or
91
      // should it just continue from where it left off?
92
      this.gameCursor = 0;
×
93

94
      const request = {
×
95
        type: "connect_request",
96
        cursor: this.gameCursor,
97
      };
98
      const packet = new enet.Packet(JSON.stringify(request), enet.PACKET_FLAG.RELIABLE);
×
99
      this.peer.send(0, packet);
×
100
    });
101

102
    this.peer.on("message", (packet: any) => {
×
103
      const data = packet.data();
×
104
      if (data.length === 0) {
×
105
        return;
×
106
      }
107

108
      const dataString = data.toString("ascii");
×
109
      const message = JSON.parse(dataString);
×
110
      const { dolphin_closed } = message;
×
111
      if (dolphin_closed) {
×
112
        // We got a disconnection request
113
        this.disconnect();
×
114
        return;
×
115
      }
116
      this.emit(ConnectionEvent.MESSAGE, message);
×
117
      switch (message.type) {
×
118
        case DolphinMessageType.CONNECT_REPLY:
119
          this.connectionStatus = ConnectionStatus.CONNECTED;
×
120
          this.gameCursor = message.cursor;
×
121
          this.nickname = message.nick;
×
122
          this.version = message.version;
×
123
          this.emit(ConnectionEvent.HANDSHAKE, this.getDetails());
×
124
          break;
×
125
        case DolphinMessageType.GAME_EVENT: {
126
          const { payload } = message;
×
127
          //TODO: remove after game start and end messages have been in stable Ishii for a bit
128
          if (!payload) {
×
129
            // We got a disconnection request
130
            this.disconnect();
×
131
            return;
×
132
          }
133

134
          this._updateCursor(message, dataString);
×
135

136
          const gameData = Buffer.from(payload, "base64");
×
137
          this._handleReplayData(gameData);
×
138
          break;
×
139
        }
140
        case DolphinMessageType.START_GAME: {
141
          this._updateCursor(message, dataString);
×
142
          break;
×
143
        }
144
        case DolphinMessageType.END_GAME: {
145
          this._updateCursor(message, dataString);
×
146
          break;
×
147
        }
148
      }
149
    });
150

151
    this.peer.on("disconnect", () => {
×
152
      this.disconnect();
×
153
    });
154

155
    this._setStatus(ConnectionStatus.CONNECTING);
×
156
  }
157

158
  public disconnect(): void {
159
    if (this.peer) {
×
160
      this.peer.disconnect();
×
161
      this.peer = null;
×
162
    }
163
    if (this.client) {
×
164
      this.client.destroy();
×
165
      this.client = null;
×
166
    }
167
    this._setStatus(ConnectionStatus.DISCONNECTED);
×
168
  }
169

170
  private _handleReplayData(data: Uint8Array): void {
171
    this.emit(ConnectionEvent.DATA, data);
×
172
  }
173

174
  private _setStatus(status: ConnectionStatus): void {
175
    // Don't fire the event if the status hasn't actually changed
176
    if (this.connectionStatus !== status) {
×
177
      this.connectionStatus = status;
×
178
      this.emit(ConnectionEvent.STATUS_CHANGE, this.connectionStatus);
×
179
    }
180
  }
181

182
  private _updateCursor(message: { cursor: number; next_cursor: number }, dataString: string): void {
183
    const { cursor, next_cursor } = message;
×
184

185
    if (this.gameCursor !== cursor) {
×
186
      const err = new Error(
×
187
        `Unexpected game data cursor. Expected: ${this.gameCursor} but got: ${cursor}. Payload: ${dataString}`,
188
      );
189
      console.warn(err);
×
190
      this.emit(ConnectionEvent.ERROR, err);
×
191
    }
192

193
    this.gameCursor = next_cursor;
×
194
  }
195
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc