diff --git a/assets/disk0/tvdos/TVDOS.SYS b/assets/disk0/tvdos/TVDOS.SYS index 226a86c..a79881e 100644 --- a/assets/disk0/tvdos/TVDOS.SYS +++ b/assets/disk0/tvdos/TVDOS.SYS @@ -55,10 +55,12 @@ class PmemFSfile { // string representation (preferable) if (typeof bytes === 'string' || bytes instanceof String) { this.data = bytes + this.length = bytes.length } // Javascript array OR JVM byte[] else if (Array.isArray(bytes) || bytes.toString().startsWith("[B")) { - this.bdata = bytes[i] + this.bdata = bytes + this.length = bytes.length } else { throw Error("Invalid type for directory") @@ -76,10 +78,10 @@ class PmemFSfile { dataAsBytes() { if (this.bdata !== undefined) return this.bdata - this.bdata = new Int8Array(this.data.length) + this.bdata = new Uint8Array(this.data.length) for (let i = 0; i < this.data.length; i++) { let p = this.data.charCodeAt(i) - this.bdata[i] = (p > 127) ? p - 255 : p + this.bdata[i] = p } return this.bdata } @@ -164,16 +166,16 @@ class TVDOSFileDescriptor { constructor(path0, driverID) { if (path0.startsWith("$")) { - let path1 = path0.substring(3) - let slashPos = path1.indexOf("/") + let path1 = path0.replaceAll("/", "\\").substring(3) + let slashPos = path1.indexOf("\\") let devName = path1.substring(0, (slashPos < 0) ? path1.length : slashPos) if (!files.reservedNames.includes(devName)) { throw Error(`${devName} is not a valid device file`) } - this._driveLetter = undefined - this._path = path0 + this._driveLetter = '$' + this._path = '\\' + path1 this._driverID = `DEV${devName}` this._driver = _TVDOS.DRV.FS[`DEV${devName}`] // can't just put `driverID` here } @@ -939,8 +941,9 @@ _TVDOS.DRV.FS.DEVTMP.bread = (fd) => { _TVDOS.DRV.FS.DEVTMP.pread = (fd, ptr, count, offset) => { if (_TVDOS.TMPFS[fd.path] === undefined) throw Error(`No such file: ${fd.fullPath}`) let str = _TVDOS.TMPFS[fd.path].dataAsString() - for (let i = 0; i < count - (offset || 0); i++) { - sys.poke(ptr + i, String.charCodeAt(i + (offset || 0))) + let off = offset || 0 + for (let i = 0; i < count; i++) { + sys.poke(ptr + i, str.charCodeAt(off + i)) } } @@ -988,6 +991,7 @@ _TVDOS.DRV.FS.DEVTMP.remove = (fd) => { return true } _TVDOS.DRV.FS.DEVTMP.exists = (fd) => (_TVDOS.TMPFS[fd.path] !== undefined) +_TVDOS.DRV.FS.DEVTMP.getFileLen = (fd) => (_TVDOS.TMPFS[fd.path].length) Object.freeze(_TVDOS.DRV.FS.DEVTMP) diff --git a/assets/disk0/tvdos/bin/lfs.js b/assets/disk0/tvdos/bin/lfs.js index 92ee7a3..a830d03 100644 --- a/assets/disk0/tvdos/bin/lfs.js +++ b/assets/disk0/tvdos/bin/lfs.js @@ -15,7 +15,10 @@ Uint16 Encoding 10 00 : UTF-8 10 01 : UTF-16BE 10 02 : UTF-16LE -Byte[5] Padding +Byte Flags + 0b 0000 000r + r: path is relative +Bytes[4] Reserved # FileBlocks Uint8 File type (only 1 is used) @@ -28,27 +31,36 @@ instead of compressing individual files) function printUsage() { println(`Collects files under a directory into a single archive. -Usage: lfs [-c/-x/-t] dest.lfs path\\to\\source +Usage: lfs [-c/-x/-t] [-r] dest.lfs path\\to\\source To collect a directory into myarchive.lfs: lfs -c myarchive.lfs path\\to\\directory +To collect a directory into myarchive.lfs, using relative path: + lfs -c -r myarchive.lfs path\\to\\directory To extract an archive to path\\to\\my\\files: lfs -x myarchive.lfs path\\to\\my\\files To list the collected files: lfs -t myarchive.lfs`) } -let option = exec_args[1] -const lfsPath = exec_args[2] -const dirPath = exec_args[3] +let option = undefined +let useRelative = false +const positional = [] +for (let i = 1; i < exec_args.length; i++) { + const a = exec_args[i] + if (a === undefined) continue + const au = a.toUpperCase() + if (au === "-C" || au === "-X" || au === "-T") option = au + else if (au === "-R") useRelative = true + else positional.push(a) +} +const lfsPath = positional[0] +const dirPath = positional[1] - -if (option === undefined || lfsPath === undefined || option.toUpperCase() != "-T" && dirPath === undefined) { +if (option === undefined || lfsPath === undefined || (option != "-T" && dirPath === undefined)) { printUsage() return 0 } -option = option.toUpperCase() - function recurseDir(file, action) { if (!file.isDirectory) { @@ -76,13 +88,14 @@ if ("-C" == option) { return 1 } - let out = "TVDOSLFS\x01\x00\x00\x00\x00\x00\x00\x00" + const flagsByte = useRelative ? 0x01 : 0x00 + let out = "TVDOSLFS\x01\x00\x00" + String.fromCharCode(flagsByte) + "\x00\x00\x00\x00" const rootDirPathLen = rootDir.fullPath.length recurseDir(rootDir, file=>{ let f = files.open(file.fullPath) let flen = f.size - let fname = file.fullPath.substring(rootDirPathLen + 1) + let fname = useRelative ? file.fullPath.substring(rootDirPathLen + 1) : file.fullPath let plen = fname.length out += "\x01" + String.fromCharCode( @@ -116,6 +129,8 @@ else if ("-T" == option || "-X" == option) { return 2 } + const archiveRelative = (bytes.charCodeAt(11) & 0x01) !== 0 + if ("-X" == option && !rootDir.exists) { rootDir.mkDir() } @@ -132,9 +147,12 @@ else if ("-T" == option || "-X" == option) { if ("-X" == option) { let filebytes = bytes.substring(curs, curs + filelen) - let outfile = files.open(`${rootDir.fullPath}\\${path}`) + // Fully qualified paths (e.g. "A:\foo\bar.txt") get their drive prefix + // stripped so the archive contents re-root under the destination dir. + let subPath = archiveRelative ? path : path.replace(/^[A-Za-z]:[\\\/]?/, "") + let outfile = files.open(`${rootDir.fullPath}\\${subPath}`) - mkDirs(files.open(`${rootDir.driveLetter}:${files.open(`${rootDir.fullPath}\\${path}`).parentPath}`)) + mkDirs(files.open(`${outfile.driveLetter}:${outfile.parentPath}`)) outfile.mkFile() outfile.swrite(filebytes) } diff --git a/assets/disk0/tvdos/include/lfs.mjs b/assets/disk0/tvdos/include/lfs.mjs new file mode 100644 index 0000000..c0ee609 --- /dev/null +++ b/assets/disk0/tvdos/include/lfs.mjs @@ -0,0 +1,171 @@ +/* + * lfs.mjs — programmatic extractor for TVDOS Linear File Strip archives. + * + * let lfs = require("A:/tvdos/include/lfs.mjs") + * + * // Pull one entry out: + * let fd = lfs.extractOne("A:/path/archive.lfs", "wanted.bin") + * // → file descriptor for $:/TMP//wanted.bin + * + * // Unpack the whole archive: + * let dir = lfs.extractAll("A:/path/archive.lfs") + * // → directory descriptor for $:/TMP// + * + * Both functions accept an `autoDecompress` boolean (default true). When + * a payload's first four bytes match the gzip (1F 8B 08 xx) or zstd + * (28 B5 2F FD) magic, the payload is inflated through gzip.decomp() + * before being written. The check is done on the payload bytes — the + * archived filename is irrelevant. + * + * Both functions require a relative-path archive (one produced by + * `lfs -c -r`); fully qualified archives carry drive letters that would + * not make sense rerooted under $:/TMP. + */ + +const TMP_ROOT = "$:/TMP" +const HASH_ALPHABET = "YBNDRFG8EJKMCPQXOTLVWIS2A345H769" +const HASH_LEN = 32 +const LFS_HEADER = "TVDOSLFS\x01" +const LFS_HEADER_LEN = 16 +const LFS_FLAG_RELATIVE = 0x01 + + +function _makeHash(n) { + let s = "" + const m = HASH_ALPHABET.length + for (let i = 0; i < n; i++) s += HASH_ALPHABET[Math.floor(Math.random() * m)] + return s +} + +function _isCompressed(s) { + if (s.length < 4) return false + const b0 = s.charCodeAt(0), b1 = s.charCodeAt(1) + const b2 = s.charCodeAt(2), b3 = s.charCodeAt(3) + if (b0 === 0x1f && b1 === 0x8b && b2 === 0x08) return true // gzip + if (b0 === 0x28 && b1 === 0xb5 && b2 === 0x2f && b3 === 0xfd) return true // zstd + return false +} + +function _decompress(payload) { + // gzip.decomp transparently handles both gzip and zstd; returns Java byte[]. + return btostr(gzip.decomp(payload)) +} + +function _readArchive(lfsPath) { + const fd = files.open(lfsPath) + if (!fd.exists) throw new Error("LFS archive not found: " + lfsPath) + if (fd.isDirectory) throw new Error("LFS archive is a directory: " + lfsPath) + + const bytes = fd.sread() + try { fd.close() } catch (_) {} + + if (bytes.substring(0, LFS_HEADER.length) !== LFS_HEADER) + throw new Error("Not an LFS archive: " + lfsPath) + + const flags = bytes.charCodeAt(11) + if ((flags & LFS_FLAG_RELATIVE) === 0) + throw new Error("LFS archive does not use relative paths: " + lfsPath) + + return bytes +} + +function _allocTmpDir() { + const path = TMP_ROOT + "/" + _makeHash(HASH_LEN) + const dir = files.open(path) + dir.mkDir() + return { fd: dir, path: path } +} + +function _normPath(p) { + return p.replace(/\//g, "\\") +} + +function _writeFile(destDirPath, archivePath, payload) { + const parts = _normPath(archivePath).split("\\").filter(p => p.length > 0) + if (parts.length === 0) return null + + const leaf = parts.pop() + let curPath = destDirPath + for (let i = 0; i < parts.length; i++) { + curPath = curPath + "/" + parts[i] + const cur = files.open(curPath) + if (!cur.exists) cur.mkDir() + } + const outfile = files.open(curPath + "/" + leaf) + if (!outfile.exists) outfile.mkFile() + outfile.swrite(payload) + return outfile +} + + +function extractOne(lfsPath, filename, autoDecompress) { + if (autoDecompress === undefined) autoDecompress = true + if (filename === undefined || filename === null || filename === "") + throw new Error("filename is required") + + const bytes = _readArchive(lfsPath) + const needle = _normPath(filename) + + let curs = LFS_HEADER_LEN + while (curs < bytes.length) { + const fileType = bytes.charCodeAt(curs) + const pathlen = (bytes.charCodeAt(curs+1) << 8) | bytes.charCodeAt(curs+2) + curs += 3 + const path = bytes.substring(curs, curs + pathlen) + curs += pathlen + const filelen = (bytes.charCodeAt(curs) << 24) + | (bytes.charCodeAt(curs+1) << 16) + | (bytes.charCodeAt(curs+2) << 8) + | bytes.charCodeAt(curs+3) + curs += 4 + + if (_normPath(path) === needle) { + let payload = bytes.substring(curs, curs + filelen) + if (autoDecompress && _isCompressed(payload)) payload = _decompress(payload) + + const dest = _allocTmpDir() + const leaf = needle.split("\\").pop() + const outfile = files.open(dest.path + "/" + leaf) + if (!outfile.exists) outfile.mkFile() + outfile.swrite(payload) + return outfile + } + + curs += filelen + } + + throw new Error("File not found in archive: " + filename) +} + + +function extractAll(lfsPath, autoDecompress) { + if (autoDecompress === undefined) autoDecompress = true + + const bytes = _readArchive(lfsPath) + const dest = _allocTmpDir() + + let curs = LFS_HEADER_LEN + while (curs < bytes.length) { + const fileType = bytes.charCodeAt(curs) + const pathlen = (bytes.charCodeAt(curs+1) << 8) | bytes.charCodeAt(curs+2) + curs += 3 + const path = bytes.substring(curs, curs + pathlen) + curs += pathlen + const filelen = (bytes.charCodeAt(curs) << 24) + | (bytes.charCodeAt(curs+1) << 16) + | (bytes.charCodeAt(curs+2) << 8) + | bytes.charCodeAt(curs+3) + curs += 4 + + let payload = bytes.substring(curs, curs + filelen) + if (autoDecompress && _isCompressed(payload)) payload = _decompress(payload) + _writeFile(dest.path, path, payload) + + curs += filelen + } + + return dest.fd +} + + +exports = { extractOne, extractAll } diff --git a/assets/disk0/tvdos/include/typesetter.mjs b/assets/disk0/tvdos/include/typesetter.mjs new file mode 100644 index 0000000..f4e957b --- /dev/null +++ b/assets/disk0/tvdos/include/typesetter.mjs @@ -0,0 +1,331 @@ +/* + * typesetter.mjs - Rich-text typesetter for TVDOS console output. + * + * Wraps and aligns text using a tiny markup language. Originally lifted + * out of taut_helpmsg.js so other tools (motd, help popups, ...) can + * share the same formatter. + * + * Markup + * ------ + * ... emphasised foreground colour + * ... centre-align this source line + * ... right-align this source line + * ... left-align this source line + * ... virtual typesetting box. Left anchor is the cursor + * column at the open tag, right anchor is the wrap edge. + * default alignment is fully justified (override per-call via opts). + * + * Entities + * -------- + * µtone; "Microtone" wordmark + * &bul; &ddot; &mdot; bullet glyphs + * &updn; &udlr; arrow glyphs + * &keyoffsym; ¬ecutsym; + * &demisharp; ♯ &sesquisharp; &doublesharp; &triplesharp; &quadsharp; + * &demiflat; ♭ &sesquiflat; &doubleflat; &tripleflat; &quadflat; + * &accuptick; &accdntick; &accupup; &accdndn; + *   non-breaking space + * ­ soft hyphen (currently dropped) + * < > literal angle brackets + * + * Usage + * ----- + * let ts = require("typesetter") + * let lines = ts.typeset(text, width) // array of width-wide strings + * let lines = ts.typeset(text) // width = rest of current row + * let lines = ts.typeset(text, width, { defaultAlign: 'l' }) + */ + + +/////////////////////////////////////////////////////////////////////////////// +// Palette / ANSI helpers +/////////////////////////////////////////////////////////////////////////////// + +const COL_TEXT = 239 // popup body default (== colWHITE) +const COL_EMPH = 230 // ... highlight (== colVoiceHdr) +const COL_BRAND = 211 // first half of "Microtone" +const COL_BRAND_DIM = 239 // second half of "Microtone" + +const fgEsc = (n) => `\x1B[38;5;${n}m` +const ESC_DEFAULT = fgEsc(COL_TEXT) +const ESC_EMPH = fgEsc(COL_EMPH) +const MICROTONE = `${fgEsc(COL_BRAND)}Micro${fgEsc(COL_BRAND_DIM)}tone${ESC_DEFAULT}` + + +/////////////////////////////////////////////////////////////////////////////// +// Entity expansion +/////////////////////////////////////////////////////////////////////////////// + +// Replace &xxx; entities with their final printable representations. +function expandEntities(s) { + return s + .replaceAll('µtone;', MICROTONE) + .replaceAll('&bul;', '\u00F9') + .replaceAll('&ddot;', '\u008419u') + .replaceAll('&mdot;', '\u00FA') + .replaceAll('&updn;', '\u008418u') + .replaceAll('&udlr;', '\u008428u\u008429u') + .replaceAll('&keyoffsym;', '\u00A0\u00B1\u00B1\u00A1') + .replaceAll('¬ecutsym;', '\u00A4\u00A4\u00A4\u00A4') + .replaceAll(' ', '\u007F') + .replaceAll('­', '') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('&demisharp;', '\u0080\u0081') + .replaceAll('♯', '\u0082\u0083') + .replaceAll('&sesquisharp;', '\u0084132u\u0085') + .replaceAll('&doublesharp;', '\u0086\u0087') + .replaceAll('&triplesharp;', '\u0088\u0089') + .replaceAll('&quadsharp;', '\u008A\u008B') + .replaceAll('&demiflat;', '\u008C\u008D') + .replaceAll('♭', '\u008E\u008F') + .replaceAll('&sesquiflat;', '\u0090\u0091') + .replaceAll('&doubleflat;', '\u0092\u0093') + .replaceAll('&tripleflat;', '\u0094\u0095') + .replaceAll('&quadflat;', '\u0096\u0097') + .replaceAll('&accuptick;', '\u009A') + .replaceAll('&accdntick;', '\u009B') + .replaceAll('&accupup;', '\u009C') + .replaceAll('&accdndn;', '\u009D') +} + + +/////////////////////////////////////////////////////////////////////////////// +// Tokeniser +/////////////////////////////////////////////////////////////////////////////// + +// Tokenise a (post-entity-expansion) line. Returns an array of: +// {type:'word', text:String, w:int} - non-breakable run of visible chars (may carry ANSI escapes) +// {type:'sp'} - a single soft space (eligible for break/expansion) +// {type:'anchor', open:Boolean} - / markers (zero width) +// +// Width accounting: +// - ANSI escapes (`\x1B[...m`) : 0 visible chars +// - TSVM unicode escapes (`\u0084..u`) : 1 visible char +// - non-breaking space (\u007F) : 1 visible char (consumed as part of a word) +// - soft hyphen (\u00AD) : dropped (not implemented as a break point) +// - everything else : 1 visible char +function tokenise(line) { + const tokens = [] + let buf = '' + let bufW = 0 + let i = 0 + + const flushWord = () => { + if (buf.length > 0) { + tokens.push({type: 'word', text: buf, w: bufW}) + buf = '' + bufW = 0 + } + } + + while (i < line.length) { + // inline tags (case-sensitive for , case-insensitive for ) + if (line.slice(i, i + 3) === '') { buf += ESC_EMPH; i += 3; continue } + if (line.slice(i, i + 4) === '') { buf += ESC_DEFAULT; i += 4; continue } + const head3 = line.slice(i, i + 3).toLowerCase() + const head4 = line.slice(i, i + 4).toLowerCase() + if (head3 === '') { flushWord(); tokens.push({type: 'anchor', open: true}); i += 3; continue } + if (head4 === '') { flushWord(); tokens.push({type: 'anchor', open: false}); i += 4; continue } + + const c = line[i] + const cc = line.charCodeAt(i) + + if (cc === 0x1B) { + // pre-existing ANSI escape - copy verbatim, zero visible width + const m = line.indexOf('m', i) + const end = (m < 0) ? line.length : m + 1 + buf += line.slice(i, end) + i = end + } + else if (cc === 0x84) { + // TSVM \u0084u escape - copy verbatim, one visible char + const u = line.indexOf('u', i) + const end = (u < 0) ? line.length : u + 1 + buf += line.slice(i, end) + bufW += 1 + i = end + } + else if (c === ' ') { + flushWord() + tokens.push({type: 'sp'}) + i += 1 + } + else if (cc === 0x00AD) { + // soft hyphen: drop (no break-point handling for now) + i += 1 + } + else { + buf += c + bufW += 1 + i += 1 + } + } + flushWord() + return tokens +} + + +/////////////////////////////////////////////////////////////////////////////// +// Line builder +/////////////////////////////////////////////////////////////////////////////// + +// Build wrapped lines from a token stream then format each one according to alignment. +// Returns an array of strings, each exactly `width` visible chars wide (padded with +// trailing spaces) so the caller can blit them without further math. +function wrapAndAlign(tokens, width, alignment) { + const lines = [] // each: {tokens, indent, contentW} + let curTokens = [] + let curW = 0 + let curIndent = 0 + let nextIndent = 0 // indent the *next* flushed line should use + + const flushLine = () => { + // strip trailing soft spaces + while (curTokens.length > 0 && curTokens[curTokens.length - 1].type === 'sp') { + curTokens.pop() + curW -= 1 + } + lines.push({tokens: curTokens, indent: curIndent, contentW: curW}) + curTokens = [] + curW = 0 + curIndent = nextIndent + } + + for (const tok of tokens) { + if (tok.type === 'anchor') { + // anchor opens at the current visible column (accounting for indent) + if (tok.open) nextIndent = curIndent + curW + else nextIndent = 0 + continue + } + + if (tok.type === 'sp') { + // ignore leading soft spaces on a fresh line + if (curW === 0) continue + // hard wrap if the line is already at the right edge + if (curIndent + curW + 1 > width) { flushLine(); continue } + curTokens.push(tok) + curW += 1 + continue + } + + // word + const tw = tok.w + if (curIndent + curW + tw > width) { + flushLine() + // word too wide for the wrapped line: emit it on its own row (possibly clipped by terminal) + if (curIndent + tw > width) { + curTokens.push(tok) + curW += tw + flushLine() + continue + } + } + curTokens.push(tok) + curW += tw + } + + if (curTokens.length > 0 || lines.length === 0) flushLine() + + return lines.map((line, i) => formatLine(line, width, alignment, i === lines.length - 1)) +} + +function formatLine(line, totalWidth, alignment, isLast) { + if (line.tokens.length === 0) return ' '.repeat(totalWidth) + + const indent = ' '.repeat(line.indent) + const remaining = totalWidth - line.indent - line.contentW + const pad = (n) => (n > 0) ? ' '.repeat(n) : '' + const flatText = () => line.tokens.map(t => (t.type === 'sp') ? ' ' : t.text).join('') + + if (alignment === 'c') { + const left = remaining >> 1 + return indent + pad(left) + flatText() + pad(remaining - left) + } + if (alignment === 'r') return indent + pad(remaining) + flatText() + if (alignment === 'l') return indent + flatText() + pad(remaining) + + // justified: only expand spaces when there's slack and we're not on the + // last (or single) wrapped line + if (isLast || remaining <= 0) return indent + flatText() + pad(remaining) + + const spaceCount = line.tokens.reduce((n, t) => n + (t.type === 'sp' ? 1 : 0), 0) + if (spaceCount === 0) return indent + flatText() + pad(remaining) + + const baseExtra = (remaining / spaceCount) | 0 + let leftover = remaining - baseExtra * spaceCount + + let out = indent + for (const tok of line.tokens) { + if (tok.type === 'sp') { + const extra = baseExtra + (leftover > 0 ? 1 : 0) + if (leftover > 0) leftover -= 1 + out += ' '.repeat(1 + extra) + } else { + out += tok.text + } + } + return out +} + +// Process a single source line: peel a leading // alignment tag (if present), +// strip its matching close tag, then tokenise + wrap. +function typesetSourceLine(line, width, defaultAlign) { + if (line.length === 0) return [' '.repeat(width)] + + let alignment = defaultAlign || 'j' // justified default + const startMatch = line.match(/^<([crl])>/i) + if (startMatch) { + alignment = startMatch[1].toLowerCase() + line = line.slice(startMatch[0].length) + const closeRe = new RegExp(`$`, 'i') + line = line.replace(closeRe, '') + } + + const tokens = tokenise(line) + return wrapAndAlign(tokens, width, alignment) +} + +function typesetText(text, width, defaultAlign) { + text = expandEntities(text) + const out = [] + for (const srcLine of text.split('\n')) { + for (const outLine of typesetSourceLine(srcLine, width, defaultAlign)) out.push(outLine) + } + return out +} + +// Convenience entry: `typeset(text)` defaults the wrap width to "rest of current row". +// `opts` may be `{ defaultAlign: 'l' | 'c' | 'r' | 'j' }`. +function typeset(text, customWidth, opts) { + let typesetWidth = customWidth + if (typesetWidth === undefined) { + const SCRW = con.getmaxyx()[1] + const currentPosX = con.getyx()[1] // 1-indexed + typesetWidth = SCRW - currentPosX + 1 + } + let defaultAlign = (opts && opts.defaultAlign) || 'j' + return typesetText(text, typesetWidth, defaultAlign) +} + + +/////////////////////////////////////////////////////////////////////////////// +// Module exports +/////////////////////////////////////////////////////////////////////////////// + +exports = { + typeset, + typesetText, + typesetSourceLine, + tokenise, + expandEntities, + fgEsc, + COL_TEXT, + COL_EMPH, + COL_BRAND, + COL_BRAND_DIM, + ESC_DEFAULT, + ESC_EMPH, + MICROTONE, +} diff --git a/tsvm_core/src/net/torvald/tsvm/VMJSR223Delegate.kt b/tsvm_core/src/net/torvald/tsvm/VMJSR223Delegate.kt index cb2cf31..3bf938f 100644 --- a/tsvm_core/src/net/torvald/tsvm/VMJSR223Delegate.kt +++ b/tsvm_core/src/net/torvald/tsvm/VMJSR223Delegate.kt @@ -305,7 +305,6 @@ class VMJSR223Delegate(private val vm: VM) { fun sleep(time: Long) { vm.isIdle.set(true) Thread.sleep(time) - Thread.sleep(4L) } fun waitForMemChg(addr: Int, andMask: Int, xorMask: Int) {