Added run post run statistics view in the non-overlay window. Provides some stats for a run in aggregation on the zone id and then also a toggle to see an unaggregated run

main 1.7.0
isark 1 year ago
parent 5db8663819
commit 3272c3bda2

@ -0,0 +1,47 @@
{
"root": true,
"ignorePatterns": [
"projects/**/*"
],
"overrides": [
{
"files": [
"*.ts"
],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:@angular-eslint/recommended",
"plugin:@angular-eslint/template/process-inline-templates"
],
"rules": {
"@angular-eslint/directive-selector": [
"error",
{
"type": "attribute",
"prefix": "app",
"style": "camelCase"
}
],
"@angular-eslint/component-selector": [
"error",
{
"type": "element",
"prefix": "app",
"style": "kebab-case"
}
]
}
},
{
"files": [
"*.html"
],
"extends": [
"plugin:@angular-eslint/template/recommended",
"plugin:@angular-eslint/template/accessibility"
],
"rules": {}
}
]
}

@ -31,8 +31,7 @@
"styles": [
"src/styles.scss"
],
"scripts": [
]
"scripts": []
},
"configurations": {
"production": {
@ -72,6 +71,15 @@
}
},
"defaultConfiguration": "development"
},
"lint": {
"builder": "@angular-eslint/builder:lint",
"options": {
"lintFilePatterns": [
"src/**/*.ts",
"src/**/*.html"
]
}
}
}
}
@ -82,6 +90,9 @@
}
},
"cli": {
"analytics": false
"analytics": false,
"schematicCollections": [
"@angular-eslint/schematics"
]
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

@ -6,7 +6,8 @@
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development"
"watch": "ng build --watch --configuration development",
"lint": "ng lint"
},
"private": true,
"dependencies": {
@ -40,10 +41,18 @@
},
"devDependencies": {
"@angular-devkit/build-angular": "^16.1.4",
"@angular-eslint/builder": "17.3.0",
"@angular-eslint/eslint-plugin": "17.3.0",
"@angular-eslint/eslint-plugin-template": "17.3.0",
"@angular-eslint/schematics": "17.3.0",
"@angular-eslint/template-parser": "17.3.0",
"@angular/cli": "~16.1.4",
"@angular/compiler-cli": "^16.1.4",
"@tauri-apps/cli": "^1.4.0",
"@types/jasmine": "~4.3.0",
"@typescript-eslint/eslint-plugin": "7.2.0",
"@typescript-eslint/parser": "7.2.0",
"eslint": "^8.57.0",
"jasmine-core": "~5.0.1",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.2.0",
@ -52,4 +61,4 @@
"karma-jasmine-html-reporter": "~2.1.0",
"typescript": "~5.1.6"
}
}
}

