Skip to content

Commit 4c91c23

Browse files
committed
[Update] Fix Monster HP Threshold Reporting & Misc
1 parent 5c715a3 commit 4c91c23

File tree

22 files changed

+928
-434
lines changed

22 files changed

+928
-434
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ A DPS meter for Blue Protocol built with Electron, React 19, and TypeScript. Thi
7272
| Command | Description |
7373
|---------|-------------|
7474
| `npm run dev` | Start development mode with hot reload |
75+
| `npm run dev:windows` | Start development with hot reload - and utf8 output |
7576
| `npm run build` | Build renderer and main process |
7677
| `npm run build:server` | Build standalone server |
7778
| `npm run build:all` | Build everything |

algo/packet.ts

Lines changed: 94 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,23 @@ import zlib from 'zlib';
22
import Long from 'long';
33
import pbjs from 'protobufjs/minimal.js';
44
import fs from 'fs';
5+
import path from 'path';
56
import pb from './blueprotobuf-esm';
6-
import type { Logger } from 'winston';
7+
import type { Logger } from '../src/types';
78
import type { UserDataManager } from '../src/server/dataManager';
89
import { initialize, TRACKED_MONSTER_IDS } from '../src/utils/bpTimer';
910

11+
const TRANSLATIONS_DIR = path.join(__dirname, "../main/translations");
12+
let monsterNames: Record<string, string> = JSON.parse(fs.readFileSync(path.join(TRANSLATIONS_DIR, "en.json"), "utf-8")).monsters;
13+
14+
export function reloadMonsterTranslations(language: string): void {
15+
monsterNames = JSON.parse(fs.readFileSync(path.join(TRANSLATIONS_DIR, `${language}.json`), "utf-8")).monsters;
16+
}
17+
18+
export function getMonsterNames(): Record<string, string> {
19+
return monsterNames;
20+
}
21+
1022
class BinaryReader {
1123
public buffer: Buffer;
1224
public offset: number;
@@ -288,25 +300,10 @@ class PacketProcessor {
288300
if (attrCollection && attrCollection.Attrs) {
289301
if (isTargetPlayer) {
290302
this.#processPlayerAttrs(targetUuid.toNumber(), attrCollection.Attrs);
303+
this.#processPositionAttrs(targetUuid.toNumber(), 'player', attrCollection.Attrs);
291304
} else if (isTargetMonster) {
292305
this.#processEnemyAttrs(targetUuid.toNumber(), attrCollection.Attrs);
293-
}
294-
295-
for (const attr of attrCollection.Attrs) {
296-
if ((attr.Id === 52 || attr.Id === 53) && attr.RawData) {
297-
try {
298-
const position = pb.Position.decode(attr.RawData);
299-
const x = position.X ?? 0;
300-
const y = position.Y ?? 0;
301-
const z = position.Z ?? 0;
302-
303-
if (isTargetMonster) {
304-
this.userDataManager.enemyCache.position.set(targetUuid.toNumber(), { x, y, z });
305-
} else if (isTargetPlayer && targetUuid.toNumber() === this.userDataManager.localPlayerUid) {
306-
this.userDataManager.setLocalPlayerPosition({ x, y, z });
307-
}
308-
} catch (e) {}
309-
}
306+
this.#processPositionAttrs(targetUuid.toNumber(), 'monster', attrCollection.Attrs);
310307
}
311308
}
312309

@@ -393,16 +390,30 @@ class PacketProcessor {
393390
}
394391

395392
if (isDead) {
396-
const enemyUid = `${targetUuid.toNumber()}`;
393+
const enemyUid = targetUuid.toNumber();
397394
const monsterId = this.userDataManager.enemyCache.monsterId.get(enemyUid);
398-
if (monsterId && TRACKED_MONSTER_IDS.has(String(monsterId))) {
399-
if (this.userDataManager.globalSettings.enableBPTimerSubmission !== false) {
400-
const line = this.userDataManager.getCurrentLineId();
401-
const bpTimer = initialize(this.logger, this.userDataManager.globalSettings);
402-
setTimeout(() => {
403-
bpTimer.resetMonster(monsterId, line);
404-
this.logger.debug(`[BPTimer] Reset tracking for monster ${monsterId} on line ${line} (isDead flag)`);
405-
}, 3000);
395+
const monsterName = this.userDataManager.enemyCache.name.get(enemyUid);
396+
397+
if (!this.userDataManager.enemyCache.isDead.get(enemyUid)) {
398+
this.userDataManager.enemyCache.isDead.set(enemyUid, true);
399+
this.userDataManager.enemyCache.hp.set(enemyUid, 0);
400+
401+
if (monsterId && TRACKED_MONSTER_IDS.has(String(monsterId))) {
402+
this.logger.debug(`[BPTimer] Tracked boss ${monsterName || 'Unknown'} [ID: ${monsterId}] (${enemyUid}) died on line ${this.userDataManager.getCurrentLineId()}`);
403+
404+
if (this.userDataManager.globalSettings.enableBPTimerSubmission !== false) {
405+
const line = this.userDataManager.getCurrentLineId();
406+
const bpTimer = initialize(this.logger, this.userDataManager.globalSettings);
407+
408+
bpTimer.createHpReport(monsterId, 0, line).catch(err => {
409+
this.logger.error(`[BPTimer] Failed to report death: ${err.message}`);
410+
});
411+
412+
setTimeout(() => {
413+
bpTimer.resetMonster(monsterId, line);
414+
this.logger.debug(`[BPTimer] Reset tracking for monster ${monsterId} on line ${line} (isDead flag)`);
415+
}, 3000);
416+
}
406417
}
407418
}
408419
}
@@ -457,7 +468,7 @@ class PacketProcessor {
457468
`EXT: ${extra.join('|')}`,
458469
];
459470
const dmgLog = dmgLogArr.join(' ');
460-
this.logger.info(dmgLog);
471+
this.logger.debug(dmgLog);
461472
this.userDataManager.addLog(dmgLog);
462473
}
463474
}
@@ -626,6 +637,31 @@ class PacketProcessor {
626637
// this.logger.debug(syncContainerDirtyData.VData.Buffer.toString('hex'));
627638
}
628639

640+
#processPositionAttrs(targetUID: number, targetType: 'monster' | 'player', attrs: any[]) {
641+
for (const attr of attrs) {
642+
if (!attr.Id || !attr.RawData) continue;
643+
644+
switch (attr.Id) {
645+
case 52: // X
646+
case 53: // Y
647+
case 54: // Z
648+
const position = pb.Position.decode(attr.RawData);
649+
const x = position.X ?? 0;
650+
const y = position.Y ?? 0;
651+
const z = position.Z ?? 0;
652+
653+
if (targetType === 'monster') {
654+
this.userDataManager.enemyCache.position.set(targetUID, { x, y, z });
655+
} else if (targetType === 'player' && targetUID === this.userDataManager.localPlayerUid) {
656+
this.userDataManager.setLocalPlayerPosition({ x, y, z });
657+
}
658+
break;
659+
default:
660+
break;
661+
}
662+
}
663+
}
664+
629665
#processPlayerAttrs(playerUid: number, attrs: any[]) {
630666
for (const attr of attrs) {
631667
if (!attr.Id || !attr.RawData) continue;
@@ -694,25 +730,35 @@ class PacketProcessor {
694730
for (const attr of attrs) {
695731
if (!attr.Id || !attr.RawData) continue;
696732
const reader = pbjs.Reader.create(attr.RawData);
697-
const base64Data = attr.RawData.toString('base64');
698733

699734
switch (attr.Id) {
700735
case AttrType.AttrName:
701736
const enemyName = reader.string();
702737
this.userDataManager.enemyCache.name.set(enemyUid, enemyName);
703738
this.userDataManager.enemyCache.lastSeen.set(enemyUid, Date.now());
704-
this.logger.info(`Found monster name ${enemyName} for id ${enemyUid}`);
739+
this.logger.debug(`Found monster name ${enemyName} for id ${enemyUid}`);
705740
break;
706741
case AttrType.AttrId:
707742
const attrId = reader.int32();
708-
this.logger.info(`Found monster attrId ${attrId} for id ${enemyUid}`);
743+
this.logger.debug(`Found monster attrId ${attrId} for id ${enemyUid}`);
709744
this.userDataManager.enemyCache.monsterId.set(enemyUid, attrId);
710745
this.userDataManager.enemyCache.lastSeen.set(enemyUid, Date.now());
746+
747+
const translatedName = monsterNames[String(attrId)];
748+
if (translatedName && !this.userDataManager.enemyCache.name.has(enemyUid)) {
749+
this.userDataManager.enemyCache.name.set(enemyUid, translatedName);
750+
this.logger.debug(`Set monster name from translation: ${translatedName} for id ${enemyUid}`);
751+
}
711752
break;
712753
case AttrType.AttrHp:
713754
const enemyHp = reader.int32();
714755
this.userDataManager.enemyCache.hp.set(enemyUid, enemyHp);
715756
this.userDataManager.enemyCache.lastSeen.set(enemyUid, Date.now());
757+
758+
if (enemyHp > 0 && this.userDataManager.enemyCache.isDead.get(enemyUid)) {
759+
this.userDataManager.enemyCache.isDead.delete(enemyUid);
760+
}
761+
716762
this.#reportBossHpThreshold(enemyUid, enemyHp);
717763
break;
718764
case AttrType.AttrMaxHp:
@@ -733,6 +779,11 @@ class PacketProcessor {
733779
return;
734780
}
735781

782+
// Don't report if monster is already marked as dead
783+
if (this.userDataManager.enemyCache.isDead.get(enemyUid)) {
784+
return;
785+
}
786+
736787
const monsterId = this.userDataManager.enemyCache.monsterId.get(enemyUid);
737788
const maxHp = this.userDataManager.enemyCache.maxHp.get(enemyUid);
738789

@@ -744,12 +795,23 @@ class PacketProcessor {
744795
return;
745796
}
746797

798+
// If HP reaches 0 but isDead wasn't set yet, report it here as fallback
747799
if (currentHp === 0 || currentHp <= maxHp * 0.001) {
748800
const line = this.userDataManager.getCurrentLineId();
749801
const bpTimer = initialize(this.logger, this.userDataManager.globalSettings);
802+
803+
// Report 0% HP (death) as fallback
804+
bpTimer.createHpReport(monsterId, 0, line).catch(err => {
805+
this.logger.debug(`[BPTimer] Failed to report death (fallback): ${err.message}`);
806+
});
807+
808+
// Mark as dead to prevent duplicate reports
809+
this.userDataManager.enemyCache.isDead.set(enemyUid, true);
810+
811+
// Reset tracking after reporting death
750812
setTimeout(() => {
751813
bpTimer.resetMonster(monsterId, line);
752-
this.logger.debug(`[BPTimer] Reset tracking for monster ${monsterId} on line ${line} (HP reached 0)`);
814+
this.logger.debug(`[BPTimer] Reset tracking for monster ${monsterId} on line ${line} (HP reached 0 fallback)`);
753815
}, 3000);
754816
return;
755817
}

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "bpsr-meter",
3-
"version": "0.3.5",
3+
"version": "0.3.6",
44
"description": "BPSR Meter",
55
"author": "Denoder",
66
"type": "module",
@@ -13,6 +13,7 @@
1313
},
1414
"scripts": {
1515
"dev": "electron-vite dev",
16+
"dev:windows": "chcp 65001 > nul && electron-vite dev",
1617
"build": "electron-vite build",
1718
"build:server": "tsc src/main/server.ts --outDir dist/server --module ES2022 --target ES2020 --moduleResolution bundler --esModuleInterop --resolveJsonModule --skipLibCheck",
1819
"build:all": "npm run build && npm run build:server",

src/main/index.ts

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,6 @@ function rotateLogFile(logPath: string): void {
7171
}
7272

7373
function logToFile(msg: string): void {
74-
const timestamp = new Date().toISOString();
7574
const logPath = path.join(userDataPath, "information_log.txt");
7675

7776
try {
@@ -84,7 +83,10 @@ function logToFile(msg: string): void {
8483
}
8584
}
8685

87-
fs.appendFileSync(logPath, `[${timestamp}] ${msg}\n`);
86+
// eslint-disable-next-line no-control-regex
87+
const cleanMsg = msg.replace(/\x1b\[[0-9;]*m/g, '');
88+
89+
fs.appendFileSync(logPath, `${cleanMsg}\n`, { encoding: "utf8" });
8890
} catch (e) {
8991
console.error("Log failed:", e);
9092
}
@@ -374,6 +376,12 @@ function setupIpcHandlers() {
374376
windows.main.webContents.send("height-step-changed", settings.heightStep);
375377
}
376378
}
379+
380+
if (settings.hasOwnProperty("enableManualHeight")) {
381+
if (windows.main && !windows.main.isDestroyed()) {
382+
windows.main.webContents.send("manual-height-changed", settings.enableManualHeight);
383+
}
384+
}
377385
} catch (error) {
378386
logToFile(`Error updating global settings: ${error}`);
379387
}
@@ -469,18 +477,27 @@ function startServer(): Promise<void> {
469477
const serverPath = path.join(__dirname, "../../out/main/server.js");
470478
logToFile(`Launching server on port ${serverPort} from: ${serverPath}`);
471479

480+
const env = {
481+
...process.env,
482+
resourcesPath: process.resourcesPath,
483+
USER_DATA_PATH: userDataPath
484+
};
485+
472486
serverProcess = fork(serverPath, [serverPort.toString()], {
473487
stdio: ["pipe", "pipe", "pipe", "ipc"],
474-
env: { ...process.env, resourcesPath: process.resourcesPath, USER_DATA_PATH: userDataPath }
488+
env: env
475489
});
476490

477491
const timeout = setTimeout(() => {
478492
reject(new Error("Server did not respond in time (10s timeout)"));
479493
}, 10000);
480494

481-
serverProcess.stdout?.on("data", (data: Buffer) => {
482-
const output = data.toString().trim();
483-
logToFile(`SERVER: ${output}`);
495+
serverProcess.stdout?.setEncoding('utf8');
496+
serverProcess.stderr?.setEncoding('utf8');
497+
498+
serverProcess.stdout?.on("data", (data: any) => {
499+
const output = typeof data === 'string' ? data.trim() : data.toString().trim();
500+
logToFile(`${output}`);
484501

485502
const match = output.match(/Web server started at (http:\/\/localhost:\d+)/);
486503
if (match && windows.main) {
@@ -490,8 +507,9 @@ function startServer(): Promise<void> {
490507
}
491508
});
492509

493-
serverProcess.stderr?.on("data", (data: Buffer) => {
494-
logToFile(`SERVER ERROR: ${data.toString().trim()}`);
510+
serverProcess.stderr?.on("data", (data: any) => {
511+
const output = typeof data === 'string' ? data.trim() : data.toString().trim();
512+
logToFile(`SERVER ERROR: ${output}`);
495513
});
496514

497515
serverProcess.on("error", reject);

0 commit comments

Comments
 (0)