Added DefinitionProvider and refactored AsmAnalyser to make it easier to delegate to a web worker or possibly an LSP server in the future.

This commit is contained in:
Daan Vanden Bosch 2020-12-07 20:05:25 +01:00
parent 0133e82d3f
commit fb7aaf2906
25 changed files with 838 additions and 429 deletions

View File

@ -2,7 +2,7 @@ package world.phantasmal.lib.asm
import world.phantasmal.core.isDigit
private val HEX_INT_REGEX = Regex("""^0x[\da-fA-F]+$""")
private val HEX_INT_REGEX = Regex("""^0[xX][0-9a-fA-F]+$""")
private val FLOAT_REGEX = Regex("""^-?\d+(\.\d+)?(e-?\d+)?$""")
private val IDENT_REGEX = Regex("""^[a-z][a-z0-9_=<>!]*$""")
@ -179,7 +179,7 @@ private class LineTokenizer(private var line: String) {
private fun tokenizeNumberOrLabel(): Token {
mark()
val col = this.col
skip()
val firstChar = next()
var isLabel = false
while (hasNext()) {
@ -187,7 +187,7 @@ private class LineTokenizer(private var line: String) {
if (char == '.' || char == 'e') {
return tokenizeFloat(col)
} else if (char == 'x') {
} else if (firstChar == '0' && (char == 'x' || char == 'X')) {
return tokenizeHexNumber(col)
} else if (char == ':') {
isLabel = true
@ -221,7 +221,7 @@ private class LineTokenizer(private var line: String) {
val hexStr = slice()
if (HEX_INT_REGEX.matches(hexStr)) {
hexStr.toIntOrNull(16)?.let { value ->
hexStr.drop(2).toIntOrNull(16)?.let { value ->
return Token.Int32(col, markedLen(), value)
}
}

View File

@ -19,16 +19,16 @@ class AssemblyProblem(
) : Problem(severity, uiMessage, message, cause)
fun assemble(
assembly: List<String>,
asm: List<String>,
inlineStackArgs: Boolean = true,
): PwResult<List<Segment>> {
): PwResult<BytecodeIr> {
logger.trace {
"Assembling ${assembly.size} lines with ${
"Assembling ${asm.size} lines with ${
if (inlineStackArgs) "inline stack arguments" else "stack push instructions"
}."
}
val result = Assembler(assembly, inlineStackArgs).assemble()
val result = Assembler(asm, inlineStackArgs).assemble()
logger.trace {
val warnings = result.problems.count { it.severity == Severity.Warning }
@ -40,7 +40,7 @@ fun assemble(
return result
}
private class Assembler(private val assembly: List<String>, private val inlineStackArgs: Boolean) {
private class Assembler(private val asm: List<String>, private val inlineStackArgs: Boolean) {
private var lineNo = 1
private lateinit var tokens: MutableList<Token>
private var ir: MutableList<Segment> = mutableListOf()
@ -58,11 +58,11 @@ private class Assembler(private val assembly: List<String>, private val inlineSt
private var firstSectionMarker = true
private var prevLineHadLabel = false
private val result = PwResult.build<List<Segment>>(logger)
private val result = PwResult.build<BytecodeIr>(logger)
fun assemble(): PwResult<List<Segment>> {
fun assemble(): PwResult<BytecodeIr> {
// Tokenize and assemble line by line.
for (line in assembly) {
for (line in asm) {
tokens = tokenizeLine(line)
if (tokens.isNotEmpty()) {
@ -115,7 +115,7 @@ private class Assembler(private val assembly: List<String>, private val inlineSt
lineNo++
}
return result.success(ir)
return result.success(BytecodeIr(ir))
}
private fun addInstruction(

View File

@ -0,0 +1,215 @@
package world.phantasmal.lib.asm
import world.phantasmal.lib.buffer.Buffer
/**
* Intermediate representation of PSO bytecode. Used by most ASM/bytecode analysis code.
*/
class BytecodeIr(
val segments: List<Segment>,
) {
fun instructionSegments(): List<InstructionSegment> =
segments.filterIsInstance<InstructionSegment>()
}
enum class SegmentType {
Instructions,
Data,
String,
}
/**
* Segment of byte code. A segment starts with an instruction, byte or string character that is
* referenced by one or more labels. The segment ends right before the next instruction, byte or
* string character that is referenced by a label.
*/
sealed class Segment(
val type: SegmentType,
val labels: MutableList<Int>,
val srcLoc: SegmentSrcLoc,
)
class InstructionSegment(
labels: MutableList<Int>,
val instructions: MutableList<Instruction>,
srcLoc: SegmentSrcLoc,
) : Segment(SegmentType.Instructions, labels, srcLoc)
class DataSegment(
labels: MutableList<Int>,
val data: Buffer,
srcLoc: SegmentSrcLoc,
) : Segment(SegmentType.Data, labels, srcLoc)
class StringSegment(
labels: MutableList<Int>,
var value: String,
srcLoc: SegmentSrcLoc,
) : Segment(SegmentType.String, labels, srcLoc)
/**
* Opcode invocation.
*/
class Instruction(
val opcode: Opcode,
/**
* Immediate arguments for the opcode.
*/
val args: List<Arg>,
val srcLoc: InstructionSrcLoc?,
) {
/**
* Maps each parameter by index to its immediate arguments.
*/
private val paramToArgs: List<List<Arg>>
init {
val paramToArgs: MutableList<MutableList<Arg>> = mutableListOf()
this.paramToArgs = paramToArgs
if (opcode.stack != StackInteraction.Pop) {
for (i in opcode.params.indices) {
val type = opcode.params[i].type
val pArgs = mutableListOf<Arg>()
paramToArgs.add(pArgs)
// Variable length arguments are always last, so we can just gobble up all arguments
// from this point.
if (type is ILabelVarType || type is RegRefVarType) {
check(i == opcode.params.lastIndex)
for (j in i until args.size) {
pArgs.add(args[j])
}
} else {
pArgs.add(args[i])
}
}
}
}
/**
* Returns the immediate arguments for the parameter at the given index.
*/
fun getArgs(paramIndex: Int): List<Arg> = paramToArgs[paramIndex]
/**
* Returns the source locations of the immediate arguments for the parameter at the given index.
*/
fun getArgSrcLocs(paramIndex: Int): List<SrcLoc> {
val argSrcLocs = srcLoc?.args
?: return emptyList()
val type = opcode.params[paramIndex].type
// Variable length arguments are always last, so we can just gobble up all SrcLocs from
// paramIndex onward.
return if (type is ILabelVarType || type is RegRefVarType) {
argSrcLocs.drop(paramIndex)
} else {
listOf(argSrcLocs[paramIndex])
}
}
/**
* Returns the source locations of the stack arguments for the parameter at the given index.
*/
fun getStackArgSrcLocs(paramIndex: Int): List<StackArgSrcLoc> {
val argSrcLocs = srcLoc?.stackArgs
if (argSrcLocs == null || paramIndex > argSrcLocs.lastIndex) {
return emptyList()
}
val type = opcode.params[paramIndex].type
// Variable length arguments are always last, so we can just gobble up all SrcLocs from
// paramIndex onward.
return if (type is ILabelVarType || type is RegRefVarType) {
argSrcLocs.drop(paramIndex)
} else {
listOf(argSrcLocs[paramIndex])
}
}
/**
* Returns the byte size of the entire instruction, i.e. the sum of the opcode size and all
* argument sizes.
*/
fun getSize(dcGcFormat: Boolean): Int {
var size = opcode.size
if (opcode.stack == StackInteraction.Pop) return size
for (i in opcode.params.indices) {
val type = opcode.params[i].type
val args = getArgs(i)
size += when (type) {
is ByteType,
is RegRefType,
is RegTupRefType,
-> 1
// Ensure this case is before the LabelType case because ILabelVarType extends
// LabelType.
is ILabelVarType -> 1 + 2 * args.size
is ShortType,
is LabelType,
-> 2
is IntType,
is FloatType,
-> 4
is StringType -> {
if (dcGcFormat) {
(args[0].value as String).length + 1
} else {
2 * (args[0].value as String).length + 2
}
}
is RegRefVarType -> 1 + args.size
else -> error("Parameter type ${type::class} not implemented.")
}
}
return size
}
}
/**
* Instruction argument.
*/
data class Arg(val value: Any)
/**
* Position and length of related source assembly code.
*/
open class SrcLoc(
val lineNo: Int,
val col: Int,
val len: Int,
)
/**
* Locations of the instruction parts in the source assembly code.
*/
class InstructionSrcLoc(
val mnemonic: SrcLoc?,
val args: List<SrcLoc>,
val stackArgs: List<StackArgSrcLoc>,
)
/**
* Locations of an instruction's stack arguments in the source assembly code.
*/
class StackArgSrcLoc(lineNo: Int, col: Int, len: Int, val value: Any) : SrcLoc(lineNo, col, len)
/**
* Locations of a segment's labels in the source assembly code.
*/
class SegmentSrcLoc(val labels: MutableList<SrcLoc> = mutableListOf())

View File

@ -13,9 +13,9 @@ private val INDENT = " ".repeat(INDENT_WIDTH)
* @param inlineStackArgs If true, will output stack arguments inline instead of outputting stack
* management instructions (argpush variants).
*/
fun disassemble(bytecodeIr: List<Segment>, inlineStackArgs: Boolean = true): List<String> {
fun disassemble(bytecodeIr: BytecodeIr, inlineStackArgs: Boolean = true): List<String> {
logger.trace {
"Disassembling ${bytecodeIr.size} segments with ${
"Disassembling ${bytecodeIr.segments.size} segments with ${
if (inlineStackArgs) "inline stack arguments" else "stack push instructions"
}."
}
@ -24,7 +24,7 @@ fun disassemble(bytecodeIr: List<Segment>, inlineStackArgs: Boolean = true): Lis
val stack = mutableListOf<ArgWithType>()
var sectionType: SegmentType? = null
for (segment in bytecodeIr) {
for (segment in bytecodeIr.segments) {
// Section marker (.code, .data or .string).
if (sectionType != segment.type) {
sectionType = segment.type

View File

@ -1,156 +0,0 @@
package world.phantasmal.lib.asm
import world.phantasmal.lib.buffer.Buffer
import kotlin.math.min
/**
* Opcode invocation.
*/
class Instruction(
val opcode: Opcode,
val args: List<Arg>,
val srcLoc: InstructionSrcLoc?,
) {
/**
* Maps each parameter by index to its arguments.
*/
val paramToArgs: List<List<Arg>>
init {
val len = min(opcode.params.size, args.size)
val paramToArgs: MutableList<MutableList<Arg>> = mutableListOf()
for (i in 0 until len) {
val type = opcode.params[i].type
val arg = args[i]
val pArgs = mutableListOf<Arg>()
paramToArgs.add(pArgs)
if (type is ILabelVarType || type is RegRefVarType) {
for (j in i until args.size) {
pArgs.add(args[j])
}
} else {
pArgs.add(arg)
}
}
this.paramToArgs = paramToArgs
}
}
/**
* Returns the byte size of the entire instruction, i.e. the sum of the opcode size and all
* argument sizes.
*/
fun instructionSize(instruction: Instruction, dcGcFormat: Boolean): Int {
val opcode = instruction.opcode
val pLen = min(opcode.params.size, instruction.paramToArgs.size)
var argSize = 0
for (i in 0 until pLen) {
val type = opcode.params[i].type
val args = instruction.paramToArgs[i]
argSize += when (type) {
is ByteType,
is RegRefType,
is RegTupRefType,
-> 1
// Ensure this case is before the LabelType case because ILabelVarType extends
// LabelType.
is ILabelVarType -> 1 + 2 * args.size
is ShortType,
is LabelType,
-> 2
is IntType,
is FloatType,
-> 4
is StringType -> {
if (dcGcFormat) {
(args[0].value as String).length + 1
} else {
2 * (args[0].value as String).length + 2
}
}
is RegRefVarType -> 1 + args.size
else -> error("Parameter type ${type::class} not implemented.")
}
}
return opcode.size + argSize
}
/**
* Instruction argument.
*/
data class Arg(val value: Any)
enum class SegmentType {
Instructions,
Data,
String,
}
/**
* Segment of byte code. A segment starts with an instruction, byte or string character that is
* referenced by one or more labels. The segment ends right before the next instruction, byte or
* string character that is referenced by a label.
*/
sealed class Segment(
val type: SegmentType,
val labels: MutableList<Int>,
val srcLoc: SegmentSrcLoc,
)
class InstructionSegment(
labels: MutableList<Int>,
val instructions: MutableList<Instruction>,
srcLoc: SegmentSrcLoc,
) : Segment(SegmentType.Instructions, labels, srcLoc)
class DataSegment(
labels: MutableList<Int>,
val data: Buffer,
srcLoc: SegmentSrcLoc,
) : Segment(SegmentType.Data, labels, srcLoc)
class StringSegment(
labels: MutableList<Int>,
var value: String,
srcLoc: SegmentSrcLoc,
) : Segment(SegmentType.String, labels, srcLoc)
/**
* Position and length of related source assembly code.
*/
open class SrcLoc(
val lineNo: Int,
val col: Int,
val len: Int,
)
/**
* Locations of the instruction parts in the source assembly code.
*/
class InstructionSrcLoc(
val mnemonic: SrcLoc?,
val args: List<SrcLoc>,
val stackArgs: List<StackArgSrcLoc>,
)
/**
* Locations of an instruction's stack arguments in the source assembly code.
*/
class StackArgSrcLoc(lineNo: Int, col: Int, len: Int, val value: Any) : SrcLoc(lineNo, col, len)
/**
* Locations of a segment's labels in the source assembly code.
*/
class SegmentSrcLoc(val labels: MutableList<SrcLoc> = mutableListOf())

View File

@ -43,16 +43,19 @@ val BUILTIN_FUNCTIONS = setOf(
860,
)
/**
* Parses bytecode into bytecode IR.
*/
fun parseBytecode(
bytecode: Buffer,
labelOffsets: IntArray,
entryLabels: Set<Int>,
dcGcFormat: Boolean,
lenient: Boolean,
): PwResult<List<Segment>> {
): PwResult<BytecodeIr> {
val cursor = BufferCursor(bytecode)
val labelHolder = LabelHolder(labelOffsets)
val result = PwResult.build<List<Segment>>(logger)
val result = PwResult.build<BytecodeIr>(logger)
val offsetToSegment = mutableMapOf<Int, Segment>()
findAndParseSegments(
@ -110,7 +113,7 @@ fun parseBytecode(
segments.add(segment)
offset += when (segment) {
is InstructionSegment -> segment.instructions.sumBy { instructionSize(it, dcGcFormat) }
is InstructionSegment -> segment.instructions.sumBy { it.getSize(dcGcFormat) }
is DataSegment -> segment.data.size
@ -150,7 +153,7 @@ fun parseBytecode(
}
}
return result.success(segments)
return result.success(BytecodeIr(segments))
}
private fun findAndParseSegments(

View File

@ -5,9 +5,9 @@ import world.phantasmal.core.PwResult
import world.phantasmal.core.PwResultBuilder
import world.phantasmal.core.Severity
import world.phantasmal.core.Success
import world.phantasmal.lib.asm.BytecodeIr
import world.phantasmal.lib.asm.InstructionSegment
import world.phantasmal.lib.asm.OP_SET_EPISODE
import world.phantasmal.lib.asm.Segment
import world.phantasmal.lib.asm.dataFlowAnalysis.getMapDesignations
import world.phantasmal.lib.compression.prs.prsDecompress
import world.phantasmal.lib.cursor.Cursor
@ -29,7 +29,7 @@ class Quest(
* (Partial) raw DAT data that can't be parsed yet by Phantasmal.
*/
val datUnknowns: List<DatUnknown>,
val bytecodeIr: List<Segment>,
val bytecodeIr: BytecodeIr,
val shopItems: UIntArray,
val mapDesignations: Map<Int, Int>,
)
@ -83,10 +83,10 @@ fun parseBinDatToQuest(
val bytecodeIr = parseBytecodeResult.value
if (bytecodeIr.isEmpty()) {
if (bytecodeIr.segments.isEmpty()) {
result.addProblem(Severity.Warning, "File contains no instruction labels.")
} else {
val instructionSegments = bytecodeIr.filterIsInstance<InstructionSegment>()
val instructionSegments = bytecodeIr.instructionSegments()
var label0Segment: InstructionSegment? = null

View File

@ -6,6 +6,17 @@ import kotlin.test.Test
import kotlin.test.assertEquals
class AsmTokenizationTests : LibTestSuite() {
@Test
fun hexadecimal_numbers_are_parsed_as_ints() {
assertEquals(0x00, (tokenizeLine("0X00")[0] as Token.Int32).value)
assertEquals(0x70, (tokenizeLine("0x70")[0] as Token.Int32).value)
assertEquals(0xA1, (tokenizeLine("0xa1")[0] as Token.Int32).value)
assertEquals(0xAB, (tokenizeLine("0xAB")[0] as Token.Int32).value)
assertEquals(0xAB, (tokenizeLine("0xAb")[0] as Token.Int32).value)
assertEquals(0xAB, (tokenizeLine("0xaB")[0] as Token.Int32).value)
assertEquals(0xFF, (tokenizeLine("0xff")[0] as Token.Int32).value)
}
@Test
fun valid_floats_are_parsed_as_Float32_tokens() {
assertCloseTo(808.9f, (tokenizeLine("808.9")[0] as Token.Float32).value)

View File

@ -31,6 +31,6 @@ class AssemblyTests : LibTestSuite() {
assertTrue(result is Success)
assertTrue(result.problems.isEmpty())
assertEquals(3, result.value.size)
assertEquals(3, result.value.segments.size)
}
}

View File

@ -30,8 +30,8 @@ class BytecodeTests : LibTestSuite() {
assertTrue(result is Success)
assertTrue(result.problems.isEmpty())
val segments = result.value
val segment = segments[0]
val ir = result.value
val segment = ir.segments[0]
assertTrue(segment is InstructionSegment)
assertEquals(OP_SET_EPISODE, segment.instructions[0].opcode)

View File

@ -42,7 +42,7 @@ class QuestTests : LibTestSuite() {
assertEquals(4, quest.mapDesignations[10])
assertEquals(0, quest.mapDesignations[14])
val seg1 = quest.bytecodeIr[0]
val seg1 = quest.bytecodeIr.segments[0]
assertTrue(seg1 is InstructionSegment)
assertTrue(0 in seg1.labels)
assertEquals(OP_SET_EPISODE, seg1.instructions[0].opcode)
@ -53,15 +53,15 @@ class QuestTests : LibTestSuite() {
assertEquals(150, seg1.instructions[2].args[0].value)
assertEquals(OP_SET_FLOOR_HANDLER, seg1.instructions[3].opcode)
val seg2 = quest.bytecodeIr[1]
val seg2 = quest.bytecodeIr.segments[1]
assertTrue(seg2 is InstructionSegment)
assertTrue(1 in seg2.labels)
val seg3 = quest.bytecodeIr[2]
val seg3 = quest.bytecodeIr.segments[2]
assertTrue(seg3 is InstructionSegment)
assertTrue(10 in seg3.labels)
val seg4 = quest.bytecodeIr[3]
val seg4 = quest.bytecodeIr.segments[3]
assertTrue(seg4 is InstructionSegment)
assertTrue(150 in seg4.labels)
assertEquals(1, seg4.instructions.size)

View File

@ -14,5 +14,5 @@ fun toInstructions(assembly: String): List<InstructionSegment> {
assertTrue(result is Success)
assertTrue(result.problems.isEmpty())
return result.value.filterIsInstance<InstructionSegment>()
return result.value.instructionSegments()
}

View File

@ -5,6 +5,7 @@
package world.phantasmal.web.externals.monacoEditor
import kotlin.js.Promise
import kotlin.js.RegExp
external fun register(language: ILanguageExtensionPoint)
@ -35,6 +36,14 @@ external fun registerSignatureHelpProvider(
provider: SignatureHelpProvider,
): IDisposable
/**
* Register a definition provider (used by e.g. go to definition).
*/
external fun registerDefinitionProvider(
languageId: String,
provider: DefinitionProvider,
): IDisposable
/**
* Register a hover provider (used by e.g. editor hover).
*/
@ -474,7 +483,7 @@ external interface CompletionItemProvider {
position: Position,
context: CompletionContext,
token: CancellationToken,
): CompletionList /* type ProviderResult<T> = T | undefined | null | Thenable<T | undefined | null> */
): Promise<CompletionList?> /* type ProviderResult<T> = T | undefined | null | Thenable<T | undefined | null> */
}
/**
@ -588,7 +597,7 @@ external interface SignatureHelpProvider {
position: Position,
token: CancellationToken,
context: SignatureHelpContext,
): SignatureHelpResult? /* type ProviderResult<T> = T | undefined | null | Thenable<T | undefined | null> */
): Promise<SignatureHelpResult?> /* type ProviderResult<T> = T | undefined | null | Thenable<T | undefined | null> */
}
/**
@ -619,5 +628,44 @@ external interface HoverProvider {
model: ITextModel,
position: Position,
token: CancellationToken,
): Hover? /* type ProviderResult<T> = T | undefined | null | Thenable<T | undefined | null> */
): Promise<Hover?> /* type ProviderResult<T> = T | undefined | null | Thenable<T | undefined | null> */
}
external interface LocationLink {
/**
* A range to select where this link originates from.
*/
var originSelectionRange: IRange?
/**
* The target uri this link points to.
*/
var uri: Uri
/**
* The full range this link points to.
*/
var range: IRange
/**
* A range to select this link points to. Must be contained
* in `LocationLink.range`.
*/
var targetSelectionRange: IRange?
}
/**
* The definition provider interface defines the contract between extensions and
* the [go to definition](https://code.visualstudio.com/docs/editor/editingevolved#_go-to-definition)
* and peek definition features.
*/
external interface DefinitionProvider {
/**
* Provide the definition of the symbol at the given position and document.
*/
fun provideDefinition(
model: ITextModel,
position: Position,
token: CancellationToken,
): Promise<Array<LocationLink>?>
}

View File

@ -34,22 +34,22 @@ external enum class MarkerSeverity {
}
external interface IRange {
var startLineNumber: Number
var startColumn: Number
var endLineNumber: Number
var endColumn: Number
var startLineNumber: Int
var startColumn: Int
var endLineNumber: Int
var endColumn: Int
}
open external class Range(
startLineNumber: Number,
startColumn: Number,
endLineNumber: Number,
endColumn: Number,
startLineNumber: Int,
startColumn: Int,
endLineNumber: Int,
endColumn: Int,
) {
open var startLineNumber: Number
open var startColumn: Number
open var endLineNumber: Number
open var endColumn: Number
open var startLineNumber: Int
open var startColumn: Int
open var endLineNumber: Int
open var endColumn: Int
open fun isEmpty(): Boolean
open fun containsPosition(position: IPosition): Boolean
open fun containsRange(range: IRange): Boolean
@ -60,8 +60,8 @@ open external class Range(
open fun getEndPosition(): Position
open fun getStartPosition(): Position
override fun toString(): String
open fun setEndPosition(endLineNumber: Number, endColumn: Number): Range
open fun setStartPosition(startLineNumber: Number, startColumn: Number): Range
open fun setEndPosition(endLineNumber: Int, endColumn: Int): Range
open fun setStartPosition(startLineNumber: Int, startColumn: Int): Range
open fun collapseToStart(): Range
companion object {
@ -88,28 +88,28 @@ open external class Range(
}
external interface ISelection {
var selectionStartLineNumber: Number
var selectionStartColumn: Number
var positionLineNumber: Number
var positionColumn: Number
var selectionStartLineNumber: Int
var selectionStartColumn: Int
var positionLineNumber: Int
var positionColumn: Int
}
open external class Selection(
selectionStartLineNumber: Number,
selectionStartColumn: Number,
positionLineNumber: Number,
positionColumn: Number,
selectionStartLineNumber: Int,
selectionStartColumn: Int,
positionLineNumber: Int,
positionColumn: Int,
) : Range {
open var selectionStartLineNumber: Number
open var selectionStartColumn: Number
open var positionLineNumber: Number
open var positionColumn: Number
open var selectionStartLineNumber: Int
open var selectionStartColumn: Int
open var positionLineNumber: Int
open var positionColumn: Int
override fun toString(): String
open fun equalsSelection(other: ISelection): Boolean
open fun getDirection(): SelectionDirection
override fun setEndPosition(endLineNumber: Number, endColumn: Number): Selection
override fun setEndPosition(endLineNumber: Int, endColumn: Int): Selection
open fun getPosition(): Position
override fun setStartPosition(startLineNumber: Number, startColumn: Number): Selection
override fun setStartPosition(startLineNumber: Int, startColumn: Int): Selection
companion object {
fun selectionsEqual(a: ISelection, b: ISelection): Boolean
@ -118,10 +118,10 @@ open external class Selection(
fun selectionsArrEqual(a: Array<ISelection>, b: Array<ISelection>): Boolean
fun isISelection(obj: Any): Boolean
fun createWithDirection(
startLineNumber: Number,
startColumn: Number,
endLineNumber: Number,
endColumn: Number,
startLineNumber: Int,
startColumn: Int,
endLineNumber: Int,
endColumn: Int,
direction: SelectionDirection,
): Selection
}

View File

@ -1,8 +1,318 @@
package world.phantasmal.web.questEditor.asm
import world.phantasmal.core.disposable.TrackedDisposable
import world.phantasmal.core.Success
import world.phantasmal.lib.asm.*
import world.phantasmal.lib.asm.dataFlowAnalysis.getMapDesignations
import world.phantasmal.observable.value.Val
import world.phantasmal.observable.value.list.ListVal
import world.phantasmal.observable.value.list.mutableListVal
import world.phantasmal.observable.value.mutableVal
import kotlin.math.min
class AsmAnalyser : TrackedDisposable() {
fun setAssembly(assembly: List<String>) {
// TODO: Delegate to web worker?
@Suppress("ObjectPropertyName") // Suppress warnings about private properties starting with "_".
object AsmAnalyser {
private val KEYWORD_REGEX = Regex("""^\s*\.[a-z]+${'$'}""")
private val KEYWORD_SUGGESTIONS: List<CompletionItem> =
listOf(
CompletionItem(
label = ".code",
type = CompletionItemType.Keyword,
insertText = "code",
),
CompletionItem(
label = ".data",
type = CompletionItemType.Keyword,
insertText = "data",
),
CompletionItem(
label = ".string",
type = CompletionItemType.Keyword,
insertText = "string",
),
)
private val INSTRUCTION_REGEX = Regex("""^\s*([a-z][a-z0-9_=<>!]*)?${'$'}""")
private val INSTRUCTION_SUGGESTIONS: List<CompletionItem> =
(OPCODES + OPCODES_F8 + OPCODES_F9)
.filterNotNull()
.map { opcode ->
CompletionItem(
label = opcode.mnemonic,
// TODO: Add signature?
type = CompletionItemType.Opcode,
insertText = opcode.mnemonic,
)
}
private var inlineStackArgs: Boolean = true
private var asm: List<String> = emptyList()
private var _bytecodeIr = mutableVal(BytecodeIr(emptyList()))
private var _mapDesignations = mutableVal<Map<Int, Int>>(emptyMap())
private val _problems = mutableListVal<AssemblyProblem>()
val bytecodeIr: Val<BytecodeIr> = _bytecodeIr
val mapDesignations: Val<Map<Int, Int>> = _mapDesignations
val problems: ListVal<AssemblyProblem> = _problems
suspend fun setAsm(asm: List<String>, inlineStackArgs: Boolean) {
this.inlineStackArgs = inlineStackArgs
this.asm = asm
processAsm()
}
private fun processAsm() {
val assemblyResult = assemble(asm, inlineStackArgs)
@Suppress("UNCHECKED_CAST")
_problems.value = assemblyResult.problems as List<AssemblyProblem>
if (assemblyResult is Success) {
val bytecodeIr = assemblyResult.value
_bytecodeIr.value = bytecodeIr
val instructionSegments = bytecodeIr.instructionSegments()
instructionSegments.find { 0 in it.labels }?.let { label0Segment ->
_mapDesignations.value = getMapDesignations(instructionSegments, label0Segment)
}
}
}
suspend fun getCompletions(lineNo: Int, col: Int): List<CompletionItem> {
val text = getLine(lineNo)?.take(col) ?: ""
return when {
KEYWORD_REGEX.matches(text) -> KEYWORD_SUGGESTIONS
INSTRUCTION_REGEX.matches(text) -> INSTRUCTION_SUGGESTIONS
else -> emptyList()
}
}
suspend fun getSignatureHelp(lineNo: Int, col: Int): SignatureHelp? {
// Hacky way of providing parameter hints.
// We just tokenize the current line and look for the first identifier and check whether
// it's a valid opcode.
var signature: Signature? = null
var activeParam = -1
getLine(lineNo)?.let { text ->
val tokens = tokenizeLine(text)
tokens.find { it is Token.Ident }?.let { ident ->
ident as Token.Ident
mnemonicToOpcode(ident.value)?.let { opcode ->
signature = getSignature(opcode)
for (tkn in tokens) {
if (tkn.col + tkn.len > col) {
break
} else if (tkn is Token.Ident && activeParam == -1) {
activeParam = 0
} else if (tkn is Token.ArgSeparator) {
activeParam++
}
}
}
}
}
return signature?.let { sig ->
SignatureHelp(
signature = sig,
activeParameter = activeParam,
)
}
}
private fun getSignature(opcode: Opcode): Signature {
var signature = opcode.mnemonic + " "
val params = mutableListOf<Parameter>()
var first = true
for (param in opcode.params) {
if (first) {
first = false
} else {
signature += ", "
}
val paramTypeStr = when (param.type) {
ByteType -> "Byte"
ShortType -> "Short"
IntType -> "Int"
FloatType -> "Float"
ILabelType -> "&Function"
DLabelType -> "&Data"
SLabelType -> "&String"
ILabelVarType -> "...&Function"
StringType -> "String"
RegRefType, is RegTupRefType -> "Register"
RegRefVarType -> "...Register"
PointerType -> "Pointer"
else -> "Any"
}
params.add(
Parameter(
labelStart = signature.length,
labelEnd = signature.length + paramTypeStr.length,
documentation = param.doc,
)
)
signature += paramTypeStr
}
return Signature(
label = signature,
documentation = opcode.doc,
parameters = params,
)
}
suspend fun getHover(lineNo: Int, col: Int): Hover? {
val help = getSignatureHelp(lineNo, col)
?: return null
val sig = help.signature
val param = sig.parameters.getOrNull(help.activeParameter)
val contents = mutableListOf<String>()
// Instruction signature. Parameter highlighted if possible.
contents.add(
if (param == null) {
sig.label
} else {
// TODO: Figure out how to underline the active parameter in addition to
// bolding it to make it match the look of the signature help.
sig.label.substring(0, param.labelStart) +
"__" +
sig.label.substring(param.labelStart, param.labelEnd) +
"__" +
sig.label.substring(param.labelEnd)
}
)
// Put the parameter doc and the instruction doc in the same string to match the look of the
// signature help.
var doc = ""
// Parameter doc.
if (param?.documentation != null) {
doc += param.documentation
// TODO: Figure out how add an empty line here to make it match the look of the
// signature help.
doc += "\n\n"
}
// Instruction doc.
sig.documentation?.let { doc += it }
if (doc.isNotEmpty()) {
contents.add(doc)
}
return Hover(contents)
}
suspend fun getDefinition(lineNo: Int, col: Int): List<TextRange> {
getInstruction(lineNo, col)?.let { inst ->
for ((paramIdx, param) in inst.opcode.params.withIndex()) {
if (param.type is LabelType) {
if (inst.opcode.stack != StackInteraction.Pop) {
// Immediate arguments.
val args = inst.getArgs(paramIdx)
val argSrcLocs = inst.getArgSrcLocs(paramIdx)
for (i in 0 until min(args.size, argSrcLocs.size)) {
val arg = args[i]
val srcLoc = argSrcLocs[i]
if (positionInside(lineNo, col, srcLoc)) {
val label = arg.value as Int
return getLabelDefinitions(label)
}
}
} else {
// Stack arguments.
val argSrcLocs = inst.getStackArgSrcLocs(paramIdx)
for (srcLoc in argSrcLocs) {
if (positionInside(lineNo, col, srcLoc)) {
val label = srcLoc.value as Int
return getLabelDefinitions(label)
}
}
}
}
}
}
return emptyList()
}
private fun getInstruction(lineNo: Int, col: Int): Instruction? {
for (segment in bytecodeIr.value.segments) {
if (segment is InstructionSegment) {
// Loop over instructions in reverse order so stack popping instructions will be
// handled before the related stack pushing instructions when inlineStackArgs is on.
for (i in segment.instructions.lastIndex downTo 0) {
val inst = segment.instructions[i]
inst.srcLoc?.let { srcLoc ->
if (positionInside(lineNo, col, srcLoc.mnemonic)) {
return inst
}
for (argSrcLoc in srcLoc.args) {
if (positionInside(lineNo, col, argSrcLoc)) {
return inst
}
}
if (inlineStackArgs) {
for (argSrcLoc in srcLoc.stackArgs) {
if (positionInside(lineNo, col, argSrcLoc)) {
return inst
}
}
}
}
}
}
}
return null
}
private fun getLabelDefinitions(label: Int): List<TextRange> =
bytecodeIr.value.segments.asSequence()
.filter { label in it.labels }
.mapNotNull { segment ->
val labelIdx = segment.labels.indexOf(label)
segment.srcLoc.labels.getOrNull(labelIdx)?.let { labelSrcLoc ->
TextRange(
startLineNo = labelSrcLoc.lineNo,
startCol = labelSrcLoc.col,
endLineNo = labelSrcLoc.lineNo,
endCol = labelSrcLoc.col + labelSrcLoc.len,
)
}
}
.toList()
private fun positionInside(lineNo: Int, col: Int, srcLoc: SrcLoc?): Boolean =
if (srcLoc == null) {
false
} else {
lineNo == srcLoc.lineNo && col >= srcLoc.col && col <= srcLoc.col + srcLoc.len
}
private fun getLine(lineNo: Int): String? = asm.getOrNull(lineNo - 1)
}

View File

@ -1,10 +1,10 @@
package world.phantasmal.web.questEditor.asm
import world.phantasmal.lib.asm.OPCODES
import world.phantasmal.lib.asm.OPCODES_F8
import world.phantasmal.lib.asm.OPCODES_F9
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.promise
import world.phantasmal.web.externals.monacoEditor.*
import world.phantasmal.webui.obj
import kotlin.js.Promise
object AsmCompletionItemProvider : CompletionItemProvider {
override fun provideCompletionItems(
@ -12,59 +12,27 @@ object AsmCompletionItemProvider : CompletionItemProvider {
position: Position,
context: CompletionContext,
token: CancellationToken,
): CompletionList {
val text = model.getValueInRange(obj {
startLineNumber = position.lineNumber
endLineNumber = position.lineNumber
startColumn = 1
endColumn = position.column
})
): Promise<CompletionList> =
GlobalScope.promise {
val completions = AsmAnalyser.getCompletions(
position.lineNumber,
position.column,
)
val suggestions = when {
KEYWORD_REGEX.matches(text) -> KEYWORD_SUGGESTIONS
INSTRUCTION_REGEX.matches(text) -> INSTRUCTION_SUGGESTIONS
else -> emptyArray()
obj {
suggestions = Array(completions.size) { i ->
val completion = completions[i]
obj {
label = obj { name = completion.label }
kind = when (completion.type) {
CompletionItemType.Keyword -> CompletionItemKind.Keyword
CompletionItemType.Opcode -> CompletionItemKind.Function
}
insertText = completion.insertText
}
}
return obj {
this.suggestions = suggestions
incomplete = false
}
}
private val KEYWORD_REGEX = Regex("""^\s*\.[a-z]+${'$'}""")
private val KEYWORD_SUGGESTIONS: Array<CompletionItem> =
arrayOf(
obj {
label = obj { name = ".code" }
kind = CompletionItemKind.Keyword
insertText = "code"
},
obj {
label = obj { name = ".data" }
kind = CompletionItemKind.Keyword
insertText = "data"
},
obj {
label = obj { name = ".string" }
kind = CompletionItemKind.Keyword
insertText = "string"
},
)
private val INSTRUCTION_REGEX = Regex("""^\s*([a-z][a-z0-9_=<>!]*)?${'$'}""")
private val INSTRUCTION_SUGGESTIONS: Array<CompletionItem> =
(OPCODES + OPCODES_F8 + OPCODES_F9)
.filterNotNull()
.map { opcode ->
obj<CompletionItem> {
label = obj {
name = opcode.mnemonic
// TODO: Add signature?
}
kind = CompletionItemKind.Function
insertText = opcode.mnemonic
}
}
.toTypedArray()
}

View File

@ -0,0 +1,32 @@
package world.phantasmal.web.questEditor.asm
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.promise
import world.phantasmal.web.externals.monacoEditor.*
import world.phantasmal.webui.obj
import kotlin.js.Promise
object AsmDefinitionProvider : DefinitionProvider {
override fun provideDefinition(
model: ITextModel,
position: Position,
token: CancellationToken,
): Promise<Array<LocationLink>?> =
GlobalScope.promise {
val defs = AsmAnalyser.getDefinition(position.lineNumber, position.column)
Array(defs.size) {
val def = defs[it]
obj {
uri = model.uri
range = obj {
startLineNumber = def.startLineNo
startColumn = def.startCol
endLineNumber = def.endLineNo
endColumn = def.endCol
}
}
}
}
}

View File

@ -1,60 +1,32 @@
package world.phantasmal.web.questEditor.asm
import world.phantasmal.core.asArray
import world.phantasmal.core.jsArrayOf
import world.phantasmal.web.externals.monacoEditor.*
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.promise
import world.phantasmal.web.externals.monacoEditor.CancellationToken
import world.phantasmal.web.externals.monacoEditor.HoverProvider
import world.phantasmal.web.externals.monacoEditor.ITextModel
import world.phantasmal.web.externals.monacoEditor.Position
import world.phantasmal.webui.obj
import kotlin.js.Promise
import world.phantasmal.web.externals.monacoEditor.Hover as MonacoHover
object AsmHoverProvider : HoverProvider {
override fun provideHover(
model: ITextModel,
position: Position,
token: CancellationToken,
): Hover? {
val help = AsmSignatureHelpProvider.getSignatureHelp(model, position)
?: return null
): Promise<MonacoHover?> =
GlobalScope.promise {
AsmAnalyser.getHover(position.lineNumber, position.column)?.let { hover ->
obj<MonacoHover> {
contents = Array(hover.contents.size) { i ->
val content = hover.contents[i]
val sig = help.signatures[help.activeSignature]
val param = sig.parameters.getOrNull(help.activeParameter)
val contents = jsArrayOf<IMarkdownString>()
// Instruction signature. Parameter highlighted if possible.
contents.push(
obj {
value =
if (param == null) {
sig.label
} else {
// TODO: Figure out how to underline the active parameter in addition to
// bolding it to make it match the look of the signature help.
sig.label.substring(0, param.label[0]) +
"__" +
sig.label.substring(param.label[0], param.label[1]) +
"__" +
sig.label.substring(param.label[1])
value = content
}
}
}
)
// Put the parameter doc and the instruction doc in the same string to match the look of the
// signature help.
var doc = ""
// Parameter doc.
if (param?.documentation != null) {
doc += param.documentation
// TODO: Figure out how add an empty line here to make it match the look of the
// signature help.
doc += "\n\n"
}
// Instruction doc.
sig.documentation?.let { doc += it }
contents.push(obj { value = doc })
return obj<Hover> { this.contents = contents.asArray() }
}
}

View File

@ -1,10 +1,11 @@
package world.phantasmal.web.questEditor.asm
import world.phantasmal.core.asArray
import world.phantasmal.core.jsArrayOf
import world.phantasmal.lib.asm.*
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.promise
import world.phantasmal.web.externals.monacoEditor.*
import world.phantasmal.webui.obj
import kotlin.js.Promise
import world.phantasmal.web.externals.monacoEditor.SignatureHelp as MonacoSigHelp
object AsmSignatureHelpProvider : SignatureHelpProvider {
override val signatureHelpTriggerCharacters: Array<String> =
@ -18,96 +19,34 @@ object AsmSignatureHelpProvider : SignatureHelpProvider {
position: Position,
token: CancellationToken,
context: SignatureHelpContext,
): SignatureHelpResult? =
getSignatureHelp(model, position)?.let { signatureHelp ->
): Promise<SignatureHelpResult?> =
GlobalScope.promise {
AsmAnalyser.getSignatureHelp(position.lineNumber, position.column)
?.let { sigHelp ->
val monacoSigHelp = obj<MonacoSigHelp> {
signatures = arrayOf(
obj {
label = sigHelp.signature.label
sigHelp.signature.documentation?.let { documentation = it }
parameters = sigHelp.signature.parameters.map { param ->
obj<ParameterInformation> {
label = arrayOf(param.labelStart, param.labelEnd)
param.documentation?.let { documentation = it }
}
}.toTypedArray()
}
)
activeSignature = 0
activeParameter = sigHelp.activeParameter
}
object : SignatureHelpResult {
override var value: SignatureHelp = signatureHelp
override var value = monacoSigHelp
override fun dispose() {
// Nothing to dispose.
}
}
}
fun getSignatureHelp(model: ITextModel, position: Position): SignatureHelp? {
// Hacky way of providing parameter hints.
// We just tokenize the current line and look for the first identifier and check whether
// it's a valid opcode.
var signatureInfo: SignatureInformation? = null
var activeParam = -1
val line = model.getLineContent(position.lineNumber)
val tokens = tokenizeLine(line)
tokens.find { it is Token.Ident }?.let { ident ->
ident as Token.Ident
mnemonicToOpcode(ident.value)?.let { opcode ->
signatureInfo = getSignatureInformation(opcode)
for (tkn in tokens) {
if (tkn.col + tkn.len > position.column) {
break
} else if (tkn is Token.Ident && activeParam == -1) {
activeParam = 0
} else if (tkn is Token.ArgSeparator) {
activeParam++
}
}
}
}
return signatureInfo?.let { sigInfo ->
obj<SignatureHelp> {
signatures = arrayOf(sigInfo)
activeSignature = 0
activeParameter = activeParam
}
}
}
private fun getSignatureInformation(opcode: Opcode): SignatureInformation {
var signature = opcode.mnemonic + " "
val params = jsArrayOf<ParameterInformation>()
var first = true
for (param in opcode.params) {
if (first) {
first = false
} else {
signature += ", "
}
val paramTypeStr = when (param.type) {
ByteType -> "Byte"
ShortType -> "Short"
IntType -> "Int"
FloatType -> "Float"
ILabelType -> "&Function"
DLabelType -> "&Data"
SLabelType -> "&String"
ILabelVarType -> "...&Function"
StringType -> "String"
RegRefType, is RegTupRefType -> "Register"
RegRefVarType -> "...Register"
PointerType -> "Pointer"
else -> "Any"
}
params.push(
obj {
label = arrayOf(signature.length, signature.length + paramTypeStr.length)
param.doc?.let { documentation = it }
}
)
signature += paramTypeStr
}
return obj {
label = signature
opcode.doc?.let { documentation = it }
parameters = params.asArray()
}
}
}

View File

@ -0,0 +1,37 @@
package world.phantasmal.web.questEditor.asm
class TextRange(
var startLineNo: Int,
var startCol: Int,
var endLineNo: Int,
var endCol: Int,
)
enum class CompletionItemType {
Keyword, Opcode
}
class CompletionItem(val label: String, val type: CompletionItemType, val insertText: String)
class SignatureHelp(val signature: Signature, val activeParameter: Int)
class Signature(val label: String, val documentation: String?, val parameters: List<Parameter>)
class Parameter(
/**
* Start column of the parameter label within [Signature.label].
*/
val labelStart: Int,
/**
* End column (exclusive) of the parameter label within [Signature.label].
*/
val labelEnd: Int,
val documentation: String?,
)
class Hover(
/**
* List of markdown strings.
*/
val contents: List<String>,
)

View File

@ -1,6 +1,6 @@
package world.phantasmal.web.questEditor.models
import world.phantasmal.lib.asm.Segment
import world.phantasmal.lib.asm.BytecodeIr
import world.phantasmal.lib.fileFormats.quest.Episode
import world.phantasmal.observable.value.Val
import world.phantasmal.observable.value.list.ListVal
@ -18,7 +18,7 @@ class QuestModel(
mapDesignations: Map<Int, Int>,
npcs: MutableList<QuestNpcModel>,
objects: MutableList<QuestObjectModel>,
val bytecodeIr: List<Segment>,
bytecodeIr: BytecodeIr,
getVariant: (Episode, areaId: Int, variantId: Int) -> AreaVariantModel?,
) {
private val _id = mutableVal(0)
@ -54,6 +54,9 @@ class QuestModel(
val npcs: ListVal<QuestNpcModel> = _npcs
val objects: ListVal<QuestObjectModel> = _objects
var bytecodeIr: BytecodeIr = bytecodeIr
private set
init {
setId(id)
setLanguage(language)
@ -140,6 +143,10 @@ class QuestModel(
}
}
fun setMapDesignations(mapDesignations: Map<Int, Int>) {
_mapDesignations.value = mapDesignations
}
fun addNpc(npc: QuestNpcModel) {
_npcs.add(npc)
}
@ -154,4 +161,8 @@ class QuestModel(
is QuestObjectModel -> _objects.remove(entity)
}
}
fun setBytecodeIr(bytecodeIr: BytecodeIr) {
this.bytecodeIr = bytecodeIr
}
}

View File

@ -1,5 +1,6 @@
package world.phantasmal.web.questEditor.stores
import kotlinx.coroutines.launch
import world.phantasmal.lib.asm.disassemble
import world.phantasmal.observable.ChangeEvent
import world.phantasmal.observable.Observable
@ -11,7 +12,6 @@ import world.phantasmal.web.core.undo.SimpleUndo
import world.phantasmal.web.core.undo.UndoManager
import world.phantasmal.web.externals.monacoEditor.*
import world.phantasmal.web.questEditor.asm.*
import world.phantasmal.web.questEditor.models.QuestModel
import world.phantasmal.webui.obj
import world.phantasmal.webui.stores.Store
@ -40,9 +40,25 @@ class AsmStore(
val didRedo: Observable<Unit> = _didRedo
init {
observe(questEditorStore.currentQuest, inlineStackArgs) { quest, inlineArgs ->
observe(questEditorStore.currentQuest, inlineStackArgs) { quest, inlineStackArgs ->
_textModel.value?.dispose()
_textModel.value = quest?.let { createModel(quest, inlineArgs) }
quest?.let {
val asm = disassemble(quest.bytecodeIr, inlineStackArgs)
scope.launch { AsmAnalyser.setAsm(asm, inlineStackArgs) }
_textModel.value =
createModel(asm.joinToString("\n"), ASM_LANG_ID)
.also(::addModelChangeListener)
}
}
observe(AsmAnalyser.bytecodeIr) {
questEditorStore.currentQuest.value?.setBytecodeIr(it)
}
observe(AsmAnalyser.mapDesignations) {
questEditorStore.currentQuest.value?.setMapDesignations(it)
}
}
@ -50,13 +66,6 @@ class AsmStore(
undoManager.setCurrent(undo)
}
private fun createModel(quest: QuestModel, inlineArgs: Boolean): ITextModel {
val assembly = disassemble(quest.bytecodeIr, inlineArgs)
val model = createModel(assembly.joinToString("\n"), ASM_LANG_ID)
addModelChangeListener(model)
return model
}
/**
* Sets up undo/redo, code analysis and breakpoint updates on model change.
*/
@ -108,6 +117,7 @@ class AsmStore(
registerCompletionItemProvider(ASM_LANG_ID, AsmCompletionItemProvider)
registerSignatureHelpProvider(ASM_LANG_ID, AsmSignatureHelpProvider)
registerHoverProvider(ASM_LANG_ID, AsmHoverProvider)
registerDefinitionProvider(ASM_LANG_ID, AsmDefinitionProvider)
}
}
}

View File

@ -15,6 +15,7 @@ class QuestEditorToolbarControllerTests : WebTestSuite() {
@Test
fun can_create_a_new_quest() = asyncTest {
val ctrl = disposer.add(QuestEditorToolbarController(
components.uiStore,
components.questLoader,
components.areaStore,
components.questEditorStore,
@ -28,6 +29,7 @@ class QuestEditorToolbarControllerTests : WebTestSuite() {
@Test
fun a_failure_is_exposed_when_openFiles_fails() = asyncTest {
val ctrl = disposer.add(QuestEditorToolbarController(
components.uiStore,
components.questLoader,
components.areaStore,
components.questEditorStore,
@ -51,6 +53,7 @@ class QuestEditorToolbarControllerTests : WebTestSuite() {
@Test
fun undo_state_changes_correctly() = asyncTest {
val ctrl = disposer.add(QuestEditorToolbarController(
components.uiStore,
components.questLoader,
components.areaStore,
components.questEditorStore,
@ -102,6 +105,7 @@ class QuestEditorToolbarControllerTests : WebTestSuite() {
@Test
fun area_state_changes_correctly() = asyncTest {
val ctrl = disposer.add(QuestEditorToolbarController(
components.uiStore,
components.questLoader,
components.areaStore,
components.questEditorStore,

View File

@ -13,6 +13,7 @@ import world.phantasmal.web.core.loading.AssetLoader
import world.phantasmal.web.core.rendering.DisposableThreeRenderer
import world.phantasmal.web.core.stores.ApplicationUrl
import world.phantasmal.web.core.stores.UiStore
import world.phantasmal.web.core.undo.UndoManager
import world.phantasmal.web.externals.three.WebGLRenderer
import world.phantasmal.web.questEditor.loading.AreaAssetLoader
import world.phantasmal.web.questEditor.loading.QuestLoader
@ -51,6 +52,10 @@ class TestComponents(private val ctx: TestContext) {
var questLoader: QuestLoader by default { QuestLoader(assetLoader) }
// Undo
var undoManager: UndoManager by default { UndoManager() }
// Stores
var uiStore: UiStore by default { UiStore(applicationUrl) }
@ -58,7 +63,7 @@ class TestComponents(private val ctx: TestContext) {
var areaStore: AreaStore by default { AreaStore(areaAssetLoader) }
var questEditorStore: QuestEditorStore by default {
QuestEditorStore(uiStore, areaStore)
QuestEditorStore(uiStore, areaStore, undoManager)
}
// Rendering

View File

@ -1,6 +1,6 @@
package world.phantasmal.web.test
import world.phantasmal.lib.asm.Segment
import world.phantasmal.lib.asm.BytecodeIr
import world.phantasmal.lib.fileFormats.quest.Episode
import world.phantasmal.lib.fileFormats.quest.NpcType
import world.phantasmal.lib.fileFormats.quest.QuestNpc
@ -16,7 +16,7 @@ fun createQuestModel(
episode: Episode = Episode.I,
npcs: List<QuestNpcModel> = emptyList(),
objects: List<QuestObjectModel> = emptyList(),
bytecodeIr: List<Segment> = emptyList(),
bytecodeIr: BytecodeIr = BytecodeIr(emptyList()),
): QuestModel =
QuestModel(
id,