@ -37,9 +37,6 @@ struct UnprocessedArea {
#[serde(rename = "IsVaalArea")]
pub is_vaal_area: bool,
#[serde(rename = "Unknown64")]
pub unknown64: bool,
#[serde(rename = "Unknown9")]
pub unknown9: i64,
}
@ -61,8 +58,7 @@ pub type AreaId = usize;
pub type WorldAreasMap = HashMap<String, WorldArea>;
#[allow(dead_code)]
pub fn load_world_areas_map(content: &'static str) -> WorldAreasMap {
serde_json::from_str::<WorldAreasMap>(content)
.expect("Could not load world areas json")
serde_json::from_str::<WorldAreasMap>(content).expect("Could not load world areas json")
}
#[macro_export]
@ -206,7 +202,6 @@ pub fn repack(content: &str, out_path: &str) {
if w_a.act < 11
&& !w_a.is_vaal_area
&& !w_a.connections_world_areas_keys.is_empty()
&& !w_a.unknown64
&& !w_a.named_id.starts_with("Map")
&& !w_a.named_id.starts_with("Descent")
&& !w_a.named_id.starts_with("EndlessLedge")
@ -234,3 +229,31 @@ pub fn repack(content: &str, out_path: &str) {
std::fs::write(out_path, &new_json).expect("Could not write to file");
}
#[allow(dead_code)]
pub fn repack_full(content: &str, out_path: &str) {
let areas_json = serde_json::from_str::<UnprocessedAreas>(content);
let area_map = areas_json
.expect("Could not read world areas")
.into_iter()
.map(|w_a| {
(
w_a.named_id.clone(),
WorldArea {
name: w_a.name,
named_id: w_a.named_id,
act: w_a.act,
is_town: w_a.is_town,
has_waypoint: w_a.has_waypoint,
connections_world_areas_keys: w_a.connections_world_areas_keys,
key_id: w_a.key_id,
},
)
})
.collect::<WorldAreasMap>();
let new_json = serde_json::to_string(&area_map).unwrap();
std::fs::write(out_path, &new_json).expect("Could not write to file");
}

@ -6,8 +6,10 @@ fn main() {
tauri_build::build();
const OUT_PROCESSED: &'static str = "../data/processed_world_areas.json";
const OUT_PROCESSED_FULL: &'static str = "../data/processed_world_areas_full.json";
println!("cargo:rerun-if-changed=../data/WorldAreas.json");
poe_data::world_area::repack(&include_str!("../../data/WorldAreas.json"), OUT_PROCESSED);
poe_data::world_area::repack_full(&include_str!("../../data/WorldAreas.json"), OUT_PROCESSED_FULL);
//Let's go with uppercase names, allows automatic import to work...
config::Rect::export_to("../src/app/_models/generated/Rect.ts").expect("Could not generate config struct");

@ -41,6 +41,12 @@ lazy_static! {
);
}
lazy_static! {
static ref FULL_WORLD_AREAS_MAP: WorldAreasMap = poe_data::world_area::load_world_areas_map(
include_str!("../../data/processed_world_areas_full.json")
);
}
#[tauri::command]
fn set_interactable(interactable: bool, state: tauri::State<Sender<Event>>) {
if interactable {
@ -56,6 +62,13 @@ fn load_world_areas() -> WorldAreasMap {
WORLD_AREAS_MAP.clone()
}
#[tauri::command]
fn load_full_world_areas() -> WorldAreasMap {
log::info!("Loading world areas");
FULL_WORLD_AREAS_MAP.clone()
}
#[tauri::command]
fn load_config(state: tauri::State<Mutex<Storage>>) -> Option<Config> {
Some(state.lock().ok()?.config.clone())
@ -140,11 +153,13 @@ fn main() {
let settings = CustomMenuItem::new("settings".to_string(), "Settings");
let editor = CustomMenuItem::new("editor".to_string(), "Plan Editor");
let runstats = CustomMenuItem::new("runstats".to_string(), "Run stats");
let force_show = CustomMenuItem::new("force_show".to_string(), "Force show");
let exit = CustomMenuItem::new("exit".to_string(), "Exit");
let tray_menu = SystemTrayMenu::new()
.add_item(settings)
.add_item(editor)
.add_item(runstats)
.add_item(force_show)
.add_native_item(SystemTrayMenuItem::Separator)
.add_item(exit);
@ -177,6 +192,7 @@ fn main() {
.invoke_handler(tauri::generate_handler![
set_interactable,
load_world_areas,
load_full_world_areas,
load_config,
update_config,
enumerate_stored_plans,
@ -194,7 +210,7 @@ fn main() {
"exit" => {
std::process::exit(0);
}
"editor" | "settings" => {
"editor" | "settings" | "runstats" => {
if let Some(window) = app.get_window("Normal") {
window.show().ok();
window.emit_to("Normal", "loadTab", id).ok();
@ -224,7 +240,6 @@ fn listen_for_zone_changes(poe_client_log_path: Option<PathBuf>, window: Window)
std::thread::spawn(move || {
// need _watcher
if let Some((enter_area_receiver, _watcher)) = receiver_from_path(&poe_client_log_path) {
let world_areas: WorldAreasMap = load_world_areas();
for area in blocking_area_filtered_rx(&enter_area_receiver) {
if let Some(area) = filter_func(area) {
// if let Some(entered) = world_areas.get(&area) {

@ -8,7 +8,7 @@
},
"package": {
"productName": "Nothing",
"version": "1.6.0"
"version": "1.7.0"
},
"tauri": {
"systemTray": {

@ -101,6 +101,10 @@ export class TimeTrackerService {
return !!this.currentRunHistory;
}
public get storedHistories(): Observable<Map<string, RunHistoryMetadata>> {
return this.storedHistoriesSubject;
}
onNewRun(plan: Plan) {
if (this.timerSubscription && !this.timerSubscription.closed) this.timerSubscription.unsubscribe();
if (this.debouncedSaveStopwatch && !this.debouncedSaveStopwatch.closed) this.debouncedSaveStopwatch.unsubscribe();
@ -223,9 +227,31 @@ export class TimeTrackerService {
}
}
public hmsTimestamp(elapsed?: number): string {
if (!elapsed) return "";
const h = String(Math.floor(elapsed / 3600000)).padStart(2, '0');
const m = String(Math.floor((elapsed % 3600000) / 60000)).padStart(2, '0');
const s = String(Math.floor((elapsed % 60000) / 1000)).padStart(2, '0');
return `${h}:${m}:${s}`;
}
private loadCache() {
from(invoke<Map<string, RunHistoryMetadata>>('load_cache')).subscribe(data => {
this.storedHistoriesSubject.next(data);
this.zone.run(() => {
const cache = new Map<string, RunHistoryMetadata>();
Object.values(data).forEach((value) => {
cache.set(value.uuid, {
uuid: value.uuid,
currentElapsedMillis: value.current_elapsed_millis,
associatedName: value.associated_name,
last_updated: value.last_updated,
});
});
this.storedHistoriesSubject.next(cache);
})
});
}

@ -13,6 +13,7 @@ export class WorldAreaService {
public matcher?: Fuzzr;
private worldAreasSubject = new ReplaySubject<Map<string, WorldArea>>();
private fullWorldAreasSubject = new ReplaySubject<Map<string, WorldArea>>();
constructor(private zone: NgZone) {
from(invoke<Map<string, WorldArea>>('load_world_areas')).subscribe((data) => {
@ -23,6 +24,12 @@ export class WorldAreaService {
toString: (e: [string, WorldArea]) => e[1].name
})
});
from(invoke<Map<string, WorldArea>>('load_full_world_areas')).subscribe((data) => {
const entries = Object.entries(data).sort((a, b) => a[1].key_id - b[1].key_id);
this.worldAreas = new Map(entries);
this.zone.run(() => this.fullWorldAreasSubject.next(this.worldAreas!));
});
}
getWorldAreas(): Observable<Map<string, WorldArea>> {
@ -31,6 +38,12 @@ export class WorldAreaService {
);
}
getFullWorldAreas(): Observable<Map<string, WorldArea>> {
return this.fullWorldAreasSubject.asObservable().pipe(
filter(worldAreas => !!worldAreas),
);
}
hasTrial(key: string): boolean {
switch (key) {
case "1_1_7_1": return true;

@ -11,6 +11,9 @@
<mat-tab label="Editor">
<plan-editor class="content"></plan-editor>
</mat-tab>
<mat-tab label="Previous runs" *ngIf="configService.config.enableStopwatch">
<app-run-stats class="content"></app-run-stats>
</mat-tab>
</mat-tab-group>
<tooltip class="tooltip">

@ -22,7 +22,7 @@ export class AppComponent implements OnInit {
isBinding: boolean = false;
overlayShowSettings: boolean = false;
hasAttachedOnce: boolean = false;
selected = new FormControl(0);
constructor(
@ -33,15 +33,24 @@ export class AppComponent implements OnInit {
private events: EventsService,
) {
this.events.listen<String>("loadTab").subscribe(event => {
if (event.payload === "editor") {
this.selected.setValue(1);
} else {
this.selected.setValue(0);
switch (event.payload) {
case "settings":
this.selected.setValue(0);
break;
case "editor":
this.selected.setValue(1);
break;
case "runstats":
this.selected.setValue(2);
break;
default:
this.selected.setValue(0);
break;
}
})
}
ngOnInit(): void {
}
}

@ -17,6 +17,7 @@ import { MAT_DIALOG_DEFAULT_OPTIONS } from "@angular/material/dialog";
import { TooltipComponent } from "./tooltip/tooltip.component";
import { HttpClientModule } from "@angular/common/http";
import { AngularSvgIconModule } from "angular-svg-icon";
import { RunStatsComponent } from "./run-stats/run-stats.component";
// import { GemFinderComponent } from "./gem-finder/gem-finder.component";
export function initializeApp(configService: ConfigService) {
@ -43,6 +44,7 @@ export function initializeApp(configService: ConfigService) {
TooltipComponent,
HttpClientModule,
AngularSvgIconModule.forRoot(),
RunStatsComponent
],
providers: [
{

@ -47,7 +47,7 @@
zones</span>
</div>
</tooltip>
<span *ngIf="shouldDisplayTimer()" class="timer">{{currentTime(timeTrackerService.elapsedTimeMillis)}}</span>
<span *ngIf="shouldDisplayTimer()" class="timer">{{timeTrackerService.hmsTimestamp(timeTrackerService.elapsedTimeMillis)}}</span>
</div>
<ngx-moveable #moveable [target]="targetRef" [draggable]="draggable && overlayService.interactable"

@ -393,16 +393,6 @@ export class PlanDisplayComponent implements AfterViewInit, OnInit {
return "";
}
currentTime(elapsed?: number) {
if (!elapsed) return "";
const h = String(Math.floor(elapsed / 3600000)).padStart(2, '0');
const m = String(Math.floor((elapsed % 3600000) / 60000)).padStart(2, '0');
const s = String(Math.floor((elapsed % 60000) / 1000)).padStart(2, '0');
return `${h}:${m}:${s}`;
}
shouldDisplayTimer(): boolean {
if (!this.configService.config.enableStopwatch) return false;

@ -0,0 +1,61 @@
<button mat-stroked-button color="warn" (click)="reset()">Reset view</button>
<ng-container *ngIf="!shouldShowTable && cache">
<div class="cache-viewport">
<div *ngFor="let item of this.cache | keyvalue" class="cache-item d-flex flex-row p-4">
<div class="d-flex flex-column">
<span>Plan: {{item.value.associatedName}}</span>
<span>Run time: {{hms(item.value.currentElapsedMillis)}}</span>
<span>Last updated: {{dateFormat(item.value.last_updated)}}</span>
</div>
<div class="d-flex flex-column justify-content-center align-items-center">
<button mat-stroked-button color="accent" (click)="loadMain(item.key)">Load</button>
</div>
</div>
</div>
</ng-container>
<ng-container *ngIf="shouldShowTable">
<mat-slide-toggle [(ngModel)]="this.aggregate">Show aggregated results</mat-slide-toggle>
<table mat-table [dataSource]="dataSource">
<ng-container matColumnDef="zoneName">
<th mat-header-cell *matHeaderCellDef> Zone </th>
<td mat-cell *matCellDef="let entry"> {{entry.zoneName}} </td>
</ng-container>
<ng-container matColumnDef="entryTime">
<th mat-header-cell *matHeaderCellDef> Time of entry </th>
<td mat-cell *matCellDef="let entry"> {{entry.entryTime}} </td>
</ng-container>
<ng-container matColumnDef="estimatedExit">
<th mat-header-cell *matHeaderCellDef> Time of estimated exit </th>
<td mat-cell *matCellDef="let entry"> {{entry.estimatedExit}} </td>
</ng-container>
<ng-container matColumnDef="estimatedTimeSpent">
<th mat-header-cell *matHeaderCellDef> Estimated time spent </th>
<td mat-cell *matCellDef="let entry"> {{entry.estimatedTimeSpent}} </td>
</ng-container>
<ng-container matColumnDef="aggregateFirstEntry">
<th mat-header-cell *matHeaderCellDef> Time of first entry </th>
<td mat-cell *matCellDef="let entry"> {{entry.aggregateFirstEntry}} </td>
</ng-container>
<ng-container matColumnDef="aggregateLastExit">
<th mat-header-cell *matHeaderCellDef> Time of estimated last exit </th>
<td mat-cell *matCellDef="let entry"> {{entry.aggregateLastExit}} </td>
</ng-container>
<ng-container matColumnDef="aggregateTimeSpent">
<th mat-header-cell *matHeaderCellDef> Estimated total time spent </th>
<td mat-cell *matCellDef="let entry"> {{entry.aggregateTimeSpent}} </td>
</ng-container>
<ng-container matColumnDef="aggregateNumEntries">
<th mat-header-cell *matHeaderCellDef> Number of entries </th>
<td mat-cell *matCellDef="let entry"> {{entry.aggregateNumEntries}} </td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: true"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
</ng-container>

@ -0,0 +1,3 @@
.cache-item {
gap: 12px;
}

@ -0,0 +1,21 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RunStatsComponent } from './run-stats.component';
describe('RunStatsComponent', () => {
let component: RunStatsComponent;
let fixture: ComponentFixture<RunStatsComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [RunStatsComponent]
});
fixture = TestBed.createComponent(RunStatsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

@ -0,0 +1,193 @@
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RunHistory, RunHistoryMetadata, TimeTrackerService } from '../_services/time-tracker.service';
import { MatTableModule } from '@angular/material/table';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { FormsModule } from '@angular/forms';
import { ScrollingModule } from '@angular/cdk/scrolling';
import { MatButtonModule } from '@angular/material/button';
import { WorldAreaService } from '../_services/world-area.service';
import { WorldArea } from '../_models/world-area';
export interface RunStat {
zoneName: string;
entryTime: string;
estimatedExit: string;
estimatedTimeSpent: string;
}
export interface AggregateRunStat {
zoneName: string;
aggregateFirstEntry: string;
aggregateLastExit: string;
aggregateTimeSpent: string;
aggregateNumEntries: string;
}
interface UnformattedAggregateRunStat {
zoneId: string;
aggregateFirstEntry: number;
aggregateLastExit: number;
aggregateTimeSpent: number;
aggregateNumEntries: number;
}
type RunStatType = RunStat | AggregateRunStat;
@Component({
selector: 'app-run-stats',
standalone: true,
imports: [CommonModule, MatTableModule, MatSlideToggleModule, FormsModule, ScrollingModule, MatButtonModule],
templateUrl: './run-stats.component.html',
styleUrls: ['./run-stats.component.scss']
})
export class RunStatsComponent implements OnInit {
aggregated?: AggregateRunStat[];
/// practically which zone can't have a last exit time as last exit is not determinable for the last entry
aggregateNAId?: string;
direct?: RunStat[];
aggregate: boolean = true;
cache?: Map<string, RunHistoryMetadata>;
worldAreaMap?: Map<string, WorldArea>;
constructor(private timeTrackerService: TimeTrackerService, private worldAreaService: WorldAreaService) {
this.worldAreaService.getFullWorldAreas().subscribe((data) => {
this.worldAreaMap = data;
})
}
ngOnInit(): void {
this.timeTrackerService.storedHistories.subscribe((data) => {
this.cache = data;
})
}
get dataSource(): RunStatType[] {
return (this.aggregate ? this.aggregated : this.direct) ?? [];
}
get hasInitializedData() {
return this.aggregated || this.direct;
}
get shouldShowTable() {
return this.hasInitializedData;
}
loadMain(uuid: string) {
this.timeTrackerService.loadHistory(uuid).subscribe((data => {
if (data) {
this.onLoad(data);
}
}));
}
dateFormat(value: number) {
return new Date(value).toLocaleString();
}
onLoad(data: RunHistory) {
this.direct = this.calcDirect(data);
this.aggregated = this.calcAggregated(data);
}
calcAggregated(data: RunHistory): AggregateRunStat[] {
const aggregation = new Map<string, UnformattedAggregateRunStat>();
this.aggregateNAId = data.entries[data.entries.length - 1].zone;
data.entries.forEach((entry, index) => {
const hasExit = !(data.entries.length - 1 === index);
let aggregate: UnformattedAggregateRunStat = {
zoneId: entry.zone,
aggregateFirstEntry: entry.current_elapsed_millis,
aggregateLastExit: hasExit ? data.entries[index + 1].current_elapsed_millis : 0,
aggregateTimeSpent: hasExit ? (data.entries[index + 1].current_elapsed_millis - data.entries[index].current_elapsed_millis) : 0,
aggregateNumEntries: 1,
}
const existing = aggregation.get(entry.zone);
if (existing) {
existing.aggregateLastExit = aggregate.aggregateLastExit;
existing.aggregateTimeSpent += aggregate.aggregateTimeSpent;
existing.aggregateNumEntries++;
}
aggregation.set(entry.zone, existing ?? aggregate);
});
return Array.from(aggregation.values()).map((entry) => {
let aggregateTimeSpent;
if (this.aggregateNAId === entry.zoneId) {
aggregateTimeSpent = this.timeTrackerService.hmsTimestamp(entry.aggregateTimeSpent) + " + N/A"
} else {
aggregateTimeSpent = this.timeTrackerService.hmsTimestamp(entry.aggregateTimeSpent);
}
return {
zoneName: this.resolveZone(entry.zoneId),
aggregateFirstEntry: this.timeTrackerService.hmsTimestamp(entry.aggregateFirstEntry),
aggregateLastExit: this.aggregateNAId === entry.zoneId ? "N/A" : this.timeTrackerService.hmsTimestamp(entry.aggregateLastExit),
aggregateTimeSpent: aggregateTimeSpent,
aggregateNumEntries: entry.aggregateNumEntries.toString(),
}
});
}
calcDirect(data: RunHistory): RunStat[] {
return data.entries.map((entry, index) => {
const hasExit = !(data.entries.length - 1 === index);
return {
zoneName: this.resolveZone(entry.zone),
entryTime: this.timeTrackerService.hmsTimestamp(entry.current_elapsed_millis),
estimatedExit: hasExit ? this.timeTrackerService.hmsTimestamp(data.entries[index + 1].current_elapsed_millis) : "N/A",
estimatedTimeSpent: hasExit ? this.timeTrackerService.hmsTimestamp(data.entries[index + 1].current_elapsed_millis - data.entries[index].current_elapsed_millis) : "N/A",
}
})
}
hms(time: number) {
return this.timeTrackerService.hmsTimestamp(time);
}
private resolveZone(zoneId: string) {
const area = this.worldAreaMap?.get(zoneId);
if (!area) {
return "Unknown zone: " + zoneId;
}
// Act might not be very reasonable but it's the best we have it..
return area.name + " (A" + area.act + ")"
}
reset() {
this.aggregated = undefined;
this.direct = undefined;
}
get displayedColumns() {
if (this.aggregate) {
return [
"zoneName",
"aggregateFirstEntry",
"aggregateLastExit",
"aggregateTimeSpent",
"aggregateNumEntries",
]
} else {
return [
"zoneName",
"entryTime",
"estimatedExit",
"estimatedTimeSpent",
]
}
}
}

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save