mirror of
https://github.com/DaanVandenBosch/phantasmal-world.git
synced 2025-04-04 22:58:29 +08:00
The model viewer now shows a problems popup when loading a file failed or succeeded with some problems.
This commit is contained in:
parent
7f5accf790
commit
8580cd4f66
@ -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.
|
||||
|
@ -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`;
|
||||
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
@ -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
87
src/core/Result.ts
Normal 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
20
src/core/Severity.ts
Normal 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;
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
}),
|
||||
);
|
||||
|
42
src/core/gui/ProblemsPopup.css
Normal file
42
src/core/gui/ProblemsPopup.css
Normal 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;
|
||||
}
|
103
src/core/gui/ProblemsPopup.ts
Normal file
103
src/core/gui/ProblemsPopup.ts
Normal 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();
|
||||
},
|
||||
};
|
||||
}
|
@ -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 });
|
||||
|
@ -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[]
|
||||
|
@ -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 */
|
||||
|
||||
|
@ -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),
|
||||
),
|
||||
);
|
||||
|
@ -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];
|
||||
|
@ -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,
|
||||
|
@ -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),
|
||||
);
|
||||
};
|
||||
|
@ -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),
|
||||
);
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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(
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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}".`);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user