sc2ts
Version:
TypeScript library for parsing MPQ (MoPaQ) archive files
181 lines (180 loc) • 6.58 kB
JavaScript
"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`;",
];