mirror of
https://github.com/Kvan7/Exiled-Exchange-2.git
synced 2026-09-28 07:35:40 +00:00
log watcher
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
const dict = {
|
||||
'channel.system': /^: (?<body>.+)$/,
|
||||
'channel.trade': /^\$(?:<(?<guild_tag>.+?)> )?(?<char_name>.+?): (?<body>.+)$/,
|
||||
'channel.global': /^#(?:<(?<guild_tag>.+?)> )?(?<char_name>.+?): (?<body>.+)$/,
|
||||
'channel.party': /^%(?:<(?<guild_tag>.+?)> )?(?<char_name>.+?): (?<body>.+)$/,
|
||||
'channel.guild': /^&(?:<(?<guild_tag>.+?)> )?(?<char_name>.+?): (?<body>.+)$/,
|
||||
'channel.whisper.to': /^@To (?<char_name>.+?): (?<body>.+)$/,
|
||||
'channel.whisper.from': /^@From (?:<(?<guild_tag>.+?)> )?(?<char_name>.+?): (?<body>.+)$/,
|
||||
'trade.gem': /^level (?<gem_lvl>\d+) (?<gem_qual>\d+)% (?<gem_name>.+)$/
|
||||
}
|
||||
|
||||
export default dict
|
||||
|
||||
export type ClientLogDict = typeof dict
|
||||
@@ -1,10 +1,12 @@
|
||||
import type { TranslationDict } from '@/assets/data/en/client_strings'
|
||||
import type { ClientLogDict } from '@/assets/data/en/client_log'
|
||||
import type { BaseType, DropEntry, Stat, StatMatcher, UniqueItem } from './interfaces'
|
||||
import { Config } from '@/web/Config'
|
||||
|
||||
export * from './interfaces'
|
||||
|
||||
export let CLIENT_STRINGS: TranslationDict
|
||||
export let CLIENTLOG_STRINGS: ClientLogDict
|
||||
export let ITEM_NAME_REF_BY_TRANSLATED: Map<string | undefined, string>
|
||||
export let TRANSLATED_ITEM_NAME_BY_REF: Map<string | undefined, string>
|
||||
|
||||
@@ -28,6 +30,7 @@ export const ITEM_DROP = new Map<string, DropEntry>()
|
||||
;(async function initData () { /* eslint-disable no-lone-blocks */
|
||||
{
|
||||
CLIENT_STRINGS = (require(`./${Config.store.language}/client_strings`).default)
|
||||
CLIENTLOG_STRINGS = (require(`./${Config.store.language}/client_log`).default)
|
||||
|
||||
const itemNames: Array<[string, string]> = (require(`./${Config.store.language}/item-names.json`))
|
||||
TRANSLATED_ITEM_NAME_BY_REF = new Map(itemNames)
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { ClientLogDict } from '../en/client_log'
|
||||
|
||||
const dict: ClientLogDict = {
|
||||
'channel.system': /^: (?<body>.+)$/,
|
||||
'channel.trade': /^\$(?:<(?<guild_tag>.+?)> )?(?<char_name>.+?): (?<body>.+)$/,
|
||||
'channel.global': /^#(?:<(?<guild_tag>.+?)> )?(?<char_name>.+?): (?<body>.+)$/,
|
||||
'channel.party': /^%(?:<(?<guild_tag>.+?)> )?(?<char_name>.+?): (?<body>.+)$/,
|
||||
'channel.guild': /^&(?:<(?<guild_tag>.+?)> )?(?<char_name>.+?): (?<body>.+)$/,
|
||||
'channel.whisper.to': /^@Кому (?<char_name>.+?): (?<body>.+)$/,
|
||||
'channel.whisper.from': /^@От кого (?:<(?<guild_tag>.+?)> )?(?<char_name>.+?): (?<body>.+)$/,
|
||||
'trade.gem': /^уровень (?<gem_lvl>\d+) (?<gem_qual>\d+)% (?<gem_name>.+)$/
|
||||
}
|
||||
|
||||
export default dict
|
||||
@@ -17,6 +17,7 @@ import { createOverlayWindow } from './main/overlay-window'
|
||||
import { setupAltVisibility } from './main/alt-visibility'
|
||||
import { setupBuiltinBrowser } from './main/builtin-browser'
|
||||
import { createFileProtocol } from './main/app-file-protocol'
|
||||
import { LogWatcher } from './main/LogWatcher'
|
||||
const isDevelopment = process.env.NODE_ENV !== 'production'
|
||||
|
||||
if (!app.requestSingleInstanceLock()) {
|
||||
@@ -70,6 +71,7 @@ app.on('ready', async () => {
|
||||
setupCfProtection()
|
||||
setupShortcuts()
|
||||
setupAltVisibility()
|
||||
LogWatcher.start()
|
||||
},
|
||||
// fixes(linux): window is black instead of transparent
|
||||
process.platform === 'linux' ? 1000 : 0
|
||||
|
||||
@@ -66,3 +66,8 @@ export const OPEN_WIKI = 'MAIN->OVERLAY::open-wiki'
|
||||
export const IMPORT_FILE = 'OVERLAY->MAIN::import-file'
|
||||
|
||||
export const TOGGLE_DELVE_GRID = 'MAIN->OVERLAY::delve-grid'
|
||||
|
||||
export const CLIENT_LOG_UPDATE = 'MAIN->OVERLAY::client-log'
|
||||
export interface IpcClientLog {
|
||||
lines: string[]
|
||||
}
|
||||
|
||||
@@ -59,6 +59,14 @@ class MainProcessBinding extends EventTarget {
|
||||
electron.ipcRenderer.on(ipcEvent.TOGGLE_DELVE_GRID, (e) => {
|
||||
this.dispatchEvent(new CustomEvent(ipcEvent.TOGGLE_DELVE_GRID))
|
||||
})
|
||||
|
||||
electron.ipcRenderer.on(ipcEvent.CLIENT_LOG_UPDATE, (e, data: ipcEvent.IpcClientLog) => {
|
||||
data.lines.forEach(line => {
|
||||
this.dispatchEvent(new CustomEvent(ipcEvent.CLIENT_LOG_UPDATE, {
|
||||
detail: line
|
||||
}))
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import { promises as fs, watchFile, unwatchFile } from 'fs'
|
||||
import { overlayWindow } from './overlay-window'
|
||||
import * as ipc from '@/ipc/ipc-event'
|
||||
import { config } from './config'
|
||||
import { logger } from './logger'
|
||||
|
||||
const COMMON_PATH = [
|
||||
'C:/Program Files (x86)/Grinding Gear Games/Path of Exile/logs/Client.txt',
|
||||
'D:/Program Files (x86)/Grinding Gear Games/Path of Exile/logs/Client.txt',
|
||||
'C:/Program Files (x86)/Steam/steamapps/common/Path of Exile/logs/Client.txt',
|
||||
'D:/Program Files (x86)/Steam/steamapps/common/Path of Exile/logs/Client.txt'
|
||||
]
|
||||
|
||||
export class LogWatcher {
|
||||
static offset = 0
|
||||
static filePath?: string
|
||||
static file?: fs.FileHandle
|
||||
static isReading = false
|
||||
static readBuff = Buffer.allocUnsafe(64 * 1024)
|
||||
|
||||
static async start () {
|
||||
let logFile = config.get('clientLog')
|
||||
|
||||
if (!logFile && process.platform === 'win32') {
|
||||
for (const filePath of COMMON_PATH) {
|
||||
try {
|
||||
await fs.access(filePath)
|
||||
config.set('clientLog', filePath)
|
||||
logFile = filePath
|
||||
break
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
if (logFile) {
|
||||
try {
|
||||
await LogWatcher.watch(logFile)
|
||||
} catch {
|
||||
logger.error('Failed to watch', { source: 'log-watcher', file: logFile })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static async watch (path: string) {
|
||||
if (this.file) {
|
||||
unwatchFile(this.filePath!)
|
||||
await this.file.close()
|
||||
this.file = undefined
|
||||
this.isReading = false
|
||||
}
|
||||
|
||||
watchFile(path, { interval: 450 }, () => {
|
||||
this.handleFileChange()
|
||||
})
|
||||
this.filePath = path
|
||||
|
||||
this.file = await fs.open(path, 'r')
|
||||
const stats = await this.file.stat()
|
||||
this.offset = stats.size
|
||||
}
|
||||
|
||||
static handleFileChange () {
|
||||
if (!this.isReading) {
|
||||
this.isReading = true
|
||||
this.readToEOF()
|
||||
}
|
||||
}
|
||||
|
||||
static async readToEOF () {
|
||||
if (!this.file) {
|
||||
this.isReading = false
|
||||
return
|
||||
}
|
||||
|
||||
const { bytesRead } = await this.file.read(this.readBuff, 0, this.readBuff.length, this.offset)
|
||||
|
||||
if (bytesRead) {
|
||||
const str = this.readBuff.toString('utf8', 0, bytesRead)
|
||||
const lines = str.split('\n').map(line => line.trim()).filter(line => line.length)
|
||||
overlayWindow!.webContents.send(ipc.CLIENT_LOG_UPDATE, { lines } as ipc.IpcClientLog)
|
||||
}
|
||||
|
||||
if (bytesRead) {
|
||||
this.offset += bytesRead
|
||||
this.readToEOF()
|
||||
} else {
|
||||
this.isReading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
+16
-12
@@ -6,19 +6,16 @@ import { forbidden, forbiddenCtrl } from '@/ipc/KeyToCode'
|
||||
import { GET_CONFIG, PUSH_CONFIG, CLOSE_SETTINGS_WINDOW } from '@/ipc/ipc-event'
|
||||
import { overlayWindow } from './overlay-window'
|
||||
import { logger } from './logger'
|
||||
import { LogWatcher } from './LogWatcher'
|
||||
|
||||
export function setupConfigEvents () {
|
||||
ipcMain.on(GET_CONFIG, (e) => {
|
||||
e.returnValue = config.store
|
||||
})
|
||||
ipcMain.on(PUSH_CONFIG, (e, cfg) => {
|
||||
const old = config.store
|
||||
Object.setPrototypeOf(old, Object.prototype)
|
||||
if (!isDeepEq(cfg, old)) {
|
||||
batchUpdateConfig(cfg, false)
|
||||
}
|
||||
ipcMain.on(PUSH_CONFIG, (e, cfg: Config) => {
|
||||
batchUpdateConfig(cfg, false)
|
||||
})
|
||||
ipcMain.on(CLOSE_SETTINGS_WINDOW, (e, cfg) => {
|
||||
ipcMain.on(CLOSE_SETTINGS_WINDOW, (e, cfg: Config | undefined) => {
|
||||
if (cfg != null) {
|
||||
batchUpdateConfig(cfg, true)
|
||||
}
|
||||
@@ -84,10 +81,17 @@ export const config = (() => {
|
||||
return store
|
||||
})()
|
||||
|
||||
export function batchUpdateConfig (upd: Config, push = true) {
|
||||
config.store = upd
|
||||
logger.verbose('Saved', { source: 'config', push })
|
||||
if (push) {
|
||||
overlayWindow!.webContents.send(PUSH_CONFIG, upd)
|
||||
export function batchUpdateConfig (newCfg: Config, push = true) {
|
||||
const oldCfg = config.store
|
||||
Object.setPrototypeOf(oldCfg, Object.prototype)
|
||||
if (!isDeepEq(newCfg, oldCfg)) {
|
||||
config.store = newCfg
|
||||
logger.verbose('Saved', { source: 'config', push })
|
||||
if (push) {
|
||||
overlayWindow!.webContents.send(PUSH_CONFIG, newCfg)
|
||||
}
|
||||
if (oldCfg.clientLog !== newCfg.clientLog) {
|
||||
LogWatcher.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
/* eslint-disable camelcase */
|
||||
import { CLIENTLOG_STRINGS as _$, ITEM_NAME_REF_BY_TRANSLATED, TRADE_TAGS } from '@/assets/data'
|
||||
import { Config } from '@/web/Config'
|
||||
|
||||
const enum MessageChannel {
|
||||
SYSTEM = 'system',
|
||||
TRADE = 'trade',
|
||||
GLOBAL = 'global',
|
||||
PARTY = 'party',
|
||||
GUILD = 'guild',
|
||||
WHISPER_FROM = 'whisper.from',
|
||||
WHISPER_TO = 'whisper.to'
|
||||
}
|
||||
|
||||
interface LogEntry {
|
||||
сhannel: MessageChannel
|
||||
guildTag?: string
|
||||
charName?: string
|
||||
body: string
|
||||
trade?: {
|
||||
item: {
|
||||
name: string
|
||||
amount?: number
|
||||
gem?: { level: number, quality: number }
|
||||
}
|
||||
price: {
|
||||
name: string
|
||||
amount: number
|
||||
}
|
||||
tab?: {
|
||||
name: string
|
||||
left: number
|
||||
top: number
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function handleLine (line: string) {
|
||||
const text = line.split('] ').slice(1).join('] ')
|
||||
if (!text) return
|
||||
|
||||
const entry = {} as LogEntry
|
||||
let match: RegExpMatchArray | null
|
||||
if ((match = text.match(_$['channel.global']))) {
|
||||
entry.сhannel = MessageChannel.GLOBAL
|
||||
} else if ((match = text.match(_$['channel.trade']))) {
|
||||
entry.сhannel = MessageChannel.TRADE
|
||||
} else if ((match = text.match(_$['channel.system']))) {
|
||||
entry.сhannel = MessageChannel.SYSTEM
|
||||
} else if ((match = text.match(_$['channel.whisper.to']))) {
|
||||
entry.сhannel = MessageChannel.WHISPER_TO
|
||||
} else if ((match = text.match(_$['channel.whisper.from']))) {
|
||||
entry.сhannel = MessageChannel.WHISPER_FROM
|
||||
} else if ((match = text.match(_$['channel.party']))) {
|
||||
entry.сhannel = MessageChannel.PARTY
|
||||
} else if ((match = text.match(_$['channel.guild']))) {
|
||||
entry.сhannel = MessageChannel.GUILD
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
entry.guildTag = match.groups!.guild_tag
|
||||
entry.charName = match.groups!.char_name
|
||||
entry.body = match.groups!.body
|
||||
|
||||
if (entry.сhannel === MessageChannel.WHISPER_FROM) {
|
||||
if ((match = entry.body.match(TRADE_WHISPER[Config.store.language]))) {
|
||||
const [pAmount, pTag] = [
|
||||
match.groups!.price.split(' ', 1).toString(),
|
||||
match.groups!.price.split(' ').slice(1).join(' ')
|
||||
]
|
||||
const gemMatch = match.groups!.item.match(_$['trade.gem'])
|
||||
if (gemMatch) {
|
||||
match.groups!.item = gemMatch.groups!.gem_name
|
||||
}
|
||||
|
||||
entry.trade = {
|
||||
item: {
|
||||
name: ITEM_NAME_REF_BY_TRANSLATED.get(match.groups!.item) ||
|
||||
match.groups!.item
|
||||
},
|
||||
price: {
|
||||
amount: Number(pAmount),
|
||||
name: TRADE_TAGS.find(pair => pair[1] === pTag)?.[0] || pTag
|
||||
},
|
||||
tab: {
|
||||
name: match.groups!.tab_name,
|
||||
top: Number(match.groups!.tab_top),
|
||||
left: Number(match.groups!.tab_left)
|
||||
}
|
||||
}
|
||||
|
||||
if (gemMatch) {
|
||||
entry.trade.item.gem = {
|
||||
level: Number(gemMatch.groups!.gem_lvl),
|
||||
quality: Number(gemMatch.groups!.gem_qual)
|
||||
}
|
||||
}
|
||||
|
||||
console.log(entry)
|
||||
} else if ((match = entry.body.match(TRADE_BULK_WHISPER[Config.store.language]))) {
|
||||
const [pAmount, pName] = [
|
||||
match.groups!.price.split(' ', 1).toString(),
|
||||
match.groups!.price.split(' ').slice(1).join(' ')
|
||||
]
|
||||
const [iAmount, iName] = [
|
||||
match.groups!.item.split(' ', 1).toString(),
|
||||
match.groups!.item.split(' ').slice(1).join(' ')
|
||||
]
|
||||
|
||||
entry.trade = {
|
||||
item: {
|
||||
amount: Number(iAmount),
|
||||
name: ITEM_NAME_REF_BY_TRANSLATED.get(iName) || iName
|
||||
},
|
||||
price: {
|
||||
amount: Number(pAmount),
|
||||
name: ITEM_NAME_REF_BY_TRANSLATED.get(pName) || pName
|
||||
}
|
||||
}
|
||||
|
||||
console.log(entry)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const TRADE_WHISPER = {
|
||||
en: /^Hi, I would like to buy your (?<item>.+) listed for (?<price>.+) in (?<league>.+) \(stash tab "(?<tab_name>.*)"; position: left (?<tab_left>\d+), top (?<tab_top>\d+)\)(?<message>.+)?$/,
|
||||
ru: /^Здравствуйте, хочу купить у вас (?<item>.+) за (?<price>.+) в лиге (?<league>.+) \(секция "(?<tab_name>.*)"; позиция: (?<tab_left>\d+) столбец, (?<tab_top>\d+) ряд\)(?<message>.+)?$/,
|
||||
ko: /^안녕하세요, (?<league>.+)\(보관함 탭 "(?<tab_name>.*)", 위치: 왼쪽 (?<tab_left>\d+), 상단 (?<tab_top>\d+)\)에 (?<price>.+)\(으\)로 올려놓은 (?<item>.+)\(을\)를 구매하고 싶습니다(?<message>.+)?$/,
|
||||
de: /^Hi, ich möchte '(?<item>.+)' zum angebotenen Preis von (?<price>.+) in der (?<league>.+)-Liga kaufen \(Truhenfach "(?<tab_name>.*)"; Position: (?<tab_left>\d+) von links, (?<tab_top>\d+) von oben\)(?<message>.+)?$/,
|
||||
fr: /^Bonjour, je souhaiterais t'acheter (?<item>.+) pour (?<price>.+) dans la ligue (?<league>.+) \(onglet de réserve "(?<tab_name>.*)" ; (?<tab_left>\d+)e en partant de la gauche, (?<tab_top>\d+)e en partant du haut\)(?<message>.+)?$/,
|
||||
es: /^Hola, quisiera comprar tu (?<item>.+) listado por (?<price>.+) en (?<league>.+) \(pestaña de alijo "(?<tab_name>.*)"; posición: izquierda(?<tab_left>\d+), arriba (?<tab_top>\d+)\)(?<message>.+)?$/,
|
||||
pt: /^Olá, eu gostaria de comprar o seu item (?<item>.+) listado por (?<price>.+) na (?<league>.+) \(aba do baú: "(?<tab_name>.*)"; posição: esquerda (?<tab_left>\d+), topo (?<tab_top>\d+)\)(?<message>.+)?$/,
|
||||
th: /^สวัสดี, เราต้องการจะชื้อของคุณ (?<item>.+) ใน ราคา (?<price>.+) ใน (?<league>.+) \(stash tab "(?<tab_name>.*)"; ตำแหน่ง: ซ้าย (?<tab_left>\d+), บน (?<tab_top>\d+)\)(?<message>.+)?$/
|
||||
}
|
||||
|
||||
const TRADE_BULK_WHISPER = {
|
||||
en: /^Hi, I'd like to buy your (?<item>.+) for my (?<price>.+) in (?<league>.+)\.(?<message>.+)?$/,
|
||||
ru: /^Здравствуйте, хочу купить у вас (?<item>.+) за (?<price>.+) в лиге (?<league>.+)\.(?<message>.+)?$/
|
||||
}
|
||||
@@ -1,9 +1,14 @@
|
||||
import { MainProcess } from '@/ipc/main-process-bindings'
|
||||
import * as ipc from '@/ipc/ipc-event'
|
||||
import { openWiki } from './wiki'
|
||||
import { handleLine } from '../client-log/client-log'
|
||||
|
||||
export function registerOtherServices () {
|
||||
MainProcess.addEventListener(ipc.OPEN_WIKI, (e) => {
|
||||
openWiki((e as CustomEvent<string>).detail)
|
||||
})
|
||||
|
||||
MainProcess.addEventListener(ipc.CLIENT_LOG_UPDATE, (e) => {
|
||||
handleLine((e as CustomEvent<string>).detail)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user