UNPKG

sc2ts

Version:

TypeScript library for parsing MPQ (MoPaQ) archive files

181 lines (180 loc) 6.58 kB
"use strict"; // SC2 Replay Parser // Based on Blizzard's s2protocol implementation Object.defineProperty(exports, "__esModule", { value: true }); exports.SC2Replay = void 0; const logger_1 = require("./logger"); const mpq_archive_1 = require("./mpq-archive"); const protocol_1 = require("./protocol"); const logger = (0, logger_1.getScLogger)("sc2-replay"); class SC2Replay { constructor(mpqArchive) { this.header = null; this.details = null; this.initData = null; this._gameEvents = []; this._messageEvents = []; this._trackerEvents = []; this._mpqArchive = mpqArchive; this.decoder = new protocol_1.VersionedProtocol(); } static async fromFile(filepath, options) { const mpqArchive = await mpq_archive_1.MpqArchive.open(filepath, { listFile: this.listFiles.join("\n") }); const replay = new SC2Replay(mpqArchive); replay.parse(options); return replay; } static fromBuffer(buffer, options) { const mpqArchive = mpq_archive_1.MpqArchive.fromBuffer(buffer, { listFile: this.listFiles.join("\n") }); const replay = new SC2Replay(mpqArchive); replay.parse(options); return replay; } parse(options) { // Parse header from MPQ archive header this.parseHeader(); this.decoder = new protocol_1.VersionedProtocol(this.header?.version?.build); // Parse details this.parseDetails(); // Parse init data if requested if (options?.decodeInitData === true) { this.parseInitData(); } // Parse events if requested if (options?.decodeGameEvents !== false) { this.parseGameEvents(); } if (options?.decodeMessageEvents !== false) { this.parseMessageEvents(); } if (options?.decodeTrackerEvents !== false) { this.parseTrackerEvents(); } } parseHeader() { const mpqHeader = this.mpqArchive.archiveHeader; if (!mpqHeader) { throw new Error("No MPQ header found"); } // Get user data content from the MPQ archive const userDataContent = this.mpqArchive.getUserDataContent(); if (!userDataContent) { logger.warn("No user data content found, using default values"); throw new Error("No user data content"); } const header = this.decoder.decodeReplayHeader(userDataContent); logger.debug("Decoded header info:", { signature: header.signature, version: header.version, length: header.length }); // Create SC2 replay header using all information from headerInfo this.header = header; } parseDetails() { const detailsFile = this.mpqArchive.getFile("replay.details"); const details = this.decoder.decodeReplayDetails(detailsFile.data); this.details = details; } parseInitData() { const initDataFile = this.mpqArchive.getFile("replay.initData"); const initData = this.decoder.decodeReplayInitdata(initDataFile.data); this.initData = initData; } parseGameEvents() { const gameEventsFile = this.mpqArchive.getFile("replay.game.events"); const rawGameEvents = this.decoder.decodeReplayGameEvents(gameEventsFile.data); // Buffer 필드를 문자열로 변환 this._gameEvents = this.convertBufferFieldsToStringsGame(rawGameEvents); } parseMessageEvents() { const messageEventsFile = this.mpqArchive.getFile("replay.message.events"); this._messageEvents = this.decoder.decodeReplayMessageEvents(messageEventsFile.data); } parseTrackerEvents() { const trackerEventsFile = this.mpqArchive.getFile("replay.tracker.events"); const rawTrackerEvents = this.decoder.decodeReplayTrackerEvents(trackerEventsFile.data); // Buffer 필드를 문자열로 변환 this._trackerEvents = this.convertBufferFieldsToStrings(rawTrackerEvents); } convertBufferFieldsToStrings(events) { // Buffer를 문자열로 변환해야 하는 필드들 const stringFields = [ "m_unitTypeName", "m_upgradeTypeName", "m_creatorAbilityName", ]; return events.map(event => { const convertedEvent = { ...event }; for (const field of stringFields) { if (field in convertedEvent && Buffer.isBuffer(convertedEvent[field])) { convertedEvent[field] = convertedEvent[field].toString("utf8"); } } return convertedEvent; }); } convertBufferFieldsToStringsGame(events) { // Buffer를 문자열로 변환해야 하는 필드들 const stringFields = [ "m_hotkeyProfile", ]; return events.map(event => { const convertedEvent = { ...event }; for (const field of stringFields) { if (field in convertedEvent && Buffer.isBuffer(convertedEvent[field])) { convertedEvent[field] = convertedEvent[field].toString("utf8"); } } return convertedEvent; }); } // Public API get replayHeader() { return this.header; } get replayDetails() { return this.details; } get replayInitData() { return this.initData; } get players() { return this.details?.playerList ?? []; } get gameEvents() { return this._gameEvents; } get messageEvents() { return this._messageEvents; } get trackerEvents() { return this._trackerEvents; } // Utility getters get gameLength() { const lastEvent = [...this._gameEvents, ...this._trackerEvents] .sort((a, b) => b.loop - a.loop)[0]; return lastEvent ? lastEvent.loop : 0; } get winner() { return this.players.find(p => p.result === 1) ?? null; } get duration() { // Convert game loops to seconds (approximately 16 loops per second) return Math.round(this.gameLength / 16); } get mpqArchive() { return this._mpqArchive; } } exports.SC2Replay = SC2Replay; SC2Replay.listFiles = [ "(attributes)", "(listfile)", "replay.attributes.events", "replay.details", "replay.game.events", "replay.initData", "replay.load.info", "replay.message.events", "replay.server.battlelobby", "replay.sync.events", "replay.tracker.events`;", ];