diff --git a/package.json b/package.json index f1af13d7..3febd04b 100644 --- a/package.json +++ b/package.json @@ -1,54 +1,54 @@ -{ - "name": "@ffxiv-teamcraft/pcap-ffxiv", - "version": "0.11.4", - "description": "A Machina wrapper for FINAL FANTASY XIV packet capture in JS/TS.", - "main": "lib/index.js", - "repository": "https://github.com/ffxiv-teamcraft/pcap-ffxiv.git", - "author": "karashiiro <49822414+karashiiro@users.noreply.github.com>", - "license": "MIT", - "scripts": { - "prebuild": "rimraf lib/* && yarn pcap-types:generate && yarn copyfiles && node prebuild.js", - "build": "yarn prebuild && tsc", - "build:watch": "yarn prebuild && tsc -w", - "docs:generate": "typedoc", - "docs:deploy": "yarn docs:generate && node gh-pages.js", - "prepublish": "yarn format && yarn build && yarn docs:deploy && node gh-pages.js", - "format": "prettier . --write", - "copyfiles": "copyfiles ./MachinaWrapper/* ./lib", - "pcap-types:generate": "barrelsby -D -d ./src/definitions && barrelsby -D -d ./src/models" - }, - "files": [ - "lib/" - ], - "eslintConfig": { - "extends": [ - "prettier" - ], - "env": { - "es11": true - } - }, - "prettier": { - "singleQuote": false, - "printWidth": 120, - "semi": true, - "useTabs": true, - "trailingComma": "all", - "endOfLine": "crlf" - }, - "devDependencies": { - "@types/node": "^14.14.19", - "barrelsby": "^2.2.0", - "copyfiles": "^2.4.1", - "eslint": "^7.17.0", - "eslint-config-prettier": "^7.1.0", - "gh-pages": "^3.1.0", - "prettier": "^2.2.1", - "rimraf": "^3.0.2", - "typedoc": "^0.20.35", - "typescript": "^4.1.3" - }, - "dependencies": { - "heap-js": "^2.1.5" - } -} +{ + "name": "@ffxiv-teamcraft/pcap-ffxiv", + "version": "0.11.4", + "description": "A Machina wrapper for FINAL FANTASY XIV packet capture in JS/TS.", + "main": "lib/index.js", + "repository": "https://github.com/ffxiv-teamcraft/pcap-ffxiv.git", + "author": "karashiiro <49822414+karashiiro@users.noreply.github.com>", + "license": "MIT", + "scripts": { + "prebuild": "rimraf lib/* && yarn pcap-types:generate && yarn copyfiles && node prebuild.js", + "build": "yarn prebuild && tsc", + "build:watch": "yarn prebuild && tsc -w", + "docs:generate": "typedoc", + "docs:deploy": "yarn docs:generate && node gh-pages.js", + "prepublish": "yarn format && yarn build && yarn docs:deploy && node gh-pages.js", + "format": "prettier . --write", + "copyfiles": "copyfiles ./MachinaWrapper/* ./lib", + "pcap-types:generate": "barrelsby -D -d ./src/definitions && barrelsby -D -d ./src/models" + }, + "files": [ + "lib/" + ], + "eslintConfig": { + "extends": [ + "prettier" + ], + "env": { + "es11": true + } + }, + "prettier": { + "singleQuote": false, + "printWidth": 120, + "semi": true, + "useTabs": true, + "trailingComma": "all", + "endOfLine": "lf" + }, + "devDependencies": { + "@types/node": "^14.14.19", + "barrelsby": "^2.2.0", + "copyfiles": "^2.4.1", + "eslint": "^7.17.0", + "eslint-config-prettier": "^7.1.0", + "gh-pages": "^3.1.0", + "prettier": "^2.2.1", + "rimraf": "^3.0.2", + "typedoc": "^0.20.35", + "typescript": "^4.1.3" + }, + "dependencies": { + "heap-js": "^2.1.5" + } +} diff --git a/prebuild.js b/prebuild.js index 7f34644f..2d79c937 100644 --- a/prebuild.js +++ b/prebuild.js @@ -74,14 +74,14 @@ function generateInterfaces(processors, parentInterfaceName, excludeImports) { function createMessageType() { const { processors } = generateImportsAndProcessors("processors"); const actorControlProcessors = generateImportsAndProcessors("processors/actor-control").processors; - const resultDialogProcessors = generateImportsAndProcessors("processors/result-dialog").processors; + const eventResumeProcessors = generateImportsAndProcessors("processors/event-resume").processors; const entries = [ ...generateInterfaces(processors), ...generateInterfaces(actorControlProcessors, "ActorControl"), ...generateInterfaces(actorControlProcessors, "ActorControlSelf", true), ...generateInterfaces(actorControlProcessors, "ActorControlTarget", true), - ...generateInterfaces(resultDialogProcessors, "ResultDialog"), + ...generateInterfaces(eventResumeProcessors, "EventResume"), ]; const fileContent = `import { GenericMessage } from "./GenericMessage"; @@ -116,12 +116,12 @@ createProcessorsLoader( ); createProcessorsLoader( - "result-dialog-packet-processors", - "resultDialogPacketProcessors", - "processors/result-dialog", - "SuperPacketProcessor", + "event-resume-packet-processors", + "eventResumePacketProcessors", + "processors/event-resume", + "SuperPacketProcessor", "SuperPacketProcessor", - [`import { ResultDialog } from "../definitions";`], + [`import { EventResume } from "../definitions";`], ); createMessageType(); diff --git a/src/definitions/DesynthResult.ts b/src/definitions/DesynthResult.ts deleted file mode 100644 index 3a872db4..00000000 --- a/src/definitions/DesynthResult.ts +++ /dev/null @@ -1,11 +0,0 @@ -export interface DesynthResult { - unknown0: number; - unknown1: number; - itemId: number; - itemHq: boolean; - result: { - itemId: number; - itemHq: boolean; - itemQuantity: number; - }[]; -} diff --git a/src/definitions/EventResume.ts b/src/definitions/EventResume.ts new file mode 100644 index 00000000..52235e2e --- /dev/null +++ b/src/definitions/EventResume.ts @@ -0,0 +1,9 @@ +import { EventHandlerType } from "../models"; +import { SuperPacket } from "../models/SuperPacket"; + +export interface EventResume extends SuperPacket { + category: EventHandlerType; + subcategory: number; + scene: number; + params: Array; +} diff --git a/src/definitions/ReductionResult.ts b/src/definitions/ReductionResult.ts deleted file mode 100644 index c03712bd..00000000 --- a/src/definitions/ReductionResult.ts +++ /dev/null @@ -1,11 +0,0 @@ -export interface ReductionResult { - unknown0: number; - unknown1: number; - itemId: number; - unknown2: number; - result: { - itemId: number; - itemHq: boolean; - itemQuantity: number; - }[]; -} diff --git a/src/definitions/ResultDialog.ts b/src/definitions/ResultDialog.ts deleted file mode 100644 index 4647dd51..00000000 --- a/src/definitions/ResultDialog.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { SuperPacket } from "../models/SuperPacket"; - -export interface ResultDialog extends SuperPacket {} diff --git a/src/definitions/event-resume/DesynthResult.ts b/src/definitions/event-resume/DesynthResult.ts new file mode 100644 index 00000000..b8d3e21c --- /dev/null +++ b/src/definitions/event-resume/DesynthResult.ts @@ -0,0 +1,14 @@ +import { EventResume } from "../EventResume"; + +export interface DesynthResult extends EventResume { + itemId: number; + itemHq: boolean; + result: { + itemId: number; + itemHq: boolean; + itemQuantity: number; + }[]; + unknown: boolean; + unknown2: number; // TODO: Gather data, probably ClassJobId for skill up + unknown3: number; // TODO: Gather data, probably experience gained +} diff --git a/src/definitions/event-resume/ReductionResult.ts b/src/definitions/event-resume/ReductionResult.ts new file mode 100644 index 00000000..2f9ccfdf --- /dev/null +++ b/src/definitions/event-resume/ReductionResult.ts @@ -0,0 +1,11 @@ +import { EventResume } from "../EventResume"; + +export interface ReductionResult extends EventResume { + itemId: number; + unknown: boolean; + result: { + itemId: number; + itemHq: boolean; + itemQuantity: number; + }[]; +} diff --git a/src/definitions/event-resume/RetrieveMateriaResult.ts b/src/definitions/event-resume/RetrieveMateriaResult.ts new file mode 100644 index 00000000..02dd7e76 --- /dev/null +++ b/src/definitions/event-resume/RetrieveMateriaResult.ts @@ -0,0 +1,17 @@ +import { EventResume } from "../EventResume"; + +export enum RetrieveMateriaOutcome { + InventoryFull = "inventoryfull", + Shattered = "shattered", + Retrieved = "retrieved", + + Unknown = "unknown", +} + +export interface RetrieveMateriaResult extends EventResume { + outcome: RetrieveMateriaOutcome; + + itemId: number; + itemHq: boolean; + materiaId: number; +} diff --git a/src/definitions/event-resume/TalkEvent.ts b/src/definitions/event-resume/TalkEvent.ts new file mode 100644 index 00000000..8562452d --- /dev/null +++ b/src/definitions/event-resume/TalkEvent.ts @@ -0,0 +1,3 @@ +import { EventResume } from "../EventResume"; + +export interface TalkEvent extends EventResume {} diff --git a/src/definitions/result-dialog/MarketTaxRates.ts b/src/definitions/event-resume/talk-event/MarketTaxRates.ts similarity index 59% rename from src/definitions/result-dialog/MarketTaxRates.ts rename to src/definitions/event-resume/talk-event/MarketTaxRates.ts index ee87689c..cde401b3 100644 --- a/src/definitions/result-dialog/MarketTaxRates.ts +++ b/src/definitions/event-resume/talk-event/MarketTaxRates.ts @@ -1,10 +1,11 @@ -export interface MarketTaxRates { - category: number; - limsaLominsa: number; - gridania: number; - uldah: number; - ishgard: number; - kugane: number; - crystarium: number; - oldSharlayan: number; -} +import { TalkEvent } from "../TalkEvent"; + +export interface MarketTaxRates extends TalkEvent { + limsaLominsa: number; + gridania: number; + uldah: number; + ishgard: number; + kugane: number; + crystarium: number; + oldSharlayan: number; +} diff --git a/src/definitions/index.ts b/src/definitions/index.ts index ff7c3a51..d2a6844e 100644 --- a/src/definitions/index.ts +++ b/src/definitions/index.ts @@ -28,7 +28,6 @@ export * from "./ContainerInfo"; export * from "./CraftingLog"; export * from "./CurrencyCrystalInfo"; export * from "./DesynthOrReductionResult"; -export * from "./DesynthResult"; export * from "./EffectResult"; export * from "./EorzeaTimeOffset"; export * from "./EquipDisplayFlags"; @@ -38,6 +37,7 @@ export * from "./EventPlay32"; export * from "./EventPlay4"; export * from "./EventPlay8"; export * from "./EventPlayN"; +export * from "./EventResume"; export * from "./EventStart"; export * from "./FreeCompanyDialog"; export * from "./FreeCompanyInfo"; @@ -67,8 +67,6 @@ export * from "./PlayerStats"; export * from "./PlayTime"; export * from "./Position3"; export * from "./PrepareZoning"; -export * from "./ReductionResult"; -export * from "./ResultDialog"; export * from "./RetainerInformation"; export * from "./ServerNotice"; export * from "./StatusEffectList"; @@ -93,4 +91,8 @@ export * from "./actor-control/SetMountSpeed"; export * from "./actor-control/StatusEffectLose"; export * from "./actor-control/ToggleWeapon"; export * from "./actor-control/UpdateRestedExp"; -export * from "./result-dialog/MarketTaxRates"; +export * from "./event-resume/DesynthResult"; +export * from "./event-resume/ReductionResult"; +export * from "./event-resume/RetrieveMateriaResult"; +export * from "./event-resume/TalkEvent"; +export * from "./event-resume/talk-event/MarketTaxRates"; diff --git a/src/models/EventHandlerType.ts b/src/models/EventHandlerType.ts new file mode 100644 index 00000000..21158a67 --- /dev/null +++ b/src/models/EventHandlerType.ts @@ -0,0 +1,7 @@ +export enum EventHandlerType { + TalkEvent = 0xb0009, + + DesynthesisResult = 0x390000, + RetrieveMateriaResult = 0x390001, + ReductionResult = 0x390002, +} diff --git a/src/models/Message.ts b/src/models/Message.ts index 4fd87489..c841e10f 100644 --- a/src/models/Message.ts +++ b/src/models/Message.ts @@ -23,7 +23,6 @@ import { ClientTrigger } from "../definitions"; import { ContainerInfo } from "../definitions"; import { CraftingLog } from "../definitions"; import { CurrencyCrystalInfo } from "../definitions"; -import { DesynthResult } from "../definitions"; import { EffectResult } from "../definitions"; import { EorzeaTimeOffset } from "../definitions"; import { EquipDisplayFlags } from "../definitions"; @@ -33,6 +32,7 @@ import { EventPlay32 } from "../definitions"; import { EventPlay4 } from "../definitions"; import { EventPlay8 } from "../definitions"; import { EventPlayN } from "../definitions"; +import { EventResume } from "../definitions"; import { EventStart } from "../definitions"; import { FreeCompanyDialog } from "../definitions"; import { FreeCompanyInfo } from "../definitions"; @@ -61,7 +61,6 @@ import { PlayerSpawn } from "../definitions"; import { PlayerStats } from "../definitions"; import { PlayTime } from "../definitions"; import { PrepareZoning } from "../definitions"; -import { ResultDialog } from "../definitions"; import { RetainerInformation } from "../definitions"; import { ServerNotice } from "../definitions"; import { StatusEffectList } from "../definitions"; @@ -85,8 +84,10 @@ import { SetMountSpeed } from "../definitions"; import { StatusEffectLose } from "../definitions"; import { ToggleWeapon } from "../definitions"; import { UpdateRestedExp } from "../definitions"; -import { MarketTaxRates } from "../definitions"; +import { DesynthResult } from "../definitions"; import { ReductionResult } from "../definitions"; +import { RetrieveMateriaResult } from "../definitions"; +import { TalkEvent } from "../definitions"; /** * THIS IS A GENERATED FILE, DO NOT EDIT IT BY HAND. @@ -189,10 +190,6 @@ export interface CurrencyCrystalInfoMessage extends GenericMessage { - type: "desynthResult"; -} - export interface EffectResultMessage extends GenericMessage { type: "effectResult"; } @@ -229,6 +226,10 @@ export interface EventPlayNMessage extends GenericMessage { type: "eventPlayN"; } +export interface EventResumeMessage extends GenericMessage { + type: "eventResume"; +} + export interface EventStartMessage extends GenericMessage { type: "eventStart"; } @@ -341,10 +342,6 @@ export interface PrepareZoningMessage extends GenericMessage { type: "prepareZoning"; } -export interface ResultDialogMessage extends GenericMessage { - type: "resultDialog"; -} - export interface RetainerInformationMessage extends GenericMessage { type: "retainerInformation"; } @@ -536,16 +533,26 @@ export interface ActorControlTargetUpdateRestedExpMessage extends GenericMessage subType: "updateRestedExp"; } -export interface ResultDialogMarketTaxRatesMessage extends GenericMessage { - type: "resultDialog"; - subType: "marketTaxRates"; +export interface EventResumeDesynthResultMessage extends GenericMessage { + type: "eventResume"; + subType: "desynthResult"; } -export interface ResultDialogReductionResultMessage extends GenericMessage { - type: "resultDialog"; +export interface EventResumeReductionResultMessage extends GenericMessage { + type: "eventResume"; subType: "reductionResult"; } +export interface EventResumeRetrieveMateriaResultMessage extends GenericMessage { + type: "eventResume"; + subType: "retrieveMateriaResult"; +} + +export interface EventResumeTalkEventMessage extends GenericMessage { + type: "eventResume"; + subType: "talkEvent"; +} + export type Message = | ActorCastMessage | ActorControlMessage @@ -571,7 +578,6 @@ export type Message = | ContainerInfoMessage | CraftingLogMessage | CurrencyCrystalInfoMessage - | DesynthResultMessage | EffectResultMessage | EorzeaTimeOffsetMessage | EquipDisplayFlagsMessage @@ -581,6 +587,7 @@ export type Message = | EventPlay4Message | EventPlay8Message | EventPlayNMessage + | EventResumeMessage | EventStartMessage | FreeCompanyDialogMessage | FreeCompanyInfoMessage @@ -609,7 +616,6 @@ export type Message = | PlayerStatsMessage | PlayTimeMessage | PrepareZoningMessage - | ResultDialogMessage | RetainerInformationMessage | ServerNoticeMessage | StatusEffectListMessage @@ -651,5 +657,7 @@ export type Message = | ActorControlTargetStatusEffectLoseMessage | ActorControlTargetToggleWeaponMessage | ActorControlTargetUpdateRestedExpMessage - | ResultDialogMarketTaxRatesMessage - | ResultDialogReductionResultMessage; + | EventResumeDesynthResultMessage + | EventResumeReductionResultMessage + | EventResumeRetrieveMateriaResultMessage + | EventResumeTalkEventMessage; diff --git a/src/models/OpcodeList.ts b/src/models/OpcodeList.ts index 374807fd..e9895fe0 100644 --- a/src/models/OpcodeList.ts +++ b/src/models/OpcodeList.ts @@ -5,7 +5,7 @@ export interface OpcodeList { lists: { ServerZoneIpcType: { name: string; - opcode: number; + opcode: number | Array; }[]; ClientZoneIpcType: { name: string; diff --git a/src/models/ResultDialogType.ts b/src/models/ResultDialogType.ts deleted file mode 100644 index 0c00bd2a..00000000 --- a/src/models/ResultDialogType.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum ResultDialogType { - MarketTaxRates = 0xb0009, - ReductionResult = 0x390002, -} diff --git a/src/models/TalkEventType.ts b/src/models/TalkEventType.ts new file mode 100644 index 00000000..37f44eca --- /dev/null +++ b/src/models/TalkEventType.ts @@ -0,0 +1,3 @@ +export enum TalkEventType { + MarketTaxRates = 7, +} diff --git a/src/models/index.ts b/src/models/index.ts index 00a010be..db6f2840 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -4,6 +4,7 @@ export * from "./ActorControlType"; export * from "./ConstantsList"; +export * from "./EventHandlerType"; export * from "./GenericMessage"; export * from "./InventoryOperation"; export * from "./Message"; @@ -11,9 +12,9 @@ export * from "./OpcodeList"; export * from "./Origin"; export * from "./PacketProcessor"; export * from "./Region"; -export * from "./ResultDialogType"; export * from "./Segment"; export * from "./SegmentHeader"; export * from "./SegmentType"; export * from "./SuperPacket"; export * from "./SuperPacketProcessor"; +export * from "./TalkEventType"; diff --git a/src/packet-processors/event-resume-packet-processors.ts b/src/packet-processors/event-resume-packet-processors.ts new file mode 100644 index 00000000..93cb8dba --- /dev/null +++ b/src/packet-processors/event-resume-packet-processors.ts @@ -0,0 +1,19 @@ +import { SuperPacketProcessor } from "../models"; +import { desynthResult } from "./processors/event-resume/desynthResult"; +import { reductionResult } from "./processors/event-resume/reductionResult"; +import { retrieveMateriaResult } from "./processors/event-resume/retrieveMateriaResult"; +import { talkEvent } from "./processors/event-resume/talkEvent"; +import { EventResume } from "../definitions"; + +/** +* THIS IS A GENERATED FILE, DO NOT EDIT IT BY HAND. +* +* To update it, restart the build process. +*/ + +export const eventResumePacketProcessors: Record> = { + desynthResult, + reductionResult, + retrieveMateriaResult, + talkEvent, +}; diff --git a/src/packet-processors/packet-processors.ts b/src/packet-processors/packet-processors.ts index 50ba37ae..1bb09ea4 100644 --- a/src/packet-processors/packet-processors.ts +++ b/src/packet-processors/packet-processors.ts @@ -23,7 +23,6 @@ import { clientTrigger } from "./processors/clientTrigger"; import { containerInfo } from "./processors/containerInfo"; import { craftingLog } from "./processors/craftingLog"; import { currencyCrystalInfo } from "./processors/currencyCrystalInfo"; -import { desynthResult } from "./processors/desynthResult"; import { effectResult } from "./processors/effectResult"; import { eorzeaTimeOffset } from "./processors/eorzeaTimeOffset"; import { equipDisplayFlags } from "./processors/equipDisplayFlags"; @@ -33,6 +32,7 @@ import { eventPlay32 } from "./processors/eventPlay32"; import { eventPlay4 } from "./processors/eventPlay4"; import { eventPlay8 } from "./processors/eventPlay8"; import { eventPlayN } from "./processors/eventPlayN"; +import { eventResume } from "./processors/eventResume"; import { eventStart } from "./processors/eventStart"; import { freeCompanyDialog } from "./processors/freeCompanyDialog"; import { freeCompanyInfo } from "./processors/freeCompanyInfo"; @@ -61,7 +61,6 @@ import { playerSpawn } from "./processors/playerSpawn"; import { playerStats } from "./processors/playerStats"; import { playTime } from "./processors/playTime"; import { prepareZoning } from "./processors/prepareZoning"; -import { resultDialog } from "./processors/resultDialog"; import { retainerInformation } from "./processors/retainerInformation"; import { serverNotice } from "./processors/serverNotice"; import { statusEffectList } from "./processors/statusEffectList"; @@ -108,7 +107,6 @@ export const packetProcessors: Record = { containerInfo, craftingLog, currencyCrystalInfo, - desynthResult, effectResult, eorzeaTimeOffset, equipDisplayFlags, @@ -118,6 +116,7 @@ export const packetProcessors: Record = { eventPlay4, eventPlay8, eventPlayN, + eventResume, eventStart, freeCompanyDialog, freeCompanyInfo, @@ -146,7 +145,6 @@ export const packetProcessors: Record = { playerStats, playTime, prepareZoning, - resultDialog, retainerInformation, serverNotice, statusEffectList, diff --git a/src/packet-processors/processors/desynthResult.ts b/src/packet-processors/processors/desynthResult.ts deleted file mode 100644 index cb8aeb80..00000000 --- a/src/packet-processors/processors/desynthResult.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { BufferReader } from "../../BufferReader"; -import { DesynthResult } from "../../definitions"; - -export function desynthResult(reader: BufferReader): DesynthResult { - const unknown00 = reader.nextUInt32(); - const unknown01 = reader.nextUInt32(); - const itemResult = reader.nextUInt32(); - return { - unknown0: unknown00, - unknown1: unknown01, - itemId: itemResult % 1000000, - itemHq: itemResult > 1000000, - result: Array(3) - .fill(null) - .map(() => { - const itemResult = reader.nextUInt32(); - return { - itemId: itemResult % 1000000, - itemHq: itemResult > 1000000, - itemQuantity: reader.nextUInt32(), - }; - }), - }; -} diff --git a/src/packet-processors/processors/event-resume/desynthResult.ts b/src/packet-processors/processors/event-resume/desynthResult.ts new file mode 100644 index 00000000..76be1f99 --- /dev/null +++ b/src/packet-processors/processors/event-resume/desynthResult.ts @@ -0,0 +1,19 @@ +import { DesynthResult, EventResume } from "../../../definitions"; + +export function desynthResult(packet: EventResume): DesynthResult { + return { + ...packet, + itemId: packet.params[0] % 1000000, + itemHq: packet.params[0] > 1000000, + result: [1, 3, 5].map((index) => { + return { + itemId: packet.params[index] % 1000000, + itemHq: packet.params[index] > 1000000, + itemQuantity: packet.params[index + 1], + }; + }), + unknown: packet.params[7] != 0, + unknown2: packet.params[8], + unknown3: packet.params[9], + }; +} diff --git a/src/packet-processors/processors/event-resume/reductionResult.ts b/src/packet-processors/processors/event-resume/reductionResult.ts new file mode 100644 index 00000000..4825fd7c --- /dev/null +++ b/src/packet-processors/processors/event-resume/reductionResult.ts @@ -0,0 +1,16 @@ +import { ReductionResult, EventResume } from "../../../definitions"; + +export function reductionResult(packet: EventResume): ReductionResult { + return { + ...packet, + itemId: packet.params[0] % 500000, + unknown: packet.params[1] != 0, + result: [2, 4, 6].map((index) => { + return { + itemId: packet.params[index] % 1000000, + itemHq: packet.params[index] > 1000000, + itemQuantity: packet.params[index + 1], + }; + }), + }; +} diff --git a/src/packet-processors/processors/event-resume/retrieveMateriaResult.ts b/src/packet-processors/processors/event-resume/retrieveMateriaResult.ts new file mode 100644 index 00000000..fc750fcf --- /dev/null +++ b/src/packet-processors/processors/event-resume/retrieveMateriaResult.ts @@ -0,0 +1,24 @@ +import { EventResume } from "../../../definitions"; +import { RetrieveMateriaOutcome, RetrieveMateriaResult } from "../../../definitions/event-resume/RetrieveMateriaResult"; + +export function retrieveMateriaResult(packet: EventResume): RetrieveMateriaResult { + let outcome = RetrieveMateriaOutcome.Unknown; + + if (packet.scene == 1 && packet.params[0] == 25) { + outcome = RetrieveMateriaOutcome.InventoryFull; + } else if (packet.scene == 0) { + if (packet.params[0] == 0) { + outcome = RetrieveMateriaOutcome.Shattered; + } else { + outcome = RetrieveMateriaOutcome.Retrieved; + } + } + + return { + ...packet, + outcome: outcome, + itemId: packet.params[1] % 1000000, + itemHq: packet.params[1] > 1000000, + materiaId: packet.params[2], + }; +} diff --git a/src/packet-processors/processors/event-resume/talk-event/marketTaxRates.ts b/src/packet-processors/processors/event-resume/talk-event/marketTaxRates.ts new file mode 100644 index 00000000..f9df505f --- /dev/null +++ b/src/packet-processors/processors/event-resume/talk-event/marketTaxRates.ts @@ -0,0 +1,15 @@ +import { TalkEvent } from "../../../../definitions"; +import { MarketTaxRates } from "../../../../definitions/event-resume/talk-event/MarketTaxRates"; + +export function marketTaxRates(packet: TalkEvent): MarketTaxRates { + return { + ...packet, + limsaLominsa: packet.params[0], + gridania: packet.params[1], + uldah: packet.params[2], + ishgard: packet.params[3], + kugane: packet.params[4], + crystarium: packet.params[5], + oldSharlayan: packet.params[6], + }; +} diff --git a/src/packet-processors/processors/event-resume/talkEvent.ts b/src/packet-processors/processors/event-resume/talkEvent.ts new file mode 100644 index 00000000..ccddefdf --- /dev/null +++ b/src/packet-processors/processors/event-resume/talkEvent.ts @@ -0,0 +1,15 @@ +import { EventResume } from "../../../definitions"; +import { TalkEvent } from "../../../definitions/event-resume/TalkEvent"; +import { TalkEventType } from "../../../models/TalkEventType"; +import { marketTaxRates } from "./talk-event/marketTaxRates"; + +export function talkEvent(packet: EventResume): TalkEvent { + switch (packet.subcategory as TalkEventType) { + case TalkEventType.MarketTaxRates: + return marketTaxRates(packet); + default: + return { + ...packet, + }; + } +} diff --git a/src/packet-processors/processors/eventResume.ts b/src/packet-processors/processors/eventResume.ts new file mode 100644 index 00000000..041fd6f7 --- /dev/null +++ b/src/packet-processors/processors/eventResume.ts @@ -0,0 +1,13 @@ +import { BufferReader } from "../../BufferReader"; +import { EventResume } from "../../definitions"; + +export function eventResume(reader: BufferReader): EventResume { + return { + category: reader.nextUInt32(), + subcategory: reader.nextUInt16(), + scene: reader.nextUInt8(), + params: Array(reader.nextUInt8()) + .fill(null) + .map(() => reader.nextUInt32()), + }; +} diff --git a/src/packet-processors/processors/result-dialog/marketTaxRates.ts b/src/packet-processors/processors/result-dialog/marketTaxRates.ts deleted file mode 100644 index 28e17135..00000000 --- a/src/packet-processors/processors/result-dialog/marketTaxRates.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { MarketTaxRates, ResultDialog } from "../../../definitions"; -import { BufferReader } from "../../../BufferReader"; - -export function marketTaxRates(packet: ResultDialog, reader: BufferReader): MarketTaxRates { - return { - ...packet, - limsaLominsa: reader.skip(0x08).nextUInt32(), - gridania: reader.nextUInt32(), - uldah: reader.nextUInt32(), - ishgard: reader.nextUInt32(), - kugane: reader.nextUInt32(), - crystarium: reader.nextUInt32(), - oldSharlayan: reader.nextUInt32(), - }; -} diff --git a/src/packet-processors/processors/result-dialog/reductionResult.ts b/src/packet-processors/processors/result-dialog/reductionResult.ts deleted file mode 100644 index b6bd69fd..00000000 --- a/src/packet-processors/processors/result-dialog/reductionResult.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { ReductionResult, ResultDialog } from "../../../definitions"; -import { BufferReader } from "../../../BufferReader"; - -export function reductionResult(packet: ResultDialog, reader: BufferReader): ReductionResult { - return { - ...packet, - unknown0: reader.nextUInt32(), - unknown1: reader.nextUInt32(), - itemId: reader.nextUInt32() % 500000, - unknown2: reader.nextUInt32(), - result: Array(3) - .fill(null) - .map(() => { - const itemResult = reader.nextUInt32(); - return { - itemId: itemResult % 1000000, - itemHq: itemResult > 1000000, - itemQuantity: reader.nextUInt32(), - }; - }), - }; -} diff --git a/src/packet-processors/processors/resultDialog.ts b/src/packet-processors/processors/resultDialog.ts deleted file mode 100644 index 0db99959..00000000 --- a/src/packet-processors/processors/resultDialog.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { BufferReader } from "../../BufferReader"; -import { ResultDialog } from "../../definitions"; - -export function resultDialog(reader: BufferReader): ResultDialog { - return { - category: reader.nextUInt32(), - }; -} diff --git a/src/packet-processors/result-dialog-packet-processors.ts b/src/packet-processors/result-dialog-packet-processors.ts deleted file mode 100644 index 3cc7d843..00000000 --- a/src/packet-processors/result-dialog-packet-processors.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { SuperPacketProcessor } from "../models"; -import { marketTaxRates } from "./processors/result-dialog/marketTaxRates"; -import { reductionResult } from "./processors/result-dialog/reductionResult"; -import { ResultDialog } from "../definitions"; - -/** -* THIS IS A GENERATED FILE, DO NOT EDIT IT BY HAND. -* -* To update it, restart the build process. -*/ - -export const resultDialogPacketProcessors: Record> = { - marketTaxRates, - reductionResult, -}; diff --git a/src/pcap-ffxiv.ts b/src/pcap-ffxiv.ts index b6f26e41..3998c3e2 100644 --- a/src/pcap-ffxiv.ts +++ b/src/pcap-ffxiv.ts @@ -1,396 +1,410 @@ -import { EventEmitter } from "events"; -import { - ActorControlType, - ConstantsList, - Message, - OpcodeList, - PacketProcessor, - Region, - ResultDialogType, - SegmentHeader, - SegmentType, - SuperPacket, - SuperPacketProcessor, -} from "./models"; -import { downloadJson } from "./json-downloader"; -import { packetProcessors } from "./packet-processors/packet-processors"; -import { BufferReader } from "./BufferReader"; -import { createServer as createHttpServer, Server } from "http"; -import { ChildProcessWithoutNullStreams, spawn } from "child_process"; -import { join } from "path"; -import { existsSync, readFileSync } from "fs"; -import { actorControlPacketProcessors } from "./packet-processors/actor-control-packet-processors"; -import { resultDialogPacketProcessors } from "./packet-processors/result-dialog-packet-processors"; -import { CaptureInterfaceOptions } from "./capture-interface-options"; -import Heap from "heap-js"; - -interface SegmentQueueEntry { - reader: BufferReader; - index: bigint; -} - -export class CaptureInterface extends EventEmitter { - private _opcodeLists: OpcodeList[] | undefined; - private _constants: Record | undefined; - private readonly _packetDefs: Record; - private readonly _superPacketDefs: Record>>; - private _opcodes: { C: Record; S: Record } = { - C: {}, - S: {}, - }; - private _segmentQueue = new Heap((a, b) => Number(a.index - b.index)); - - private _httpServer: Server | undefined = undefined; - private _monitor: ChildProcessWithoutNullStreams | undefined; - - readonly _options: CaptureInterfaceOptions; - - private expectedPacketIndex = BigInt(0); - - private skippedPackets = 0; - - public get constants(): ConstantsList | undefined { - return this._constants ? this._constants[this._options.region] : undefined; - } - - constructor(options: Partial) { - super(); - - const defaultOptions: CaptureInterfaceOptions = { - region: "Global", - exePath: join(__dirname, "./MachinaWrapper/MachinaWrapper.exe"), - monitorType: "WinPCap", - port: 13346, - filter: () => true, - logger: (payload) => console[payload.type](payload.message), - winePrefix: "$HOME/.Wine", - hasWine: false, - }; - - this._options = { - ...defaultOptions, - ...options, - }; - - if (!existsSync(this._options.exePath)) { - throw new Error(`MachinaWrapper not found in ${this._options.exePath}`); - } - - this._packetDefs = packetProcessors; - this._superPacketDefs = { - actorControl: actorControlPacketProcessors, - actorControlSelf: actorControlPacketProcessors, - actorControlTarget: actorControlPacketProcessors, - resultDialog: resultDialogPacketProcessors, - }; - - this._loadOpcodes().then(async () => { - await this._loadConstants(); - this.emit("ready"); - }); - - process.on("exit", () => { - this.stop().then(() => { - process.exit(); - }); - }); - } - - start(): Promise { - return new Promise((resolve, reject) => { - try { - this.spawnMachina(); - this._httpServer = createHttpServer((req, res) => { - let data: any[] = []; - req.on("data", (chunk) => { - data.push(chunk); - }); - req.on("end", () => { - const segmentBuffer = Buffer.concat(data); - const segmentReader = new BufferReader(segmentBuffer); - const index = segmentReader.skip(1).nextUInt64(); - this._segmentQueue.push({ - index, - reader: segmentReader.reset(), - }); - this._processNextSegment(); - res.writeHead(200); - res.end(); - }); - }).listen(this._options.port, "localhost"); - setTimeout(() => { - this._monitor?.stdin.write("start\n", (err) => { - if (err) { - reject(err); - } else { - resolve(); - } - }); - }, 200); - } catch (e) { - reject(e); - } - }); - } - - private _processNextSegment() { - const peek = this._segmentQueue.peek(); - if (peek && peek.index <= this.expectedPacketIndex) { - const next = this._segmentQueue.pop(); - // This is really just for the compiler because if we're here, there's something to pop. - if (next) { - this._processSegment(next.reader); - this.expectedPacketIndex++; - this.skippedPackets = 0; - this._processNextSegment(); - } - } else { - this.skippedPackets++; - } - // If we skipped more than 10 packets, something isn't right, let's just bump to the next available index - if (this.skippedPackets > 10 && peek) { - this._options.logger({ - type: "warn", - message: `Waited for packet #${this.expectedPacketIndex} for too long, bumping to ${peek.index}.`, - }); - this.expectedPacketIndex = peek.index; - this.skippedPackets = 0; - } - } - - private spawnMachina(): void { - const args: string[] = [ - "--MonitorType", - this._options.monitorType, - "--Region", - this._options.region, - "--Port", - this._options.port.toString(), - ]; - if (this._options.pid) args.push("--ProcessID", this._options.pid.toString()); - - if (this._options.hasWine) { - this._monitor = spawn(`WINEPREFIX="${this._options.winePrefix}" wine ${this._options.exePath}`, args); - } else { - this._monitor = spawn(this._options.exePath, args); - } - - this._monitor?.stderr.on("data", (err: Buffer) => { - this._options.logger({ - type: "error", - message: `MachinaWrapper failed to start: - ${err.toLocaleString()}`, - }); - this.emit("error", new Error(`MachinaWrapper failed to start: ${err.toString("utf8")}`)); - }); - - this._monitor.on("exit", () => { - this.closeHttpServer(); - }); - - this._options.logger({ - type: "info", - message: `MachinaWrapper started with args: ${args.join(" ")}`, - }); - } - - stop(): Promise { - return new Promise(() => { - this._monitor?.stdin.write("kill\n", () => { - return this.closeHttpServer(); - }); - }); - } - - private closeHttpServer(): Promise { - return new Promise((resolve, reject) => { - this._httpServer?.close((err) => { - delete this._httpServer; - if (err) { - reject(err); - } else { - resolve(); - } - }); - }); - } - - setRegion(region: Region) { - this._options.region = region; - this.updateOpcodesCache(); - } - - private static opcodesToRegistry(opcodes: { name: string; opcode: number }[]): Record { - return opcodes.reduce((acc, entry) => { - return { - ...acc, - [entry.opcode]: entry.name, - }; - }, {}) as Record; - } - - updateOpcodesCache(): void { - const regionOpcodes = this._opcodeLists?.find((ol) => ol.region === this._options.region); - this._opcodes = { - C: CaptureInterface.opcodesToRegistry(regionOpcodes?.lists.ClientZoneIpcType || []), - S: CaptureInterface.opcodesToRegistry(regionOpcodes?.lists.ServerZoneIpcType || []), - }; - } - - private async _fetchFFXIVOpcodes(file: string) { - const { localOpcodesPath, localDataPath } = this._options; - const localPath = localDataPath || localOpcodesPath; - if (localPath) { - try { - const content = readFileSync(join(localPath, file), "utf-8"); - - this._options.logger({ - type: "info", - message: `Loading ${file} from ${localPath}`, - }); - - return JSON.parse(content); - } catch (e) {} - } - - this._options.logger({ - type: "info", - message: `Loading ${file} from ${this._options.region === "CN" ? "ffcafe" : "github"}`, - }); - - const baseUrl = - this._options.region === "CN" - ? "https://opcodes.xivcdn.com/" - : "https://raw.githubusercontent.com/karashiiro/FFXIVOpcodes/master/"; - - return downloadJson(`${baseUrl}${file}`); - } - - private async _loadOpcodes() { - this._opcodeLists = await this._fetchFFXIVOpcodes("opcodes.min.json"); - this.updateOpcodesCache(); - } - - private async _loadConstants() { - this._constants = await this._fetchFFXIVOpcodes("constants.min.json"); - } - - private _processSuperPacket( - typeName: string, - message: Message, - reader: BufferReader, - ): Message { - let subTypesEnum: Record; - // Let's get the corresponding enum - switch (typeName) { - case "actorControl": - case "actorControlSelf": - case "actorControlTarget": - subTypesEnum = ActorControlType; - break; - case "resultDialog": - subTypesEnum = ResultDialogType; - break; - default: - this._options.logger({ - type: "error", - message: `Got super packet of type ${typeName} with super processors but no type enum.`, - }); - return message; - } - - let subTypeName = subTypesEnum[(message.parsedIpcData as SuperPacket).category] as string; - if (!subTypeName) { - message.subType = `unknown${(message.parsedIpcData as SuperPacket).category}`; - // Unknown subtype, return packet as-is - return message; - } - - subTypeName = subTypeName[0].toLowerCase() + subTypeName.slice(1); - message.subType = subTypeName; - const superProcessor = this._superPacketDefs[typeName][subTypeName]; - if (!superProcessor) { - // No processor for this sub packet, return packet as-is - return message; - } - - message.parsedIpcData = superProcessor( - message.parsedIpcData, - reader, - this.constants as ConstantsList, - this._options.region, - ); - return message; - } - - private _getOriginKey(originIndex: number): null | "C" | "S" { - return [null, "C", "S"][originIndex] as null | "C" | "S"; - } - - private _getOrigin(originKey: "C" | "S"): "send" | "receive" { - return { C: "send", S: "receive" }[originKey] as "send" | "receive"; - } - - private _processSegment(dataReader: BufferReader): void { - const originIndex = dataReader.nextUInt8(); - const origin = this._getOriginKey(originIndex); - if (!origin) { - this._options.logger({ - type: "error", - message: `Received a packet with origin ${originIndex}, should be 1 or 2.`, - }); - return; - } - const reader = dataReader.skip(8).restAsBuffer(true); - const header: SegmentHeader = { - size: reader.nextUInt32(), - sourceActor: reader.nextUInt32(), - targetActor: reader.nextUInt32(), - segmentType: reader.nextUInt16(), - padding: reader.nextUInt16(), - operation: this._getOrigin(origin), - }; - if (header.segmentType === SegmentType.Ipc) { - const opcode = reader.skip(2).nextUInt16(); - const ipcData = reader.skip(12).restAsBuffer(); - let typeName = this._opcodes[origin][opcode] || "unknown"; - typeName = typeName[0].toLowerCase() + typeName.slice(1); - - let message: Message = { - header, - opcode, - type: typeName as any, - data: Buffer.from(dataReader.buffer), - }; - - if (this._options.filter(header, typeName)) { - // Unmarshal the data, if possible. - if (this._packetDefs[typeName] && this._constants) { - const processorName: keyof typeof packetProcessors = typeName; - const ipcDataReader = new BufferReader(ipcData); - const processor = this._packetDefs[processorName]; - message.parsedIpcData = processor(ipcDataReader, this._constants[this._options.region], this._options.region); - - // If this is a super packet - if (this._superPacketDefs[typeName]) { - message = this._processSuperPacket(typeName, message, ipcDataReader.reset()); - } - } - - this.emit("message", message); - } - } - } -} - -export interface CaptureInterfaceEvents { - ready: () => void; - error: (err: Error) => void; - message: (message: Message) => void; -} - -export declare interface CaptureInterface { - on(event: U, listener: CaptureInterfaceEvents[U]): this; - - emit(event: U, ...args: Parameters): boolean; -} +import { EventEmitter } from "events"; +import { + ActorControlType, + ConstantsList, + Message, + OpcodeList, + PacketProcessor, + Region, + EventHandlerType, + SegmentHeader, + SegmentType, + SuperPacket, + SuperPacketProcessor, +} from "./models"; +import { downloadJson } from "./json-downloader"; +import { packetProcessors } from "./packet-processors/packet-processors"; +import { BufferReader } from "./BufferReader"; +import { createServer as createHttpServer, Server } from "http"; +import { ChildProcessWithoutNullStreams, spawn } from "child_process"; +import { join } from "path"; +import { existsSync, readFileSync } from "fs"; +import { actorControlPacketProcessors } from "./packet-processors/actor-control-packet-processors"; +import { eventResumePacketProcessors } from "./packet-processors/event-resume-packet-processors"; +import { CaptureInterfaceOptions } from "./capture-interface-options"; +import Heap from "heap-js"; + +interface SegmentQueueEntry { + reader: BufferReader; + index: bigint; +} + +export class CaptureInterface extends EventEmitter { + private _opcodeLists: OpcodeList[] | undefined; + private _constants: Record | undefined; + private readonly _packetDefs: Record; + private readonly _superPacketDefs: Record>>; + private _opcodes: { C: Record; S: Record } = { + C: {}, + S: {}, + }; + private _segmentQueue = new Heap((a, b) => Number(a.index - b.index)); + + private _httpServer: Server | undefined = undefined; + private _monitor: ChildProcessWithoutNullStreams | undefined; + + readonly _options: CaptureInterfaceOptions; + + private expectedPacketIndex = BigInt(0); + + private skippedPackets = 0; + + public get constants(): ConstantsList | undefined { + return this._constants ? this._constants[this._options.region] : undefined; + } + + constructor(options: Partial) { + super(); + + const defaultOptions: CaptureInterfaceOptions = { + region: "Global", + exePath: join(__dirname, "./MachinaWrapper/MachinaWrapper.exe"), + monitorType: "WinPCap", + port: 13346, + filter: () => true, + logger: (payload) => console[payload.type](payload.message), + winePrefix: "$HOME/.Wine", + hasWine: false, + }; + + this._options = { + ...defaultOptions, + ...options, + }; + + if (!existsSync(this._options.exePath)) { + throw new Error(`MachinaWrapper not found in ${this._options.exePath}`); + } + + this._packetDefs = packetProcessors; + this._superPacketDefs = { + actorControl: actorControlPacketProcessors, + actorControlSelf: actorControlPacketProcessors, + actorControlTarget: actorControlPacketProcessors, + eventResume: eventResumePacketProcessors, + }; + + this._loadOpcodes().then(async () => { + await this._loadConstants(); + this.emit("ready"); + }); + + process.on("exit", () => { + this.stop().then(() => { + process.exit(); + }); + }); + } + + start(): Promise { + return new Promise((resolve, reject) => { + try { + this.spawnMachina(); + this._httpServer = createHttpServer((req, res) => { + let data: any[] = []; + req.on("data", (chunk) => { + data.push(chunk); + }); + req.on("end", () => { + const segmentBuffer = Buffer.concat(data); + const segmentReader = new BufferReader(segmentBuffer); + const index = segmentReader.skip(1).nextUInt64(); + this._segmentQueue.push({ + index, + reader: segmentReader.reset(), + }); + this._processNextSegment(); + res.writeHead(200); + res.end(); + }); + }).listen(this._options.port, "localhost"); + setTimeout(() => { + this._monitor?.stdin.write("start\n", (err) => { + if (err) { + reject(err); + } else { + resolve(); + } + }); + }, 200); + } catch (e) { + reject(e); + } + }); + } + + private _processNextSegment() { + const peek = this._segmentQueue.peek(); + if (peek && peek.index <= this.expectedPacketIndex) { + const next = this._segmentQueue.pop(); + // This is really just for the compiler because if we're here, there's something to pop. + if (next) { + this._processSegment(next.reader); + this.expectedPacketIndex++; + this.skippedPackets = 0; + this._processNextSegment(); + } + } else { + this.skippedPackets++; + } + // If we skipped more than 10 packets, something isn't right, let's just bump to the next available index + if (this.skippedPackets > 10 && peek) { + this._options.logger({ + type: "warn", + message: `Waited for packet #${this.expectedPacketIndex} for too long, bumping to ${peek.index}.`, + }); + this.expectedPacketIndex = peek.index; + this.skippedPackets = 0; + } + } + + private spawnMachina(): void { + const args: string[] = [ + "--MonitorType", + this._options.monitorType, + "--Region", + this._options.region, + "--Port", + this._options.port.toString(), + ]; + if (this._options.pid) args.push("--ProcessID", this._options.pid.toString()); + + if (this._options.hasWine) { + this._monitor = spawn(`WINEPREFIX="${this._options.winePrefix}" wine ${this._options.exePath}`, args); + } else { + this._monitor = spawn(this._options.exePath, args); + } + + this._monitor?.stderr.on("data", (err: Buffer) => { + this._options.logger({ + type: "error", + message: `MachinaWrapper failed to start: + ${err.toLocaleString()}`, + }); + this.emit("error", new Error(`MachinaWrapper failed to start: ${err.toString("utf8")}`)); + }); + + this._monitor.on("exit", () => { + this.closeHttpServer(); + }); + + this._options.logger({ + type: "info", + message: `MachinaWrapper started with args: ${args.join(" ")}`, + }); + } + + stop(): Promise { + return new Promise(() => { + this._monitor?.stdin.write("kill\n", () => { + return this.closeHttpServer(); + }); + }); + } + + private closeHttpServer(): Promise { + return new Promise((resolve, reject) => { + this._httpServer?.close((err) => { + delete this._httpServer; + if (err) { + reject(err); + } else { + resolve(); + } + }); + }); + } + + setRegion(region: Region) { + this._options.region = region; + this.updateOpcodesCache(); + } + + private static opcodesToRegistry( + opcodes: { name: string; opcode: number | Array }[], + ): Record { + return opcodes.reduce((acc, entry) => { + if (typeof entry.opcode === "number") { + return { + ...acc, + [entry.opcode]: entry.name, + }; + } else { + return { + ...acc, + ...entry.opcode.reduce((accOp, op) => { + return { + ...accOp, + [op]: entry.name, + }; + }, {}), + }; + } + }, {}) as Record; + } + + updateOpcodesCache(): void { + const regionOpcodes = this._opcodeLists?.find((ol) => ol.region === this._options.region); + this._opcodes = { + C: CaptureInterface.opcodesToRegistry(regionOpcodes?.lists.ClientZoneIpcType || []), + S: CaptureInterface.opcodesToRegistry(regionOpcodes?.lists.ServerZoneIpcType || []), + }; + } + + private async _fetchFFXIVOpcodes(file: string) { + const { localOpcodesPath, localDataPath } = this._options; + const localPath = localDataPath || localOpcodesPath; + if (localPath) { + try { + const content = readFileSync(join(localPath, file), "utf-8"); + + this._options.logger({ + type: "info", + message: `Loading ${file} from ${localPath}`, + }); + + return JSON.parse(content); + } catch (e) {} + } + + this._options.logger({ + type: "info", + message: `Loading ${file} from ${this._options.region === "CN" ? "ffcafe" : "github"}`, + }); + + const baseUrl = + this._options.region === "CN" + ? "https://opcodes.xivcdn.com/" + : "https://raw.githubusercontent.com/karashiiro/FFXIVOpcodes/master/"; + + return downloadJson(`${baseUrl}${file}`); + } + + private async _loadOpcodes() { + this._opcodeLists = await this._fetchFFXIVOpcodes("opcodes.min.json"); + this.updateOpcodesCache(); + } + + private async _loadConstants() { + this._constants = await this._fetchFFXIVOpcodes("constants.min.json"); + } + + private _processSuperPacket( + typeName: string, + message: Message, + reader: BufferReader, + ): Message { + let subTypesEnum: Record; + // Let's get the corresponding enum + switch (typeName) { + case "actorControl": + case "actorControlSelf": + case "actorControlTarget": + subTypesEnum = ActorControlType; + break; + case "eventResume": + subTypesEnum = EventHandlerType; + break; + default: + this._options.logger({ + type: "error", + message: `Got super packet of type ${typeName} with super processors but no type enum.`, + }); + return message; + } + + let subTypeName = subTypesEnum[(message.parsedIpcData as SuperPacket).category] as string; + if (!subTypeName) { + message.subType = `unknown${(message.parsedIpcData as SuperPacket).category}`; + // Unknown subtype, return packet as-is + return message; + } + + subTypeName = subTypeName[0].toLowerCase() + subTypeName.slice(1); + message.subType = subTypeName; + const superProcessor = this._superPacketDefs[typeName][subTypeName]; + if (!superProcessor) { + // No processor for this sub packet, return packet as-is + return message; + } + + message.parsedIpcData = superProcessor( + message.parsedIpcData, + reader, + this.constants as ConstantsList, + this._options.region, + ); + return message; + } + + private _getOriginKey(originIndex: number): null | "C" | "S" { + return [null, "C", "S"][originIndex] as null | "C" | "S"; + } + + private _getOrigin(originKey: "C" | "S"): "send" | "receive" { + return { C: "send", S: "receive" }[originKey] as "send" | "receive"; + } + + private _processSegment(dataReader: BufferReader): void { + const originIndex = dataReader.nextUInt8(); + const origin = this._getOriginKey(originIndex); + if (!origin) { + this._options.logger({ + type: "error", + message: `Received a packet with origin ${originIndex}, should be 1 or 2.`, + }); + return; + } + const reader = dataReader.skip(8).restAsBuffer(true); + const header: SegmentHeader = { + size: reader.nextUInt32(), + sourceActor: reader.nextUInt32(), + targetActor: reader.nextUInt32(), + segmentType: reader.nextUInt16(), + padding: reader.nextUInt16(), + operation: this._getOrigin(origin), + }; + if (header.segmentType === SegmentType.Ipc) { + const opcode = reader.skip(2).nextUInt16(); + const ipcData = reader.skip(12).restAsBuffer(); + let typeName = this._opcodes[origin][opcode] || "unknown"; + typeName = typeName[0].toLowerCase() + typeName.slice(1); + + let message: Message = { + header, + opcode, + type: typeName as any, + data: Buffer.from(dataReader.buffer), + }; + + if (this._options.filter(header, typeName)) { + // Unmarshal the data, if possible. + if (this._packetDefs[typeName] && this._constants) { + const processorName: keyof typeof packetProcessors = typeName; + const ipcDataReader = new BufferReader(ipcData); + const processor = this._packetDefs[processorName]; + message.parsedIpcData = processor(ipcDataReader, this._constants[this._options.region], this._options.region); + + // If this is a super packet + if (this._superPacketDefs[typeName]) { + message = this._processSuperPacket(typeName, message, ipcDataReader.reset()); + } + } + + this.emit("message", message); + } + } + } +} + +export interface CaptureInterfaceEvents { + ready: () => void; + error: (err: Error) => void; + message: (message: Message) => void; +} + +export declare interface CaptureInterface { + on(event: U, listener: CaptureInterfaceEvents[U]): this; + + emit(event: U, ...args: Parameters): boolean; +}