The model viewer now shows a problems popup when loading a file failed or succeeded with some problems.

This commit is contained in:
Daan Vanden Bosch 2020-01-06 21:09:44 +01:00
parent 7f5accf790
commit 8580cd4f66
27 changed files with 526 additions and 162 deletions

View File

@ -12,15 +12,16 @@ import { Endianness } from "../src/core/data_formats/Endianness";
import { ItemTypeDto } from "../src/core/dto/ItemTypeDto";
import { QuestDto } from "../src/hunt_optimizer/dto/QuestDto";
import { BoxDropDto, EnemyDropDto } from "../src/hunt_optimizer/dto/drops";
import { LogLevel, LogManager } from "../src/core/Logger";
import { LogManager } from "../src/core/Logger";
import { Severity } from "../src/core/Severity";
const logger = LogManager.get("assets_generation/update_ephinea_data");
LogManager.default_level = LogLevel.Error;
logger.level = LogLevel.Info;
LogManager.get("static/update_drops_ephinea").level = LogLevel.Info;
LogManager.get("core/data_formats/parsing/quest").level = LogLevel.Off;
LogManager.get("core/data_formats/parsing/quest/bin").level = LogLevel.Off;
LogManager.default_severity = Severity.Error;
logger.severity = Severity.Info;
LogManager.get("static/update_drops_ephinea").severity = Severity.Info;
LogManager.get("core/data_formats/parsing/quest").severity = Severity.Off;
LogManager.get("core/data_formats/parsing/quest/bin").severity = Severity.Off;
/**
* Used by static data generation scripts.

View File

@ -4,11 +4,12 @@ import { BufferCursor } from "../src/core/data_formats/cursor/BufferCursor";
import { parse_rlc } from "../src/core/data_formats/parsing/rlc";
import * as yaml from "yaml";
import { Endianness } from "../src/core/data_formats/Endianness";
import { LogLevel, LogManager } from "../src/core/Logger";
import { LogManager } from "../src/core/Logger";
import { Severity } from "../src/core/Severity";
const logger = LogManager.get("assets_generation/update_generic_data");
LogManager.default_level = LogLevel.Trace;
LogManager.default_severity = Severity.Trace;
const OPCODES_YML_FILE = `${RESOURCE_DIR}/asm/opcodes.yml`;
const OPCODES_SRC_FILE = `${SRC_DIR}/core/data_formats/asm/opcodes.ts`;

View File

@ -1,9 +1,10 @@
import { initialize_application } from "./index";
import { LogHandler, LogLevel, LogManager } from "../core/Logger";
import { LogHandler, LogManager } from "../core/Logger";
import { FileSystemHttpClient } from "../../test/src/core/FileSystemHttpClient";
import { timeout } from "../../test/src/utils";
import { StubThreeRenderer } from "../../test/src/core/rendering/StubThreeRenderer";
import { Random } from "../core/Random";
import { Severity } from "../core/Severity";
for (const path of [undefined, "/viewer", "/quest_editor", "/hunt_optimizer"]) {
const with_path = path == undefined ? "without specific path" : `with path ${path}`;
@ -11,8 +12,8 @@ for (const path of [undefined, "/viewer", "/quest_editor", "/hunt_optimizer"]) {
test(`Initialization and shutdown ${with_path} should succeed without throwing or logging errors.`, async () => {
const logged_errors: string[] = [];
const handler: LogHandler = ({ level, message }) => {
if (level >= LogLevel.Error) {
const handler: LogHandler = ({ severity, message }) => {
if (severity >= Severity.Error) {
logged_errors.push(message);
}
};

View File

@ -1,54 +1,35 @@
import { enum_values } from "./enums";
import { assert } from "./util";
// Log level names in order of importance.
export enum LogLevel {
Trace,
Debug,
Info,
Warn,
Error,
Off,
}
export const LogLevels = enum_values<LogLevel>(LogLevel);
export function log_level_from_string(str: string): LogLevel {
const level = (LogLevel as any)[str.slice(0, 1).toUpperCase() + str.slice(1).toLowerCase()];
assert(level != undefined, () => `"${str}" is not a valid log level.`);
return level;
}
import { Severity, severity_from_string } from "./Severity";
export type LogEntry = {
readonly time: Date;
readonly message: string;
readonly level: LogLevel;
readonly severity: Severity;
readonly logger: Logger;
readonly cause?: any;
};
export type LogHandler = (entry: LogEntry, logger_name: string) => void;
function default_log_handler({ time, message, level, logger, cause }: LogEntry): void {
const str = `${time_to_string(time)} [${LogLevel[level]}] ${logger.name} - ${message}`;
function default_log_handler({ time, message, severity, logger, cause }: LogEntry): void {
const str = `${time_to_string(time)} [${Severity[severity]}] ${logger.name} - ${message}`;
/* eslint-disable no-console */
let method: (...args: any[]) => void;
switch (level) {
case LogLevel.Trace:
switch (severity) {
case Severity.Trace:
method = console.trace;
break;
case LogLevel.Debug:
case Severity.Debug:
method = console.debug;
break;
case LogLevel.Info:
case Severity.Info:
method = console.info;
break;
case LogLevel.Warn:
case Severity.Warn:
method = console.warn;
break;
case LogLevel.Error:
case Severity.Error:
method = console.error;
break;
default:
@ -76,14 +57,14 @@ function time_part_to_string(value: number, n: number): string {
}
export class Logger {
private _level?: LogLevel;
private _severity?: Severity;
get level(): LogLevel {
return this._level ?? LogManager.default_level;
get severity(): Severity {
return this._severity ?? LogManager.default_severity;
}
set level(level: LogLevel) {
this._level = level;
set severity(severity: Severity) {
this._severity = severity;
}
private _handler?: LogHandler;
@ -99,28 +80,28 @@ export class Logger {
constructor(readonly name: string) {}
trace = (message: string, cause?: any): void => {
this.handle(LogLevel.Trace, message, cause);
this.log(Severity.Trace, message, cause);
};
debug = (message: string, cause?: any): void => {
this.handle(LogLevel.Debug, message, cause);
this.log(Severity.Debug, message, cause);
};
info = (message: string, cause?: any): void => {
this.handle(LogLevel.Info, message, cause);
this.log(Severity.Info, message, cause);
};
warn = (message: string, cause?: any): void => {
this.handle(LogLevel.Warn, message, cause);
this.log(Severity.Warn, message, cause);
};
error = (message: string, cause?: any): void => {
this.handle(LogLevel.Error, message, cause);
this.log(Severity.Error, message, cause);
};
private handle(level: LogLevel, message: string, cause?: any): void {
if (level >= this.level) {
this.handler({ time: new Date(), message, level, logger: this, cause }, this.name);
log(severity: Severity, message: string, cause?: any): void {
if (severity >= this.severity) {
this.handler({ time: new Date(), message, severity, logger: this, cause }, this.name);
}
}
}
@ -128,7 +109,7 @@ export class Logger {
export class LogManager {
private static readonly loggers = new Map<string, Logger>();
static default_level: LogLevel = log_level_from_string(process.env["LOG_LEVEL"] ?? "Info");
static default_severity: Severity = severity_from_string(process.env["LOG_LEVEL"] ?? "Info");
static default_handler: LogHandler = default_log_handler;
static get(name: string): Logger {

87
src/core/Result.ts Normal file
View File

@ -0,0 +1,87 @@
import { Logger } from "./Logger";
import { Severity } from "./Severity";
export type Result<T> = Success<T> | Failure;
export type Success<T> = {
readonly success: true;
readonly value: T;
readonly problems: readonly Problem[];
};
export type Failure = {
readonly success: false;
readonly value?: undefined;
readonly problems: readonly Problem[];
};
export type Problem = {
readonly severity: Severity;
readonly ui_message: string;
};
export function success<T>(value: T, problems?: readonly Problem[]): Success<T> {
return {
success: true,
value,
problems: problems ?? [],
};
}
export function failure(problems?: readonly Problem[]): Failure {
return {
success: false,
problems: problems ?? [],
};
}
/**
* "Unwraps" the given result by either return its value if it's a success or throwing an error with
* its problems as message if it was a failure.
*/
export function unwrap<T>(result: Result<T>): T {
if (result.success) {
return result.value;
} else {
throw new Error(result.problems.join("\n"));
}
}
export function result_builder<T>(logger: Logger): ResultBuilder<T> {
return new ResultBuilder(logger);
}
/**
* Useful for building up a {@link Result} and logging problems at the same time. Use
* {@link result_builder} to instantiate.
*/
export class ResultBuilder<T> {
private readonly problems: Problem[] = [];
constructor(private readonly logger: Logger) {}
/**
* Add a problem to the problems array and log it with {@link logger}.
*/
add_problem(severity: Severity, ui_message: string, message: string, cause?: any): this {
this.logger.log(severity, message, cause);
this.problems.push({ severity, ui_message });
return this;
}
/**
* Add the given result's problems.
*/
add_result(result: Result<unknown>): this {
this.problems.push(...result.problems);
return this;
}
success(value: T): Success<T> {
return success(value, this.problems);
}
failure(): Failure {
return failure(this.problems);
}
}

20
src/core/Severity.ts Normal file
View File

@ -0,0 +1,20 @@
// Severities in order of importance.
import { enum_values } from "./enums";
import { assert } from "./util";
export enum Severity {
Trace,
Debug,
Info,
Warn,
Error,
Off,
}
export const Severities = enum_values<Severity>(Severity);
export function severity_from_string(str: string): Severity {
const severity = (Severity as any)[str.slice(0, 1).toUpperCase() + str.slice(1).toLowerCase()];
assert(severity != undefined, () => `"${str}" is not a valid severity.`);
return severity;
}

View File

@ -1,11 +1,24 @@
import { Cursor } from "../cursor/Cursor";
import { Result, result_builder } from "../../Result";
import { LogManager } from "../../Logger";
import { Severity } from "../../Severity";
const logger = LogManager.get("core/data_formats/parsing/iff");
export type IffChunk = {
/**
* 32-bit unsigned integer.
*/
type: number;
data: Cursor;
readonly type: number;
readonly data: Cursor;
};
export type IffChunkHeader = {
/**
* 32-bit unsigned integer.
*/
readonly type: number;
readonly size: number;
};
/**
@ -13,22 +26,55 @@ export type IffChunk = {
* IFF files contain chunks preceded by an 8-byte header.
* The header consists of 4 ASCII characters for the "Type ID" and a 32-bit integer specifying the chunk size.
*/
export function parse_iff(cursor: Cursor): IffChunk[] {
const chunks: IffChunk[] = [];
export function parse_iff(cursor: Cursor, silent = false): Result<IffChunk[]> {
return parse(cursor, silent, [], (cursor, type, size) => {
return { type, data: cursor.take(size) };
});
}
while (cursor.bytes_left) {
/**
* Parses just the chunk headers.
*/
export function parse_iff_headers(cursor: Cursor, silent = false): Result<IffChunkHeader[]> {
return parse(cursor, silent, [], (_, type, size) => {
return { type, size };
});
}
function parse<T>(
cursor: Cursor,
silent: boolean,
chunks: T[],
get_chunk: (cursor: Cursor, type: number, size: number) => T,
): Result<T[]> {
const result = result_builder<T[]>(logger);
let corrupted = false;
while (cursor.bytes_left >= 8) {
const type = cursor.u32();
const size_pos = cursor.position;
const size = cursor.u32();
if (size > cursor.bytes_left) {
corrupted = true;
if (!silent) {
result.add_problem(
chunks.length === 0 ? Severity.Error : Severity.Warn,
"Invalid IFF format.",
`Size ${size} was too large (only ${cursor.bytes_left} bytes left) at position ${size_pos}.`,
);
}
break;
}
chunks.push({
type,
data: cursor.take(size),
});
chunks.push(get_chunk(cursor, type, size));
}
return chunks;
if (corrupted && chunks.length === 0) {
return result.failure();
} else {
return result.success(chunks);
}
}

View File

@ -3,6 +3,7 @@ import { Vec3 } from "../../vector";
import { parse_iff } from "../iff";
import { NjcmModel, parse_njcm_model } from "./njcm";
import { parse_xj_model, XjModel } from "./xj";
import { Result, success } from "../../../Result";
export const ANGLE_TO_RAD = (2 * Math.PI) / 0xffff;
@ -113,14 +114,14 @@ export type NjEvaluationFlags = {
/**
* Parses an NJCM file.
*/
export function parse_nj(cursor: Cursor): NjObject<NjcmModel>[] {
export function parse_nj(cursor: Cursor): Result<NjObject<NjcmModel>[]> {
return parse_ninja(cursor, parse_njcm_model, []);
}
/**
* Parses an NJCM file.
*/
export function parse_xj(cursor: Cursor): NjObject<XjModel>[] {
export function parse_xj(cursor: Cursor): Result<NjObject<XjModel>[]> {
return parse_ninja(cursor, parse_xj_model, undefined);
}
@ -135,16 +136,22 @@ function parse_ninja<M extends NjModel>(
cursor: Cursor,
parse_model: (cursor: Cursor, context: any) => M,
context: any,
): NjObject<M>[] {
): Result<NjObject<M>[]> {
const parse_iff_result = parse_iff(cursor);
if (!parse_iff_result.success) {
return parse_iff_result;
}
// POF0 and other chunks types are ignored.
const njcm_chunks = parse_iff(cursor).filter(chunk => chunk.type === NJCM);
const njcm_chunks = parse_iff_result.value.filter(chunk => chunk.type === NJCM);
const objects: NjObject<M>[] = [];
for (const chunk of njcm_chunks) {
objects.push(...parse_sibling_objects(chunk.data, parse_model, context));
}
return objects;
return success(objects, parse_iff_result.problems);
}
// TODO: cache model and object offsets so we don't reparse the same data.

View File

@ -1,6 +1,8 @@
import { Cursor } from "../../cursor/Cursor";
import { parse_iff } from "../iff";
import { parse_iff, parse_iff_headers } from "../iff";
import { LogManager } from "../../../Logger";
import { Result, result_builder } from "../../../Result";
import { Severity } from "../../../Severity";
const logger = LogManager.get("core/data_formats/parsing/ninja/texture");
@ -43,8 +45,26 @@ export function parse_xvr(cursor: Cursor): XvrTexture {
};
}
export function parse_xvm(cursor: Cursor): Xvm | undefined {
const chunks = parse_iff(cursor);
export function is_xvm(cursor: Cursor): boolean {
const iff_result = parse_iff_headers(cursor, true);
if (!iff_result.success) {
return false;
}
return iff_result.value.find(chunk => chunk.type === XVMH || chunk.type === XVRT) != undefined;
}
export function parse_xvm(cursor: Cursor): Result<Xvm> {
const iff_result = parse_iff(cursor);
if (!iff_result.success) {
return iff_result;
}
const result = result_builder<Xvm>(logger);
result.add_result(iff_result);
const chunks = iff_result.value;
const header_chunk = chunks.find(chunk => chunk.type === XVMH);
const header = header_chunk && parse_header(header_chunk.data);
@ -53,16 +73,24 @@ export function parse_xvm(cursor: Cursor): Xvm | undefined {
.map(chunk => parse_xvr(chunk.data));
if (!header && textures.length === 0) {
return undefined;
result.add_problem(
Severity.Error,
"Corrupted XVM file.",
"No header and no XVRT chunks found.",
);
return result.failure();
}
if (header && header.texture_count !== textures.length) {
logger.warn(
result.add_problem(
Severity.Warn,
"Corrupted XVM file.",
`Found ${textures.length} textures instead of ${header.texture_count} as defined in the header.`,
);
}
return { textures };
return result.success({ textures });
}
function parse_header(cursor: Cursor): Header {

View File

@ -3,7 +3,6 @@ import "./Button.css";
import { Observable } from "../observable/Observable";
import { emitter } from "../observable";
import { Control } from "./Control";
import { Emitter } from "../observable/Emitter";
import { WidgetOptions } from "./Widget";
import { Property } from "../observable/property/Property";
import { WritableProperty } from "../observable/property/WritableProperty";
@ -16,20 +15,16 @@ export type ButtonOptions = WidgetOptions & {
};
export class Button extends Control {
private readonly _mousedown: Emitter<MouseEvent>;
private readonly _mouseup: Emitter<MouseEvent>;
private readonly _click: Emitter<MouseEvent>;
private readonly _keydown: Emitter<KeyboardEvent>;
private readonly _keyup: Emitter<KeyboardEvent>;
private readonly _onmouseup = emitter<MouseEvent>();
private readonly _onclick = emitter<MouseEvent>();
private readonly _onkeydown = emitter<KeyboardEvent>();
private readonly _text: WidgetProperty<string>;
private readonly center_element: HTMLSpanElement;
readonly element = button({ className: "core_Button" });
readonly mousedown: Observable<MouseEvent>;
readonly mouseup: Observable<MouseEvent>;
readonly click: Observable<MouseEvent>;
readonly keydown: Observable<KeyboardEvent>;
readonly keyup: Observable<KeyboardEvent>;
readonly onmouseup: Observable<MouseEvent> = this._onmouseup;
readonly onclick: Observable<MouseEvent> = this._onclick;
readonly onkeydown: Observable<KeyboardEvent> = this._onkeydown;
readonly text: WritableProperty<string>;
constructor(options?: ButtonOptions) {
@ -50,25 +45,9 @@ export class Button extends Control {
);
}
this._mousedown = emitter<MouseEvent>();
this.mousedown = this._mousedown;
this.element.onmousedown = (e: MouseEvent) => this._mousedown.emit({ value: e });
this._mouseup = emitter<MouseEvent>();
this.mouseup = this._mouseup;
this.element.onmouseup = (e: MouseEvent) => this._mouseup.emit({ value: e });
this._click = emitter<MouseEvent>();
this.click = this._click;
this.element.onclick = (e: MouseEvent) => this._click.emit({ value: e });
this._keydown = emitter<KeyboardEvent>();
this.keydown = this._keydown;
this.element.onkeydown = (e: KeyboardEvent) => this._keydown.emit({ value: e });
this._keyup = emitter<KeyboardEvent>();
this.keyup = this._keyup;
this.element.onkeyup = (e: KeyboardEvent) => this._keyup.emit({ value: e });
this.element.onmouseup = (e: MouseEvent) => this._onmouseup.emit({ value: e });
this.element.onclick = (e: MouseEvent) => this._onclick.emit({ value: e });
this.element.onkeydown = (e: KeyboardEvent) => this._onkeydown.emit({ value: e });
this._text = new WidgetProperty<string>(this, "", this.set_text);
this.text = this._text;

View File

@ -53,9 +53,9 @@ export class DropDown<T> extends Control {
capture: true,
}),
this.button.mouseup.observe(this.button_mouseup),
this.button.onmouseup.observe(this.button_mouseup),
this.button.keydown.observe(this.button_keydown),
this.button.onkeydown.observe(this.button_keydown),
this.menu.selected.observe(({ value }) => {
if (value !== undefined) {

View File

@ -20,7 +20,7 @@ export class FileButton extends Button {
this.element.classList.add("core_FileButton");
this.disposables(
this.click.observe(async () => {
this.onclick.observe(async () => {
this._files.val = await open_files(options);
}),
);

View File

@ -0,0 +1,42 @@
.core_ProblemsPopup {
display: flex;
flex-direction: column;
outline: none;
position: fixed;
background-color: var(--bg-color);
border: var(--border);
padding: 10px;
box-shadow: black 0 0 10px -2px;
}
.core_ProblemsPopup:focus-within {
border: var(--border-focus);
}
.core_ProblemsPopup h1 {
font-size: 20px;
margin: 0 0 10px 0;
padding-bottom: 4px;
border-bottom: var(--border);
}
.core_ProblemsPopup_description {
user-select: text;
cursor: text;
}
.core_ProblemsPopup_body {
user-select: text;
overflow: auto;
margin: 4px 0;
}
.core_ProblemsPopup_body ul {
cursor: text;
}
.core_ProblemsPopup_footer {
display: flex;
flex-direction: row;
justify-content: flex-end;
}

View File

@ -0,0 +1,103 @@
import { ResizableWidget } from "./ResizableWidget";
import { Widget } from "./Widget";
import { div, h1, li, section, ul } from "./dom";
import { Problem } from "../Result";
import { Button } from "./Button";
import { Disposable } from "../observable/Disposable";
import "./ProblemsPopup.css";
const POPUP_WIDTH = 500;
const POPUP_HEIGHT = 500;
export class ProblemsPopup extends ResizableWidget {
private x = 0;
private y = 0;
private prev_mouse_x = 0;
private prev_mouse_y = 0;
readonly element: HTMLElement;
readonly children: readonly Widget[] = [];
readonly dismiss_button = this.disposable(new Button({ text: "Dismiss" }));
constructor(description: string, problems: readonly Problem[] = []) {
super();
let header_element: HTMLElement;
this.element = section(
{ className: "core_ProblemsPopup", tabIndex: 0 },
(header_element = h1("Problems")),
div({ className: "core_ProblemsPopup_description" }, description),
div(
{ className: "core_ProblemsPopup_body" },
ul(...problems.map(problem => li(problem.ui_message))),
),
div({ className: "core_ProblemsPopup_footer" }, this.dismiss_button.element),
);
this.element.style.width = `${POPUP_WIDTH}px`;
this.element.style.maxHeight = `${POPUP_HEIGHT}px`;
this.set_position(
(window.innerWidth - POPUP_WIDTH) / 2,
(window.innerHeight - POPUP_HEIGHT) / 2,
);
this.element.addEventListener("keydown", this.keydown);
header_element.addEventListener("mousedown", this.mousedown);
this.finalize_construction();
}
set_position(x: number, y: number): void {
this.x = x;
this.y = y;
this.element.style.transform = `translate(${Math.floor(x)}px, ${Math.floor(y)}px)`;
}
private mousedown = (evt: MouseEvent): void => {
this.prev_mouse_x = evt.clientX;
this.prev_mouse_y = evt.clientY;
window.addEventListener("mousemove", this.window_mousemove);
window.addEventListener("mouseup", this.window_mouseup);
};
private window_mousemove = (evt: MouseEvent): void => {
evt.preventDefault();
this.set_position(
this.x + evt.clientX - this.prev_mouse_x,
this.y + evt.clientY - this.prev_mouse_y,
);
this.prev_mouse_x = evt.clientX;
this.prev_mouse_y = evt.clientY;
};
private window_mouseup = (evt: MouseEvent): void => {
evt.preventDefault();
window.removeEventListener("mousemove", this.window_mousemove);
window.removeEventListener("mouseup", this.window_mouseup);
};
private keydown = (evt: KeyboardEvent): void => {
if (evt.key === "Escape") {
this.dispose();
}
};
}
export function show_problems_popup(
description: string,
problems?: readonly Problem[],
): Disposable {
const popup = new ProblemsPopup(description, problems);
const onclick = popup.dismiss_button.onclick.observe(() => popup.dispose());
document.body.append(popup.element);
popup.focus();
return {
dispose() {
onclick.dispose();
popup.dispose();
},
};
}

View File

@ -58,9 +58,9 @@ export class Select<T> extends LabelledControl {
this.disposables(
disposable_listener(this.button.element, "mousedown", this.button_mousedown),
this.button.mouseup.observe(this.button_mouseup),
this.button.onmouseup.observe(this.button_mouseup),
this.button.keydown.observe(this.button_keydown),
this.button.onkeydown.observe(this.button_keydown),
this.menu.selected.observe(({ value }) => {
this._selected.set_val(value, { silent: false });

View File

@ -42,6 +42,13 @@ export function div(attributes?: Attributes<HTMLDivElement>, ...children: Child[
return create_element("div", attributes, ...children);
}
export function h1(
attributes?: Attributes<HTMLHeadingElement>,
...children: Child[]
): HTMLHeadingElement {
return create_element("h1", attributes, ...children);
}
export function h2(
attributes?: Attributes<HTMLHeadingElement>,
...children: Child[]
@ -81,6 +88,10 @@ export function p(
return create_element("p", attributes, ...children);
}
export function section(attributes?: Attributes<HTMLElement>, ...children: Child[]): HTMLElement {
return create_element("section", attributes, ...children);
}
export function span(
attributes?: Attributes<HTMLSpanElement>,
...children: Child[]

View File

@ -6,7 +6,9 @@
--text-color-disabled: hsl(0, 0%, 55%);
--font-family: Verdana, Geneva, sans-serif;
--border-color: hsl(0, 0%, 25%);
--border-color-focus: hsl(0, 0%, 35%);
--border: solid 1px var(--border-color);
--border-focus: solid 1px var(--border-color-focus);
/* Scrollbars */

View File

@ -102,7 +102,7 @@ export class WantedItemsView extends View {
const remove_button = row_disposer.add(new Button({ icon_left: Icon.Remove }));
row_disposer.add(
remove_button.click.observe(async () =>
remove_button.onclick.observe(async () =>
(await this.hunt_optimizer_stores.current.val).remove_wanted_item(wanted_item),
),
);

View File

@ -151,7 +151,7 @@ export class EventView extends View {
const remove_button = disposer.add(new Button({ icon_left: Icon.Remove }));
disposer.add_all(
remove_button.click.observe(() => this.ctrl.remove_action(this.event, action)),
remove_button.onclick.observe(() => this.ctrl.remove_action(this.event, action)),
);
return [tr(th(label), td(node), td(remove_button.element)), disposer];

View File

@ -52,7 +52,7 @@ export class EventsView extends ResizableView {
this.enabled.bind_to(ctrl.enabled),
this.add_event_button.click.observe(ctrl.add_event),
this.add_event_button.onclick.observe(ctrl.add_event),
bind_children_to(
this.dag_container_element,

View File

@ -3,8 +3,9 @@ import { ToolBar } from "../../core/gui/ToolBar";
import "./LogView.css";
import { log_store } from "../stores/LogStore";
import { Select } from "../../core/gui/Select";
import { LogEntry, LogLevel, LogLevels, time_to_string } from "../../core/Logger";
import { LogEntry, time_to_string } from "../../core/Logger";
import { ResizableView } from "../../core/gui/ResizableView";
import { Severities, Severity } from "../../core/Severity";
const AUTOSCROLL_TRESHOLD = 5;
@ -15,7 +16,7 @@ export class LogView extends ResizableView {
private readonly list_container: HTMLElement;
private readonly list_element: HTMLElement;
private readonly level_filter: Select<LogLevel>;
private readonly level_filter: Select<Severity>;
private readonly settings_bar: ToolBar;
private should_scroll_to_bottom = true;
@ -30,8 +31,8 @@ export class LogView extends ResizableView {
new Select({
class: "quest_editor_LogView_level_filter",
label: "Level:",
items: LogLevels,
to_label: level => LogLevel[level],
items: Severities,
to_label: level => Severity[level],
}),
);
@ -47,10 +48,10 @@ export class LogView extends ResizableView {
}),
this.level_filter.selected.observe(
({ value }) => value != undefined && log_store.set_level(value),
({ value }) => value != undefined && log_store.set_severity(value),
),
log_store.level.observe(
log_store.severity.observe(
({ value }) => {
this.level_filter.selected.val = value;
},
@ -83,16 +84,16 @@ export class LogView extends ResizableView {
}
};
private create_message_element = ({ time, level, message }: LogEntry): HTMLElement => {
private create_message_element = ({ time, severity, message }: LogEntry): HTMLElement => {
return div(
{
className: [
"quest_editor_LogView_message",
"quest_editor_LogView_" + LogLevel[level] + "_message",
"quest_editor_LogView_" + Severity[severity] + "_message",
].join(" "),
},
div({ className: "quest_editor_LogView_message_timestamp" }, time_to_string(time)),
div({ className: "quest_editor_LogView_message_level" }, "[" + LogLevel[level] + "]"),
div({ className: "quest_editor_LogView_message_level" }, "[" + Severity[severity] + "]"),
div({ className: "quest_editor_LogView_message_contents" }, message),
);
};

View File

@ -111,35 +111,35 @@ export class QuestEditorToolBar extends ToolBar {
open_file_button.files.observe(({ value: files }) => ctrl.parse_files(files)),
save_as_button.click.observe(ctrl.save_as),
save_as_button.onclick.observe(ctrl.save_as),
save_as_button.enabled.bind_to(ctrl.can_save),
undo_button.click.observe(() => undo_manager.undo()),
undo_button.onclick.observe(() => undo_manager.undo()),
undo_button.enabled.bind_to(ctrl.can_undo),
redo_button.click.observe(() => undo_manager.redo()),
redo_button.onclick.observe(() => undo_manager.redo()),
redo_button.enabled.bind_to(ctrl.can_redo),
area_select.selected.bind_to(ctrl.current_area),
area_select.selected.observe(({ value }) => ctrl.set_area(value!)),
area_select.enabled.bind_to(ctrl.can_select_area),
debug_button.click.observe(ctrl.debug),
debug_button.onclick.observe(ctrl.debug),
debug_button.enabled.bind_to(ctrl.can_debug),
resume_button.click.observe(ctrl.resume),
resume_button.onclick.observe(ctrl.resume),
resume_button.enabled.bind_to(ctrl.can_step),
step_over_button.click.observe(ctrl.step_over),
step_over_button.onclick.observe(ctrl.step_over),
step_over_button.enabled.bind_to(ctrl.can_step),
step_in_button.click.observe(ctrl.step_in),
step_in_button.onclick.observe(ctrl.step_in),
step_in_button.enabled.bind_to(ctrl.can_step),
step_out_button.click.observe(ctrl.step_out),
step_out_button.onclick.observe(ctrl.step_out),
step_out_button.enabled.bind_to(ctrl.can_step),
stop_button.click.observe(ctrl.stop),
stop_button.onclick.observe(ctrl.stop),
stop_button.enabled.bind_to(ctrl.can_stop),
);

View File

@ -56,8 +56,8 @@ export class EntityAssetLoader implements Disposable {
const cursor = new ArrayBufferCursor(data, Endianness.Little);
const nj_objects = url.endsWith(".nj") ? parse_nj(cursor) : parse_xj(cursor);
if (nj_objects.length) {
return ninja_object_to_buffer_geometry(nj_objects[0]);
if (nj_objects.success && nj_objects.value.length) {
return ninja_object_to_buffer_geometry(nj_objects.value[0]);
} else {
logger.warn(`Couldn't parse ${url} for ${entity_type_to_string(type)}.`);
return DEFAULT_ENTITY;
@ -79,7 +79,7 @@ export class EntityAssetLoader implements Disposable {
.then(({ data }) => {
const cursor = new ArrayBufferCursor(data, Endianness.Little);
const xvm = parse_xvm(cursor);
return xvm === undefined ? [] : xvm_to_textures(xvm);
return xvm.success ? xvm_to_textures(xvm.value) : [];
})
.catch(e => {
logger.warn(

View File

@ -3,27 +3,28 @@ import { Property } from "../../core/observable/property/Property";
import { list_property, property } from "../../core/observable";
import { Disposable } from "../../core/observable/Disposable";
import { Disposer } from "../../core/observable/Disposer";
import { LogEntry, Logger, LogHandler, LogLevel, LogManager } from "../../core/Logger";
import { LogEntry, Logger, LogHandler, LogManager } from "../../core/Logger";
import { Severity } from "../../core/Severity";
const logger = LogManager.get("quest_editor/stroes/LogStore");
const logger = LogManager.get("quest_editor/stores/LogStore");
export class LogStore implements Disposable {
private readonly disposer = new Disposer();
private readonly default_log_level = LogLevel.Info;
private readonly default_log_severity = Severity.Info;
private readonly log_buffer: LogEntry[] = [];
private readonly logger_name_buffer: string[] = [];
private readonly _level = property<LogLevel>(this.default_log_level);
private readonly _severity = property<Severity>(this.default_log_severity);
private readonly _log = list_property<LogEntry>();
private readonly handler: LogHandler = (entry: LogEntry, logger_name: string): void => {
this.buffer_log_entry(entry, logger_name);
};
readonly level: Property<LogLevel> = this._level;
readonly severity: Property<Severity> = this._severity;
readonly log: ListProperty<LogEntry> = this._log.filtered(
this.level.map(level => message => message.level >= level),
this.severity.map(severity => message => message.severity >= severity),
);
get_logger(name: string): Logger {
@ -36,8 +37,8 @@ export class LogStore implements Disposable {
this.disposer.dispose();
}
set_level(log_level: LogLevel): void {
this._level.val = log_level;
set_severity(severity: Severity): void {
this._severity.val = severity;
}
private buffer_log_entry(entry: LogEntry, logger_name: string): void {
@ -64,7 +65,7 @@ export class LogStore implements Disposable {
this.log_buffer.splice(DROP_THRESHOLD_HALF, drop_len, {
time: new Date(),
message: `...dropped ${drop_len} messages...`,
level: LogLevel.Warn,
severity: Severity.Warn,
logger,
});
this.logger_name_buffer.splice(

View File

@ -17,6 +17,7 @@ export class TextureController extends Controller {
private readonly _textures: WritableListProperty<XvrTexture> = list_property();
readonly textures: ListProperty<XvrTexture> = this._textures;
// TODO: notify user of problems.
load_file = async (file: File): Promise<void> => {
try {
const ext = filename_extension(file.name).toLowerCase();
@ -25,8 +26,8 @@ export class TextureController extends Controller {
if (ext === "xvm") {
const xvm = parse_xvm(new ArrayBufferCursor(buffer, Endianness.Little));
if (xvm) {
this._textures.splice(0, Infinity, ...xvm.textures);
if (xvm.success) {
this._textures.splice(0, Infinity, ...xvm.value.textures);
}
} else if (ext === "afs") {
const afs = parse_afs(new ArrayBufferCursor(buffer, Endianness.Little));
@ -36,13 +37,13 @@ export class TextureController extends Controller {
const cursor = new ArrayBufferCursor(buffer, Endianness.Little);
const xvm = parse_xvm(cursor);
if (xvm) {
textures.push(...xvm.textures);
if (xvm.success) {
textures.push(...xvm.value.textures);
} else {
const xvm = parse_xvm(prs_decompress(cursor.seek_start(0)));
if (xvm) {
textures.push(...xvm.textures);
if (xvm.success) {
textures.push(...xvm.value.textures);
}
}
}

View File

@ -6,9 +6,13 @@ import { ArrayBufferCursor } from "../../../core/data_formats/cursor/ArrayBuffer
import { Endianness } from "../../../core/data_formats/Endianness";
import { parse_nj, parse_xj } from "../../../core/data_formats/parsing/ninja";
import { parse_njm } from "../../../core/data_formats/parsing/ninja/motion";
import { parse_xvm, XvrTexture } from "../../../core/data_formats/parsing/ninja/texture";
import { is_xvm, parse_xvm, XvrTexture } from "../../../core/data_formats/parsing/ninja/texture";
import { parse_afs } from "../../../core/data_formats/parsing/afs";
import { LogManager } from "../../../core/Logger";
import { prs_decompress } from "../../../core/data_formats/compression/prs/decompress";
import { show_problems_popup } from "../../../core/gui/ProblemsPopup";
import { failure, Result, result_builder } from "../../../core/Result";
import { Severity } from "../../../core/Severity";
const logger = LogManager.get("viewer/controllers/model/ModelToolBarController");
@ -49,16 +53,24 @@ export class ModelToolBarController extends Controller {
this.store.set_animation_frame(frame);
};
// TODO: notify user of problems.
load_file = async (file: File): Promise<void> => {
try {
const buffer = await read_file(file);
const cursor = new ArrayBufferCursor(buffer, Endianness.Little);
let result: Result<any> | undefined;
if (file.name.endsWith(".nj")) {
this.store.set_current_nj_object(parse_nj(cursor)[0]);
result = parse_nj(cursor);
if (result.success) {
this.store.set_current_nj_object(result.value[0]);
}
} else if (file.name.endsWith(".xj")) {
this.store.set_current_nj_object(parse_xj(cursor)[0]);
result = parse_xj(cursor);
if (result.success) {
this.store.set_current_nj_object(result.value[0]);
}
} else if (file.name.endsWith(".njm")) {
this.store.set_current_animation(undefined);
this.store.set_current_nj_motion(undefined);
@ -70,20 +82,59 @@ export class ModelToolBarController extends Controller {
this.store.set_current_nj_motion(parse_njm(cursor, nj_object.bone_count()));
}
} else if (file.name.endsWith(".xvm")) {
this.store.set_current_textures(parse_xvm(cursor)?.textures ?? []);
result = parse_xvm(cursor);
if (result.success) {
this.store.set_current_textures(result.value.textures);
} else {
this.store.set_current_textures([]);
}
} else if (file.name.endsWith(".afs")) {
const files = parse_afs(cursor);
const textures: XvrTexture[] = files.flatMap(
file =>
parse_xvm(new ArrayBufferCursor(file, Endianness.Little))?.textures ?? [],
);
const rb = result_builder(logger);
const textures: XvrTexture[] = files.flatMap(file => {
const cursor = new ArrayBufferCursor(file, Endianness.Little);
if (is_xvm(cursor)) {
const xvm_result = parse_xvm(cursor);
rb.add_result(xvm_result);
return xvm_result.value?.textures ?? [];
} else {
const xvm_result = parse_xvm(prs_decompress(cursor.seek_start(0)));
rb.add_result(xvm_result);
return xvm_result.value?.textures ?? [];
}
});
if (textures.length) {
result = rb.success(textures);
} else {
result = rb.failure();
}
this.store.set_current_textures(textures);
} else {
logger.error(`Unknown file extension in filename "${file.name}".`);
logger.error(`Unsupported file extension in filename "${file.name}".`);
show_problems_popup("Unsupported file type.");
}
if (result) {
let description: string;
if (result.success) {
description = `Encountered some problems while opening "${file.name}".`;
} else {
description = `Couldn't open "${file.name}" because of these problems.`;
}
if (result.problems.length) {
show_problems_popup(description, result.problems);
}
}
} catch (e) {
logger.error("Couldn't read file.", e);
show_problems_popup(`Couldn't open "${file.name}".`);
}
};
}

View File

@ -24,6 +24,7 @@ import {
import { parse_xvm, XvrTexture } from "../../core/data_formats/parsing/ninja/texture";
import { parse_afs } from "../../core/data_formats/parsing/afs";
import { SectionId } from "../../core/model";
import { unwrap } from "../../core/Result";
export class CharacterClassAssetLoader implements Disposable {
private readonly nj_object_cache: Map<
@ -126,7 +127,7 @@ export class CharacterClassAssetLoader implements Disposable {
return this.http_client
.get(character_class_to_url(player_class, body_part, no))
.array_buffer()
.then(buffer => parse_nj(new ArrayBufferCursor(buffer, Endianness.Little))[0]);
.then(buffer => unwrap(parse_nj(new ArrayBufferCursor(buffer, Endianness.Little)))[0]);
}
/**
@ -174,8 +175,8 @@ export class CharacterClassAssetLoader implements Disposable {
for (const file of afs) {
const xvm = parse_xvm(new ArrayBufferCursor(file, Endianness.Little));
if (xvm) {
textures.push(...xvm.textures);
if (xvm.success) {
textures.push(...xvm.value.textures);
}
}