mirror of
https://github.com/DaanVandenBosch/phantasmal-world.git
synced 2025-04-05 15:28:29 +08:00
Quest script assembly can now be viewed.
This commit is contained in:
parent
99d401e785
commit
73e199d724
@ -32,6 +32,7 @@
|
|||||||
"no-console": "warn",
|
"no-console": "warn",
|
||||||
"no-constant-condition": ["warn", { "checkLoops": false }],
|
"no-constant-condition": ["warn", { "checkLoops": false }],
|
||||||
"no-empty": "warn",
|
"no-empty": "warn",
|
||||||
|
"no-useless-escape": "warn",
|
||||||
"prettier/prettier": "warn",
|
"prettier/prettier": "warn",
|
||||||
"react/no-unescaped-entities": "off",
|
"react/no-unescaped-entities": "off",
|
||||||
"@typescript-eslint/no-non-null-assertion": "off"
|
"@typescript-eslint/no-non-null-assertion": "off"
|
||||||
|
@ -1,7 +1,15 @@
|
|||||||
const CracoAntDesignPlugin = require("craco-antd");
|
const CracoAntDesignPlugin = require("craco-antd");
|
||||||
|
const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: [{ plugin: CracoAntDesignPlugin }],
|
plugins: [
|
||||||
|
{ plugin: CracoAntDesignPlugin },
|
||||||
|
{
|
||||||
|
plugin: new MonacoWebpackPlugin({
|
||||||
|
languages: []
|
||||||
|
})
|
||||||
|
}
|
||||||
|
],
|
||||||
eslint: {
|
eslint: {
|
||||||
mode: "file"
|
mode: "file"
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,8 @@
|
|||||||
"mobx": "^5.11.0",
|
"mobx": "^5.11.0",
|
||||||
"mobx-react": "^6.1.1",
|
"mobx-react": "^6.1.1",
|
||||||
"moment": "^2.24.0",
|
"moment": "^2.24.0",
|
||||||
|
"monaco-editor": "^0.17.1",
|
||||||
|
"monaco-editor-webpack-plugin": "^1.7.0",
|
||||||
"react": "^16.8.6",
|
"react": "^16.8.6",
|
||||||
"react-dom": "^16.8.6",
|
"react-dom": "^16.8.6",
|
||||||
"react-scripts": "^3.0.1",
|
"react-scripts": "^3.0.1",
|
||||||
|
@ -309,7 +309,7 @@ export class ResizableBufferCursor implements Cursor {
|
|||||||
const max_pos = Math.min(this.position + max_byte_length, this.size);
|
const max_pos = Math.min(this.position + max_byte_length, this.size);
|
||||||
|
|
||||||
for (let i = this.position; i < max_pos; ++i) {
|
for (let i = this.position; i < max_pos; ++i) {
|
||||||
if (this.dv.getUint8(i) === value) {
|
if (this.dv.getUint8(this.offset + i) === value) {
|
||||||
return i;
|
return i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -321,7 +321,7 @@ export class ResizableBufferCursor implements Cursor {
|
|||||||
const max_pos = Math.min(this.position + max_byte_length, this.size);
|
const max_pos = Math.min(this.position + max_byte_length, this.size);
|
||||||
|
|
||||||
for (let i = this.position; i < max_pos; i += 2) {
|
for (let i = this.position; i < max_pos; i += 2) {
|
||||||
if (this.dv.getUint16(i, this.little_endian) === value) {
|
if (this.dv.getUint16(this.offset + i, this.little_endian) === value) {
|
||||||
return i;
|
return i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -7,7 +7,7 @@ import { ArrayBufferCursor } from "../../cursor/ArrayBufferCursor";
|
|||||||
import { Cursor } from "../../cursor/Cursor";
|
import { Cursor } from "../../cursor/Cursor";
|
||||||
import { ResizableBufferCursor } from "../../cursor/ResizableBufferCursor";
|
import { ResizableBufferCursor } from "../../cursor/ResizableBufferCursor";
|
||||||
import { Vec3 } from "../../vector";
|
import { Vec3 } from "../../vector";
|
||||||
import { BB_MAP_DESIGNATE, Instruction, OP_RET, parse_bin, SET_EPISODE, write_bin } from "./bin";
|
import { BB_MAP_DESIGNATE, Instruction, parse_bin, SET_EPISODE, write_bin, BinFile } from "./bin";
|
||||||
import { DatFile, DatNpc, DatObject, parse_dat, write_dat } from "./dat";
|
import { DatFile, DatNpc, DatObject, parse_dat, write_dat } from "./dat";
|
||||||
import { parse_qst, QstContainedFile, write_qst } from "./qst";
|
import { parse_qst, QstContainedFile, write_qst } from "./qst";
|
||||||
|
|
||||||
@ -58,17 +58,21 @@ export function parse_quest(cursor: Cursor, lenient: boolean = false): Quest | u
|
|||||||
let episode = 1;
|
let episode = 1;
|
||||||
let area_variants: AreaVariant[] = [];
|
let area_variants: AreaVariant[] = [];
|
||||||
|
|
||||||
if (bin.function_offsets.length) {
|
if (bin.labels.size) {
|
||||||
const func_0_ops = get_func_instructions(bin.instructions, bin.function_offsets[0]);
|
if (bin.labels.has(0)) {
|
||||||
|
const label_0_instructions = bin.get_label_instructions(0);
|
||||||
|
|
||||||
if (func_0_ops) {
|
if (label_0_instructions) {
|
||||||
episode = get_episode(func_0_ops);
|
episode = get_episode(label_0_instructions);
|
||||||
area_variants = get_area_variants(dat, episode, func_0_ops, lenient);
|
area_variants = get_area_variants(dat, episode, label_0_instructions, lenient);
|
||||||
} else {
|
} else {
|
||||||
logger.warn(`Offset ${bin.function_offsets[0]} for function 0 is invalid.`);
|
logger.warn(`Index ${bin.labels.get(0)} for label 0 is invalid.`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
logger.warn("File contains no functions.");
|
logger.warn(`Label 0 not found.`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.warn("File contains no labels.");
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Quest(
|
return new Quest(
|
||||||
@ -82,7 +86,8 @@ export function parse_quest(cursor: Cursor, lenient: boolean = false): Quest | u
|
|||||||
parse_obj_data(dat.objs),
|
parse_obj_data(dat.objs),
|
||||||
parse_npc_data(episode, dat.npcs),
|
parse_npc_data(episode, dat.npcs),
|
||||||
dat.unknowns,
|
dat.unknowns,
|
||||||
bin.function_offsets,
|
bin.labels,
|
||||||
|
bin.instructions,
|
||||||
bin.object_code,
|
bin.object_code,
|
||||||
bin.unknown
|
bin.unknown
|
||||||
);
|
);
|
||||||
@ -94,17 +99,19 @@ export function write_quest_qst(quest: Quest, file_name: string): ArrayBuffer {
|
|||||||
npcs: npcs_to_dat_data(quest.npcs),
|
npcs: npcs_to_dat_data(quest.npcs),
|
||||||
unknowns: quest.dat_unknowns,
|
unknowns: quest.dat_unknowns,
|
||||||
});
|
});
|
||||||
const bin = write_bin({
|
const bin = write_bin(
|
||||||
quest_id: quest.id,
|
new BinFile(
|
||||||
language: quest.language,
|
quest.id,
|
||||||
quest_name: quest.name,
|
quest.language,
|
||||||
short_description: quest.short_description,
|
quest.name,
|
||||||
long_description: quest.long_description,
|
quest.short_description,
|
||||||
function_offsets: quest.function_offsets,
|
quest.long_description,
|
||||||
instructions: [],
|
quest.labels,
|
||||||
object_code: quest.object_code,
|
[],
|
||||||
unknown: quest.bin_unknown,
|
quest.object_code,
|
||||||
});
|
quest.bin_unknown
|
||||||
|
)
|
||||||
|
);
|
||||||
const ext_start = file_name.lastIndexOf(".");
|
const ext_start = file_name.lastIndexOf(".");
|
||||||
const base_file_name =
|
const base_file_name =
|
||||||
ext_start === -1 ? file_name.slice(0, 12) : file_name.slice(0, Math.min(12, ext_start));
|
ext_start === -1 ? file_name.slice(0, 12) : file_name.slice(0, Math.min(12, ext_start));
|
||||||
@ -194,34 +201,6 @@ function get_area_variants(
|
|||||||
return area_variants_array.sort((a, b) => a.area.order - b.area.order || a.id - b.id);
|
return area_variants_array.sort((a, b) => a.area.order - b.area.order || a.id - b.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
function get_func_instructions(
|
|
||||||
instructions: Instruction[],
|
|
||||||
func_offset: number
|
|
||||||
): Instruction[] | undefined {
|
|
||||||
let position = 0;
|
|
||||||
let func_found = false;
|
|
||||||
const func_ops: Instruction[] = [];
|
|
||||||
|
|
||||||
for (const instruction of instructions) {
|
|
||||||
if (position === func_offset) {
|
|
||||||
func_found = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (func_found) {
|
|
||||||
func_ops.push(instruction);
|
|
||||||
|
|
||||||
// Break when ret is encountered.
|
|
||||||
if (instruction.opcode === OP_RET) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
position += instruction.size;
|
|
||||||
}
|
|
||||||
|
|
||||||
return func_found ? func_ops : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function parse_obj_data(objs: DatObject[]): QuestObject[] {
|
function parse_obj_data(objs: DatObject[]): QuestObject[] {
|
||||||
return objs.map(obj_data => {
|
return objs.map(obj_data => {
|
||||||
return new QuestObject(
|
return new QuestObject(
|
||||||
|
@ -5,6 +5,7 @@ import { enum_values } from "../enums";
|
|||||||
import { ItemType } from "./items";
|
import { ItemType } from "./items";
|
||||||
import { NpcType } from "./NpcType";
|
import { NpcType } from "./NpcType";
|
||||||
import { ObjectType } from "./ObjectType";
|
import { ObjectType } from "./ObjectType";
|
||||||
|
import { Instruction } from "../data_formats/parsing/quest/bin";
|
||||||
|
|
||||||
export * from "./items";
|
export * from "./items";
|
||||||
export * from "./NpcType";
|
export * from "./NpcType";
|
||||||
@ -92,7 +93,8 @@ export class Quest {
|
|||||||
* (Partial) raw DAT data that can't be parsed yet by Phantasmal.
|
* (Partial) raw DAT data that can't be parsed yet by Phantasmal.
|
||||||
*/
|
*/
|
||||||
dat_unknowns: DatUnknown[];
|
dat_unknowns: DatUnknown[];
|
||||||
function_offsets: number[];
|
labels: Map<number, number>;
|
||||||
|
instructions: Instruction[];
|
||||||
object_code: ArrayBuffer;
|
object_code: ArrayBuffer;
|
||||||
bin_unknown: ArrayBuffer;
|
bin_unknown: ArrayBuffer;
|
||||||
|
|
||||||
@ -107,7 +109,8 @@ export class Quest {
|
|||||||
objects: QuestObject[],
|
objects: QuestObject[],
|
||||||
npcs: QuestNpc[],
|
npcs: QuestNpc[],
|
||||||
dat_unknowns: DatUnknown[],
|
dat_unknowns: DatUnknown[],
|
||||||
function_offsets: number[],
|
labels: Map<number, number>,
|
||||||
|
instructions: Instruction[],
|
||||||
object_code: ArrayBuffer,
|
object_code: ArrayBuffer,
|
||||||
bin_unknown: ArrayBuffer
|
bin_unknown: ArrayBuffer
|
||||||
) {
|
) {
|
||||||
@ -119,7 +122,8 @@ export class Quest {
|
|||||||
if (!objects || !(objects instanceof Array)) throw new Error("objs is required.");
|
if (!objects || !(objects instanceof Array)) throw new Error("objs is required.");
|
||||||
if (!npcs || !(npcs instanceof Array)) throw new Error("npcs is required.");
|
if (!npcs || !(npcs instanceof Array)) throw new Error("npcs is required.");
|
||||||
if (!dat_unknowns) throw new Error("dat_unknowns is required.");
|
if (!dat_unknowns) throw new Error("dat_unknowns is required.");
|
||||||
if (!function_offsets) throw new Error("function_offsets is required.");
|
if (!labels) throw new Error("labels is required.");
|
||||||
|
if (!instructions) throw new Error("instructions is required.");
|
||||||
if (!object_code) throw new Error("object_code is required.");
|
if (!object_code) throw new Error("object_code is required.");
|
||||||
if (!bin_unknown) throw new Error("bin_unknown is required.");
|
if (!bin_unknown) throw new Error("bin_unknown is required.");
|
||||||
|
|
||||||
@ -133,7 +137,8 @@ export class Quest {
|
|||||||
this.objects = objects;
|
this.objects = objects;
|
||||||
this.npcs = npcs;
|
this.npcs = npcs;
|
||||||
this.dat_unknowns = dat_unknowns;
|
this.dat_unknowns = dat_unknowns;
|
||||||
this.function_offsets = function_offsets;
|
this.labels = labels;
|
||||||
|
this.instructions = instructions;
|
||||||
this.object_code = object_code;
|
this.object_code = object_code;
|
||||||
this.bin_unknown = bin_unknown;
|
this.bin_unknown = bin_unknown;
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,7 @@ class QuestEditorStore {
|
|||||||
@observable current_quest_filename?: string;
|
@observable current_quest_filename?: string;
|
||||||
@observable current_quest?: Quest;
|
@observable current_quest?: Quest;
|
||||||
@observable current_area?: Area;
|
@observable current_area?: Area;
|
||||||
|
|
||||||
@observable selected_entity?: QuestEntity;
|
@observable selected_entity?: QuestEntity;
|
||||||
|
|
||||||
@observable save_dialog_filename?: string;
|
@observable save_dialog_filename?: string;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Quest, ObjectType, QuestObject, Episode, QuestNpc, NpcType } from "../domain";
|
|
||||||
import { area_store } from "./AreaStore";
|
|
||||||
import { Vec3 } from "../data_formats/vector";
|
import { Vec3 } from "../data_formats/vector";
|
||||||
|
import { Episode, NpcType, ObjectType, Quest, QuestNpc, QuestObject } from "../domain";
|
||||||
|
import { area_store } from "./AreaStore";
|
||||||
|
|
||||||
export function create_new_quest(episode: Episode): Quest {
|
export function create_new_quest(episode: Episode): Quest {
|
||||||
if (episode === Episode.II) throw new Error("Episode II not yet supported.");
|
if (episode === Episode.II) throw new Error("Episode II not yet supported.");
|
||||||
@ -16,6 +16,7 @@ export function create_new_quest(episode: Episode): Quest {
|
|||||||
create_default_objects(),
|
create_default_objects(),
|
||||||
create_default_npcs(),
|
create_default_npcs(),
|
||||||
[],
|
[],
|
||||||
|
new Map(),
|
||||||
[],
|
[],
|
||||||
new ArrayBuffer(0),
|
new ArrayBuffer(0),
|
||||||
new ArrayBuffer(0)
|
new ArrayBuffer(0)
|
||||||
|
@ -1,3 +0,0 @@
|
|||||||
.RendererComponent {
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
@ -1,33 +1,31 @@
|
|||||||
import React, { Component, ReactNode } from "react";
|
import React, { Component, ReactNode } from "react";
|
||||||
import { Renderer } from "../rendering/Renderer";
|
|
||||||
import "./RendererComponent.less";
|
|
||||||
import { Camera } from "three";
|
import { Camera } from "three";
|
||||||
|
import { Renderer } from "../rendering/Renderer";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
renderer: Renderer<Camera>;
|
renderer: Renderer<Camera>;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
debug?: boolean;
|
debug?: boolean;
|
||||||
className?: string;
|
|
||||||
on_will_unmount?: () => void;
|
on_will_unmount?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class RendererComponent extends Component<Props> {
|
export class RendererComponent extends Component<Props> {
|
||||||
render(): ReactNode {
|
render(): ReactNode {
|
||||||
let className = "RendererComponent";
|
return <div className="RendererComponent" ref={this.modify_dom} />;
|
||||||
if (this.props.className) className += " " + this.props.className;
|
|
||||||
|
|
||||||
return <div className={className} ref={this.modifyDom} />;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps(props: Props): void {
|
UNSAFE_componentWillReceiveProps(props: Props): void {
|
||||||
|
if (this.props.debug !== props.debug) {
|
||||||
this.props.renderer.debug = !!props.debug;
|
this.props.renderer.debug = !!props.debug;
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount(): void {
|
if (this.props.width !== props.width || this.props.height !== props.height) {
|
||||||
window.addEventListener("resize", this.onResize);
|
this.props.renderer.set_size(props.width, props.height);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount(): void {
|
componentWillUnmount(): void {
|
||||||
window.removeEventListener("resize", this.onResize);
|
|
||||||
this.props.on_will_unmount && this.props.on_will_unmount();
|
this.props.on_will_unmount && this.props.on_will_unmount();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -35,15 +33,10 @@ export class RendererComponent extends Component<Props> {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private modifyDom = (div: HTMLDivElement | null) => {
|
private modify_dom = (div: HTMLDivElement | null) => {
|
||||||
if (div) {
|
if (div) {
|
||||||
this.props.renderer.set_size(div.clientWidth, div.clientHeight);
|
this.props.renderer.set_size(this.props.width, this.props.height);
|
||||||
div.appendChild(this.props.renderer.dom_element);
|
div.appendChild(this.props.renderer.dom_element);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private onResize = () => {
|
|
||||||
const wrapper_div = this.props.renderer.dom_element.parentNode as HTMLDivElement;
|
|
||||||
this.props.renderer.set_size(wrapper_div.clientWidth, wrapper_div.clientHeight);
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
@ -1,14 +0,0 @@
|
|||||||
.qe-QuestEditorComponent {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.qe-QuestEditorComponent-main {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.qe-QuestEditorComponent-main > div:nth-child(2) {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
31
src/ui/quest_editor/QuestEditorComponent.less
Normal file
31
src/ui/quest_editor/QuestEditorComponent.less
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
.qe-QuestEditorComponent {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qe-QuestEditorComponent-main {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qe-QuestEditorComponent-tabcontainer {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
& > .ant-tabs-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.qe-QuestEditorComponent-tab.ant-tabs-tabpane-active {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qe-QuestEditorComponent-tab-main {
|
||||||
|
flex: 1;
|
||||||
|
}
|
@ -5,9 +5,12 @@ import { application_store } from "../../stores/ApplicationStore";
|
|||||||
import { quest_editor_store } from "../../stores/QuestEditorStore";
|
import { quest_editor_store } from "../../stores/QuestEditorStore";
|
||||||
import { RendererComponent } from "../RendererComponent";
|
import { RendererComponent } from "../RendererComponent";
|
||||||
import { EntityInfoComponent } from "./EntityInfoComponent";
|
import { EntityInfoComponent } from "./EntityInfoComponent";
|
||||||
import "./QuestEditorComponent.css";
|
import "./QuestEditorComponent.less";
|
||||||
import { QuestInfoComponent } from "./QuestInfoComponent";
|
import { QuestInfoComponent } from "./QuestInfoComponent";
|
||||||
import { Toolbar } from "./Toolbar";
|
import { Toolbar } from "./Toolbar";
|
||||||
|
import { Tabs } from "antd";
|
||||||
|
import { ScriptEditorComponent } from "./ScriptEditorComponent";
|
||||||
|
import { AutoSizer } from "react-virtualized";
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class QuestEditorComponent extends Component<{}, { debug: boolean }> {
|
export class QuestEditorComponent extends Component<{}, { debug: boolean }> {
|
||||||
@ -25,8 +28,34 @@ export class QuestEditorComponent extends Component<{}, { debug: boolean }> {
|
|||||||
<Toolbar />
|
<Toolbar />
|
||||||
<div className="qe-QuestEditorComponent-main">
|
<div className="qe-QuestEditorComponent-main">
|
||||||
<QuestInfoComponent quest={quest} />
|
<QuestInfoComponent quest={quest} />
|
||||||
<RendererComponent renderer={get_quest_renderer()} debug={this.state.debug} />
|
<Tabs type="card" className="qe-QuestEditorComponent-tabcontainer">
|
||||||
|
<Tabs.TabPane
|
||||||
|
tab="Entities"
|
||||||
|
key="entities"
|
||||||
|
className="qe-QuestEditorComponent-tab"
|
||||||
|
>
|
||||||
|
<div className="qe-QuestEditorComponent-tab-main">
|
||||||
|
<AutoSizer>
|
||||||
|
{({ width, height }) => (
|
||||||
|
<RendererComponent
|
||||||
|
renderer={get_quest_renderer()}
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
debug={this.state.debug}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</AutoSizer>
|
||||||
|
</div>
|
||||||
<EntityInfoComponent entity={quest_editor_store.selected_entity} />
|
<EntityInfoComponent entity={quest_editor_store.selected_entity} />
|
||||||
|
</Tabs.TabPane>
|
||||||
|
<Tabs.TabPane
|
||||||
|
tab="Script"
|
||||||
|
key="script"
|
||||||
|
className="qe-QuestEditorComponent-tab"
|
||||||
|
>
|
||||||
|
<ScriptEditorComponent className="qe-QuestEditorComponent-tab-main" />
|
||||||
|
</Tabs.TabPane>
|
||||||
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
0
src/ui/quest_editor/ScriptEditorComponent.less
Normal file
0
src/ui/quest_editor/ScriptEditorComponent.less
Normal file
178
src/ui/quest_editor/ScriptEditorComponent.tsx
Normal file
178
src/ui/quest_editor/ScriptEditorComponent.tsx
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
import { editor, languages } from "monaco-editor";
|
||||||
|
import React, { Component, createRef, ReactNode } from "react";
|
||||||
|
import { AutoSizer } from "react-virtualized";
|
||||||
|
import { OPCODES } from "../../data_formats/parsing/quest/bin";
|
||||||
|
import { quest_editor_store } from "../../stores/QuestEditorStore";
|
||||||
|
import "./ScriptEditorComponent.less";
|
||||||
|
import { disassemble } from "../scripting/disassembly";
|
||||||
|
import { IReactionDisposer, autorun } from "mobx";
|
||||||
|
|
||||||
|
const ASM_SYNTAX: languages.IMonarchLanguage = {
|
||||||
|
defaultToken: "invalid",
|
||||||
|
|
||||||
|
tokenizer: {
|
||||||
|
root: [
|
||||||
|
// Identifiers.
|
||||||
|
[/[a-z][\w=<>!]*/, "identifier"],
|
||||||
|
|
||||||
|
// Labels.
|
||||||
|
[/^\d+:/, "tag"],
|
||||||
|
|
||||||
|
// Registers.
|
||||||
|
[/r\d+/, "predefined"],
|
||||||
|
|
||||||
|
// Whitespace.
|
||||||
|
[/[ \t\r\n]+/, "white"],
|
||||||
|
[/\/\*/, "comment", "@comment"],
|
||||||
|
[/\/\/.*$/, "comment"],
|
||||||
|
|
||||||
|
// Numbers.
|
||||||
|
[/-?\d*\.\d+([eE][-+]?\d+)?/, "number.float"],
|
||||||
|
[/-?0[xX][0-9a-fA-F]+/, "number.hex"],
|
||||||
|
[/-?\d+/, "number"],
|
||||||
|
|
||||||
|
// Delimiters.
|
||||||
|
[/,/, "delimiter"],
|
||||||
|
|
||||||
|
// Strings.
|
||||||
|
[/"([^"\\]|\\.)*$/, "string.invalid"], // Unterminated string.
|
||||||
|
[/"/, { token: "string.quote", bracket: "@open", next: "@string" }],
|
||||||
|
],
|
||||||
|
|
||||||
|
comment: [
|
||||||
|
[/[^/*]+/, "comment"],
|
||||||
|
[/\/\*/, "comment", "@push"], // Nested comment.
|
||||||
|
[/\*\//, "comment", "@pop"],
|
||||||
|
[/[/*]/, "comment"],
|
||||||
|
],
|
||||||
|
|
||||||
|
string: [
|
||||||
|
[/[^\\"]+/, "string"],
|
||||||
|
[/\\(?:[n\\"])/, "string.escape"],
|
||||||
|
[/\\./, "string.escape.invalid"],
|
||||||
|
[/"/, { token: "string.quote", bracket: "@close", next: "@pop" }],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const INSTRUCTION_SUGGESTIONS = OPCODES.map(opcode => {
|
||||||
|
return ({
|
||||||
|
label: opcode.mnemonic,
|
||||||
|
kind: languages.CompletionItemKind.Function,
|
||||||
|
insertText: opcode.mnemonic,
|
||||||
|
} as any) as languages.CompletionItem;
|
||||||
|
});
|
||||||
|
|
||||||
|
languages.register({ id: "psoasm" });
|
||||||
|
languages.setMonarchTokensProvider("psoasm", ASM_SYNTAX);
|
||||||
|
languages.registerCompletionItemProvider("psoasm", {
|
||||||
|
provideCompletionItems: (model, position) => {
|
||||||
|
const value = model.getValueInRange({
|
||||||
|
startLineNumber: position.lineNumber,
|
||||||
|
endLineNumber: position.lineNumber,
|
||||||
|
startColumn: 1,
|
||||||
|
endColumn: position.column + 1,
|
||||||
|
});
|
||||||
|
const suggest = /^\s*([a-z][\w=<>!]*)?$/.test(value);
|
||||||
|
|
||||||
|
return {
|
||||||
|
suggestions: suggest ? INSTRUCTION_SUGGESTIONS : [],
|
||||||
|
incomplete: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
languages.setLanguageConfiguration("psoasm", {
|
||||||
|
indentationRules: {
|
||||||
|
increaseIndentPattern: /\d+:/,
|
||||||
|
decreaseIndentPattern: /\d+/,
|
||||||
|
},
|
||||||
|
autoClosingPairs: [{ open: '"', close: '"' }],
|
||||||
|
surroundingPairs: [{ open: '"', close: '"' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
editor.defineTheme("phantasmal-world", {
|
||||||
|
base: "vs-dark",
|
||||||
|
inherit: true,
|
||||||
|
rules: [{ token: "", background: "151c21" }],
|
||||||
|
colors: {
|
||||||
|
"editor.background": "#151c21",
|
||||||
|
"editor.lineHighlightBackground": "#1a2228",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export class ScriptEditorComponent extends Component<{ className?: string }> {
|
||||||
|
render(): ReactNode {
|
||||||
|
let className = "qe-ScriptEditorComponent";
|
||||||
|
if (this.props.className) className += " " + this.props.className;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className={className}>
|
||||||
|
<AutoSizer>
|
||||||
|
{({ width, height }) => <MonacoComponent width={width} height={height} />}
|
||||||
|
</AutoSizer>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type MonacoProps = {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
class MonacoComponent extends Component<MonacoProps> {
|
||||||
|
private div_ref = createRef<HTMLDivElement>();
|
||||||
|
private editor?: editor.IStandaloneCodeEditor;
|
||||||
|
private disposer?: IReactionDisposer;
|
||||||
|
|
||||||
|
render(): ReactNode {
|
||||||
|
return <div ref={this.div_ref} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount(): void {
|
||||||
|
if (this.div_ref.current) {
|
||||||
|
// model.onDidChangeContent(e => {
|
||||||
|
// e.changes[0].range
|
||||||
|
// })
|
||||||
|
|
||||||
|
this.editor = editor.create(this.div_ref.current, {
|
||||||
|
theme: "phantasmal-world",
|
||||||
|
scrollBeyondLastLine: false,
|
||||||
|
autoIndent: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.disposer = autorun(() => {
|
||||||
|
const quest = quest_editor_store.current_quest;
|
||||||
|
const model = quest && editor.createModel(disassemble(quest), "psoasm");
|
||||||
|
|
||||||
|
if (model && this.editor) {
|
||||||
|
this.editor.setModel(model);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount(): void {
|
||||||
|
if (this.editor) {
|
||||||
|
const model = this.editor.getModel();
|
||||||
|
if (model) model.dispose();
|
||||||
|
|
||||||
|
this.editor.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.disposer) this.disposer();
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldComponentUpdate(): boolean {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
UNSAFE_componentWillReceiveProps(props: MonacoProps): void {
|
||||||
|
if (
|
||||||
|
(this.props.width !== props.width || this.props.height !== props.height) &&
|
||||||
|
this.editor
|
||||||
|
) {
|
||||||
|
this.editor.layout(props);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
68
src/ui/scripting/disassembly.ts
Normal file
68
src/ui/scripting/disassembly.ts
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import { Arg, Type } from "../../data_formats/parsing/quest/bin";
|
||||||
|
import { Quest } from "../../domain";
|
||||||
|
|
||||||
|
export function disassemble(quest: Quest, manual_stack: boolean = false): string {
|
||||||
|
const lines: string[] = [];
|
||||||
|
const index_to_label = [...quest.labels.entries()].reduce(
|
||||||
|
(map, [l, i]) => map.set(i, l),
|
||||||
|
new Map<number, number>()
|
||||||
|
);
|
||||||
|
|
||||||
|
const stack: Arg[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < quest.instructions.length; ++i) {
|
||||||
|
const ins = quest.instructions[i];
|
||||||
|
const label = index_to_label.get(i);
|
||||||
|
|
||||||
|
if (!manual_stack && ins.opcode.push_stack) {
|
||||||
|
stack.push(...ins.args);
|
||||||
|
} else {
|
||||||
|
let args: string[] = [];
|
||||||
|
|
||||||
|
for (let j = 0; j < ins.opcode.params.length; j++) {
|
||||||
|
const param_type = ins.opcode.params[j];
|
||||||
|
const arg = ins.args[j];
|
||||||
|
args.push(...arg_to_strings(param_type, arg));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!manual_stack) {
|
||||||
|
for (let j = ins.opcode.stack_params.length - 1; j >= 0; j--) {
|
||||||
|
const param_type = ins.opcode.stack_params[j];
|
||||||
|
const arg = stack.pop();
|
||||||
|
|
||||||
|
if (!arg) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
args.push(...arg_to_strings(param_type, arg));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (label != null) {
|
||||||
|
lines.push(`${label}:`);
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push(" " + ins.opcode.mnemonic + (args.length ? " " + args.join(", ") : ""));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function arg_to_strings(param_type: Type, arg: Arg): string[] {
|
||||||
|
switch (param_type) {
|
||||||
|
case Type.U8:
|
||||||
|
case Type.U16:
|
||||||
|
case Type.U32:
|
||||||
|
case Type.I32:
|
||||||
|
case Type.F32:
|
||||||
|
return [arg.value.toString()];
|
||||||
|
case Type.Register:
|
||||||
|
return ["r" + arg.value];
|
||||||
|
case Type.SwitchData:
|
||||||
|
case Type.JumpData:
|
||||||
|
return arg.value.map(String);
|
||||||
|
case Type.String:
|
||||||
|
return [JSON.stringify(arg.value)];
|
||||||
|
}
|
||||||
|
}
|
@ -28,8 +28,8 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
& > div:nth-child(3) {
|
.v-m-ModelViewerComponent-renderer {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
@ -3,12 +3,13 @@ import { UploadChangeParam } from "antd/lib/upload";
|
|||||||
import { UploadFile } from "antd/lib/upload/interface";
|
import { UploadFile } from "antd/lib/upload/interface";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import React, { Component, ReactNode } from "react";
|
import React, { Component, ReactNode } from "react";
|
||||||
|
import { AutoSizer } from "react-virtualized";
|
||||||
|
import { get_model_renderer } from "../../../rendering/ModelRenderer";
|
||||||
import { model_viewer_store } from "../../../stores/ModelViewerStore";
|
import { model_viewer_store } from "../../../stores/ModelViewerStore";
|
||||||
|
import { RendererComponent } from "../../RendererComponent";
|
||||||
import { AnimationSelectionComponent } from "./AnimationSelectionComponent";
|
import { AnimationSelectionComponent } from "./AnimationSelectionComponent";
|
||||||
import { ModelSelectionComponent } from "./ModelSelectionComponent";
|
import { ModelSelectionComponent } from "./ModelSelectionComponent";
|
||||||
import "./ModelViewerComponent.less";
|
import "./ModelViewerComponent.less";
|
||||||
import { get_model_renderer } from "../../../rendering/ModelRenderer";
|
|
||||||
import { RendererComponent } from "../../RendererComponent";
|
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class ModelViewerComponent extends Component {
|
export class ModelViewerComponent extends Component {
|
||||||
@ -25,10 +26,18 @@ export class ModelViewerComponent extends Component {
|
|||||||
<div className="v-m-ModelViewerComponent-main">
|
<div className="v-m-ModelViewerComponent-main">
|
||||||
<ModelSelectionComponent />
|
<ModelSelectionComponent />
|
||||||
<AnimationSelectionComponent />
|
<AnimationSelectionComponent />
|
||||||
|
<div className="v-m-ModelViewerComponent-renderer">
|
||||||
|
<AutoSizer>
|
||||||
|
{({ width, height }) => (
|
||||||
<RendererComponent
|
<RendererComponent
|
||||||
renderer={get_model_renderer()}
|
renderer={get_model_renderer()}
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
on_will_unmount={model_viewer_store.pause_animation}
|
on_will_unmount={model_viewer_store.pause_animation}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
</AutoSizer>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -6,16 +6,24 @@ import { get_texture_renderer } from "../../../rendering/TextureRenderer";
|
|||||||
import { texture_viewer_store } from "../../../stores/TextureViewerStore";
|
import { texture_viewer_store } from "../../../stores/TextureViewerStore";
|
||||||
import { RendererComponent } from "../../RendererComponent";
|
import { RendererComponent } from "../../RendererComponent";
|
||||||
import "./TextureViewerComponent.less";
|
import "./TextureViewerComponent.less";
|
||||||
|
import { AutoSizer } from "react-virtualized";
|
||||||
|
|
||||||
export class TextureViewerComponent extends Component {
|
export class TextureViewerComponent extends Component {
|
||||||
render(): ReactNode {
|
render(): ReactNode {
|
||||||
return (
|
return (
|
||||||
<section className="v-t-TextureViewerComponent">
|
<section className="v-t-TextureViewerComponent">
|
||||||
<Toolbar />
|
<Toolbar />
|
||||||
|
<div className="v-t-TextureViewerComponent-renderer">
|
||||||
|
<AutoSizer>
|
||||||
|
{({ width, height }) => (
|
||||||
<RendererComponent
|
<RendererComponent
|
||||||
renderer={get_texture_renderer()}
|
renderer={get_texture_renderer()}
|
||||||
className={"v-t-TextureViewerComponent-renderer"}
|
width={width}
|
||||||
|
height={height}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
</AutoSizer>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
40
yarn.lock
40
yarn.lock
@ -1270,6 +1270,11 @@
|
|||||||
"@svgr/plugin-svgo" "^4.0.3"
|
"@svgr/plugin-svgo" "^4.0.3"
|
||||||
loader-utils "^1.1.0"
|
loader-utils "^1.1.0"
|
||||||
|
|
||||||
|
"@types/anymatch@*":
|
||||||
|
version "1.3.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/anymatch/-/anymatch-1.3.1.tgz#336badc1beecb9dacc38bea2cf32adf627a8421a"
|
||||||
|
integrity sha512-/+CRPXpBDpo2RK9C68N3b2cOvO0Cf5B9aPijHsoDQTHivnGSObdOF2BRQOYjojWTDy6nQvMjmqRXIxH55VjxxA==
|
||||||
|
|
||||||
"@types/babel__core@^7.1.0":
|
"@types/babel__core@^7.1.0":
|
||||||
version "7.1.2"
|
version "7.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.2.tgz#608c74f55928033fce18b99b213c16be4b3d114f"
|
resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.2.tgz#608c74f55928033fce18b99b213c16be4b3d114f"
|
||||||
@ -1426,11 +1431,23 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e"
|
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e"
|
||||||
integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==
|
integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==
|
||||||
|
|
||||||
|
"@types/tapable@*":
|
||||||
|
version "1.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.4.tgz#b4ffc7dc97b498c969b360a41eee247f82616370"
|
||||||
|
integrity sha512-78AdXtlhpCHT0K3EytMpn4JNxaf5tbqbLcbIRoQIHzpTIyjpxLQKRoxU55ujBXAtg3Nl2h/XWvfDa9dsMOd0pQ==
|
||||||
|
|
||||||
"@types/text-encoding@^0.0.35":
|
"@types/text-encoding@^0.0.35":
|
||||||
version "0.0.35"
|
version "0.0.35"
|
||||||
resolved "https://registry.yarnpkg.com/@types/text-encoding/-/text-encoding-0.0.35.tgz#6f14474e0b232bc70c59677aadc65dcc5a99c3a9"
|
resolved "https://registry.yarnpkg.com/@types/text-encoding/-/text-encoding-0.0.35.tgz#6f14474e0b232bc70c59677aadc65dcc5a99c3a9"
|
||||||
integrity sha512-jfo/A88XIiAweUa8np+1mPbm3h2w0s425YrI8t3wk5QxhH6UI7w517MboNVnGDeMSuoFwA8Rwmklno+FicvV4g==
|
integrity sha512-jfo/A88XIiAweUa8np+1mPbm3h2w0s425YrI8t3wk5QxhH6UI7w517MboNVnGDeMSuoFwA8Rwmklno+FicvV4g==
|
||||||
|
|
||||||
|
"@types/uglify-js@*":
|
||||||
|
version "3.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.0.4.tgz#96beae23df6f561862a830b4288a49e86baac082"
|
||||||
|
integrity sha512-SudIN9TRJ+v8g5pTG8RRCqfqTMNqgWCKKd3vtynhGzkIIjxaicNAMuY5TRadJ6tzDu3Dotf3ngaMILtmOdmWEQ==
|
||||||
|
dependencies:
|
||||||
|
source-map "^0.6.1"
|
||||||
|
|
||||||
"@types/unist@*", "@types/unist@^2.0.0", "@types/unist@^2.0.2":
|
"@types/unist@*", "@types/unist@^2.0.0", "@types/unist@^2.0.2":
|
||||||
version "2.0.3"
|
version "2.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.3.tgz#9c088679876f374eb5983f150d4787aa6fb32d7e"
|
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.3.tgz#9c088679876f374eb5983f150d4787aa6fb32d7e"
|
||||||
@ -1453,6 +1470,17 @@
|
|||||||
"@types/unist" "*"
|
"@types/unist" "*"
|
||||||
"@types/vfile-message" "*"
|
"@types/vfile-message" "*"
|
||||||
|
|
||||||
|
"@types/webpack@^4.4.19":
|
||||||
|
version "4.32.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-4.32.0.tgz#bd4a149964cd471538f2781f2be5f9815175463b"
|
||||||
|
integrity sha512-kpz5wHDyG/WEpzX9gcwFp/w0oSsq0n/rmFdJelk/QBMHmNIOZdiTDInV0Lj8itGKBahQrBgJGJRss/6UHgLuKg==
|
||||||
|
dependencies:
|
||||||
|
"@types/anymatch" "*"
|
||||||
|
"@types/node" "*"
|
||||||
|
"@types/tapable" "*"
|
||||||
|
"@types/uglify-js" "*"
|
||||||
|
source-map "^0.6.0"
|
||||||
|
|
||||||
"@types/yargs@^12.0.2", "@types/yargs@^12.0.9":
|
"@types/yargs@^12.0.2", "@types/yargs@^12.0.9":
|
||||||
version "12.0.12"
|
version "12.0.12"
|
||||||
resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-12.0.12.tgz#45dd1d0638e8c8f153e87d296907659296873916"
|
resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-12.0.12.tgz#45dd1d0638e8c8f153e87d296907659296873916"
|
||||||
@ -6851,6 +6879,18 @@ moment@2.x, moment@^2.24.0:
|
|||||||
resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b"
|
resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b"
|
||||||
integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==
|
integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==
|
||||||
|
|
||||||
|
monaco-editor-webpack-plugin@^1.7.0:
|
||||||
|
version "1.7.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/monaco-editor-webpack-plugin/-/monaco-editor-webpack-plugin-1.7.0.tgz#920cbeecca25f15d70d568a7e11b0ba4daf1ae83"
|
||||||
|
integrity sha512-oItymcnlL14Sjd7EF7q+CMhucfwR/2BxsqrXIBrWL6LQplFfAfV+grLEQRmVHeGSBZ/Gk9ptzfueXnWcoEcFuA==
|
||||||
|
dependencies:
|
||||||
|
"@types/webpack" "^4.4.19"
|
||||||
|
|
||||||
|
monaco-editor@^0.17.1:
|
||||||
|
version "0.17.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.17.1.tgz#8fbe96ca54bfa75262706e044f8f780e904aa45c"
|
||||||
|
integrity sha512-JAc0mtW7NeO+0SwPRcdkfDbWLgkqL9WfP1NbpP9wNASsW6oWqgZqNIWt4teymGjZIXTElx3dnQmUYHmVrJ7HxA==
|
||||||
|
|
||||||
move-concurrently@^1.0.1:
|
move-concurrently@^1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92"
|
resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92"
|
||||||
|
Loading…
Reference in New Issue
Block a user