Replaced js-logger. Improved testability with mocks, improved test configuration and code improvements.

This commit is contained in:
Daan Vanden Bosch 2019-12-25 00:17:02 +01:00
parent 243638879c
commit 99d50d754d
90 changed files with 1498 additions and 738 deletions

View File

@ -1,10 +1,11 @@
module.exports = {
preset: "ts-jest",
testEnvironment: "node",
moduleDirectories: ["node_modules"],
setupFiles: ["./test/src/setup.js"],
roots: ["./src", "./test"],
moduleNameMapper: {
"\\.(css|gif|jpg|png|svg|ttf)$": "<rootDir>/src/__mocks__/static_files.js",
"monaco-editor": "<rootDir>/node_modules/monaco-editor/dev/vs/editor/editor.main.js",
"^monaco-editor$": "<rootDir>/node_modules/monaco-editor/esm/vs/editor/editor.main.js",
"^worker-loader!": "<rootDir>/src/__mocks__/webworkers.js",
},
};

View File

@ -9,14 +9,13 @@
"camera-controls": "^1.16.2",
"golden-layout": "^1.5.9",
"javascript-lp-solver": "0.4.17",
"js-logger": "^1.6.0",
"lodash": "^4.17.15",
"luxon": "^1.21.3",
"monaco-editor": "^0.19.0",
"three": "^0.111.0"
},
"scripts": {
"start": "webpack-dev-server --port 1623 --config webpack.dev.js",
"start": "webpack-dev-server --config webpack.dev.js",
"build": "webpack --config webpack.prod.js",
"test": "jest",
"update_generic_data": "ts-node --project=tsconfig-scripts.json assets_generation/update_generic_data.ts",

View File

@ -0,0 +1,89 @@
class Editor {
addCommand() {}
getAction() {}
addAction() {
return { dispose() {} };
}
trigger() {}
updateOptions() {}
setModel() {}
setPosition() {}
getLineDecorations() {}
deltaDecorations() {}
revealLineInCenterIfOutsideViewport() {}
revealPositionInCenterIfOutsideViewport() {}
onDidFocusEditorWidget() {
return { dispose() {} };
}
onMouseDown() {
return { dispose() {} };
}
onMouseUp() {
return { dispose() {} };
}
focus() {}
layout() {}
onDidChangeCursorPosition() {
return { dispose() {} };
}
dispose() {}
}
exports.editor = {
defineTheme() {},
createModel() {},
create() {
return new Editor();
},
};
exports.languages = {
CompletionItemKind: {
Method: 0,
Function: 1,
Constructor: 2,
Field: 3,
Variable: 4,
Class: 5,
Struct: 6,
Interface: 7,
Module: 8,
Property: 9,
Event: 10,
Operator: 11,
Unit: 12,
Value: 13,
Constant: 14,
Enum: 15,
EnumMember: 16,
Keyword: 17,
Text: 18,
Color: 19,
File: 20,
Reference: 21,
Customcolor: 22,
Folder: 23,
TypeParameter: 24,
Snippet: 25,
},
register() {},
setMonarchTokensProvider() {},
registerCompletionItemProvider() {},
registerSignatureHelpProvider() {},
setLanguageConfiguration() {},
registerDefinitionProvider() {},
};
exports.KeyMod = {
CtrlCmd: 0,
Shift: 1,
Alt: 2,
WinCtrl: 3,
};
exports.KeyCode = {
LeftArrow: 15,
UpArrow: 16,
RightArrow: 17,
DownArrow: 18,
};

View File

@ -0,0 +1,6 @@
class Worker {
onmessage() {}
postMessage() {}
}
module.exports = Worker;

View File

@ -0,0 +1,49 @@
import { initialize_application } from "./index";
import { DisposableThreeRenderer } from "../core/rendering/Renderer";
import { LogManager, LogHandler, LogLevel } from "../core/Logger";
import { FileSystemHttpClient } from "../../test/src/core/FileSystemHttpClient";
import { timeout } from "../../test/src/utils";
for (const path of [undefined, "/viewer", "/quest_editor", "/hunt_optimizer"]) {
const with_path = path == undefined ? "without specific path" : `with path ${path}`;
test(`Initialization and shutdown ${with_path} should succeed without throwing errors or logging with level Warn or above.`, async () => {
const logged_errors: string[] = [];
const handler: LogHandler = ({ level, message }) => {
if (level >= LogLevel.Warn) {
logged_errors.push(message);
}
};
return LogManager.with_default_handler(handler, async () => {
if (path != undefined) {
window.location.hash = path;
}
const app = initialize_application(
new FileSystemHttpClient(),
() => new StubRenderer(),
);
expect(app).toBeDefined();
expect(logged_errors).toEqual([]);
await timeout(2000);
app.dispose();
expect(logged_errors).toEqual([]);
});
});
}
class StubRenderer implements DisposableThreeRenderer {
domElement: HTMLCanvasElement = document.createElement("canvas");
dispose(): void {} // eslint-disable-line
render(): void {} // eslint-disable-line
setSize(): void {} // eslint-disable-line
}

132
src/application/index.ts Normal file
View File

@ -0,0 +1,132 @@
import { HttpClient } from "../core/HttpClient";
import { Disposable } from "../core/observable/Disposable";
import { GuiStore, GuiTool } from "../core/stores/GuiStore";
import { create_item_type_stores } from "../core/stores/ItemTypeStore";
import { create_item_drop_stores } from "../hunt_optimizer/stores/ItemDropStore";
import { ApplicationView } from "./gui/ApplicationView";
import { throttle } from "lodash";
import { DisposableThreeRenderer } from "../core/rendering/Renderer";
import { Disposer } from "../core/observable/Disposer";
import { disposable_custom_listener, disposable_listener } from "../core/gui/dom";
export function initialize_application(
http_client: HttpClient,
create_three_renderer: () => DisposableThreeRenderer,
): Disposable {
const disposer = new Disposer();
// Disable native undo/redo.
disposer.add(disposable_custom_listener(document, "beforeinput", before_input));
// Work-around for FireFox:
disposer.add(disposable_listener(document, "keydown", keydown));
// Disable native drag-and-drop to avoid users dragging in unsupported file formats and leaving
// the application unexpectedly.
disposer.add_all(
disposable_listener(document, "dragenter", dragenter),
disposable_listener(document, "dragover", dragover),
disposable_listener(document, "drop", drop),
);
// Initialize core stores shared by several submodules.
const gui_store = disposer.add(new GuiStore());
const item_type_stores = disposer.add(create_item_type_stores(http_client, gui_store));
const item_drop_stores = disposer.add(
create_item_drop_stores(http_client, gui_store, item_type_stores),
);
// Initialize application view.
const application_view = disposer.add(
new ApplicationView(gui_store, [
[
GuiTool.Viewer,
async () => {
const { initialize_viewer } = await import("../viewer");
const viewer = disposer.add(
initialize_viewer(http_client, gui_store, create_three_renderer),
);
return viewer.view;
},
],
[
GuiTool.QuestEditor,
async () => {
const { initialize_quest_editor } = await import("../quest_editor");
const quest_editor = disposer.add(
initialize_quest_editor(http_client, gui_store, create_three_renderer),
);
return quest_editor.view;
},
],
[
GuiTool.HuntOptimizer,
async () => {
const { initialize_hunt_optimizer } = await import("../hunt_optimizer");
const hunt_optimizer = disposer.add(
initialize_hunt_optimizer(
http_client,
gui_store,
item_type_stores,
item_drop_stores,
),
);
return hunt_optimizer.view;
},
],
]),
);
// Resize the view on window resize.
const resize = throttle(
() => {
application_view.resize(window.innerWidth, window.innerHeight);
},
100,
{ leading: true, trailing: true },
);
resize();
document.body.append(application_view.element);
disposer.add(disposable_listener(window, "resize", resize));
return {
dispose(): void {
disposer.dispose();
},
};
}
function before_input(e: Event): void {
const ie = e as any;
if (ie.inputType === "historyUndo" || ie.inputType === "historyRedo") {
e.preventDefault();
}
}
function keydown(e: Event): void {
const kbe = e as KeyboardEvent;
if (kbe.ctrlKey && !kbe.altKey && kbe.key.toUpperCase() === "Z") {
kbe.preventDefault();
}
}
function dragenter(e: DragEvent): void {
e.preventDefault();
if (e.dataTransfer) {
e.dataTransfer.dropEffect = "none";
}
}
function dragover(e: DragEvent): void {
dragenter(e);
}
function drop(e: DragEvent): void {
dragenter(e);
}

View File

@ -33,12 +33,12 @@ export class StubHttpClient implements HttpClient {
get(url: string): HttpResponse {
return {
json<T>(): Promise<T> {
throw new Error(`Dummy client's json method invoked for get request to "${url}".`);
throw new Error(`Stub client's json method invoked for get request to "${url}".`);
},
array_buffer(): Promise<ArrayBuffer> {
throw new Error(
`Dummy client's array_buffer method invoked for get request to "${url}".`,
`Stub client's array_buffer method invoked for get request to "${url}".`,
);
},
};

154
src/core/Logger.ts Normal file
View File

@ -0,0 +1,154 @@
import { enum_values } from "./enums";
import { assert } from "./util";
// Log level names in order of importance.
export enum LogLevel {
Trace,
Debug,
Info,
Warn,
Error,
}
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;
}
export type LogEntry = {
readonly time: Date;
readonly message: string;
readonly level: LogLevel;
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}`;
/* eslint-disable no-console */
let method: (...args: any[]) => void;
switch (level) {
case LogLevel.Trace:
method = console.trace;
break;
case LogLevel.Debug:
method = console.debug;
break;
case LogLevel.Info:
method = console.info;
break;
case LogLevel.Warn:
method = console.warn;
break;
case LogLevel.Error:
method = console.error;
break;
default:
method = console.log;
}
if (cause == undefined) {
method.call(console, str);
} else {
method.call(console, str, cause);
}
/* eslint-enable no-console */
}
export function time_to_string(time: Date): string {
const hours = time_part_to_string(time.getHours(), 2);
const minutes = time_part_to_string(time.getMinutes(), 2);
const seconds = time_part_to_string(time.getSeconds(), 2);
const millis = time_part_to_string(time.getMilliseconds(), 3);
return `${hours}:${minutes}:${seconds}.${millis}`;
}
function time_part_to_string(value: number, n: number): string {
return value.toString().padStart(n, "0");
}
export class Logger {
private _level?: LogLevel;
get level(): LogLevel {
return this._level ?? LogManager.default_level;
}
set level(level: LogLevel) {
this._level = level;
}
private _handler?: LogHandler;
get handler(): LogHandler {
return this._handler ?? LogManager.default_handler;
}
set handler(handler: LogHandler) {
this._handler = handler;
}
constructor(readonly name: string) {}
trace = (message: string, cause?: any): void => {
this.handle(LogLevel.Trace, message, cause);
};
debug = (message: string, cause?: any): void => {
this.handle(LogLevel.Debug, message, cause);
};
info = (message: string, cause?: any): void => {
this.handle(LogLevel.Info, message, cause);
};
warn = (message: string, cause?: any): void => {
this.handle(LogLevel.Warn, message, cause);
};
error = (message: string, cause?: any): void => {
this.handle(LogLevel.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);
}
}
}
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_handler: LogHandler = default_log_handler;
static get(name: string): Logger {
let logger = this.loggers.get(name);
if (!logger) {
logger = new Logger(name);
this.loggers.set(name, logger);
}
return logger;
}
static with_default_handler<T>(handler: LogHandler, f: () => T): T {
const orig_handler = this.default_handler;
try {
this.default_handler = handler;
return f();
} finally {
this.default_handler = orig_handler;
}
}
}

View File

@ -1,7 +1,7 @@
import Logger from "js-logger";
import { Server } from "./model";
import { LogManager } from "./Logger";
const logger = Logger.get("core/persistence/Persister");
const logger = LogManager.get("core/persistence/Persister");
export abstract class Persister {
protected persist_for_server(server: Server, key: string, data: any): void {

View File

@ -1,10 +1,10 @@
import Logger from "js-logger";
import { Cursor } from "../../cursor/Cursor";
import { ResizableBufferCursor } from "../../cursor/ResizableBufferCursor";
import { WritableCursor } from "../../cursor/WritableCursor";
import { ResizableBuffer } from "../../ResizableBuffer";
import { LogManager } from "../../../Logger";
const logger = Logger.get("core/data_formats/compression/prs/decompress");
const logger = LogManager.get("core/data_formats/compression/prs/decompress");
export function prs_decompress(cursor: Cursor): Cursor {
const ctx = new Context(cursor);

View File

@ -1,8 +1,8 @@
import Logger from "js-logger";
import { Cursor } from "../../cursor/Cursor";
import { Vec2, Vec3 } from "../../vector";
import { LogManager } from "../../../Logger";
const logger = Logger.get("core/data_formats/parsing/ninja/njcm");
const logger = LogManager.get("core/data_formats/parsing/ninja/njcm");
// TODO:
// - colors

View File

@ -1,8 +1,8 @@
import Logger from "js-logger";
import { Cursor } from "../../cursor/Cursor";
import { parse_iff } from "../iff";
import { LogManager } from "../../../Logger";
const logger = Logger.get("core/data_formats/parsing/ninja/texture");
const logger = LogManager.get("core/data_formats/parsing/ninja/texture");
export type Xvm = {
textures: XvmTexture[];

View File

@ -1,8 +1,8 @@
import Logger from "js-logger";
import { Cursor } from "../../cursor/Cursor";
import { Vec2, Vec3 } from "../../vector";
import { LogManager } from "../../../Logger";
const logger = Logger.get("core/data_formats/parsing/ninja/xj");
const logger = LogManager.get("core/data_formats/parsing/ninja/xj");
// TODO:
// - vertex colors

View File

@ -1,9 +1,9 @@
import Logger from "js-logger";
import { prs_decompress } from "../compression/prs/decompress";
import { Cursor } from "../cursor/Cursor";
import { prc_decrypt } from "../encryption/prc";
import { LogManager } from "../../Logger";
const logger = Logger.get("core/data_formats/parsing/prc");
const logger = LogManager.get("core/data_formats/parsing/prc");
/**
* Decrypts and decompresses a .prc file.

View File

@ -1,4 +1,3 @@
import Logger from "js-logger";
import { Endianness } from "../../Endianness";
import { ControlFlowGraph } from "../../../../quest_editor/scripting/data_flow_analysis/ControlFlowGraph";
import { register_value } from "../../../../quest_editor/scripting/data_flow_analysis/register_value";
@ -27,8 +26,9 @@ import { Cursor } from "../../cursor/Cursor";
import { ResizableBufferCursor } from "../../cursor/ResizableBufferCursor";
import { WritableCursor } from "../../cursor/WritableCursor";
import { ResizableBuffer } from "../../ResizableBuffer";
import { LogManager } from "../../../Logger";
const logger = Logger.get("core/data_formats/parsing/quest/bin");
const logger = LogManager.get("core/data_formats/parsing/quest/bin");
export type BinFile = {
readonly quest_id: number;

View File

@ -1,4 +1,3 @@
import Logger from "js-logger";
import { groupBy } from "lodash";
import { Endianness } from "../../Endianness";
import { Cursor } from "../../cursor/Cursor";
@ -7,8 +6,9 @@ import { ResizableBuffer } from "../../ResizableBuffer";
import { Vec3 } from "../../vector";
import { WritableCursor } from "../../cursor/WritableCursor";
import { assert } from "../../../util";
import { LogManager } from "../../../Logger";
const logger = Logger.get("core/data_formats/parsing/quest/dat");
const logger = LogManager.get("core/data_formats/parsing/quest/dat");
const OBJECT_SIZE = 68;
const NPC_SIZE = 72;

View File

@ -1,4 +1,3 @@
import Logger from "js-logger";
import {
Instruction,
InstructionSegment,
@ -20,8 +19,9 @@ import { object_data, ObjectType, pso_id_to_object_type } from "./object_types";
import { parse_qst, QstContainedFile, write_qst } from "./qst";
import { npc_data, NpcType } from "./npc_types";
import { reinterpret_f32_as_i32, reinterpret_i32_as_f32 } from "../../../primitive_conversion";
import { LogManager } from "../../../Logger";
const logger = Logger.get("core/data_formats/parsing/quest");
const logger = LogManager.get("core/data_formats/parsing/quest");
export type Quest = {
readonly id: number;

View File

@ -1,4 +1,3 @@
import Logger from "js-logger";
import { Endianness } from "../../Endianness";
import { ArrayBufferCursor } from "../../cursor/ArrayBufferCursor";
import { Cursor } from "../../cursor/Cursor";
@ -6,8 +5,9 @@ import { ResizableBufferCursor } from "../../cursor/ResizableBufferCursor";
import { WritableCursor } from "../../cursor/WritableCursor";
import { ResizableBuffer } from "../../ResizableBuffer";
import { basename } from "../../../util";
import { LogManager } from "../../../Logger";
const logger = Logger.get("core/data_formats/parsing/quest/qst");
const logger = LogManager.get("core/data_formats/parsing/quest/qst");
export type QstContainedFile = {
id?: number;
@ -114,8 +114,8 @@ type QstHeader = {
function parse_headers(cursor: Cursor): QstHeader[] {
const headers: QstHeader[] = [];
let prev_quest_id: number | undefined;
let prev_file_name: string | undefined;
let prev_quest_id: number | undefined = undefined;
let prev_file_name: string | undefined = undefined;
for (let i = 0; i < 4; ++i) {
cursor.seek(4);

View File

@ -1,9 +1,9 @@
import Logger from "js-logger";
import { Endianness } from "../Endianness";
import { Cursor } from "../cursor/Cursor";
import { parse_prc } from "./prc";
import { LogManager } from "../../Logger";
const logger = Logger.get("core/data_formats/parsing/rlc");
const logger = LogManager.get("core/data_formats/parsing/rlc");
const MARKER = "RelChunkVer0.20";
/**

View File

@ -1,3 +1,5 @@
@import "./Button.css";
.core_FileButton_input {
overflow: hidden;
clip: rect(0, 0, 0, 0);

View File

@ -1,6 +1,5 @@
import { create_element, el, icon, Icon } from "./dom";
import "./FileButton.css";
import "./Button.css";
import { property } from "../observable";
import { Property } from "../observable/property/Property";
import { Control, ControlOptions } from "./Control";

View File

@ -1,4 +1,3 @@
/* eslint-disable no-dupe-class-members */
import { LabelledControl, LabelledControlOptions } from "./LabelledControl";
import { create_element, el } from "./dom";
import { WritableProperty } from "../observable/property/WritableProperty";
@ -57,13 +56,7 @@ export abstract class Input<T> extends LabelledControl {
protected abstract set_value(value: T): void;
protected set_attr<T>(attr: InputAttrsOfType<T>, value?: T | Property<T>): void;
protected set_attr<T, U>(
attr: InputAttrsOfType<U>,
value: T | Property<T> | undefined,
convert: (value: T) => U,
): void;
protected set_attr<T, U>(
protected set_attr<T, U = T>(
attr: InputAttrsOfType<U>,
value?: T | Property<T>,
convert?: (value: T) => U,

View File

@ -22,7 +22,9 @@ export class LazyWidget extends ResizableWidget {
this.initialized = true;
this.create_view().then(view => {
if (!this.disposed) {
if (this.disposed) {
view.dispose();
} else {
this.view = this.disposable(view);
this.view.resize(this.width, this.height);
this.element.append(view.element);

View File

@ -39,7 +39,7 @@ export class TabContainer extends ResizableWidget {
});
this.bar_element.append(tab_element);
const lazy_view = new LazyWidget(tab.create_view);
const lazy_view = this.disposable(new LazyWidget(tab.create_view));
this.tabs.push({
...tab,
@ -48,7 +48,6 @@ export class TabContainer extends ResizableWidget {
});
this.panes_element.append(lazy_view.element);
this.disposable(lazy_view);
}
if (this.tabs.length) {

View File

@ -4,9 +4,9 @@ import { ListProperty } from "../observable/property/list/ListProperty";
import { Disposer } from "../observable/Disposer";
import "./Table.css";
import { Disposable } from "../observable/Disposable";
import Logger = require("js-logger");
import { LogManager } from "../Logger";
const logger = Logger.get("core/gui/Table");
const logger = LogManager.get("core/gui/Table");
export type Column<T> = {
key?: string;

View File

@ -5,9 +5,9 @@ import { bind_hidden } from "./dom";
import { WritableProperty } from "../observable/property/WritableProperty";
import { WidgetProperty } from "../observable/property/WidgetProperty";
import { Property } from "../observable/property/Property";
import Logger from "js-logger";
import { LogManager } from "../Logger";
const logger = Logger.get("core/gui/Widget");
const logger = LogManager.get("core/gui/Widget");
export type WidgetOptions = {
id?: string;

View File

@ -234,17 +234,60 @@ export function section_id_icon(section_id: SectionId, options?: { size?: number
return element;
}
export function disposable_listener<K extends keyof GlobalEventHandlersEventMap>(
target: GlobalEventHandlers,
type: K,
listener: (this: GlobalEventHandlers, ev: GlobalEventHandlersEventMap[K]) => any,
options?: AddEventListenerOptions,
): Disposable;
export function disposable_listener<K extends keyof WindowEventHandlersEventMap>(
target: WindowEventHandlers,
type: K,
listener: (this: WindowEventHandlers, ev: WindowEventHandlersEventMap[K]) => any,
options?: AddEventListenerOptions,
): Disposable;
export function disposable_listener<K extends keyof DocumentAndElementEventHandlersEventMap>(
target: DocumentAndElementEventHandlers,
type: K,
listener: (
this: DocumentAndElementEventHandlers,
ev: DocumentAndElementEventHandlersEventMap[K],
) => any,
options?: AddEventListenerOptions,
): Disposable;
export function disposable_listener(
element: EventTarget,
event: string,
target:
| GlobalEventHandlers
| DocumentAndElementEventHandlers
| WindowEventHandlers
| EventTarget,
type: string,
listener: EventListenerOrEventListenerObject,
options?: AddEventListenerOptions,
): Disposable {
element.addEventListener(event, listener, options);
target.addEventListener(type, listener, options);
return {
dispose(): void {
element.removeEventListener(event, listener);
target.removeEventListener(type, listener);
},
};
}
/**
* More lax definition of {@link disposable_listener} for custom and experimental event types.
*/
export function disposable_custom_listener(
target: EventTarget,
type: string,
listener: EventListenerOrEventListenerObject,
options?: AddEventListenerOptions,
): Disposable {
target.addEventListener(type, listener, options);
return {
dispose(): void {
target.removeEventListener(type, listener);
},
};
}

View File

@ -1,7 +1,7 @@
import { Disposable } from "./Disposable";
import Logger = require("js-logger");
import { LogManager } from "../Logger";
const logger = Logger.get("core/observable/Disposer");
const logger = LogManager.get("core/observable/Disposer");
/**
* Container for disposables.
@ -29,7 +29,9 @@ export class Disposer implements Disposable {
* Add a single disposable and return the given disposable.
*/
add<T extends Disposable>(disposable: T): T {
if (!this._disposed) {
if (this.disposed) {
disposable.dispose();
} else {
this.disposables.push(disposable);
}

View File

@ -1,9 +1,9 @@
import { Disposable } from "./Disposable";
import Logger from "js-logger";
import { Emitter } from "./Emitter";
import { ChangeEvent } from "./Observable";
import { LogManager } from "../Logger";
const logger = Logger.get("core/observable/SimpleEmitter");
const logger = LogManager.get("core/observable/SimpleEmitter");
export class SimpleEmitter<T> implements Emitter<T> {
protected readonly observers: ((event: ChangeEvent<T>) => void)[] = [];

View File

@ -1,8 +1,8 @@
import { Disposable } from "../Disposable";
import Logger from "js-logger";
import { Property, PropertyChangeEvent } from "./Property";
import { LogManager } from "../../Logger";
const logger = Logger.get("core/observable/property/AbstractMinimalProperty");
const logger = LogManager.get("core/observable/property/AbstractMinimalProperty");
// This class exists purely because otherwise the resulting cyclic dependency graph would trip up commonjs.
// The dependency graph is still cyclic but for some reason it's not a problem this way.

View File

@ -3,9 +3,9 @@ import { AbstractProperty } from "../AbstractProperty";
import { Disposable } from "../../Disposable";
import { Observable } from "../../Observable";
import { Property } from "../Property";
import Logger from "js-logger";
import { LogManager } from "../../../Logger";
const logger = Logger.get("core/observable/property/list/AbstractListProperty");
const logger = LogManager.get("core/observable/property/list/AbstractListProperty");
class LengthProperty extends AbstractProperty<number> {
private length = 0;

View File

@ -0,0 +1,27 @@
import { Server } from "../model";
import { GuiStore } from "./GuiStore";
import { ServerMap } from "./ServerMap";
import { Disposable } from "../observable/Disposable";
import { Disposer } from "../observable/Disposer";
export class DisposableServerMap<T extends Disposable> extends ServerMap<T> implements Disposable {
private readonly disposer = new Disposer();
constructor(gui_store: GuiStore, get_value: (server: Server) => Promise<T>) {
super(gui_store, async server => {
const value = await get_value(server);
if (this.disposer.disposed) {
value.dispose();
} else {
this.disposer.add(value);
}
return value;
});
}
dispose(): void {
this.disposer.dispose();
}
}

View File

@ -3,6 +3,8 @@ import { Disposable } from "../observable/Disposable";
import { property } from "../observable";
import { Property } from "../observable/property/Property";
import { Server } from "../model";
import { Store } from "./Store";
import { disposable_listener } from "../gui/dom";
export enum GuiTool {
Viewer,
@ -17,24 +19,17 @@ const GUI_TOOL_TO_STRING = new Map([
]);
const STRING_TO_GUI_TOOL = new Map([...GUI_TOOL_TO_STRING.entries()].map(([k, v]) => [v, k]));
export class GuiStore implements Disposable {
readonly tool: WritableProperty<GuiTool> = property(GuiTool.Viewer);
readonly server: Property<Server>;
export class GuiStore extends Store {
private readonly _server: WritableProperty<Server> = property(Server.Ephinea);
private readonly hash_disposer = this.tool.observe(({ value: tool }) => {
let hash = `#/${gui_tool_to_string(tool)}`;
if (this.features.size) {
hash += "?features=" + [...this.features].join(",");
}
window.location.hash = hash;
});
private readonly global_keydown_handlers = new Map<string, (e: KeyboardEvent) => void>();
private readonly features: Set<string> = new Set();
readonly tool: WritableProperty<GuiTool> = property(GuiTool.Viewer);
readonly server: Property<Server> = this._server;
constructor() {
super();
const url = window.location.hash.slice(2);
const [tool_str, params_str] = url.split("?");
@ -51,18 +46,21 @@ export class GuiStore implements Disposable {
}
}
this.tool.val = string_to_gui_tool(tool_str) || GuiTool.Viewer;
this.disposables(
this.tool.observe(({ value: tool }) => {
let hash = `#/${gui_tool_to_string(tool)}`;
this.server = this._server;
window.addEventListener("keydown", this.dispatch_global_keydown);
if (this.features.size) {
hash += "?features=" + [...this.features].join(",");
}
dispose(): void {
this.hash_disposer.dispose();
this.global_keydown_handlers.clear();
window.location.hash = hash;
}),
window.removeEventListener("keydown", this.dispatch_global_keydown);
disposable_listener(window, "keydown", this.dispatch_global_keydown),
);
this.tool.val = string_to_gui_tool(tool_str) || GuiTool.Viewer;
}
on_global_keydown(

View File

@ -6,23 +6,25 @@ import {
UnitItemType,
WeaponItemType,
} from "../model/items";
import { ServerMap } from "./ServerMap";
import { Server } from "../model";
import { ItemTypeDto } from "../dto/ItemTypeDto";
import { GuiStore } from "./GuiStore";
import { HttpClient } from "../HttpClient";
import { DisposableServerMap } from "./DisposableServerMap";
import { Store } from "./Store";
export function create_item_type_stores(
http_client: HttpClient,
gui_store: GuiStore,
): ServerMap<ItemTypeStore> {
return new ServerMap(gui_store, create_loader(http_client));
): DisposableServerMap<ItemTypeStore> {
return new DisposableServerMap(gui_store, create_loader(http_client));
}
export class ItemTypeStore {
export class ItemTypeStore extends Store {
readonly item_types: ItemType[];
constructor(item_types: ItemType[], private readonly id_to_item_type: ItemType[]) {
super();
this.item_types = item_types;
}

18
src/core/stores/Store.ts Normal file
View File

@ -0,0 +1,18 @@
import { Disposable } from "../observable/Disposable";
import { Disposer } from "../observable/Disposer";
export abstract class Store implements Disposable {
private readonly disposer = new Disposer();
dispose(): void {
this.disposer.dispose();
}
protected disposable<T extends Disposable>(disposable: T): T {
return this.disposer.add(disposable);
}
protected disposables(...disposables: Disposable[]): void {
this.disposer.add_all(...disposables);
}
}

View File

@ -3,9 +3,9 @@ import { WritableListProperty } from "../observable/property/list/WritableListPr
import { Action } from "./Action";
import { list_property, map, property } from "../observable";
import { undo_manager } from "./UndoManager";
import Logger = require("js-logger");
import { LogManager } from "../Logger";
const logger = Logger.get("core/undo/UndoStack");
const logger = LogManager.get("core/undo/UndoStack");
/**
* Full-fledged linear undo/redo implementation.

View File

@ -15,9 +15,9 @@ import { SortDirection, Table } from "../../core/gui/Table";
import { list_property } from "../../core/observable";
import { ServerMap } from "../../core/stores/ServerMap";
import { HuntMethodStore } from "../stores/HuntMethodStore";
import Logger from "js-logger";
import { LogManager } from "../../core/Logger";
const logger = Logger.get("hunt_optimizer/gui/MethodsForEpisodeView");
const logger = LogManager.get("hunt_optimizer/gui/MethodsForEpisodeView");
export class MethodsForEpisodeView extends ResizableWidget {
readonly element = el.div({ class: "hunt_optimizer_MethodsForEpisodeView" });

View File

@ -10,9 +10,9 @@ import "./OptimizationResultView.css";
import { Duration } from "luxon";
import { ServerMap } from "../../core/stores/ServerMap";
import { HuntOptimizerStore } from "../stores/HuntOptimizerStore";
import Logger from "js-logger";
import { LogManager } from "../../core/Logger";
const logger = Logger.get("hunt_optimizer/gui/OptimizationResultView");
const logger = LogManager.get("hunt_optimizer/gui/OptimizationResultView");
export class OptimizationResultView extends Widget {
readonly element = el.div(
@ -31,6 +31,7 @@ export class OptimizationResultView extends Widget {
async ({ value }) => {
try {
const hunt_optimizer_store = await value;
if (this.disposed) return;
if (this.results_observer) {
this.results_observer.dispose();

View File

@ -11,9 +11,9 @@ import { ItemType } from "../../core/model/items";
import { Disposable } from "../../core/observable/Disposable";
import { ServerMap } from "../../core/stores/ServerMap";
import { HuntOptimizerStore } from "../stores/HuntOptimizerStore";
import Logger from "js-logger";
import { LogManager } from "../../core/Logger";
const logger = Logger.get("hunt_optimizer/gui/WantedItemsView");
const logger = LogManager.get("hunt_optimizer/gui/WantedItemsView");
export class WantedItemsView extends Widget {
readonly element = el.div({ class: "hunt_optimizer_WantedItemsView" });

View File

@ -1,32 +1,43 @@
import { HuntOptimizerView } from "./gui/HuntOptimizerView";
import { ServerMap } from "../core/stores/ServerMap";
import { HuntMethodStore, create_hunt_method_stores } from "./stores/HuntMethodStore";
import { create_hunt_method_stores, HuntMethodStore } from "./stores/HuntMethodStore";
import { GuiStore } from "../core/stores/GuiStore";
import { HuntOptimizerStore, create_hunt_optimizer_stores } from "./stores/HuntOptimizerStore";
import { create_hunt_optimizer_stores, HuntOptimizerStore } from "./stores/HuntOptimizerStore";
import { ItemTypeStore } from "../core/stores/ItemTypeStore";
import { HuntMethodPersister } from "./persistence/HuntMethodPersister";
import { HuntOptimizerPersister } from "./persistence/HuntOptimizerPersister";
import { ItemDropStore } from "./stores/ItemDropStore";
import { HttpClient } from "../core/HttpClient";
import { Disposable } from "../core/observable/Disposable";
import { Disposer } from "../core/observable/Disposer";
export function initialize_hunt_optimizer(
http_client: HttpClient,
gui_store: GuiStore,
item_type_stores: ServerMap<ItemTypeStore>,
item_drop_stores: ServerMap<ItemDropStore>,
): HuntOptimizerView {
const hunt_method_stores: ServerMap<HuntMethodStore> = create_hunt_method_stores(
http_client,
gui_store,
new HuntMethodPersister(),
): { view: HuntOptimizerView } & Disposable {
const disposer = new Disposer();
const hunt_method_stores: ServerMap<HuntMethodStore> = disposer.add(
create_hunt_method_stores(http_client, gui_store, new HuntMethodPersister()),
);
const hunt_optimizer_stores: ServerMap<HuntOptimizerStore> = create_hunt_optimizer_stores(
const hunt_optimizer_stores: ServerMap<HuntOptimizerStore> = disposer.add(
create_hunt_optimizer_stores(
gui_store,
new HuntOptimizerPersister(item_type_stores),
item_type_stores,
item_drop_stores,
hunt_method_stores,
),
);
return new HuntOptimizerView(hunt_optimizer_stores, hunt_method_stores);
const view = disposer.add(new HuntOptimizerView(hunt_optimizer_stores, hunt_method_stores));
return {
view,
dispose(): void {
disposer.dispose();
},
};
}

View File

@ -1,7 +1,7 @@
import { Persister } from "../../core/Persister";
import { Server } from "../../core/model";
import { HuntMethodModel } from "../model/HuntMethodModel";
import { Duration } from "luxon";
import { Persister } from "../../core/Persister";
const METHOD_USER_TIMES_KEY = "HuntMethodStore.methodUserTimes";

View File

@ -1,4 +1,3 @@
import Logger from "js-logger";
import { Server } from "../../core/model";
import { QuestDto } from "../dto/QuestDto";
import { NpcType } from "../../core/data_formats/parsing/quest/npc_types";
@ -8,13 +7,13 @@ import { HuntMethodPersister } from "../persistence/HuntMethodPersister";
import { Duration } from "luxon";
import { ListProperty } from "../../core/observable/property/list/ListProperty";
import { list_property } from "../../core/observable";
import { Disposable } from "../../core/observable/Disposable";
import { Disposer } from "../../core/observable/Disposer";
import { GuiStore } from "../../core/stores/GuiStore";
import { ServerMap } from "../../core/stores/ServerMap";
import { HttpClient } from "../../core/HttpClient";
import { Store } from "../../core/stores/Store";
import { DisposableServerMap } from "../../core/stores/DisposableServerMap";
import { LogManager } from "../../core/Logger";
const logger = Logger.get("hunt_optimizer/stores/HuntMethodStore");
const logger = LogManager.get("hunt_optimizer/stores/HuntMethodStore");
const DEFAULT_DURATION = Duration.fromObject({ minutes: 30 });
const DEFAULT_GOVERNMENT_TEST_DURATION = Duration.fromObject({ minutes: 45 });
@ -24,32 +23,28 @@ export function create_hunt_method_stores(
http_client: HttpClient,
gui_store: GuiStore,
hunt_method_persister: HuntMethodPersister,
): ServerMap<HuntMethodStore> {
return new ServerMap(gui_store, create_loader(http_client, hunt_method_persister));
): DisposableServerMap<HuntMethodStore> {
return new DisposableServerMap(gui_store, create_loader(http_client, hunt_method_persister));
}
export class HuntMethodStore implements Disposable {
export class HuntMethodStore extends Store {
readonly methods: ListProperty<HuntMethodModel>;
private readonly disposer = new Disposer();
constructor(
hunt_method_persister: HuntMethodPersister,
server: Server,
methods: HuntMethodModel[],
) {
super();
this.methods = list_property(method => [method.user_time], ...methods);
this.disposer.add(
this.disposables(
this.methods.observe_list(() =>
hunt_method_persister.persist_method_user_times(this.methods.val, server),
),
);
}
dispose(): void {
this.disposer.dispose();
}
}
function create_loader(

View File

@ -19,11 +19,11 @@ import { WritableListProperty } from "../../core/observable/property/list/Writab
import { HuntMethodStore } from "./HuntMethodStore";
import { ItemDropStore } from "./ItemDropStore";
import { ItemTypeStore } from "../../core/stores/ItemTypeStore";
import { Disposable } from "../../core/observable/Disposable";
import { Disposer } from "../../core/observable/Disposer";
import { ServerMap } from "../../core/stores/ServerMap";
import { GuiStore } from "../../core/stores/GuiStore";
import { HuntOptimizerPersister } from "../persistence/HuntOptimizerPersister";
import { DisposableServerMap } from "../../core/stores/DisposableServerMap";
import { Store } from "../../core/stores/Store";
export function create_hunt_optimizer_stores(
gui_store: GuiStore,
@ -31,8 +31,8 @@ export function create_hunt_optimizer_stores(
item_type_stores: ServerMap<ItemTypeStore>,
item_drop_stores: ServerMap<ItemDropStore>,
hunt_method_stores: ServerMap<HuntMethodStore>,
): ServerMap<HuntOptimizerStore> {
return new ServerMap(
): DisposableServerMap<HuntOptimizerStore> {
return new DisposableServerMap(
gui_store,
create_loader(
hunt_optimizer_persister,
@ -50,16 +50,15 @@ export function create_hunt_optimizer_stores(
// TODO: Show expected value or probability per item per method.
// Can be useful when deciding which item to hunt first.
// TODO: boxes.
export class HuntOptimizerStore implements Disposable {
readonly huntable_item_types: ItemType[];
// TODO: wanted items per server.
readonly wanted_items: ListProperty<WantedItemModel>;
readonly result: Property<OptimalResultModel | undefined>;
export class HuntOptimizerStore extends Store {
private readonly _wanted_items: WritableListProperty<
WantedItemModel
> = list_property(wanted_item => [wanted_item.amount]);
private readonly disposer = new Disposer();
readonly huntable_item_types: ItemType[];
// TODO: wanted items per server.
readonly wanted_items: ListProperty<WantedItemModel> = this._wanted_items;
readonly result: Property<OptimalResultModel | undefined>;
constructor(
private readonly hunt_optimizer_persister: HuntOptimizerPersister,
@ -68,21 +67,17 @@ export class HuntOptimizerStore implements Disposable {
private readonly item_drop_store: ItemDropStore,
hunt_method_store: HuntMethodStore,
) {
super();
this.huntable_item_types = item_type_store.item_types.filter(
item_type => item_drop_store.enemy_drops.get_drops_for_item_type(item_type.id).length,
);
this.wanted_items = this._wanted_items;
this.result = map(this.optimize, this.wanted_items, hunt_method_store.methods);
this.initialize_persistence();
}
dispose(): void {
this.disposer.dispose();
}
add_wanted_item(item_type: ItemType): void {
if (!this._wanted_items.val.find(wanted => wanted.item_type === item_type)) {
this._wanted_items.push(new WantedItemModel(item_type, 1));
@ -336,7 +331,7 @@ export class HuntOptimizerStore implements Disposable {
private initialize_persistence = async (): Promise<void> => {
this._wanted_items.val = await this.hunt_optimizer_persister.load_wanted_items(this.server);
this.disposer.add(
this.disposable(
this._wanted_items.observe(({ value }) => {
this.hunt_optimizer_persister.persist_wanted_items(this.server, value);
}),

View File

@ -1,27 +1,30 @@
import { Difficulties, Difficulty, SectionId, SectionIds, Server } from "../../core/model";
import { ServerMap } from "../../core/stores/ServerMap";
import Logger from "js-logger";
import { NpcType } from "../../core/data_formats/parsing/quest/npc_types";
import { EnemyDrop } from "../model/ItemDrop";
import { EnemyDropDto } from "../dto/drops";
import { GuiStore } from "../../core/stores/GuiStore";
import { ItemTypeStore } from "../../core/stores/ItemTypeStore";
import { HttpClient } from "../../core/HttpClient";
import { DisposableServerMap } from "../../core/stores/DisposableServerMap";
import { Store } from "../../core/stores/Store";
import { LogManager } from "../../core/Logger";
const logger = Logger.get("stores/ItemDropStore");
const logger = LogManager.get("stores/ItemDropStore");
export function create_item_drop_stores(
http_client: HttpClient,
gui_store: GuiStore,
item_type_stores: ServerMap<ItemTypeStore>,
): ServerMap<ItemDropStore> {
return new ServerMap(gui_store, create_loader(http_client, item_type_stores));
): DisposableServerMap<ItemDropStore> {
return new DisposableServerMap(gui_store, create_loader(http_client, item_type_stores));
}
export class ItemDropStore {
export class ItemDropStore extends Store {
readonly enemy_drops: EnemyDropTable;
constructor(enemy_drops: EnemyDropTable) {
super();
this.enemy_drops = enemy_drops;
}
}

View File

@ -1,22 +1,17 @@
import "./core/gui/index.css";
import Logger from "js-logger";
import "@fortawesome/fontawesome-free/js/fontawesome";
import "@fortawesome/fontawesome-free/js/solid";
import "@fortawesome/fontawesome-free/js/regular";
import "@fortawesome/fontawesome-free/js/brands";
import { initialize } from "./initialize";
import { initialize_application } from "./application";
import { FetchClient } from "./core/HttpClient";
import { WebGLRenderer } from "three";
import { DisposableThreeRenderer } from "./core/rendering/Renderer";
Logger.useDefaults({
defaultLevel: (Logger as any)[process.env["LOG_LEVEL"] ?? "INFO"],
});
function create_three_renderer(): DisposableThreeRenderer {
const renderer = new WebGLRenderer({ antialias: true, alpha: true });
renderer.setPixelRatio(window.devicePixelRatio);
return renderer;
}
initialize(new FetchClient(), create_three_renderer);
initialize_application(new FetchClient(), create_three_renderer);

View File

@ -1,46 +0,0 @@
/**
* @jest-environment jsdom
*/
import Logger from "js-logger";
import { IContext } from "js-logger/src/types";
import { initialize } from "./initialize";
import { StubHttpClient } from "./core/HttpClient";
import { DisposableThreeRenderer } from "./core/rendering/Renderer";
for (const path of [undefined, "/viewer", "/quest_editor", "/hunt_optimizer"]) {
const with_path = path == undefined ? "without specific path" : `with path ${path}`;
test(`Initialization and shutdown ${with_path} should succeed without throwing errors or logging with level WARN or above.`, () => {
const logged_errors: string[] = [];
Logger.setHandler((messages: any[], context: IContext) => {
if (context.level.value >= Logger.WARN.value) {
logged_errors.push(Array.prototype.join.call(messages, " "));
}
});
if (path != undefined) {
window.location.hash = path;
}
const app = initialize(new StubHttpClient(), () => new StubRenderer());
expect(app).toBeDefined();
expect(logged_errors).toEqual([]);
app.dispose();
expect(logged_errors).toEqual([]);
});
}
class StubRenderer implements DisposableThreeRenderer {
domElement: HTMLCanvasElement = document.createElement("canvas");
dispose(): void {} // eslint-disable-line
render(): void {} // eslint-disable-line
setSize(): void {} // eslint-disable-line
}

View File

@ -1,121 +0,0 @@
import { HttpClient } from "./core/HttpClient";
import { Disposable } from "./core/observable/Disposable";
import { GuiStore, GuiTool } from "./core/stores/GuiStore";
import { create_item_type_stores } from "./core/stores/ItemTypeStore";
import { create_item_drop_stores } from "./hunt_optimizer/stores/ItemDropStore";
import { ApplicationView } from "./application/gui/ApplicationView";
import { throttle } from "lodash";
import { DisposableThreeRenderer } from "./core/rendering/Renderer";
export function initialize(
http_client: HttpClient,
create_three_renderer: () => DisposableThreeRenderer,
): Disposable {
// Disable native undo/redo.
document.addEventListener("beforeinput", before_input);
// Work-around for FireFox:
document.addEventListener("keydown", keydown);
// Disable native drag-and-drop.
document.addEventListener("dragenter", dragenter);
document.addEventListener("dragover", dragover);
document.addEventListener("drop", drop);
// Initialize core stores shared by several submodules.
const gui_store = new GuiStore();
const item_type_stores = create_item_type_stores(http_client, gui_store);
const item_drop_stores = create_item_drop_stores(http_client, gui_store, item_type_stores);
// Initialize application view.
const application_view = new ApplicationView(gui_store, [
[
GuiTool.Viewer,
async () => {
return (await import("./viewer/index")).initialize_viewer(
http_client,
gui_store,
create_three_renderer,
);
},
],
[
GuiTool.QuestEditor,
async () => {
return (await import("./quest_editor/index")).initialize_quest_editor(
http_client,
gui_store,
create_three_renderer,
);
},
],
[
GuiTool.HuntOptimizer,
async () => {
return (await import("./hunt_optimizer/index")).initialize_hunt_optimizer(
http_client,
gui_store,
item_type_stores,
item_drop_stores,
);
},
],
]);
// Resize the view on window resize.
const resize = throttle(
() => {
application_view.resize(window.innerWidth, window.innerHeight);
},
100,
{ leading: true, trailing: true },
);
resize();
document.body.append(application_view.element);
window.addEventListener("resize", resize);
// Dispose view and global event listeners when necessary.
return {
dispose(): void {
window.removeEventListener("beforeinput", before_input);
window.removeEventListener("keydown", keydown);
window.removeEventListener("resize", resize);
window.removeEventListener("dragenter", dragenter);
window.removeEventListener("dragover", dragover);
window.removeEventListener("drop", drop);
application_view.dispose();
},
};
}
function before_input(e: Event): void {
const ie = e as any;
if (ie.inputType === "historyUndo" || ie.inputType === "historyRedo") {
e.preventDefault();
}
}
function keydown(e: Event): void {
const kbe = e as KeyboardEvent;
if (kbe.ctrlKey && !kbe.altKey && kbe.key.toUpperCase() === "Z") {
kbe.preventDefault();
}
}
function dragenter(e: DragEvent): void {
e.preventDefault();
if (e.dataTransfer) {
e.dataTransfer.dropEffect = "none";
}
}
function dragover(e: DragEvent): void {
dragenter(e);
}
function drop(e: DragEvent): void {
dragenter(e);
}

View File

@ -54,7 +54,7 @@ export type GameState = Readonly<GameStateInternal>;
* delegates to {@link Debugger}.
*/
export class QuestRunner {
private quest_logger = log_store.get_logger("quest_editor/QuestRunner");
private logger = log_store.get_logger("quest_editor/QuestRunner");
private animation_frame?: number;
private startup = true;
private readonly _state: WritableProperty<QuestRunnerState> = property(
@ -102,7 +102,7 @@ export class QuestRunner {
this.stop();
// Runner state.
this.quest_logger.info("Starting debugger.");
this.logger.info("Starting debugger.");
this.startup = true;
this.initial_area_id = 0;
this.npcs.splice(0, this.npcs.length, ...quest.npcs.val);
@ -148,7 +148,7 @@ export class QuestRunner {
return;
}
this.quest_logger.info("Stopping debugger.");
this.logger.info("Stopping debugger.");
if (this.animation_frame != undefined) {
cancelAnimationFrame(this.animation_frame);
@ -292,15 +292,15 @@ export class QuestRunner {
},
window_msg: (msg: string): void => {
this.quest_logger.info(`window_msg "${msg}"`);
this.logger.info(`window_msg "${msg}"`);
},
message: (msg: string): void => {
this.quest_logger.info(`message "${msg}"`);
this.logger.info(`message "${msg}"`);
},
add_msg: (msg: string): void => {
this.quest_logger.info(`add_msg "${msg}"`);
this.logger.info(`add_msg "${msg}"`);
},
winend: (): void => {
@ -317,15 +317,15 @@ export class QuestRunner {
},
list: (list_items: string[]): void => {
this.quest_logger.info(`list "[${list_items}]"`);
this.logger.info(`list "[${list_items}]"`);
},
warning: (msg: string, inst_ptr?: InstructionPointer): void => {
this.quest_logger.warning(message_with_inst_ptr(msg, inst_ptr));
this.logger.warn(message_with_inst_ptr(msg, inst_ptr));
},
error: (err: Error, inst_ptr?: InstructionPointer): void => {
this.quest_logger.error(message_with_inst_ptr(err.message, inst_ptr));
this.logger.error(message_with_inst_ptr(err.message, inst_ptr));
},
};
};
@ -339,7 +339,7 @@ export class QuestRunner {
const label = this._game_state.floor_handlers.get(area_id);
if (label == undefined) {
this.quest_logger.debug(`No floor handler registered for floor ${area_id}.`);
this.logger.debug(`No floor handler registered for floor ${area_id}.`);
} else {
this.vm.start_thread(label);
this.schedule_frame();

View File

@ -1,11 +1,9 @@
/**
* @jest-environment jsdom
*/
import { GuiStore } from "../../core/stores/GuiStore";
import { create_area_store } from "../../../test/src/quest_editor/stores/store_creation";
import { QuestEditorStore } from "../stores/QuestEditorStore";
import { QuestEditorToolBarController } from "./QuestEditorToolBarController";
import { Episode } from "../../core/data_formats/parsing/quest/Episode";
import { next_animation_frame } from "../../../test/src/utils";
test("Some widgets should only be enabled when a quest is loaded.", async () => {
const gui_store = new GuiStore();
@ -59,7 +57,3 @@ test("Debugging controls should be enabled and disabled at the right times.", as
ctrl.stop();
}
});
function next_animation_frame(): Promise<void> {
return new Promise(resolve => requestAnimationFrame(() => resolve()));
}

View File

@ -13,10 +13,10 @@ import { parse_quest, write_quest_qst } from "../../core/data_formats/parsing/qu
import { ArrayBufferCursor } from "../../core/data_formats/cursor/ArrayBufferCursor";
import { Endianness } from "../../core/data_formats/Endianness";
import { convert_quest_from_model, convert_quest_to_model } from "../stores/model_conversion";
import Logger from "js-logger";
import { create_element } from "../../core/gui/dom";
import { LogManager } from "../../core/Logger";
const logger = Logger.get("quest_editor/controllers/QuestEditorToolBarController");
const logger = LogManager.get("quest_editor/controllers/QuestEditorToolBarController");
export type AreaAndLabel = { readonly area: AreaModel; readonly label: string };

View File

@ -1,6 +1,3 @@
/**
* @jest-environment jsdom
*/
import {
create_area_store,
create_quest_editor_store,

View File

@ -39,6 +39,6 @@
color: hsl(0, 80%, 50%);
}
.quest_editor_LogView .quest_editor_LogView_Warning_message .quest_editor_LogView_message_level {
.quest_editor_LogView .quest_editor_LogView_Warn_message .quest_editor_LogView_message_level {
color: hsl(30, 80%, 50%);
}

View File

@ -2,8 +2,9 @@ import { bind_children_to, el } from "../../core/gui/dom";
import { ResizableWidget } from "../../core/gui/ResizableWidget";
import { ToolBar } from "../../core/gui/ToolBar";
import "./LogView.css";
import { log_store, LogLevel, LogLevels, LogMessage } from "../stores/LogStore";
import { log_store } from "../stores/LogStore";
import { Select } from "../../core/gui/Select";
import { LogEntry, LogLevel, LogLevels, time_to_string } from "../../core/Logger";
const AUTOSCROLL_TRESHOLD = 5;
@ -44,18 +45,15 @@ export class LogView extends ResizableWidget {
this.list_container.addEventListener("scroll", this.scrolled);
this.disposables(
bind_children_to(
this.list_element,
log_store.log_messages,
this.create_message_element,
{ after: this.scroll_to_bottom },
),
bind_children_to(this.list_element, log_store.log, this.create_message_element, {
after: this.scroll_to_bottom,
}),
this.level_filter.selected.observe(
({ value }) => value != undefined && log_store.set_log_level(value),
({ value }) => value != undefined && log_store.set_level(value),
),
log_store.log_level.observe(
log_store.level.observe(
({ value }) => {
this.level_filter.selected.val = value;
},
@ -88,25 +86,25 @@ export class LogView extends ResizableWidget {
}
};
private create_message_element = (msg: LogMessage): HTMLElement => {
private create_message_element = ({ time, level, message }: LogEntry): HTMLElement => {
return el.div(
{
class: [
"quest_editor_LogView_message",
"quest_editor_LogView_" + LogLevel[msg.level] + "_message",
"quest_editor_LogView_" + LogLevel[level] + "_message",
].join(" "),
},
el.div({
class: "quest_editor_LogView_message_timestamp",
text: msg.time.toTimeString().slice(0, 8),
text: time_to_string(time),
}),
el.div({
class: "quest_editor_LogView_message_level",
text: "[" + LogLevel[msg.level] + "]",
text: "[" + LogLevel[level] + "]",
}),
el.div({
class: "quest_editor_LogView_message_contents",
text: msg.message,
text: message,
}),
);
};

View File

@ -1,6 +1,3 @@
/**
* @jest-environment jsdom
*/
import { QuestEditorToolBarController } from "../controllers/QuestEditorToolBarController";
import { QuestEditorToolBar } from "./QuestEditorToolBar";
import { GuiStore } from "../../core/stores/GuiStore";

View File

@ -18,9 +18,9 @@ import { LogView } from "./LogView";
import { QuestRunnerRendererView } from "./QuestRunnerRendererView";
import { QuestEditorStore } from "../stores/QuestEditorStore";
import { QuestEditorUiPersister } from "../persistence/QuestEditorUiPersister";
import Logger = require("js-logger");
import { LogManager } from "../../core/Logger";
const logger = Logger.get("quest_editor/gui/QuestEditorView");
const logger = LogManager.get("quest_editor/gui/QuestEditorView");
const DEFAULT_LAYOUT_CONFIG = {
settings: {

View File

@ -1,6 +1,3 @@
/**
* @jest-environment jsdom
*/
import { QuestInfoController } from "../controllers/QuestInfoController";
import { undo_manager } from "../../core/undo/UndoManager";
import { QuestInfoView } from "./QuestInfoView";

View File

@ -0,0 +1,181 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Renders correctly. 1`] = `
<div
class="core_ToolBar"
style="height: 33px;"
>
<div
class="core_DropDown"
>
<button
class="core_Button"
>
<span
class="core_Button_inner"
>
<span
class="core_Button_left"
>
<span
class="fas fa-file-medical"
/>
</span>
<span
class="core_Button_center"
>
New quest
</span>
<span
class="core_Button_right"
>
<span
class="fas fa-caret-down"
/>
</span>
</span>
</button>
<div
class="core_Menu"
hidden=""
tabindex="-1"
>
<div
class="core_Menu_inner"
>
<div
data-index="0"
>
Episode I
</div>
</div>
</div>
</div>
<label
class="core_FileButton core_Button"
title="Open a quest file (Ctrl-O)"
>
<span
class="core_FileButton_inner core_Button_inner"
>
<span
class="core_FileButton_left core_Button_left"
>
<span
class="fas fa-file"
/>
</span>
<span
class="core_Button_center"
>
Open file...
</span>
</span>
<input
accept=".qst"
class="core_FileButton_input core_Button_inner"
type="file"
/>
</label>
<button
class="core_Button disabled"
disabled=""
title="Save this quest to new file (Ctrl-Shift-S)"
>
<span
class="core_Button_inner"
>
<span
class="core_Button_left"
>
<span
class="fas fa-save"
/>
</span>
<span
class="core_Button_center"
>
Save as...
</span>
</span>
</button>
<button
class="core_Button disabled"
disabled=""
title="Nothing to undo (Ctrl-Z)"
>
<span
class="core_Button_inner"
>
<span
class="core_Button_left"
>
<span
class="fas fa-undo"
/>
</span>
<span
class="core_Button_center"
>
Undo
</span>
</span>
</button>
<button
class="core_Button disabled"
disabled=""
title="Nothing to redo (Ctrl-Shift-Z)"
>
<span
class="core_Button_inner"
>
<span
class="core_Button_left"
>
<span
class="fas fa-redo"
/>
</span>
<span
class="core_Button_center"
>
Redo
</span>
</span>
</button>
<div
class="core_Select disabled"
>
<button
class="core_Button disabled"
disabled=""
>
<span
class="core_Button_inner"
>
<span
class="core_Button_center"
>
</span>
<span
class="core_Button_right"
>
<span
class="fas fa-caret-down"
/>
</span>
</span>
</button>
<div
class="core_Menu"
hidden=""
tabindex="-1"
>
<div
class="core_Menu_inner"
/>
</div>
</div>
</div>
`;

View File

@ -0,0 +1,218 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Renders correctly with a current quest.: should render property inputs 1`] = `
<div
class="quest_editor_QuestInfoView"
tabindex="-1"
>
<table>
<tr>
<th>
Episode:
</th>
<td>
I
</td>
</tr>
<tr>
<th>
ID:
</th>
<td>
<span
class="core_NumberInput core_Input"
style="width: 54px;"
>
<input
class="core_NumberInput_inner core_Input_inner"
step="any"
type="number"
/>
</span>
</td>
</tr>
<tr>
<th>
Name:
</th>
<td>
<span
class="core_TextInput core_Input"
>
<input
class="core_TextInput_inner core_Input_inner"
maxlength="32"
type="text"
/>
</span>
</td>
</tr>
<tr>
<th
colspan="2"
>
Short description:
</th>
</tr>
<tr>
<td
colspan="2"
>
<div
class="core_TextArea"
>
<textarea
class="core_TextArea_inner"
cols="25"
maxlength="128"
rows="5"
style="font-family: \\"Courier New\\", monospace;"
/>
</div>
</td>
</tr>
<tr>
<th
colspan="2"
>
Long description:
</th>
</tr>
<tr>
<td
colspan="2"
>
<div
class="core_TextArea"
>
<textarea
class="core_TextArea_inner"
cols="25"
maxlength="288"
rows="10"
style="font-family: \\"Courier New\\", monospace;"
/>
</div>
</td>
</tr>
</table>
<div
class="quest_editor_UnavailableView"
hidden=""
>
<label
class="core_Label disabled"
>
No quest loaded.
</label>
</div>
</div>
`;
exports[`Renders correctly without a current quest.: should render a "No quest loaded." view 1`] = `
<div
class="quest_editor_QuestInfoView"
tabindex="-1"
>
<table
hidden=""
>
<tr>
<th>
Episode:
</th>
<td />
</tr>
<tr>
<th>
ID:
</th>
<td>
<span
class="core_NumberInput core_Input"
style="width: 54px;"
>
<input
class="core_NumberInput_inner core_Input_inner"
step="any"
type="number"
/>
</span>
</td>
</tr>
<tr>
<th>
Name:
</th>
<td>
<span
class="core_TextInput core_Input"
>
<input
class="core_TextInput_inner core_Input_inner"
maxlength="32"
type="text"
/>
</span>
</td>
</tr>
<tr>
<th
colspan="2"
>
Short description:
</th>
</tr>
<tr>
<td
colspan="2"
>
<div
class="core_TextArea"
>
<textarea
class="core_TextArea_inner"
cols="25"
maxlength="128"
rows="5"
style="font-family: \\"Courier New\\", monospace;"
/>
</div>
</td>
</tr>
<tr>
<th
colspan="2"
>
Long description:
</th>
</tr>
<tr>
<td
colspan="2"
>
<div
class="core_TextArea"
>
<textarea
class="core_TextArea_inner"
cols="25"
maxlength="288"
rows="10"
style="font-family: \\"Courier New\\", monospace;"
/>
</div>
</td>
</tr>
</table>
<div
class="quest_editor_UnavailableView"
>
<label
class="core_Label disabled"
>
No quest loaded.
</label>
</div>
</div>
`;

View File

@ -22,35 +22,44 @@ import { EventsView } from "./gui/EventsView";
import { QuestRunnerRendererView } from "./gui/QuestRunnerRendererView";
import { RegistersView } from "./gui/RegistersView";
import { QuestInfoController } from "./controllers/QuestInfoController";
import { Disposer } from "../core/observable/Disposer";
import { Disposable } from "../core/observable/Disposable";
export function initialize_quest_editor(
http_client: HttpClient,
gui_store: GuiStore,
create_three_renderer: () => DisposableThreeRenderer,
): QuestEditorView {
): { view: QuestEditorView } & Disposable {
const disposer = new Disposer();
// Asset Loaders
const area_asset_loader = new AreaAssetLoader(http_client);
const entity_asset_loader = new EntityAssetLoader(http_client);
const area_asset_loader = disposer.add(new AreaAssetLoader(http_client));
const entity_asset_loader = disposer.add(new EntityAssetLoader(http_client));
// Stores
const area_store = new AreaStore(area_asset_loader);
const quest_editor_store = new QuestEditorStore(gui_store, area_store);
const asm_editor_store = new AsmEditorStore(quest_editor_store);
const area_store = disposer.add(new AreaStore(area_asset_loader));
const quest_editor_store = disposer.add(new QuestEditorStore(gui_store, area_store));
const asm_editor_store = disposer.add(new AsmEditorStore(quest_editor_store));
// Persisters
const quest_editor_ui_persister = new QuestEditorUiPersister();
// Entity Image Renderer
const entity_image_renderer = new EntityImageRenderer(entity_asset_loader);
const entity_image_renderer = disposer.add(
new EntityImageRenderer(entity_asset_loader, create_three_renderer),
);
// View
return new QuestEditorView(
const view = disposer.add(
new QuestEditorView(
gui_store,
quest_editor_store,
quest_editor_ui_persister,
disposer.add(
new QuestEditorToolBar(
new QuestEditorToolBarController(gui_store, area_store, quest_editor_store),
),
),
() => new QuestInfoView(new QuestInfoController(quest_editor_store)),
() => new NpcCountsView(quest_editor_store),
() =>
@ -75,5 +84,13 @@ export function initialize_quest_editor(
create_three_renderer(),
),
() => new RegistersView(quest_editor_store.quest_runner),
),
);
return {
view,
dispose() {
disposer.dispose();
},
};
}

View File

@ -15,14 +15,21 @@ import {
} from "../rendering/conversion/areas";
import { AreaVariantModel } from "../model/AreaVariantModel";
import { HttpClient } from "../../core/HttpClient";
import { Disposable } from "../../core/observable/Disposable";
export class AreaAssetLoader {
export class AreaAssetLoader implements Disposable {
private readonly render_object_cache = new LoadingCache<string, Promise<RenderObject>>();
private readonly collision_object_cache = new LoadingCache<string, Promise<CollisionObject>>();
private readonly area_sections_cache = new LoadingCache<string, Promise<SectionModel[]>>();
constructor(private readonly http_client: HttpClient) {}
dispose(): void {
this.render_object_cache.purge_all();
this.collision_object_cache.purge_all();
this.area_sections_cache.purge_all();
}
async load_sections(episode: Episode, area_variant: AreaVariantModel): Promise<SectionModel[]> {
const key = `${episode}-${area_variant.area.id}-${area_variant.id}`;

View File

@ -1,5 +1,4 @@
import { BufferGeometry, CylinderBufferGeometry, Texture } from "three";
import Logger from "js-logger";
import { LoadingCache } from "./LoadingCache";
import { Endianness } from "../../core/data_formats/Endianness";
import { ArrayBufferCursor } from "../../core/data_formats/cursor/ArrayBufferCursor";
@ -15,16 +14,47 @@ import {
is_npc_type,
} from "../../core/data_formats/parsing/quest/entities";
import { HttpClient } from "../../core/HttpClient";
import { Disposable } from "../../core/observable/Disposable";
import { LogManager } from "../../core/Logger";
const logger = Logger.get("quest_editor/loading/EntityAssetLoader");
const logger = LogManager.get("quest_editor/loading/EntityAssetLoader");
export class EntityAssetLoader {
constructor(private readonly http_client: HttpClient) {}
const DEFAULT_ENTITY = new CylinderBufferGeometry(3, 3, 20);
DEFAULT_ENTITY.translate(0, 10, 0);
DEFAULT_ENTITY.computeBoundingBox();
DEFAULT_ENTITY.computeBoundingSphere();
const DEFAULT_ENTITY_PROMISE: Promise<BufferGeometry> = new Promise(resolve =>
resolve(DEFAULT_ENTITY),
);
const DEFAULT_ENTITY_TEX: Texture[] = [];
const DEFAULT_ENTITY_TEX_PROMISE: Promise<Texture[]> = new Promise(resolve =>
resolve(DEFAULT_ENTITY_TEX),
);
export class EntityAssetLoader implements Disposable {
private disposed = false;
private readonly geom_cache = new LoadingCache<EntityType, Promise<BufferGeometry>>();
private readonly tex_cache = new LoadingCache<EntityType, Promise<Texture[]>>();
constructor(private readonly http_client: HttpClient) {
this.warm_up_caches();
}
dispose(): void {
this.disposed = true;
this.geom_cache.purge_all();
this.tex_cache.purge_all();
}
async load_geometry(type: EntityType): Promise<BufferGeometry> {
return geom_cache.get_or_set(type, async () => {
return this.geom_cache.get_or_set(type, async () => {
try {
const { url, data } = await this.load_data(type, AssetType.Geometry);
if (this.disposed) return DEFAULT_ENTITY;
const cursor = new ArrayBufferCursor(data, Endianness.Little);
const nj_objects = url.endsWith(".nj") ? parse_nj(cursor) : parse_xj(cursor);
@ -42,9 +72,11 @@ export class EntityAssetLoader {
}
async load_textures(type: EntityType): Promise<Texture[]> {
return tex_cache.get_or_set(type, async () => {
return this.tex_cache.get_or_set(type, async () => {
try {
const { data } = await this.load_data(type, AssetType.Texture);
if (this.disposed) return DEFAULT_ENTITY_TEX;
const cursor = new ArrayBufferCursor(data, Endianness.Little);
const xvm = parse_xvm(cursor);
return xvm_to_textures(xvm);
@ -63,27 +95,11 @@ export class EntityAssetLoader {
const data = await this.http_client.get(url).array_buffer();
return { url, data };
}
}
const DEFAULT_ENTITY = new CylinderBufferGeometry(3, 3, 20);
DEFAULT_ENTITY.translate(0, 10, 0);
DEFAULT_ENTITY.computeBoundingBox();
DEFAULT_ENTITY.computeBoundingSphere();
const DEFAULT_ENTITY_PROMISE: Promise<BufferGeometry> = new Promise(resolve =>
resolve(DEFAULT_ENTITY),
);
const DEFAULT_ENTITY_TEX: Texture[] = [];
const DEFAULT_ENTITY_TEX_PROMISE: Promise<Texture[]> = new Promise(resolve =>
resolve(DEFAULT_ENTITY_TEX),
);
const geom_cache = new LoadingCache<EntityType, Promise<BufferGeometry>>();
const tex_cache = new LoadingCache<EntityType, Promise<Texture[]>>();
/**
* Warms up the caches with default data for all entities without assets.
*/
private warm_up_caches(): void {
for (const type of [
NpcType.Unknown,
NpcType.Migium,
@ -158,8 +174,10 @@ for (const type of [
ObjectType.LabInvisibleObject,
ObjectType.UnknownItem700,
]) {
geom_cache.set(type, DEFAULT_ENTITY_PROMISE);
tex_cache.set(type, DEFAULT_ENTITY_TEX_PROMISE);
this.geom_cache.set(type, DEFAULT_ENTITY_PROMISE);
this.tex_cache.set(type, DEFAULT_ENTITY_TEX_PROMISE);
}
}
}
enum AssetType {

View File

@ -15,4 +15,8 @@ export class LoadingCache<K, V> {
return v;
}
purge_all(): void {
this.map.clear();
}
}

View File

@ -6,7 +6,6 @@ import { QuestNpcModel } from "./QuestNpcModel";
import { DatUnknown } from "../../core/data_formats/parsing/quest/dat";
import { Segment } from "../scripting/instructions";
import { Property } from "../../core/observable/property/Property";
import Logger from "js-logger";
import { AreaVariantModel } from "./AreaVariantModel";
import { ListProperty } from "../../core/observable/property/list/ListProperty";
import { WritableListProperty } from "../../core/observable/property/list/WritableListProperty";
@ -15,8 +14,9 @@ import { entity_type_to_string } from "../../core/data_formats/parsing/quest/ent
import { QuestEventDagModel } from "./QuestEventDagModel";
import { assert, defined, require_array } from "../../core/util";
import { AreaStore } from "../stores/AreaStore";
import { LogManager } from "../../core/Logger";
const logger = Logger.get("quest_editor/model/QuestModel");
const logger = LogManager.get("quest_editor/model/QuestModel");
export class QuestModel {
private readonly _id: WritableProperty<number> = property(0);

View File

@ -1,8 +1,10 @@
import { HemisphereLight, PerspectiveCamera, Scene, Vector3, WebGLRenderer } from "three";
import { HemisphereLight, PerspectiveCamera, Scene, Vector3 } from "three";
import { EntityType } from "../../core/data_formats/parsing/quest/entities";
import { create_entity_type_mesh } from "./conversion/entities";
import { sequential } from "../../core/sequential";
import { EntityAssetLoader } from "../loading/EntityAssetLoader";
import { Disposable } from "../../core/observable/Disposable";
import { DisposableThreeRenderer } from "../../core/rendering/Renderer";
const light = new HemisphereLight(0xffffff, 0x505050, 1.2);
const scene = new Scene();
@ -11,14 +13,25 @@ const camera = new PerspectiveCamera(30, 1, 10, 1000);
const camera_position = new Vector3(1, 1, 2).normalize();
const camera_dist_factor = 1.3 / Math.tan(((camera.fov / 180) * Math.PI) / 2);
export class EntityImageRenderer {
private renderer = new WebGLRenderer({ alpha: true, antialias: true });
export class EntityImageRenderer implements Disposable {
private renderer: DisposableThreeRenderer;
private readonly cache: Map<EntityType, Promise<string>> = new Map();
private disposed = false;
constructor(private readonly entity_asset_loader: EntityAssetLoader) {
constructor(
private readonly entity_asset_loader: EntityAssetLoader,
create_three_renderer: () => DisposableThreeRenderer,
) {
this.renderer = create_three_renderer();
this.renderer.setSize(100, 100);
}
dispose(): void {
this.disposed = true;
this.renderer.dispose();
this.cache.clear();
}
async render(entity: EntityType): Promise<string> {
let url = this.cache.get(entity);
@ -32,8 +45,13 @@ export class EntityImageRenderer {
private render_to_image = sequential(
async (entity: EntityType): Promise<string> => {
if (this.disposed) return "";
const geometry = await this.entity_asset_loader.load_geometry(entity);
if (this.disposed) return "";
const textures = await this.entity_asset_loader.load_textures(entity);
if (this.disposed) return "";
scene.remove(...scene.children);
scene.add(light);

View File

@ -1,4 +1,3 @@
import Logger from "js-logger";
import { Intersection, Mesh, Object3D, Raycaster, Vector3 } from "three";
import { QuestRenderer } from "./QuestRenderer";
import { QuestEntityModel } from "../model/QuestEntityModel";
@ -18,8 +17,9 @@ import { Episode } from "../../core/data_formats/parsing/quest/Episode";
import { AreaVariantModel } from "../model/AreaVariantModel";
import { EntityAssetLoader } from "../loading/EntityAssetLoader";
import { AreaAssetLoader } from "../loading/AreaAssetLoader";
import { LogManager } from "../../core/Logger";
const logger = Logger.get("quest_editor/rendering/QuestModelManager");
const logger = LogManager.get("quest_editor/rendering/QuestModelManager");
const CAMERA_POSITION = Object.freeze(new Vector3(0, 800, 700));
const CAMERA_LOOK_AT = Object.freeze(new Vector3(0, 0, 0));
@ -55,6 +55,9 @@ export abstract class QuestModelManager implements Disposable {
dispose(): void {
this.disposer.dispose();
this.npc_model_manager.remove_all();
this.object_model_manager.remove_all();
this.renderer.reset_entity_models();
}
/**
@ -266,14 +269,14 @@ class EntityModelManager {
private async load(entity: QuestEntityModel): Promise<void> {
const geom = await this.entity_asset_loader.load_geometry(entity.type);
const tex = await this.entity_asset_loader.load_textures(entity.type);
const model = create_entity_mesh(entity, geom, tex);
if (!this.queue.includes(entity)) return; // Could be cancelled by now.
// The model load might be cancelled by now.
if (this.queue.includes(entity)) {
const tex = await this.entity_asset_loader.load_textures(entity.type);
if (!this.queue.includes(entity)) return; // Could be cancelled by now.
const model = create_entity_mesh(entity, geom, tex);
this.update_entity_geometry(entity, model);
}
}
private update_entity_geometry(entity: QuestEntityModel, model: Mesh): void {
this.renderer.add_entity_model(model);

View File

@ -1,4 +1,3 @@
import Logger from "js-logger";
import { reinterpret_f32_as_i32 } from "../../core/primitive_conversion";
import {
AssemblyLexer,
@ -34,8 +33,9 @@ import {
Param,
StackInteraction,
} from "./opcodes";
import { LogManager } from "../../core/Logger";
const logger = Logger.get("quest_editor/scripting/assembly");
const logger = LogManager.get("quest_editor/scripting/assembly");
export type AssemblyWarning = {
line_no: number;

View File

@ -11,15 +11,10 @@ import {
SignatureHelpOutput,
} from "./assembly_worker_messages";
import { assemble, AssemblySettings } from "./assembly";
import Logger from "js-logger";
import { AsmToken, Segment, SegmentType } from "./instructions";
import { Kind, OP_BB_MAP_DESIGNATE, Opcode, OPCODES_BY_MNEMONIC } from "./opcodes";
import { AssemblyLexer, IdentToken, TokenType } from "./AssemblyLexer";
Logger.useDefaults({
defaultLevel: (Logger as any)[process.env["LOG_LEVEL"] || "INFO"],
});
const ctx: Worker = self as any;
let lines: string[] = [];

View File

@ -1,4 +1,3 @@
import Logger from "js-logger";
import { Instruction } from "../instructions";
import {
Kind,
@ -26,8 +25,9 @@ import {
} from "../opcodes";
import { BasicBlock, ControlFlowGraph } from "./ControlFlowGraph";
import { ValueSet } from "./ValueSet";
import { LogManager } from "../../../core/Logger";
const logger = Logger.get("quest_editor/scripting/data_flow_analysis/register_value");
const logger = LogManager.get("quest_editor/scripting/data_flow_analysis/register_value");
export const MIN_REGISTER_VALUE = MIN_SIGNED_DWORD_VALUE;
export const MAX_REGISTER_VALUE = MAX_SIGNED_DWORD_VALUE;

View File

@ -1,4 +1,3 @@
import Logger from "js-logger";
import { Instruction } from "../instructions";
import {
MAX_SIGNED_DWORD_VALUE,
@ -15,8 +14,9 @@ import {
import { BasicBlock, ControlFlowGraph } from "./ControlFlowGraph";
import { ValueSet } from "./ValueSet";
import { register_value } from "./register_value";
import { LogManager } from "../../../core/Logger";
const logger = Logger.get("quest_editor/scripting/data_flow_analysis/stack_value");
const logger = LogManager.get("quest_editor/scripting/data_flow_analysis/stack_value");
export const MIN_STACK_VALUE = MIN_SIGNED_DWORD_VALUE;
export const MAX_STACK_VALUE = MAX_SIGNED_DWORD_VALUE;

View File

@ -1,9 +1,9 @@
import { reinterpret_i32_as_f32 } from "../../core/primitive_conversion";
import { Arg, Segment, SegmentType } from "./instructions";
import { AnyType, Kind, OP_VA_END, OP_VA_START, Param, StackInteraction } from "./opcodes";
import Logger from "js-logger";
import { LogManager } from "../../core/Logger";
const logger = Logger.get("quest_editor/scripting/disassembly");
const logger = LogManager.get("quest_editor/scripting/disassembly");
type ArgWithType = Arg & {
/**

View File

@ -101,9 +101,9 @@ import { Episode } from "../../../core/data_formats/parsing/quest/Episode";
import { Endianness } from "../../../core/data_formats/Endianness";
import { Random } from "./Random";
import { Memory } from "./Memory";
import Logger from "js-logger";
import { InstructionPointer } from "./InstructionPointer";
import { StackFrame, StepMode, Thread } from "./Thread";
import { LogManager } from "../../../core/Logger";
export const REGISTER_COUNT = 256;
@ -114,7 +114,7 @@ const STRING_ARG_STORE_SIZE = 1024; // TODO: verify this value
const ENTRY_SEGMENT = 0;
const LIST_ITEM_DELIMITER = "\n";
const logger = Logger.get("quest_editor/scripting/vm/VirtualMachine");
const logger = LogManager.get("quest_editor/scripting/vm/VirtualMachine");
export enum ExecutionResult {
/**

View File

@ -1,8 +1,8 @@
import { AsmToken } from "../instructions";
import Logger from "js-logger";
import { InstructionPointer } from "./InstructionPointer";
import { LogManager } from "../../../core/Logger";
const logger = Logger.get("quest_editor/scripting/vm/io");
const logger = LogManager.get("quest_editor/scripting/vm/io");
/**
* The virtual machine calls these methods when it requires input.

View File

@ -4,11 +4,14 @@ import { Episode, EPISODES } from "../../core/data_formats/parsing/quest/Episode
import { SectionModel } from "../model/SectionModel";
import { get_areas_for_episode } from "../../core/data_formats/parsing/quest/areas";
import { AreaAssetLoader } from "../loading/AreaAssetLoader";
import { Store } from "../../core/stores/Store";
export class AreaStore {
export class AreaStore extends Store {
private readonly areas: AreaModel[][] = [];
constructor(private readonly area_asset_loader: AreaAssetLoader) {
super();
for (const episode of EPISODES) {
this.areas[episode] = get_areas_for_episode(episode).map(area => {
const observable_area = new AreaModel(area.id, area.name, area.order, []);

View File

@ -1,6 +1,5 @@
import { editor, languages, MarkerSeverity, MarkerTag, Position } from "monaco-editor";
import { AssemblyAnalyser } from "../scripting/AssemblyAnalyser";
import { Disposable } from "../../core/observable/Disposable";
import { Disposer } from "../../core/observable/Disposer";
import { SimpleUndo } from "../../core/undo/SimpleUndo";
import { ASM_SYNTAX } from "./asm_syntax";
@ -10,15 +9,16 @@ import { emitter, property } from "../../core/observable";
import { WritableProperty } from "../../core/observable/property/WritableProperty";
import { Property } from "../../core/observable/property/Property";
import { ListProperty } from "../../core/observable/property/list/ListProperty";
import { Breakpoint } from "../scripting/vm/Debugger";
import { QuestEditorStore } from "./QuestEditorStore";
import { disposable_listener } from "../../core/gui/dom";
import { Store } from "../../core/stores/Store";
import ITextModel = editor.ITextModel;
import CompletionList = languages.CompletionList;
import IMarkerData = editor.IMarkerData;
import SignatureHelpResult = languages.SignatureHelpResult;
import LocationLink = languages.LocationLink;
import IModelContentChange = editor.IModelContentChange;
import { Breakpoint } from "../scripting/vm/Debugger";
import { QuestEditorStore } from "./QuestEditorStore";
import { disposable_listener } from "../../core/gui/dom";
const assembly_analyser = new AssemblyAnalyser();
@ -85,9 +85,8 @@ languages.registerDefinitionProvider("psoasm", {
},
});
export class AsmEditorStore implements Disposable {
private readonly disposer = new Disposer();
private readonly model_disposer = this.disposer.add(new Disposer());
export class AsmEditorStore extends Store {
private readonly model_disposer = this.disposable(new Disposer());
private readonly _model: WritableProperty<ITextModel | undefined> = property(undefined);
private readonly _did_undo = emitter<string>();
private readonly _did_redo = emitter<string>();
@ -109,10 +108,12 @@ export class AsmEditorStore implements Disposable {
readonly pause_location: Property<number | undefined>;
constructor(private readonly quest_editor_store: QuestEditorStore) {
super();
this.breakpoints = quest_editor_store.quest_runner.breakpoints;
this.pause_location = quest_editor_store.quest_runner.pause_location;
this.disposer.add_all(
this.disposables(
quest_editor_store.current_quest.observe(this.quest_changed, {
call_now: true,
}),
@ -130,10 +131,6 @@ export class AsmEditorStore implements Disposable {
);
}
dispose(): void {
this.disposer.dispose();
}
set_inline_args_mode = (inline_args_mode: boolean): void => {
// don't allow changing inline args mode if there are issues
if (!this.has_issues.val) {

View File

@ -3,108 +3,69 @@ 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 Logger from "js-logger";
import { enum_values } from "../../core/enums";
import { LogEntry, Logger, LogHandler, LogLevel, LogManager } from "../../core/Logger";
export type QuestLogger = {
readonly debug: (message: string) => void;
readonly info: (message: string) => void;
readonly warning: (message: string) => void;
readonly error: (message: string) => void;
};
// Log level names in order of importance.
export enum LogLevel {
Debug,
Info,
Warning,
Error,
}
export const LogLevels = enum_values<LogLevel>(LogLevel);
export type LogMessage = {
readonly time: Date;
readonly message: string;
readonly level: LogLevel;
};
const logger = LogManager.get("quest_editor/stroes/LogStore");
export class LogStore implements Disposable {
private readonly disposer = new Disposer();
private readonly default_log_level = LogLevel.Info;
private readonly message_buffer: LogMessage[] = [];
private readonly log_buffer: LogEntry[] = [];
private readonly logger_name_buffer: string[] = [];
private readonly _log_messages = list_property<LogMessage>();
private readonly _log_level = property<LogLevel>(this.default_log_level);
private readonly _level = property<LogLevel>(this.default_log_level);
private readonly _log = list_property<LogEntry>();
readonly log_messages: ListProperty<LogMessage>;
readonly log_level: Property<LogLevel> = this._log_level;
private readonly handler: LogHandler = (entry: LogEntry, logger_name: string): void => {
this.buffer_log_entry(entry, logger_name);
};
constructor() {
this.log_messages = this._log_messages.filtered(
this.log_level.map(level => message => message.level >= level),
readonly level: Property<LogLevel> = this._level;
readonly log: ListProperty<LogEntry> = this._log.filtered(
this.level.map(level => message => message.level >= level),
);
get_logger(name: string): Logger {
const logger = LogManager.get(name);
logger.handler = this.handler;
return logger;
}
dispose(): void {
this.disposer.dispose();
}
set_log_level(log_level: LogLevel): void {
this._log_level.val = log_level;
set_level(log_level: LogLevel): void {
this._level.val = log_level;
}
/**
* @param name - js-logger logger name
*/
get_logger(name: string): QuestLogger {
return {
debug: (message: string): void => {
this.buffer_log_message(message, LogLevel.Debug, name);
},
info: (message: string): void => {
this.buffer_log_message(message, LogLevel.Info, name);
},
warning: (message: string): void => {
this.buffer_log_message(message, LogLevel.Warning, name);
},
error: (message: string): void => {
this.buffer_log_message(message, LogLevel.Error, name);
},
};
}
private buffer_log_message(message: string, level: LogLevel, logger_name: string): void {
this.message_buffer.push({
time: new Date(),
message,
level,
});
private buffer_log_entry(entry: LogEntry, logger_name: string): void {
this.log_buffer.push(entry);
this.logger_name_buffer.push(logger_name);
this.add_buffered_log_messages();
this.add_buffered_log_entries();
}
private adding_log_messages?: number;
private adding_log_entries?: number;
private add_buffered_log_messages(): void {
if (this.adding_log_messages != undefined) return;
private add_buffered_log_entries(): void {
if (this.adding_log_entries != undefined) return;
this.adding_log_messages = requestAnimationFrame(() => {
this.adding_log_entries = requestAnimationFrame(() => {
const DROP_THRESHOLD = 500;
const DROP_THRESHOLD_HALF = DROP_THRESHOLD / 2;
const BATCH_SIZE = 200;
// Drop messages if there are too many.
if (this.message_buffer.length > DROP_THRESHOLD) {
const drop_len = this.message_buffer.length - DROP_THRESHOLD;
// Drop log entries if there are too many.
if (this.log_buffer.length > DROP_THRESHOLD) {
const drop_len = this.log_buffer.length - DROP_THRESHOLD;
this.message_buffer.splice(DROP_THRESHOLD_HALF, drop_len, {
this.log_buffer.splice(DROP_THRESHOLD_HALF, drop_len, {
time: new Date(),
message: `...dropped ${drop_len} messages...`,
level: LogLevel.Warning,
level: LogLevel.Warn,
logger,
});
this.logger_name_buffer.splice(
DROP_THRESHOLD_HALF,
@ -113,42 +74,28 @@ export class LogStore implements Disposable {
);
}
const len = Math.min(BATCH_SIZE, this.message_buffer.length);
const len = Math.min(BATCH_SIZE, this.log_buffer.length);
const buffered_messages = this.message_buffer.splice(0, len);
const buffered_entries = this.log_buffer.splice(0, len);
const buffered_logger_names = this.logger_name_buffer.splice(0, len);
this._log_messages.push(...buffered_messages);
this._log.push(...buffered_entries);
for (let i = 0; i < len; i++) {
const { level, message } = buffered_messages[i];
const entry = buffered_entries[i];
const logger_name = buffered_logger_names[i];
switch (level) {
case LogLevel.Debug:
Logger.get(logger_name).debug(message);
break;
case LogLevel.Info:
Logger.get(logger_name).info(message);
break;
case LogLevel.Warning:
Logger.get(logger_name).warn(message);
break;
case LogLevel.Error:
Logger.get(logger_name).error(message);
break;
}
LogManager.default_handler(entry, logger_name);
}
// Occasionally clean up old log messages if there are too many.
if (this._log_messages.length.val > 2000) {
this._log_messages.splice(0, 1000);
if (this._log.length.val > 2000) {
this._log.splice(0, 1000);
}
this.adding_log_messages = undefined;
this.adding_log_entries = undefined;
if (this.message_buffer.length) {
this.add_buffered_log_messages();
if (this.log_buffer.length) {
this.add_buffered_log_entries();
}
});
}

View File

@ -6,8 +6,6 @@ import { QuestNpcModel } from "../model/QuestNpcModel";
import { AreaModel } from "../model/AreaModel";
import { SectionModel } from "../model/SectionModel";
import { QuestEntityModel } from "../model/QuestEntityModel";
import { Disposable } from "../../core/observable/Disposable";
import { Disposer } from "../../core/observable/Disposer";
import { GuiStore, GuiTool } from "../../core/stores/GuiStore";
import { UndoStack } from "../../core/undo/UndoStack";
import { TranslateEntityAction } from "../actions/TranslateEntityAction";
@ -22,12 +20,12 @@ import { disposable_listener } from "../../core/gui/dom";
import { QuestEventModel } from "../model/QuestEventModel";
import { EditEventSectionIdAction } from "../actions/EditEventSectionIdAction";
import { EditEventDelayAction } from "../actions/EditEventDelayAction";
import Logger = require("js-logger");
import { Store } from "../../core/stores/Store";
import { LogManager } from "../../core/Logger";
const logger = Logger.get("quest_editor/gui/QuestEditorStore");
const logger = LogManager.get("quest_editor/gui/QuestEditorStore");
export class QuestEditorStore implements Disposable {
private readonly disposer = new Disposer();
export class QuestEditorStore extends Store {
private readonly _current_quest = property<QuestModel | undefined>(undefined);
private readonly _current_area = property<AreaModel | undefined>(undefined);
private readonly _selected_entity = property<QuestEntityModel | undefined>(undefined);
@ -40,9 +38,11 @@ export class QuestEditorStore implements Disposable {
readonly selected_entity: Property<QuestEntityModel | undefined> = this._selected_entity;
constructor(gui_store: GuiStore, private readonly area_store: AreaStore) {
super();
this.quest_runner = new QuestRunner(area_store);
this.disposer.add_all(
this.disposables(
gui_store.tool.observe(
({ value: tool }) => {
if (tool === GuiTool.QuestEditor) {
@ -85,7 +85,7 @@ export class QuestEditorStore implements Disposable {
dispose(): void {
this.quest_runner.stop();
this.disposer.dispose();
super.dispose();
}
set_current_area = (area?: AreaModel): void => {

View File

@ -17,11 +17,11 @@ import {
} from "../model/QuestEventActionModel";
import { QuestEventDagModel, QuestEventDagModelMeta } from "../model/QuestEventDagModel";
import { QuestEvent } from "../../core/data_formats/parsing/quest/entities";
import Logger from "js-logger";
import { clone_segment } from "../scripting/instructions";
import { AreaStore } from "./AreaStore";
import { LogManager } from "../../core/Logger";
const logger = Logger.get("quest_editor/stores/model_conversion");
const logger = LogManager.get("quest_editor/stores/model_conversion");
export function convert_quest_to_model(area_store: AreaStore, quest: Quest): QuestModel {
// Create quest model.

View File

@ -2,30 +2,53 @@ import { ViewerView } from "./gui/ViewerView";
import { GuiStore } from "../core/stores/GuiStore";
import { HttpClient } from "../core/HttpClient";
import { DisposableThreeRenderer } from "../core/rendering/Renderer";
import { Disposable } from "../core/observable/Disposable";
import { Disposer } from "../core/observable/Disposer";
export function initialize_viewer(
http_client: HttpClient,
gui_store: GuiStore,
create_three_renderer: () => DisposableThreeRenderer,
): ViewerView {
return new ViewerView(
): { view: ViewerView } & Disposable {
const disposer = new Disposer();
const view = new ViewerView(
async () => {
const { Model3DStore } = await import("./stores/Model3DStore");
const { Model3DView } = await import("./gui/model_3d/Model3DView");
const { CharacterClassAssetLoader } = await import(
"./loading/CharacterClassAssetLoader"
);
return new Model3DView(
gui_store,
new Model3DStore(new CharacterClassAssetLoader(http_client)),
create_three_renderer(),
);
const store = new Model3DStore(new CharacterClassAssetLoader(http_client));
if (disposer.disposed) {
store.dispose();
} else {
disposer.add(store);
}
return new Model3DView(gui_store, store, create_three_renderer());
},
async () => {
const { TextureStore } = await import("./stores/TextureStore");
const { TextureView } = await import("./gui/TextureView");
return new TextureView(gui_store, new TextureStore(), create_three_renderer());
const store = new TextureStore();
if (disposer.disposed) {
store.dispose();
} else {
disposer.add(store);
}
return new TextureView(gui_store, store, create_three_renderer());
},
);
return {
view,
dispose(): void {
disposer.dispose();
},
};
}

View File

@ -13,9 +13,9 @@ import { Disposer } from "../../core/observable/Disposer";
import { Xvm } from "../../core/data_formats/parsing/ninja/texture";
import { xvm_texture_to_texture } from "../../core/rendering/conversion/ninja_textures";
import { TextureStore } from "../stores/TextureStore";
import Logger = require("js-logger");
import { LogManager } from "../../core/Logger";
const logger = Logger.get("viewer/rendering/TextureRenderer");
const logger = LogManager.get("viewer/rendering/TextureRenderer");
export class TextureRenderer extends Renderer implements Disposable {
private readonly disposer = new Disposer();

View File

@ -5,16 +5,16 @@ import { NjObject, parse_nj, parse_xj } from "../../core/data_formats/parsing/ni
import { CharacterClassModel } from "../model/CharacterClassModel";
import { CharacterClassAnimationModel } from "../model/CharacterClassAnimationModel";
import { WritableProperty } from "../../core/observable/property/WritableProperty";
import { Disposable } from "../../core/observable/Disposable";
import { read_file } from "../../core/read_file";
import { property } from "../../core/observable";
import { Property } from "../../core/observable/property/Property";
import { PSO_FRAME_RATE } from "../../core/rendering/conversion/ninja_animation";
import { parse_xvm, Xvm } from "../../core/data_formats/parsing/ninja/texture";
import Logger = require("js-logger");
import { CharacterClassAssetLoader } from "../loading/CharacterClassAssetLoader";
import { Store } from "../../core/stores/Store";
import { LogManager } from "../../core/Logger";
const logger = Logger.get("viewer/stores/ModelStore");
const logger = LogManager.get("viewer/stores/ModelStore");
const nj_object_cache: Map<string, Promise<NjObject>> = new Map();
const nj_motion_cache: Map<number, Promise<NjMotion>> = new Map();
@ -24,7 +24,7 @@ export type NjData = {
has_skeleton: boolean;
};
export class Model3DStore implements Disposable {
export class Model3DStore extends Store {
private readonly _current_model: WritableProperty<CharacterClassModel | undefined> = property(
undefined,
);
@ -38,7 +38,6 @@ export class Model3DStore implements Disposable {
private readonly _animation_playing: WritableProperty<boolean> = property(true);
private readonly _animation_frame_rate: WritableProperty<number> = property(PSO_FRAME_RATE);
private readonly _animation_frame: WritableProperty<number> = property(0);
private readonly disposables: Disposable[] = [];
readonly models: readonly CharacterClassModel[] = [
new CharacterClassModel("HUmar", 1, 10, new Set([6])),
@ -74,16 +73,14 @@ export class Model3DStore implements Disposable {
);
constructor(private readonly asset_loader: CharacterClassAssetLoader) {
this.disposables.push(
super();
this.disposables(
this.current_model.observe(({ value }) => this.load_model(value)),
this.current_animation.observe(({ value }) => this.load_animation(value)),
);
}
dispose(): void {
this.disposables.forEach(d => d.dispose());
}
set_current_model = (current_model: CharacterClassModel): void => {
this._current_model.val = current_model;
};

View File

@ -4,11 +4,12 @@ import { Property } from "../../core/observable/property/Property";
import { read_file } from "../../core/read_file";
import { ArrayBufferCursor } from "../../core/data_formats/cursor/ArrayBufferCursor";
import { Endianness } from "../../core/data_formats/Endianness";
import Logger = require("js-logger");
import { Store } from "../../core/stores/Store";
import { LogManager } from "../../core/Logger";
const logger = Logger.get("viewer/stores/TextureStore");
const logger = LogManager.get("viewer/stores/TextureStore");
export class TextureStore {
export class TextureStore extends Store {
private readonly _current_xvm = property<Xvm | undefined>(undefined);
readonly current_xvm: Property<Xvm | undefined> = this._current_xvm;

View File

@ -5,9 +5,8 @@ export class FileSystemHttpClient implements HttpClient {
get(url: string): HttpResponse {
return {
async json<T>(): Promise<T> {
throw new Error(
`FileSystemHttpClient's json method invoked for get request to "${url}".`,
);
const buf = await fs.promises.readFile(`./assets${url}`);
return JSON.parse(buf.toString());
},
async array_buffer(): Promise<ArrayBuffer> {

View File

@ -1,8 +1,4 @@
const Logger = require("js-logger");
require('dotenv').config({ path: ".env.test" })
require("dotenv").config({ path: ".env.test" });
const log_level = process.env["LOG_LEVEL"] || "WARN";
Logger.useDefaults({
defaultLevel: Logger[log_level],
});
// For GoldenLayout.
window.$ = require("jquery");

View File

@ -2,6 +2,16 @@ import * as fs from "fs";
import { InstructionSegment, SegmentType } from "../../src/quest_editor/scripting/instructions";
import { assemble } from "../../src/quest_editor/scripting/assembly";
export async function timeout(millis: number): Promise<void> {
return new Promise(resolve => {
setTimeout(() => resolve(), millis);
});
}
export function next_animation_frame(): Promise<void> {
return new Promise(resolve => requestAnimationFrame(() => resolve()));
}
/**
* Applies f to all QST files in a directory.
* f is called with the path to the file, the file name and the content of the file.

View File

@ -1,25 +1,18 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"module": "commonjs",
"target": "es6",
"lib": ["es6", "dom", "dom.iterable"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "commonjs",
"moduleResolution": "node",
"resolveJsonModule": true,
"moduleResolution": "node",
"noEmit": true,
"experimentalDecorators": true,
"downlevelIteration": true
"experimentalDecorators": true
},
"include": [
"static_generation"
]
"include": ["static_generation"]
}

View File

@ -2,12 +2,11 @@
"compilerOptions": {
"outDir": "./dist/",
"sourceMap": true,
"module": "commonjs",
"module": "esnext",
"target": "es6",
"lib": ["es6", "dom", "dom.iterable"],
"allowJs": true,
"skipLibCheck": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"strict": true,
"forceConsistentCasingInFileNames": true,

View File

@ -9,7 +9,20 @@ module.exports = {
path: path.resolve(__dirname, "dist"),
},
resolve: {
extensions: [".js", ".ts", ".tsx"],
extensions: [".js", ".ts"],
},
module: {
rules: [
{
test: /^worker-loader!/,
loader: "worker-loader",
options: { name: "worker.[hash].js" },
},
{
test: /\.(gif|jpg|png|svg|ttf)$/,
loader: "file-loader",
},
],
},
plugins: [
new HtmlWebpackPlugin({

View File

@ -8,19 +8,18 @@ const Dotenv = require("dotenv-webpack");
module.exports = merge(common, {
mode: "development",
devtool: "eval-source-map",
devServer: {
port: 1623,
},
module: {
rules: [
{
test: /\.ts$/,
use: [
{
loader: "ts-loader",
options: {
// fork-ts-checker-webpack-plugin does the type checking in a separate process.
transpileOnly: true,
},
},
],
include: path.resolve(__dirname, "src"),
},
{
@ -30,6 +29,7 @@ module.exports = merge(common, {
{
loader: MiniCssExtractPlugin.loader,
options: {
esModule: true,
hmr: true,
},
},
@ -54,10 +54,6 @@ module.exports = merge(common, {
},
],
},
{
test: /\.(gif|jpg|png|svg|ttf)$/,
use: ["file-loader"],
},
],
},
plugins: [
@ -65,6 +61,8 @@ module.exports = merge(common, {
path: "./.env.dev",
}),
new ForkTsCheckerWebpackPlugin(),
new MiniCssExtractPlugin(),
new MiniCssExtractPlugin({
ignoreOrder: true,
}),
],
});

View File

@ -34,17 +34,13 @@ module.exports = merge(common, {
rules: [
{
test: /\.ts$/,
use: "ts-loader",
loader: "ts-loader",
include: path.resolve(__dirname, "src"),
},
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, "css-loader"],
},
{
test: /\.(gif|jpg|png|svg|ttf)$/,
use: ["file-loader"],
},
],
},
plugins: [
@ -53,6 +49,7 @@ module.exports = merge(common, {
path: "./.env.prod",
}),
new MiniCssExtractPlugin({
ignoreOrder: true,
filename: "[name].[contenthash].css",
}),
new CopyWebpackPlugin([

View File

@ -4388,11 +4388,6 @@ jquery@*:
resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.4.1.tgz#714f1f8d9dde4bdfa55764ba37ef214630d80ef2"
integrity sha512-36+AdBzCL+y6qjw5Tx7HgzeGCzC81MDDgaUP8ld2zhx58HdqXGoBd+tHdrBMiyjGQs0Hxs/MLZTu/eHNJJuWPw==
js-logger@^1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/js-logger/-/js-logger-1.6.0.tgz#7abae5cfaf208c965f3ef20754533bb9e79c7aef"
integrity sha512-K4kt2AdD0jUYINbe00BPPpsL65u/rdYOgfaBBVWm/mid+ANk7qxDnoXgKI5ilm49Sjmach2Dzlc+5VxKdRA3tw==
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"