diff --git a/package.json b/package.json
index 1d80085..faa6bff 100644
--- a/package.json
+++ b/package.json
@@ -19,7 +19,7 @@
   "scripts": {
     "dev": "ts-node src/index.ts",
     "lint": "eslint \"src/**/*.ts\"",
-    "generate-proto": "protoc --plugin=./node_modules/.bin/protoc-gen-ts_proto --ts_proto_out=proto/ --ts_proto_opt=esModuleInterop=true --ts_proto_opt=exportCommonSymbols=false --ts_proto_opt=outputTypeRegistry=true Mumble.proto",
+    "generate-proto": "protoc --ts_out proto/ --ts_opt eslint_disable --proto_path . Mumble.proto",
     "prebuild": "yarn generate-proto",
     "build": "tsc --project tsconfig.build.json && tsc-alias -p tsconfig.build.json",
     "test": "jest --watch",
@@ -35,11 +35,12 @@
     "bot"
   ],
   "dependencies": {
+    "@protobuf-ts/runtime": "2.5.0",
     "lodash": "^4.17.21",
-    "rxjs": "7.5.5",
-    "ts-proto": "^1.112.1"
+    "rxjs": "7.5.5"
   },
   "devDependencies": {
+    "@protobuf-ts/plugin": "2.5.0",
     "@release-it/conventional-changelog": "5.0.0",
     "@tsconfig/node16": "1.0.2",
     "@types/jest": "27.5.1",
diff --git a/src/channel-manager.ts b/src/channel-manager.ts
index 77f3ed9..5990887 100644
--- a/src/channel-manager.ts
+++ b/src/channel-manager.ts
@@ -1,7 +1,8 @@
 import { ChannelRemove, ChannelState, PermissionQuery } from '@proto/Mumble';
-import { filter, map, tap } from 'rxjs';
+import { tap } from 'rxjs';
 import { Channel } from './channel';
 import { Client } from './client';
+import { filterPacket } from './rxjs-operators/filter-packet';
 import { MumbleSocket } from './mumble-socket';
 
 export class ChannelManager {
@@ -13,24 +14,21 @@ export class ChannelManager {
 
       socket.packet
         .pipe(
-          filter(packet => packet.$type === ChannelState.$type),
-          map(packet => packet as ChannelState),
+          filterPacket(ChannelState),
           tap(channelState => this.syncChannelState(channelState)),
         )
         .subscribe();
 
       socket.packet
         .pipe(
-          filter(packet => packet.$type === ChannelRemove.$type),
-          map(packet => packet as ChannelRemove),
+          filterPacket(ChannelRemove),
           tap(channelRemove => this.removeChannel(channelRemove)),
         )
         .subscribe();
 
       socket.packet
         .pipe(
-          filter(packet => packet.$type === PermissionQuery.$type),
-          map(packet => packet as PermissionQuery),
+          filterPacket(PermissionQuery),
           tap(permissionQuery => this.syncChannelPermissions(permissionQuery)),
         )
         .subscribe();
@@ -89,9 +87,16 @@ export class ChannelManager {
   }
 
   private syncChannelState(channelState: ChannelState) {
+    if (channelState.channelId === undefined) {
+      return;
+    }
+
     let channel = this.byId(channelState.channelId);
     if (!channel) {
-      channel = new Channel(this.client, channelState);
+      channel = new Channel(
+        this.client,
+        channelState as ChannelState & { channelId: number },
+      );
       this._channels.set(channel.id, channel);
       /**
        * Emitted whenever a channel is created.
@@ -105,6 +110,9 @@ export class ChannelManager {
   }
 
   private syncChannelPermissions(permissionQuery: PermissionQuery) {
+    if (permissionQuery.channelId === undefined) {
+      return;
+    }
     this.byId(permissionQuery.channelId)?.sync(permissionQuery);
   }
 
diff --git a/src/channel.spec.ts b/src/channel.spec.ts
index 0978b04..022c2b6 100644
--- a/src/channel.spec.ts
+++ b/src/channel.spec.ts
@@ -7,9 +7,7 @@ jest.mock('./client');
 jest.mock('./commands', () => ({
   fetchChannelPermissions: jest
     .fn()
-    .mockResolvedValue(
-      PermissionQuery.fromPartial({ permissions: 0x1 | 0x40 }),
-    ),
+    .mockResolvedValue(PermissionQuery.create({ permissions: 0x1 | 0x40 })),
 }));
 
 describe('Channel', () => {
@@ -27,11 +25,11 @@ describe('Channel', () => {
   it('should assign properties', () => {
     const channel = new Channel(
       client,
-      ChannelState.fromPartial({
+      ChannelState.create({
         channelId: 7,
         name: 'FAKE_CHANNEL_NAME',
         parent: 6,
-      }),
+      }) as ChannelState & { channelId: number },
     );
     expect(channel.id).toBe(7);
     expect(channel.name).toEqual('FAKE_CHANNEL_NAME');
@@ -41,16 +39,21 @@ describe('Channel', () => {
     let channel: Channel;
 
     beforeEach(() => {
-      channel = new Channel(client, ChannelState.fromPartial({}));
+      channel = new Channel(
+        client,
+        ChannelState.create({ channelId: 0 }) as ChannelState & {
+          channelId: number;
+        },
+      );
     });
 
     it('should update name', () => {
-      channel.sync(ChannelState.fromPartial({ name: 'NEW_CHANNEL_NAME' }));
+      channel.sync(ChannelState.create({ name: 'NEW_CHANNEL_NAME' }));
       expect(channel.name).toEqual('NEW_CHANNEL_NAME');
     });
 
     it('should update parent', () => {
-      channel.sync(ChannelState.fromPartial({ parent: 10 }));
+      channel.sync(ChannelState.create({ parent: 10 }));
       expect(channel.parent).toEqual(10);
     });
   });
@@ -59,7 +62,12 @@ describe('Channel', () => {
     let channel: Channel;
 
     beforeEach(() => {
-      channel = new Channel(client, ChannelState.fromPartial({ channelId: 7 }));
+      channel = new Channel(
+        client,
+        ChannelState.create({ channelId: 7 }) as ChannelState & {
+          channelId: number;
+        },
+      );
     });
 
     it('should attempt to create channel', async () => {
@@ -72,7 +80,12 @@ describe('Channel', () => {
     let channel: Channel;
 
     beforeEach(() => {
-      channel = new Channel(client, ChannelState.fromPartial({ channelId: 7 }));
+      channel = new Channel(
+        client,
+        ChannelState.create({ channelId: 7 }) as ChannelState & {
+          channelId: number;
+        },
+      );
     });
 
     it('should attempt to remove the channel', async () => {
diff --git a/src/channel.ts b/src/channel.ts
index 586e01f..73a811d 100644
--- a/src/channel.ts
+++ b/src/channel.ts
@@ -1,6 +1,4 @@
 import { ChannelState, PermissionQuery } from '@proto/Mumble';
-import { UnknownMessage } from '@proto/typeRegistry';
-import { isEmpty } from 'lodash';
 import { Client } from './client';
 import { fetchChannelPermissions } from './commands';
 import { InsufficientPermissionsError } from './errors';
@@ -9,11 +7,14 @@ import { User } from './user';
 
 export class Channel {
   readonly id: number;
-  name: string;
-  parent: number;
+  name?: string;
+  parent?: number;
   private permissions?: Permissions;
 
-  constructor(public readonly client: Client, channelState: ChannelState) {
+  constructor(
+    public readonly client: Client,
+    channelState: ChannelState & { channelId: number },
+  ) {
     this.id = channelState.channelId;
     this.name = channelState.name;
     this.parent = channelState.parent;
@@ -22,25 +23,18 @@ export class Channel {
   /**
    * @internal
    */
-  sync(message: UnknownMessage) {
-    switch (message.$type) {
-      case ChannelState.$type: {
-        const channelState = message as ChannelState;
-
-        if (!isEmpty(channelState.name)) {
-          this.name = channelState.name;
-        }
-
-        if (channelState.parent) {
-          this.parent = channelState.parent;
-        }
-        break;
+  sync(message: unknown) {
+    if (ChannelState.is(message)) {
+      if (message.name !== undefined) {
+        this.name = message.name;
       }
 
-      case PermissionQuery.$type: {
-        const permissionQuery = message as PermissionQuery;
-        this.permissions = new Permissions(permissionQuery.permissions);
-        break;
+      if (message.parent !== undefined) {
+        this.parent = message.parent;
+      }
+    } else if (PermissionQuery.is(message)) {
+      if (message.permissions !== undefined) {
+        this.permissions = new Permissions(message.permissions);
       }
     }
   }
@@ -81,7 +75,8 @@ export class Channel {
     }
 
     return new Permissions(
-      (await fetchChannelPermissions(this.client.socket, this.id)).permissions,
+      (await fetchChannelPermissions(this.client.socket, this.id))
+        .permissions ?? 0,
     );
   }
 }
diff --git a/src/client.spec.ts b/src/client.spec.ts
index a409074..f4ca142 100644
--- a/src/client.spec.ts
+++ b/src/client.spec.ts
@@ -5,7 +5,6 @@ import {
   ServerSync,
   Version,
 } from '@proto/Mumble';
-import { UnknownMessage } from '@proto/typeRegistry';
 import { Subject } from 'rxjs';
 import { Client } from './client';
 import { MumbleSocket } from './mumble-socket';
@@ -40,21 +39,51 @@ describe(Client.name, () => {
   });
 
   describe('when connected', () => {
-    let socket: jest.Mocked<MumbleSocket> & { packet: Subject<UnknownMessage> };
+    let socket: jest.Mocked<MumbleSocket> & { packet: Subject<unknown> };
 
     beforeEach(async () => {
       client.on('socketConnected', s => {
         socket = s;
 
-        socket.send.mockImplementation(message => {
-          switch (message.$type) {
-            case Authenticate.$type:
-              socket.packet.next(Version.fromPartial({}));
-              socket.packet.next(ServerSync.fromPartial({ session: 1234 }));
-              socket.packet.next(ServerConfig.fromPartial({}));
+        socket.send.mockImplementation(type => {
+          switch (type.typeName) {
+            case Authenticate.typeName:
+              socket.packet.next(
+                Version.create({
+                  version: 66790,
+                  release: '1.4.230',
+                  os: 'Linux',
+                  osVersion: 'Ubuntu 20.04.4 LTS [x64]',
+                }),
+              );
+              socket.packet.next(
+                ServerSync.create({
+                  session: 2,
+                  maxBandwidth: 558000,
+                  welcomeText: '',
+                  permissions: BigInt(134744846),
+                }),
+              );
+              socket.packet.next(
+                ServerConfig.create({
+                  allowHtml: true,
+                  messageLength: 5000,
+                  imageMessageLength: 131072,
+                  maxUsers: 100,
+                }),
+              );
               break;
-            case Ping.$type:
-              socket.packet.next(Ping.fromPartial({}));
+
+            case Ping.typeName:
+              socket.packet.next(
+                Ping.create({
+                  timestamp: BigInt(0),
+                  good: 0,
+                  late: 0,
+                  lost: 0,
+                  resync: 0,
+                }),
+              );
               break;
           }
 
diff --git a/src/client.ts b/src/client.ts
index bde1264..6be9fac 100644
--- a/src/client.ts
+++ b/src/client.ts
@@ -16,7 +16,6 @@ import {
   ChannelRemove,
   ChannelState,
   PermissionDenied,
-  permissionDenied_DenyTypeToJSON,
   Ping,
   Reject,
   ServerConfig,
@@ -25,7 +24,7 @@ import {
   Version,
 } from '@proto/Mumble';
 import { User } from './user';
-import { isEmpty, merge } from 'lodash';
+import { merge } from 'lodash';
 import { ChannelManager } from './channel-manager';
 import { Channel } from './channel';
 import { UserManager } from './user-manager';
@@ -33,6 +32,7 @@ import EventEmitter from 'events';
 import { encodeMumbleVersion } from './encode-mumble-version';
 import { ClientOptions } from './client-options';
 import { ConnectionRejectedError } from './errors';
+import { filterPacket } from './rxjs-operators/filter-packet';
 
 const defaultOptions: Partial<ClientOptions> = {
   port: 64738,
@@ -64,22 +64,19 @@ export class Client extends EventEmitter {
       race(
         zip(
           (this.socket as MumbleSocket).packet.pipe(
-            filter(packet => packet.$type === ServerSync.$type),
-            map(packet => packet as ServerSync),
+            filterPacket(ServerSync),
             take(1),
           ),
           (this.socket as MumbleSocket).packet.pipe(
-            filter(packet => packet.$type === ServerConfig.$type),
-            map(packet => packet as ServerConfig),
+            filterPacket(ServerConfig),
             take(1),
           ),
           (this.socket as MumbleSocket).packet.pipe(
-            filter(packet => packet.$type === Version.$type),
-            map(packet => packet as Version),
+            filterPacket(Version),
             take(1),
           ),
           (this.socket as MumbleSocket).packet.pipe(
-            filter(packet => packet.$type === Ping.$type),
+            filterPacket(Ping),
             take(1),
           ),
         ).pipe(
@@ -90,7 +87,9 @@ export class Client extends EventEmitter {
           // FIXME Find a way to detect rejected connection without adding a delay
           delay(1000),
           tap(([serverSync, serverConfig, version]) => {
-            this.user = this.users.bySession(serverSync.session);
+            if (serverSync.session) {
+              this.user = this.users.bySession(serverSync.session);
+            }
             this.welcomeText = serverSync.welcomeText;
             this.serverVersion = version;
             this.serverConfig = serverConfig;
@@ -99,13 +98,10 @@ export class Client extends EventEmitter {
           }),
           map(([serverSync]) => serverSync),
         ),
-        (this.socket as MumbleSocket).packet.pipe(
-          filter(packet => packet.$type === Reject.$type),
-          map(packet => packet as Reject),
-        ),
+        (this.socket as MumbleSocket).packet.pipe(filterPacket(Reject)),
       ).subscribe(message => {
-        if (message.$type === Reject.$type) {
-          reject(new ConnectionRejectedError(message as Reject));
+        if (Reject.is(message)) {
+          reject(new ConnectionRejectedError(message));
         } else {
           resolve(this);
         }
@@ -134,34 +130,29 @@ export class Client extends EventEmitter {
 
       race(
         this.socket.packet.pipe(
-          filter(packet => packet.$type === ChannelState.$type),
-          map(packet => packet as ChannelState),
+          filterPacket(ChannelState),
           filter(
             channelState =>
               channelState.parent === parent && channelState.name === name,
           ),
           take(1),
         ),
-        this.socket.packet.pipe(
-          filter(message => message.$type === PermissionDenied.$type),
-          map(message => message as PermissionDenied),
-          take(1),
-        ),
+        this.socket.packet.pipe(filterPacket(PermissionDenied), take(1)),
       ).subscribe(packet => {
-        if (packet.$type === PermissionDenied.$type) {
-          const reason = isEmpty(packet.reason)
-            ? permissionDenied_DenyTypeToJSON(packet.type)
-            : packet.reason;
+        if (PermissionDenied.is(packet)) {
+          const reason = packet.reason;
           reject(new Error(`failed to create channel (${reason})`));
         } else {
-          const channel = this.channels.byId(packet.channelId);
-          if (channel) {
-            resolve(channel);
+          if (packet.channelId) {
+            const channel = this.channels.byId(packet.channelId);
+            if (channel) {
+              resolve(channel);
+            }
           }
         }
       });
 
-      this.socket.send(ChannelState.fromPartial({ parent, name }));
+      this.socket.send(ChannelState, ChannelState.create({ parent, name }));
     });
   }
 
@@ -174,28 +165,21 @@ export class Client extends EventEmitter {
 
       race(
         this.socket.packet.pipe(
-          filter(packet => packet.$type === ChannelRemove.$type),
-          map(packet => packet as ChannelRemove),
+          filterPacket(ChannelRemove),
           filter(channelRemove => channelRemove.channelId === channelId),
           take(1),
         ),
-        this.socket.packet.pipe(
-          filter(message => message.$type === PermissionDenied.$type),
-          map(message => message as PermissionDenied),
-          take(1),
-        ),
+        this.socket.packet.pipe(filterPacket(PermissionDenied), take(1)),
       ).subscribe(packet => {
-        if (packet.$type === PermissionDenied.$type) {
-          const reason = isEmpty(packet.reason)
-            ? permissionDenied_DenyTypeToJSON(packet.type)
-            : packet.reason;
+        if (PermissionDenied.is(packet)) {
+          const reason = packet.reason;
           reject(new Error(`failed to remove channel (${reason})`));
         } else {
           resolve();
         }
       });
 
-      this.socket.send(ChannelRemove.fromPartial({ channelId }));
+      this.socket.send(ChannelRemove, ChannelRemove.create({ channelId }));
     });
   }
 
@@ -222,24 +206,17 @@ export class Client extends EventEmitter {
 
       race(
         this.socket.packet.pipe(
-          filter(packet => packet.$type === UserState.$type),
-          map(packet => packet as UserState),
+          filterPacket(UserState),
           filter(
             userState =>
               userState.session === userSession &&
               userState.channelId === channelId,
           ),
         ),
-        this.socket.packet.pipe(
-          filter(message => message.$type === PermissionDenied.$type),
-          map(message => message as PermissionDenied),
-          take(1),
-        ),
+        this.socket.packet.pipe(filterPacket(PermissionDenied), take(1)),
       ).subscribe(packet => {
-        if (packet.$type === PermissionDenied.$type) {
-          const reason = isEmpty(packet.reason)
-            ? permissionDenied_DenyTypeToJSON(packet.type)
-            : packet.reason;
+        if (PermissionDenied.is(packet)) {
+          const reason = packet.reason;
           reject(new Error(`failed to remove channel (${reason})`));
         } else {
           const user = this.users.bySession(userSession);
@@ -250,7 +227,8 @@ export class Client extends EventEmitter {
       });
 
       this.socket.send(
-        UserState.fromPartial({ session: userSession, channelId }),
+        UserState,
+        UserState.create({ session: userSession, channelId }),
       );
     });
   }
@@ -262,7 +240,8 @@ export class Client extends EventEmitter {
       patch: 230,
     });
     return await this.socket?.send(
-      Version.fromPartial({
+      Version,
+      Version.create({
         release: 'simple mumble bot',
         version,
       }),
@@ -271,12 +250,13 @@ export class Client extends EventEmitter {
 
   private async authenticate(): Promise<void> {
     return await this.socket?.send(
-      Authenticate.fromPartial({ username: this.options.username }),
+      Authenticate,
+      Authenticate.create({ username: this.options.username }),
     );
   }
 
   private async ping() {
-    return await this.socket?.send(Ping.fromPartial({}));
+    return await this.socket?.send(Ping, Ping.create());
   }
 
   private startPinger() {
diff --git a/src/commands/fetch-channel-permissions.ts b/src/commands/fetch-channel-permissions.ts
index ce64245..8363798 100644
--- a/src/commands/fetch-channel-permissions.ts
+++ b/src/commands/fetch-channel-permissions.ts
@@ -1,6 +1,7 @@
 import { MumbleSocket } from '@/mumble-socket';
+import { filterPacket } from '@/rxjs-operators/filter-packet';
 import { PermissionQuery } from '@proto/Mumble';
-import { filter, map, take } from 'rxjs';
+import { filter, take } from 'rxjs';
 
 export const fetchChannelPermissions = async (
   socket: MumbleSocket,
@@ -9,12 +10,11 @@ export const fetchChannelPermissions = async (
   return new Promise(resolve => {
     socket.packet
       .pipe(
-        filter(packet => packet.$type === PermissionQuery.$type),
-        map(packet => packet as PermissionQuery),
+        filterPacket(PermissionQuery),
         filter(permissionQuery => permissionQuery.channelId === channelId),
         take(1),
       )
       .subscribe(resolve);
-    socket.send(PermissionQuery.fromPartial({ channelId }));
+    socket.send(PermissionQuery, PermissionQuery.create({ channelId }));
   });
 };
diff --git a/src/mumble-socket.ts b/src/mumble-socket.ts
index 08a65ab..b742f11 100644
--- a/src/mumble-socket.ts
+++ b/src/mumble-socket.ts
@@ -1,7 +1,7 @@
 import { Observable, Subject } from 'rxjs';
 import { TLSSocket } from 'tls';
-import { messageTypeRegistry, UnknownMessage } from '@proto/typeRegistry';
-import { packetName, packetType } from './packet-type-registry';
+import { packetForType, packetType } from './packet-type-registry';
+import { MessageType } from '@protobuf-ts/runtime';
 
 interface MumbleSocketReader {
   length: number;
@@ -9,7 +9,7 @@ interface MumbleSocketReader {
 }
 
 export class MumbleSocket {
-  private _packet = new Subject<UnknownMessage>();
+  private _packet = new Subject<unknown>();
   private buffers: Buffer[] = [];
   private length = 0;
   private readers: MumbleSocketReader[] = [];
@@ -19,7 +19,7 @@ export class MumbleSocket {
     this.readPrefix();
   }
 
-  get packet(): Observable<UnknownMessage> {
+  get packet(): Observable<unknown> {
     return this._packet.asObservable();
   }
 
@@ -30,22 +30,20 @@ export class MumbleSocket {
     }
   }
 
-  async send(message: UnknownMessage): Promise<void> {
-    const messageType = messageTypeRegistry.get(message.$type);
-    if (!messageType) {
-      throw new Error(`unknown message type (${message.$type})`);
-    }
-
-    const typeNumber = packetType(message.$type);
+  async send<T extends object>(
+    message: MessageType<T>,
+    payload: T,
+  ): Promise<void> {
+    const typeNumber = packetType(message);
     if (typeNumber === undefined) {
-      throw new Error(`unknown message type (${message.$type})`);
+      throw new Error(`unknown message type (${message.typeName})`);
     }
 
-    const payload = messageType.encode(message).finish();
+    const encoded = message.toBinary(payload);
     const prefix = Buffer.alloc(6);
     prefix.writeUint16BE(typeNumber, 0);
-    prefix.writeUint32BE(payload.length, 2);
-    await this.write(Buffer.concat([prefix, payload]));
+    prefix.writeUint32BE(encoded.length, 2);
+    await this.write(Buffer.concat([prefix, encoded]));
   }
 
   write(buffer: Buffer | Uint8Array): Promise<void> {
@@ -117,14 +115,9 @@ export class MumbleSocket {
 
   private readPacket(type: number, length: number) {
     this.read(length, data => {
-      const packetTypeName = packetName(type);
-      if (packetTypeName) {
-        const message = messageTypeRegistry.get(packetTypeName);
-        if (message) {
-          this._packet.next(message?.decode(data));
-        } else {
-          console.error(`Unrecognized packet type (${packetTypeName})`);
-        }
+      const message = packetForType(type);
+      if (message) {
+        this._packet.next(message.fromBinary(data));
       } else {
         console.error(`Unrecognized packet type (${type})`);
       }
diff --git a/src/packet-type-registry.ts b/src/packet-type-registry.ts
index f01773c..e3927c7 100644
--- a/src/packet-type-registry.ts
+++ b/src/packet-type-registry.ts
@@ -26,45 +26,50 @@ import {
   Version,
   VoiceTarget,
 } from '@proto/Mumble';
+import { MessageType } from '@protobuf-ts/runtime';
 
-const packetTypeForName = new Map<string, number>([
-  [Version.$type, 0],
-  [UDPTunnel.$type, 1],
-  [Authenticate.$type, 2],
-  [Ping.$type, 3],
-  [Reject.$type, 4],
-  [ServerSync.$type, 5],
-  [ChannelRemove.$type, 6],
-  [ChannelState.$type, 7],
-  [UserRemove.$type, 8],
-  [UserState.$type, 9],
-  [BanList.$type, 10],
-  [TextMessage.$type, 11],
-  [PermissionDenied.$type, 12],
-  [ACL.$type, 13],
-  [QueryUsers.$type, 14],
-  [CryptSetup.$type, 15],
-  [ContextActionModify.$type, 16],
-  [ContextAction.$type, 17],
-  [UserList.$type, 18],
-  [VoiceTarget.$type, 19],
-  [PermissionQuery.$type, 20],
-  [CodecVersion.$type, 21],
-  [UserStats.$type, 22],
-  [RequestBlob.$type, 23],
-  [ServerConfig.$type, 24],
-  [SuggestConfig.$type, 25],
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+type AnyMessage = MessageType<any>;
+
+// https://buildmedia.readthedocs.org/media/pdf/mumble-protocol/latest/mumble-protocol.pdf
+const packetForPacketType = new Map<number, AnyMessage>([
+  [0, Version],
+  [1, UDPTunnel],
+  [2, Authenticate],
+  [3, Ping],
+  [4, Reject],
+  [5, ServerSync],
+  [6, ChannelRemove],
+  [7, ChannelState],
+  [8, UserRemove],
+  [9, UserState],
+  [10, BanList],
+  [11, TextMessage],
+  [12, PermissionDenied],
+  [13, ACL],
+  [14, QueryUsers],
+  [15, CryptSetup],
+  [16, ContextActionModify],
+  [17, ContextAction],
+  [18, UserList],
+  [19, VoiceTarget],
+  [20, PermissionQuery],
+  [21, CodecVersion],
+  [22, UserStats],
+  [23, RequestBlob],
+  [24, ServerConfig],
+  [25, SuggestConfig],
 ]);
 
-const packetNameForType = new Map<number, string>();
-for (const [key, value] of packetTypeForName.entries()) {
-  packetNameForType.set(value, key);
+const packetTypeForPacket = new Map<AnyMessage, number>();
+for (const [key, value] of packetForPacketType.entries()) {
+  packetTypeForPacket.set(value, key);
 }
 
-export const packetName = (type: number) => {
-  return packetNameForType.get(type);
+export const packetForType = (type: number) => {
+  return packetForPacketType.get(type);
 };
 
-export const packetType = (name: string) => {
-  return packetTypeForName.get(name);
+export const packetType = (packet: AnyMessage) => {
+  return packetTypeForPacket.get(packet);
 };
diff --git a/src/rxjs-operators/filter-packet.ts b/src/rxjs-operators/filter-packet.ts
new file mode 100644
index 0000000..3c50e1b
--- /dev/null
+++ b/src/rxjs-operators/filter-packet.ts
@@ -0,0 +1,12 @@
+import { MessageType } from '@protobuf-ts/runtime';
+import { filter, map, Observable } from 'rxjs';
+
+export function filterPacket<T extends object>(
+  messageType: MessageType<T>,
+): (source: Observable<unknown>) => Observable<T> {
+  return source =>
+    source.pipe(
+      filter(packet => messageType.is(packet)),
+      map(packet => packet as T),
+    );
+}
diff --git a/src/user-manager.ts b/src/user-manager.ts
index 9cf32ed..a6469cd 100644
--- a/src/user-manager.ts
+++ b/src/user-manager.ts
@@ -1,6 +1,7 @@
 import { UserRemove, UserState } from '@proto/Mumble';
-import { filter, map, tap } from 'rxjs';
+import { tap } from 'rxjs';
 import { Client } from './client';
+import { filterPacket } from './rxjs-operators/filter-packet';
 import { MumbleSocket } from './mumble-socket';
 import { User } from './user';
 
@@ -13,16 +14,14 @@ export class UserManager {
 
       socket.packet
         .pipe(
-          filter(packet => packet.$type === UserState.$type),
-          map(packet => packet as UserState),
+          filterPacket(UserState),
           tap(userState => this.syncUser(userState)),
         )
         .subscribe();
 
       socket.packet
         .pipe(
-          filter(packet => packet.$type === UserRemove.$type),
-          map(packet => packet as UserRemove),
+          filterPacket(UserRemove),
           tap(userRemove => this.removeUser(userRemove)),
         )
         .subscribe();
@@ -38,9 +37,16 @@ export class UserManager {
   }
 
   private syncUser(userState: UserState) {
+    if (userState.session === undefined) {
+      return;
+    }
+
     let user = this.bySession(userState.session);
     if (!user) {
-      user = new User(this.client, userState);
+      user = new User(
+        this.client,
+        userState as UserState & { session: number },
+      );
       this._users.set(user.session, user);
       this.client.emit('userCreate', user);
     } else {
diff --git a/src/user.ts b/src/user.ts
index 567e3a4..c537d70 100644
--- a/src/user.ts
+++ b/src/user.ts
@@ -1,23 +1,26 @@
-import { filter, map, takeWhile } from 'rxjs';
+import { filter, takeWhile } from 'rxjs';
 import { UserState } from '@proto/Mumble';
 import { Client } from './client';
-import { isEmpty } from 'lodash';
 import { Channel } from './channel';
 import { InsufficientPermissionsError, NoSuchChannelError } from './errors';
+import { filterPacket } from './rxjs-operators/filter-packet';
 
 export class User {
   readonly session: number;
-  name: string;
+  name?: string;
   channelId: number;
   selfMute: boolean;
   selfDeaf: boolean;
 
-  constructor(private readonly client: Client, userState: UserState) {
+  constructor(
+    private readonly client: Client,
+    userState: UserState & { session: number },
+  ) {
     this.session = userState.session;
     this.name = userState.name;
-    this.channelId = userState.channelId;
-    this.selfMute = userState.selfMute;
-    this.selfDeaf = userState.selfDeaf;
+    this.channelId = userState.channelId ?? 0;
+    this.selfMute = userState.selfMute ?? false;
+    this.selfDeaf = userState.selfDeaf ?? false;
   }
 
   get channel(): Channel {
@@ -30,16 +33,23 @@ export class User {
    * @internal
    */
   sync(userState: UserState) {
-    if (!isEmpty(userState.name)) {
+    if (userState.name !== undefined) {
       this.name = userState.name;
     }
 
-    if (this.channelId !== userState.channelId) {
+    if (
+      userState.channelId !== undefined &&
+      this.channelId !== userState.channelId
+    ) {
       this.channelId = userState.channelId;
     }
 
-    this.selfMute = userState.selfMute;
-    this.selfDeaf = userState.selfDeaf;
+    if (userState.selfMute !== undefined) {
+      this.selfMute = userState.selfMute;
+    }
+    if (userState.selfDeaf !== undefined) {
+      this.selfDeaf = userState.selfDeaf;
+    }
   }
 
   async moveToChannel(channelId: number): Promise<User> {
@@ -59,15 +69,15 @@ export class User {
     return new Promise(resolve => {
       this.client.socket?.packet
         .pipe(
-          filter(message => message.$type === UserState.$type),
-          map(message => message as UserState),
+          filterPacket(UserState),
           filter(userState => userState.session === this.session),
           takeWhile(userState => userState.selfMute === selfMute, true),
         )
         .subscribe(() => resolve(this));
 
       this.client.socket?.send(
-        UserState.fromPartial({ session: this.session, selfMute }),
+        UserState,
+        UserState.create({ session: this.session, selfMute }),
       );
     });
   }
@@ -76,15 +86,15 @@ export class User {
     return new Promise(resolve => {
       this.client.socket?.packet
         .pipe(
-          filter(message => message.$type === UserState.$type),
-          map(message => message as UserState),
+          filterPacket(UserState),
           filter(userState => userState.session === this.session),
           takeWhile(userState => userState.selfDeaf === selfDeaf, true),
         )
         .subscribe(() => resolve());
 
       this.client.socket?.send(
-        UserState.fromPartial({ session: this.session, selfDeaf }),
+        UserState,
+        UserState.create({ session: this.session, selfDeaf }),
       );
     });
   }
diff --git a/yarn.lock b/yarn.lock
index 82553b3..fa948db 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -932,76 +932,54 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@protobufjs/aspromise@npm:^1.1.1, @protobufjs/aspromise@npm:^1.1.2":
-  version: 1.1.2
-  resolution: "@protobufjs/aspromise@npm:1.1.2"
-  checksum: 011fe7ef0826b0fd1a95935a033a3c0fd08483903e1aa8f8b4e0704e3233406abb9ee25350ec0c20bbecb2aad8da0dcea58b392bbd77d6690736f02c143865d2
-  languageName: node
-  linkType: hard
-
-"@protobufjs/base64@npm:^1.1.2":
-  version: 1.1.2
-  resolution: "@protobufjs/base64@npm:1.1.2"
-  checksum: 67173ac34de1e242c55da52c2f5bdc65505d82453893f9b51dc74af9fe4c065cf4a657a4538e91b0d4a1a1e0a0642215e31894c31650ff6e3831471061e1ee9e
-  languageName: node
-  linkType: hard
-
-"@protobufjs/codegen@npm:^2.0.4":
-  version: 2.0.4
-  resolution: "@protobufjs/codegen@npm:2.0.4"
-  checksum: 59240c850b1d3d0b56d8f8098dd04787dcaec5c5bd8de186fa548de86b86076e1c50e80144b90335e705a044edf5bc8b0998548474c2a10a98c7e004a1547e4b
-  languageName: node
-  linkType: hard
-
-"@protobufjs/eventemitter@npm:^1.1.0":
-  version: 1.1.0
-  resolution: "@protobufjs/eventemitter@npm:1.1.0"
-  checksum: 0369163a3d226851682f855f81413cbf166cd98f131edb94a0f67f79e75342d86e89df9d7a1df08ac28be2bc77e0a7f0200526bb6c2a407abbfee1f0262d5fd7
-  languageName: node
-  linkType: hard
-
-"@protobufjs/fetch@npm:^1.1.0":
-  version: 1.1.0
-  resolution: "@protobufjs/fetch@npm:1.1.0"
+"@protobuf-ts/plugin-framework@npm:^2.5.0":
+  version: 2.5.0
+  resolution: "@protobuf-ts/plugin-framework@npm:2.5.0"
   dependencies:
-    "@protobufjs/aspromise": ^1.1.1
-    "@protobufjs/inquire": ^1.1.0
-  checksum: 3fce7e09eb3f1171dd55a192066450f65324fd5f7cc01a431df01bb00d0a895e6bfb5b0c5561ce157ee1d886349c90703d10a4e11a1a256418ff591b969b3477
-  languageName: node
-  linkType: hard
-
-"@protobufjs/float@npm:^1.0.2":
-  version: 1.0.2
-  resolution: "@protobufjs/float@npm:1.0.2"
-  checksum: 5781e1241270b8bd1591d324ca9e3a3128d2f768077a446187a049e36505e91bc4156ed5ac3159c3ce3d2ba3743dbc757b051b2d723eea9cd367bfd54ab29b2f
+    "@protobuf-ts/runtime": ^2.5.0
+    typescript: ^3.9
+  checksum: e98cddf849380d9fd334746d9417d02ae8102fbc54c619e6adbba5b23e42b3751484e105ff6877634cf774fef0e8239c2d88268df299e1dc67e3b95c2a16f48f
   languageName: node
   linkType: hard
 
-"@protobufjs/inquire@npm:^1.1.0":
-  version: 1.1.0
-  resolution: "@protobufjs/inquire@npm:1.1.0"
-  checksum: ca06f02eaf65ca36fb7498fc3492b7fc087bfcc85c702bac5b86fad34b692bdce4990e0ef444c1e2aea8c034227bd1f0484be02810d5d7e931c55445555646f4
+"@protobuf-ts/plugin@npm:2.5.0":
+  version: 2.5.0
+  resolution: "@protobuf-ts/plugin@npm:2.5.0"
+  dependencies:
+    "@protobuf-ts/plugin-framework": ^2.5.0
+    "@protobuf-ts/protoc": ^2.5.0
+    "@protobuf-ts/runtime": ^2.5.0
+    "@protobuf-ts/runtime-rpc": ^2.5.0
+    typescript: ^3.9
+  bin:
+    protoc-gen-dump: bin/protoc-gen-dump
+    protoc-gen-ts: bin/protoc-gen-ts
+  checksum: 4d8d9aa3384b14401db6ef1803c97a4493cf388e6d3a28c4b742c9f4c7f0e6d9275b7b07a99a3782e68418de01f839af62f3838856f3060df249405800065d20
   languageName: node
   linkType: hard
 
-"@protobufjs/path@npm:^1.1.2":
-  version: 1.1.2
-  resolution: "@protobufjs/path@npm:1.1.2"
-  checksum: 856eeb532b16a7aac071cacde5c5620df800db4c80cee6dbc56380524736205aae21e5ae47739114bf669ab5e8ba0e767a282ad894f3b5e124197cb9224445ee
+"@protobuf-ts/protoc@npm:^2.5.0":
+  version: 2.5.0
+  resolution: "@protobuf-ts/protoc@npm:2.5.0"
+  bin:
+    protoc: protoc.js
+  checksum: d13c45c1e2b5773a7f9d643712162fcf5ac11b6d30bc7698d588a3b292c0761220baeb2e84f139b5b95482b77f829e60468419df9b94f72ec694ff2b9ccbaaeb
   languageName: node
   linkType: hard
 
-"@protobufjs/pool@npm:^1.1.0":
-  version: 1.1.0
-  resolution: "@protobufjs/pool@npm:1.1.0"
-  checksum: d6a34fbbd24f729e2a10ee915b74e1d77d52214de626b921b2d77288bd8f2386808da2315080f2905761527cceffe7ec34c7647bd21a5ae41a25e8212ff79451
+"@protobuf-ts/runtime-rpc@npm:^2.5.0":
+  version: 2.5.0
+  resolution: "@protobuf-ts/runtime-rpc@npm:2.5.0"
+  dependencies:
+    "@protobuf-ts/runtime": ^2.5.0
+  checksum: ce80c7aa279ad39437e186f919e7df70a6db80137566ff2194300f73eeb5f51b6e2c00549023270e4af7d225dc455870632dfba46d12440e9d432bc9397a7115
   languageName: node
   linkType: hard
 
-"@protobufjs/utf8@npm:^1.1.0":
-  version: 1.1.0
-  resolution: "@protobufjs/utf8@npm:1.1.0"
-  checksum: f9bf3163d13aaa3b6f5e6fbf37a116e094ea021c0e1f2a7ccd0e12a29e2ce08dafba4e8b36e13f8ed7397e1591610ce880ed1289af4d66cf4ace8a36a9557278
+"@protobuf-ts/runtime@npm:2.5.0, @protobuf-ts/runtime@npm:^2.5.0":
+  version: 2.5.0
+  resolution: "@protobuf-ts/runtime@npm:2.5.0"
+  checksum: 1a689171e8fa9d17bcde82233a64b496187561dc40dbef0aa9a5ba05594a61f2d2c70dae3eb0c936c7df89f8b09f27a71af7c2142dffe3d9998a3e3a3a5b143b
   languageName: node
   linkType: hard
 
@@ -1079,6 +1057,8 @@ __metadata:
   version: 0.0.0-use.local
   resolution: "@tf2pickup-org/simple-mumble-bot@workspace:."
   dependencies:
+    "@protobuf-ts/plugin": 2.5.0
+    "@protobuf-ts/runtime": 2.5.0
     "@release-it/conventional-changelog": 5.0.0
     "@tsconfig/node16": 1.0.2
     "@types/jest": 27.5.1
@@ -1097,7 +1077,6 @@ __metadata:
     trace-unhandled: 2.0.1
     ts-jest: 28.0.2
     ts-node: 10.7.0
-    ts-proto: ^1.112.1
     tsc-alias: 1.6.7
     tsconfig-paths: 4.0.0
     typescript: 4.6.4
@@ -1280,13 +1259,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@types/long@npm:^4.0.1":
-  version: 4.0.2
-  resolution: "@types/long@npm:4.0.2"
-  checksum: d16cde7240d834cf44ba1eaec49e78ae3180e724cd667052b194a372f350d024cba8dd3f37b0864931683dab09ca935d52f0c4c1687178af5ada9fc85b0635f4
-  languageName: node
-  linkType: hard
-
 "@types/minimist@npm:^1.2.0":
   version: 1.2.2
   resolution: "@types/minimist@npm:1.2.2"
@@ -1294,7 +1266,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@types/node@npm:*, @types/node@npm:17.0.31, @types/node@npm:>=13.7.0":
+"@types/node@npm:*, @types/node@npm:17.0.31":
   version: 17.0.31
   resolution: "@types/node@npm:17.0.31"
   checksum: 704618350f8420d5c47db0f7778398e821b7724369946f5c441a7e6b9343295553936400eb8309f0b07d5e39c240988ab3456b983712ca86265dabc9aee4ad3d
@@ -1308,13 +1280,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@types/object-hash@npm:^1.3.0":
-  version: 1.3.4
-  resolution: "@types/object-hash@npm:1.3.4"
-  checksum: fe4aa041427f3c69cbcf63434af6e788329b40d7208b30aa845cfc1aa6bf9b0c11b23fa33a567d85ba7f2574a95c3b4a227f4b9b9b55da1eaea68ab94b4058d9
-  languageName: node
-  linkType: hard
-
 "@types/parse-json@npm:^4.0.0":
   version: 4.0.0
   resolution: "@types/parse-json@npm:4.0.0"
@@ -2643,13 +2608,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"dataloader@npm:^1.4.0":
-  version: 1.4.0
-  resolution: "dataloader@npm:1.4.0"
-  checksum: e2c93d43afde68980efc0cd9ff48e9851116e27a9687f863e02b56d46f7e7868cc762cd6dcbaf4197e1ca850a03651510c165c2ae24b8e9843fd894002ad0e20
-  languageName: node
-  linkType: hard
-
 "dateformat@npm:^3.0.0":
   version: 3.0.3
   resolution: "dateformat@npm:3.0.3"
@@ -5450,13 +5408,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"long@npm:^4.0.0":
-  version: 4.0.0
-  resolution: "long@npm:4.0.0"
-  checksum: 16afbe8f749c7c849db1f4de4e2e6a31ac6e617cead3bdc4f9605cb703cd20e1e9fc1a7baba674ffcca57d660a6e5b53a9e236d7b25a295d3855cca79cc06744
-  languageName: node
-  linkType: hard
-
 "lowercase-keys@npm:^1.0.0, lowercase-keys@npm:^1.0.1":
   version: 1.0.1
   resolution: "lowercase-keys@npm:1.0.1"
@@ -5988,13 +5939,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"object-hash@npm:^1.3.1":
-  version: 1.3.1
-  resolution: "object-hash@npm:1.3.1"
-  checksum: fdcb957a2f15a9060e30655a9f683ba1fc25dfb8809a73d32e9634bec385a2f1d686c707ac1e5f69fb773bc12df03fb64c77ce3faeed83e35f4eb1946cb1989e
-  languageName: node
-  linkType: hard
-
 "object-inspect@npm:^1.12.0, object-inspect@npm:^1.9.0":
   version: 1.12.0
   resolution: "object-inspect@npm:1.12.0"
@@ -6447,7 +6391,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"prettier@npm:2.6.2, prettier@npm:^2.5.1":
+"prettier@npm:2.6.2":
   version: 2.6.2
   resolution: "prettier@npm:2.6.2"
   bin:
@@ -6527,30 +6471,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"protobufjs@npm:^6.8.8":
-  version: 6.11.2
-  resolution: "protobufjs@npm:6.11.2"
-  dependencies:
-    "@protobufjs/aspromise": ^1.1.2
-    "@protobufjs/base64": ^1.1.2
-    "@protobufjs/codegen": ^2.0.4
-    "@protobufjs/eventemitter": ^1.1.0
-    "@protobufjs/fetch": ^1.1.0
-    "@protobufjs/float": ^1.0.2
-    "@protobufjs/inquire": ^1.1.0
-    "@protobufjs/path": ^1.1.2
-    "@protobufjs/pool": ^1.1.0
-    "@protobufjs/utf8": ^1.1.0
-    "@types/long": ^4.0.1
-    "@types/node": ">=13.7.0"
-    long: ^4.0.0
-  bin:
-    pbjs: bin/pbjs
-    pbts: bin/pbts
-  checksum: 80e9d9610c3eb66f9eae4e44a1ae30381cedb721b7d5f635d781fe4c507e2c77bb7c879addcd1dda79733d3ae589d9e66fd18d42baf99b35df7382a0f9920795
-  languageName: node
-  linkType: hard
-
 "protocols@npm:^1.1.0, protocols@npm:^1.4.0":
   version: 1.4.8
   resolution: "protocols@npm:1.4.8"
@@ -7764,42 +7684,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"ts-poet@npm:^4.11.0":
-  version: 4.11.0
-  resolution: "ts-poet@npm:4.11.0"
-  dependencies:
-    lodash: ^4.17.15
-    prettier: ^2.5.1
-  checksum: 58141523080aafdce397338c8db435a6055385ed919d77c9c77bed76f2f8f2bbe924f26e69d2681ca27270519ea0ec7e041fbadc765a4a25528fcbf2548822f5
-  languageName: node
-  linkType: hard
-
-"ts-proto-descriptors@npm:1.6.0":
-  version: 1.6.0
-  resolution: "ts-proto-descriptors@npm:1.6.0"
-  dependencies:
-    long: ^4.0.0
-    protobufjs: ^6.8.8
-  checksum: 72de9826c2408efb979495c66add49200ea550c6540ebbf05c144cf5d66d21c09722a56cd183e25936196e04a24258e6af2dbdc2ef9e47e8dd19f094de681e89
-  languageName: node
-  linkType: hard
-
-"ts-proto@npm:^1.112.1":
-  version: 1.112.1
-  resolution: "ts-proto@npm:1.112.1"
-  dependencies:
-    "@types/object-hash": ^1.3.0
-    dataloader: ^1.4.0
-    object-hash: ^1.3.1
-    protobufjs: ^6.8.8
-    ts-poet: ^4.11.0
-    ts-proto-descriptors: 1.6.0
-  bin:
-    protoc-gen-ts_proto: protoc-gen-ts_proto
-  checksum: 59755e1ae3932dafe02667ea9724b8c644deddab9ef29e255a9dff8b60a500f15f25712a8ae0a15207fe529260ef9acb88e71f0892dffc1d9616ddd528d1f60a
-  languageName: node
-  linkType: hard
-
 "tsc-alias@npm:1.6.7":
   version: 1.6.7
   resolution: "tsc-alias@npm:1.6.7"
@@ -7945,6 +7829,16 @@ __metadata:
   languageName: node
   linkType: hard
 
+"typescript@npm:^3.9":
+  version: 3.9.10
+  resolution: "typescript@npm:3.9.10"
+  bin:
+    tsc: bin/tsc
+    tsserver: bin/tsserver
+  checksum: 46c842e2cd4797b88b66ef06c9c41dd21da48b95787072ccf39d5f2aa3124361bc4c966aa1c7f709fae0509614d76751455b5231b12dbb72eb97a31369e1ff92
+  languageName: node
+  linkType: hard
+
 "typescript@patch:typescript@4.6.4#~builtin<compat/typescript>":
   version: 4.6.4
   resolution: "typescript@patch:typescript@npm%3A4.6.4#~builtin<compat/typescript>::version=4.6.4&hash=7ad353"
@@ -7955,6 +7849,16 @@ __metadata:
   languageName: node
   linkType: hard
 
+"typescript@patch:typescript@^3.9#~builtin<compat/typescript>":
+  version: 3.9.10
+  resolution: "typescript@patch:typescript@npm%3A3.9.10#~builtin<compat/typescript>::version=3.9.10&hash=7ad353"
+  bin:
+    tsc: bin/tsc
+    tsserver: bin/tsserver
+  checksum: dc7141ab555b23a8650a6787f98845fc11692063d02b75ff49433091b3af2fe3d773650dea18389d7c21f47d620fb3b110ea363dab4ab039417a6ccbbaf96fc2
+  languageName: node
+  linkType: hard
+
 "uglify-js@npm:^3.1.4":
   version: 3.15.5
   resolution: "uglify-js@npm:3.15.5"