_handleNetMessage(buffer, conn, multiId) {
		if (conn && conn != this._connection) {
			let ghostConnId = conn.connectionType[0] + conn.connectionId;
			let expectedConnId = this._connection ? (this._connection.connectionType[0] + this._connection.connectionId) : 'NO CONNECTION';
			this.emit('debug', `Received net message from ghost connection ${ghostConnId} (expected ${expectedConnId})`);
			return;
		}

		if (this._useMessageQueue && !multiId) {
			// Multi sub-messages skip the queue because we need messages contained in a decoded multi to be processed first
			this._incomingMessageQueue.push(arguments);
			this.emit('debug', `Enqueued incoming message; queue size is now ${this._incomingMessageQueue.length}`);
			return;
		}

		if (buffer === '__CLOSE__') {
			// This is an enqueued connection closure
			this._handleConnectionClose(conn);
			return;
		}

		let buf = ByteBuffer.wrap(buffer, ByteBuffer.LITTLE_ENDIAN);

		let rawEMsg = buf.readUint32();
		let eMsg = rawEMsg & ~PROTO_MASK;
		let isProtobuf = !!(rawEMsg & PROTO_MASK);

		this.emit('debug-traffic-incoming', buffer, eMsg);

		let header = {msg: eMsg};
		if ([EMsg.ChannelEncryptRequest, EMsg.ChannelEncryptResult].includes(eMsg)) {
			// for encryption setup, we just have a very small header with two fields
			header.targetJobID = buf.readUint64().toString();
			header.sourceJobID = buf.readUint64().toString();
		} else if (isProtobuf) {
			// decode the protobuf header
			let headerLength = buf.readUint32();
			header.proto = SteamUserMessages._decodeProto(Schema.CMsgProtoBufHeader, buf.slice(buf.offset, buf.offset + headerLength));
			buf.skip(headerLength);

			header.targetJobID = header.proto.jobid_target && header.proto.jobid_target.toString();
			header.sourceJobID = header.proto.jobid_source && header.proto.jobid_source.toString();
			header.steamID = header.proto.steamid && header.proto.steamid.toString();
			header.sessionID = header.proto.client_sessionid;
		} else {
			// decode the extended header
			buf.skip(3); // 1 byte for header size (fixed at 36), 2 bytes for header version (fixed at 2)
			header.targetJobID = buf.readUint64().toString();
			header.sourceJobID = buf.readUint64().toString();
			buf.skip(1); // 1 byte for header canary (fixed at 239)
			header.steamID = buf.readUint64().toString();
			header.sessionID = buf.readUint32();
		}

		let sessionID = (header.proto && header.proto.client_sessionid) || header.sessionID;
		let steamID = (header.proto && header.proto.steamid) || header.steamID;
		let ourCurrentSteamID = this.steamID ? this.steamID.toString() : null;
		if (steamID && sessionID && (sessionID != this._sessionID || steamID.toString() != ourCurrentSteamID) && steamID != 0) {
			// TODO if we get a new sessionid, should we check if it matches a previously-closed session? probably not necessary...
			this._sessionID = sessionID;
			this.steamID = new SteamID(steamID.toString());
			delete this._tempSteamID;
		}

		this._handleMessage(header, buf.slice(), conn, multiId);
	}
Handles a raw binary netmessage by parsing the header and routing it appropriately
@param {Buffer|string} buffer
@param {BaseConnection} [conn]
@param {string} [multiId]
@protected