diablo2-web/src/netproto/mcp/framing.ts

81 lines
2.0 KiB
TypeScript

/**
* MCP (`d2cs` Realm Server, Port 6113) stream framing (`u16 len | u8 id`).
* Verified against `D2MCPClient.dll` (1.13c). Client sends 1-byte greeting `0x01` on connect.
*/
import { ProtocolError } from '../core/errors.ts'
export const MCP_GREETING_BYTE = 0x01
export interface McpRawPacket {
readonly id: number
readonly payload: Uint8Array
readonly raw: Uint8Array
}
export function encodeMcpFrame(packetId: number, payload: Uint8Array): Uint8Array {
const totalLen = 3 + payload.length
if (totalLen > 0xffff) {
throw new ProtocolError('realm', packetId, 0, `MCP packet too large (${totalLen}B)`)
}
const out = new Uint8Array(totalLen)
out[0] = totalLen & 0xff
out[1] = (totalLen >>> 8) & 0xff
out[2] = packetId & 0xff
out.set(payload, 3)
return out
}
export class McpFramer {
private buf: Uint8Array = new Uint8Array(0)
push(chunk: Uint8Array): McpRawPacket[] {
if (chunk.length === 0) return []
if (this.buf.length === 0) {
this.buf = chunk.slice()
} else {
const merged = new Uint8Array(this.buf.length + chunk.length)
merged.set(this.buf, 0)
merged.set(chunk, this.buf.length)
this.buf = merged
}
const packets: McpRawPacket[] = []
let offset = 0
while (this.buf.length - offset >= 3) {
const totalLen = this.buf[offset]! | (this.buf[offset + 1]! << 8)
const id = this.buf[offset + 2]!
if (totalLen < 3) {
throw new ProtocolError(
'realm',
id,
offset,
`Invalid MCP frame length ${totalLen} (< 3)`,
this.buf.slice(offset),
)
}
if (this.buf.length - offset < totalLen) {
break
}
const raw = this.buf.slice(offset, offset + totalLen)
const payload = raw.subarray(3)
packets.push({ id, payload, raw })
offset += totalLen
}
if (offset > 0) {
this.buf = this.buf.slice(offset)
}
return packets
}
get bufferedBytes(): number {
return this.buf.length
}
reset(): void {
this.buf = new Uint8Array(0)
}
}