mirror of
https://github.com/DaanVandenBosch/phantasmal-world.git
synced 2025-04-05 07:18:29 +08:00
Replaced js-logger. Improved testability with mocks, improved test configuration and code improvements.
This commit is contained in:
parent
243638879c
commit
99d50d754d
@ -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",
|
||||
},
|
||||
};
|
||||
|
@ -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",
|
||||
|
89
src/__mocks__/monaco-editor.js
Normal file
89
src/__mocks__/monaco-editor.js
Normal 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,
|
||||
};
|
6
src/__mocks__/webworkers.js
Normal file
6
src/__mocks__/webworkers.js
Normal file
@ -0,0 +1,6 @@
|
||||
class Worker {
|
||||
onmessage() {}
|
||||
postMessage() {}
|
||||
}
|
||||
|
||||
module.exports = Worker;
|
49
src/application/index.test.ts
Normal file
49
src/application/index.test.ts
Normal 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
132
src/application/index.ts
Normal 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);
|
||||
}
|
@ -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
154
src/core/Logger.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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[];
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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";
|
||||
|
||||
/**
|
||||
|
@ -1,3 +1,5 @@
|
||||
@import "./Button.css";
|
||||
|
||||
.core_FileButton_input {
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
|
@ -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";
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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)[] = [];
|
||||
|
@ -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.
|
||||
|
@ -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;
|
||||
|
27
src/core/stores/DisposableServerMap.ts
Normal file
27
src/core/stores/DisposableServerMap.ts
Normal 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();
|
||||
}
|
||||
}
|
@ -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.disposables(
|
||||
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;
|
||||
}),
|
||||
|
||||
disposable_listener(window, "keydown", this.dispatch_global_keydown),
|
||||
);
|
||||
|
||||
this.tool.val = string_to_gui_tool(tool_str) || GuiTool.Viewer;
|
||||
|
||||
this.server = this._server;
|
||||
|
||||
window.addEventListener("keydown", this.dispatch_global_keydown);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.hash_disposer.dispose();
|
||||
this.global_keydown_handlers.clear();
|
||||
|
||||
window.removeEventListener("keydown", this.dispatch_global_keydown);
|
||||
}
|
||||
|
||||
on_global_keydown(
|
||||
|
@ -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
18
src/core/stores/Store.ts
Normal 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);
|
||||
}
|
||||
}
|
@ -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.
|
||||
|
@ -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" });
|
||||
|
@ -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();
|
||||
|
@ -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" });
|
||||
|
@ -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(
|
||||
gui_store,
|
||||
new HuntOptimizerPersister(item_type_stores),
|
||||
item_type_stores,
|
||||
item_drop_stores,
|
||||
hunt_method_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();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -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";
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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);
|
||||
}),
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
}
|
@ -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);
|
||||
}
|
@ -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();
|
||||
|
@ -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()));
|
||||
}
|
||||
|
@ -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 };
|
||||
|
||||
|
@ -1,6 +1,3 @@
|
||||
/**
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
import {
|
||||
create_area_store,
|
||||
create_quest_editor_store,
|
||||
|
@ -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%);
|
||||
}
|
||||
|
@ -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,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
@ -1,6 +1,3 @@
|
||||
/**
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
import { QuestEditorToolBarController } from "../controllers/QuestEditorToolBarController";
|
||||
import { QuestEditorToolBar } from "./QuestEditorToolBar";
|
||||
import { GuiStore } from "../../core/stores/GuiStore";
|
||||
|
@ -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: {
|
||||
|
@ -1,6 +1,3 @@
|
||||
/**
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
import { QuestInfoController } from "../controllers/QuestInfoController";
|
||||
import { undo_manager } from "../../core/undo/UndoManager";
|
||||
import { QuestInfoView } from "./QuestInfoView";
|
||||
|
@ -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>
|
||||
`;
|
218
src/quest_editor/gui/__snapshots__/QuestInfoView.test.ts.snap
Normal file
218
src/quest_editor/gui/__snapshots__/QuestInfoView.test.ts.snap
Normal 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>
|
||||
`;
|
@ -22,58 +22,75 @@ 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(
|
||||
gui_store,
|
||||
quest_editor_store,
|
||||
quest_editor_ui_persister,
|
||||
new QuestEditorToolBar(
|
||||
new QuestEditorToolBarController(gui_store, area_store, quest_editor_store),
|
||||
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),
|
||||
() =>
|
||||
new QuestEditorRendererView(
|
||||
gui_store,
|
||||
quest_editor_store,
|
||||
area_asset_loader,
|
||||
entity_asset_loader,
|
||||
create_three_renderer(),
|
||||
),
|
||||
() => new AsmEditorView(gui_store, quest_editor_store.quest_runner, asm_editor_store),
|
||||
() => new EntityInfoView(quest_editor_store),
|
||||
() => new NpcListView(quest_editor_store, entity_image_renderer),
|
||||
() => new ObjectListView(quest_editor_store, entity_image_renderer),
|
||||
() => new EventsView(quest_editor_store),
|
||||
() =>
|
||||
new QuestRunnerRendererView(
|
||||
gui_store,
|
||||
quest_editor_store,
|
||||
area_asset_loader,
|
||||
entity_asset_loader,
|
||||
create_three_renderer(),
|
||||
),
|
||||
() => new RegistersView(quest_editor_store.quest_runner),
|
||||
),
|
||||
() => new QuestInfoView(new QuestInfoController(quest_editor_store)),
|
||||
() => new NpcCountsView(quest_editor_store),
|
||||
() =>
|
||||
new QuestEditorRendererView(
|
||||
gui_store,
|
||||
quest_editor_store,
|
||||
area_asset_loader,
|
||||
entity_asset_loader,
|
||||
create_three_renderer(),
|
||||
),
|
||||
() => new AsmEditorView(gui_store, quest_editor_store.quest_runner, asm_editor_store),
|
||||
() => new EntityInfoView(quest_editor_store),
|
||||
() => new NpcListView(quest_editor_store, entity_image_renderer),
|
||||
() => new ObjectListView(quest_editor_store, entity_image_renderer),
|
||||
() => new EventsView(quest_editor_store),
|
||||
() =>
|
||||
new QuestRunnerRendererView(
|
||||
gui_store,
|
||||
quest_editor_store,
|
||||
area_asset_loader,
|
||||
entity_asset_loader,
|
||||
create_three_renderer(),
|
||||
),
|
||||
() => new RegistersView(quest_editor_store.quest_runner),
|
||||
);
|
||||
|
||||
return {
|
||||
view,
|
||||
dispose() {
|
||||
disposer.dispose();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -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}`;
|
||||
|
||||
|
@ -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,103 +95,89 @@ 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();
|
||||
/**
|
||||
* 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,
|
||||
NpcType.Hidoom,
|
||||
NpcType.DeathGunner,
|
||||
NpcType.StRappy,
|
||||
NpcType.HalloRappy,
|
||||
NpcType.EggRappy,
|
||||
NpcType.Migium2,
|
||||
NpcType.Hidoom2,
|
||||
NpcType.Recon,
|
||||
|
||||
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[]>>();
|
||||
|
||||
for (const type of [
|
||||
NpcType.Unknown,
|
||||
NpcType.Migium,
|
||||
NpcType.Hidoom,
|
||||
NpcType.DeathGunner,
|
||||
NpcType.StRappy,
|
||||
NpcType.HalloRappy,
|
||||
NpcType.EggRappy,
|
||||
NpcType.Migium2,
|
||||
NpcType.Hidoom2,
|
||||
NpcType.Recon,
|
||||
|
||||
ObjectType.Unknown,
|
||||
ObjectType.PlayerSet,
|
||||
ObjectType.Particle,
|
||||
ObjectType.LightCollision,
|
||||
ObjectType.EnvSound,
|
||||
ObjectType.FogCollision,
|
||||
ObjectType.EventCollision,
|
||||
ObjectType.CharaCollision,
|
||||
ObjectType.ObjRoomID,
|
||||
ObjectType.LensFlare,
|
||||
ObjectType.ScriptCollision,
|
||||
ObjectType.MapCollision,
|
||||
ObjectType.ScriptCollisionA,
|
||||
ObjectType.ItemLight,
|
||||
ObjectType.RadarCollision,
|
||||
ObjectType.FogCollisionSW,
|
||||
ObjectType.ImageBoard,
|
||||
ObjectType.UnknownItem29,
|
||||
ObjectType.UnknownItem30,
|
||||
ObjectType.UnknownItem31,
|
||||
ObjectType.MenuActivation,
|
||||
ObjectType.BoxDetectObject,
|
||||
ObjectType.SymbolChatObject,
|
||||
ObjectType.TouchPlateObject,
|
||||
ObjectType.TargetableObject,
|
||||
ObjectType.EffectObject,
|
||||
ObjectType.CountDownObject,
|
||||
ObjectType.UnknownItem38,
|
||||
ObjectType.UnknownItem39,
|
||||
ObjectType.UnknownItem40,
|
||||
ObjectType.UnknownItem41,
|
||||
ObjectType.TelepipeLocation,
|
||||
ObjectType.BGMCollision,
|
||||
ObjectType.Pioneer2InvisibleTouchplate,
|
||||
ObjectType.TempleMapDetect,
|
||||
ObjectType.Firework,
|
||||
ObjectType.MainRagolTeleporterBattleInNextArea,
|
||||
ObjectType.Rainbow,
|
||||
ObjectType.FloatingBlueLight,
|
||||
ObjectType.PopupTrapNoTech,
|
||||
ObjectType.Poison,
|
||||
ObjectType.EnemyTypeBoxYellow,
|
||||
ObjectType.EnemyTypeBoxBlue,
|
||||
ObjectType.EmptyTypeBoxBlue,
|
||||
ObjectType.FloatingRocks,
|
||||
ObjectType.FloatingSoul,
|
||||
ObjectType.Butterfly,
|
||||
ObjectType.UnknownItem400,
|
||||
ObjectType.CCAAreaTeleporter,
|
||||
ObjectType.UnknownItem523,
|
||||
ObjectType.WhiteBird,
|
||||
ObjectType.OrangeBird,
|
||||
ObjectType.UnknownItem529,
|
||||
ObjectType.UnknownItem530,
|
||||
ObjectType.Seagull,
|
||||
ObjectType.UnknownItem576,
|
||||
ObjectType.WarpInBarbaRayRoom,
|
||||
ObjectType.UnknownItem672,
|
||||
ObjectType.InstaWarp,
|
||||
ObjectType.LabInvisibleObject,
|
||||
ObjectType.UnknownItem700,
|
||||
]) {
|
||||
geom_cache.set(type, DEFAULT_ENTITY_PROMISE);
|
||||
tex_cache.set(type, DEFAULT_ENTITY_TEX_PROMISE);
|
||||
ObjectType.Unknown,
|
||||
ObjectType.PlayerSet,
|
||||
ObjectType.Particle,
|
||||
ObjectType.LightCollision,
|
||||
ObjectType.EnvSound,
|
||||
ObjectType.FogCollision,
|
||||
ObjectType.EventCollision,
|
||||
ObjectType.CharaCollision,
|
||||
ObjectType.ObjRoomID,
|
||||
ObjectType.LensFlare,
|
||||
ObjectType.ScriptCollision,
|
||||
ObjectType.MapCollision,
|
||||
ObjectType.ScriptCollisionA,
|
||||
ObjectType.ItemLight,
|
||||
ObjectType.RadarCollision,
|
||||
ObjectType.FogCollisionSW,
|
||||
ObjectType.ImageBoard,
|
||||
ObjectType.UnknownItem29,
|
||||
ObjectType.UnknownItem30,
|
||||
ObjectType.UnknownItem31,
|
||||
ObjectType.MenuActivation,
|
||||
ObjectType.BoxDetectObject,
|
||||
ObjectType.SymbolChatObject,
|
||||
ObjectType.TouchPlateObject,
|
||||
ObjectType.TargetableObject,
|
||||
ObjectType.EffectObject,
|
||||
ObjectType.CountDownObject,
|
||||
ObjectType.UnknownItem38,
|
||||
ObjectType.UnknownItem39,
|
||||
ObjectType.UnknownItem40,
|
||||
ObjectType.UnknownItem41,
|
||||
ObjectType.TelepipeLocation,
|
||||
ObjectType.BGMCollision,
|
||||
ObjectType.Pioneer2InvisibleTouchplate,
|
||||
ObjectType.TempleMapDetect,
|
||||
ObjectType.Firework,
|
||||
ObjectType.MainRagolTeleporterBattleInNextArea,
|
||||
ObjectType.Rainbow,
|
||||
ObjectType.FloatingBlueLight,
|
||||
ObjectType.PopupTrapNoTech,
|
||||
ObjectType.Poison,
|
||||
ObjectType.EnemyTypeBoxYellow,
|
||||
ObjectType.EnemyTypeBoxBlue,
|
||||
ObjectType.EmptyTypeBoxBlue,
|
||||
ObjectType.FloatingRocks,
|
||||
ObjectType.FloatingSoul,
|
||||
ObjectType.Butterfly,
|
||||
ObjectType.UnknownItem400,
|
||||
ObjectType.CCAAreaTeleporter,
|
||||
ObjectType.UnknownItem523,
|
||||
ObjectType.WhiteBird,
|
||||
ObjectType.OrangeBird,
|
||||
ObjectType.UnknownItem529,
|
||||
ObjectType.UnknownItem530,
|
||||
ObjectType.Seagull,
|
||||
ObjectType.UnknownItem576,
|
||||
ObjectType.WarpInBarbaRayRoom,
|
||||
ObjectType.UnknownItem672,
|
||||
ObjectType.InstaWarp,
|
||||
ObjectType.LabInvisibleObject,
|
||||
ObjectType.UnknownItem700,
|
||||
]) {
|
||||
this.geom_cache.set(type, DEFAULT_ENTITY_PROMISE);
|
||||
this.tex_cache.set(type, DEFAULT_ENTITY_TEX_PROMISE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum AssetType {
|
||||
|
@ -15,4 +15,8 @@ export class LoadingCache<K, V> {
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
purge_all(): void {
|
||||
this.map.clear();
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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,13 +269,13 @@ 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)) {
|
||||
this.update_entity_geometry(entity, model);
|
||||
}
|
||||
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 {
|
||||
|
@ -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;
|
||||
|
@ -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[] = [];
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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 & {
|
||||
/**
|
||||
|
@ -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 {
|
||||
/**
|
||||
|
@ -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.
|
||||
|
@ -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, []);
|
||||
|
@ -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) {
|
||||
|
@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -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 => {
|
||||
|
@ -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.
|
||||
|
@ -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();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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> {
|
||||
|
@ -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");
|
||||
|
@ -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.
|
||||
|
@ -1,25 +1,18 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"noEmit": true,
|
||||
"experimentalDecorators": true,
|
||||
"downlevelIteration": true
|
||||
},
|
||||
"include": [
|
||||
"static_generation"
|
||||
]
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es6",
|
||||
"lib": ["es6", "dom", "dom.iterable"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"moduleResolution": "node",
|
||||
"noEmit": true,
|
||||
"experimentalDecorators": true
|
||||
},
|
||||
"include": ["static_generation"]
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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({
|
||||
|
@ -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,
|
||||
},
|
||||
},
|
||||
],
|
||||
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,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
@ -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([
|
||||
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user