mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-06-06 13:38:30 +09:00
tracker impl, s3m converter, larger tracker sample bin
This commit is contained in:
@@ -85,6 +85,107 @@ object TinyAlphNum : BitmapFont() {
|
||||
|
||||
|
||||
|
||||
private fun isColourCodeHigh(c: Char) = c.toInt() in 0b110110_1111000000..0b110110_1111111111
|
||||
private fun isColourCodeLow(c: Char) = c.toInt() in 0b110111_0000000000..0b110111_1111111111
|
||||
|
||||
private fun getColour(charHigh: Char, charLow: Char): Color { // input: 0x10ARGB, out: RGBA8888
|
||||
val codePoint = Character.toCodePoint(charHigh, charLow)
|
||||
|
||||
if (colourBuffer.containsKey(codePoint))
|
||||
return colourBuffer[codePoint]!!
|
||||
|
||||
val a = codePoint.and(0xF000).ushr(12)
|
||||
val r = codePoint.and(0x0F00).ushr(8)
|
||||
val g = codePoint.and(0x00F0).ushr(4)
|
||||
val b = codePoint.and(0x000F)
|
||||
|
||||
val col = Color(r.shl(28) or r.shl(24) or g.shl(20) or g.shl(16) or b.shl(12) or b.shl(8) or a.shl(4) or a)
|
||||
|
||||
|
||||
colourBuffer[codePoint] = col
|
||||
return col
|
||||
}
|
||||
|
||||
private val colourBuffer = HashMap<Int, Color>()
|
||||
}
|
||||
|
||||
/**
|
||||
* Created by minjaesong on 2026-04-19.
|
||||
*/
|
||||
object PatternView : BitmapFont() {
|
||||
|
||||
internal val W = 5
|
||||
internal val H = 6
|
||||
|
||||
internal val fontSheet = TextureRegionPack(Gdx.files.internal("net/torvald/terrarum/imagefont/tiny.tga"), W, H)
|
||||
|
||||
|
||||
init {
|
||||
setOwnsTexture(true)
|
||||
setUseIntegerPositions(true)
|
||||
}
|
||||
|
||||
fun getWidth(str: String): Int {
|
||||
var l = 0
|
||||
for (char in str) {
|
||||
if (!isColourCodeHigh(char) && !isColourCodeLow(char)) {
|
||||
l += 1
|
||||
}
|
||||
}
|
||||
return W * l
|
||||
}
|
||||
|
||||
lateinit var colMain: Color
|
||||
lateinit var colShadow: Color
|
||||
|
||||
override fun draw(batch: Batch, text: CharSequence, x: Float, y: Float): GlyphLayout? {
|
||||
val originalColour = batch.color.cpy()
|
||||
colMain = batch.color.cpy()
|
||||
colShadow = colMain.cpy().mul(0.5f, 0.5f, 0.5f, 1f)
|
||||
|
||||
val x = x.roundToInt().toFloat()
|
||||
val y = y.roundToInt().toFloat()
|
||||
|
||||
var charsPrinted = 0
|
||||
text.forEachIndexed { index, c ->
|
||||
if (isColourCodeHigh(c)) {
|
||||
val cchigh = c
|
||||
val cclow = text[index + 1]
|
||||
val colour = getColour(cchigh, cclow)
|
||||
|
||||
colMain = colour
|
||||
colShadow = colMain.cpy().mul(0.5f, 0.5f, 0.5f, 1f)
|
||||
}
|
||||
else if (c in 0.toChar()..255.toChar()) {
|
||||
batch.color = colShadow
|
||||
batch.draw(fontSheet.get(c.toInt() % 16, c.toInt() / 16), x + charsPrinted * W + 1, y)
|
||||
batch.draw(fontSheet.get(c.toInt() % 16, c.toInt() / 16), x + charsPrinted * W, y + 1)
|
||||
batch.draw(fontSheet.get(c.toInt() % 16, c.toInt() / 16), x + charsPrinted * W + 1, y + 1)
|
||||
|
||||
|
||||
batch.color = colMain
|
||||
batch.draw(fontSheet.get(c.toInt() % 16, c.toInt() / 16), x + charsPrinted * W, y)
|
||||
|
||||
charsPrinted += 1
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
batch.color = originalColour
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
fun drawRalign(batch: Batch, text: CharSequence, x: Float, y: Float): GlyphLayout? {
|
||||
return draw(batch, text, x - W*text.length, y)
|
||||
}
|
||||
|
||||
override fun getLineHeight() = H.toFloat()
|
||||
override fun getCapHeight() = getLineHeight()
|
||||
override fun getXHeight() = getLineHeight()
|
||||
|
||||
|
||||
|
||||
private fun isColourCodeHigh(c: Char) = c.toInt() in 0b110110_1111000000..0b110110_1111111111
|
||||
private fun isColourCodeLow(c: Char) = c.toInt() in 0b110111_0000000000..0b110111_1111111111
|
||||
|
||||
|
||||
BIN
tsvm_executable/src/net/torvald/terrarum/imagefont/tiny.tga
LFS
Normal file
BIN
tsvm_executable/src/net/torvald/terrarum/imagefont/tiny.tga
LFS
Normal file
Binary file not shown.
@@ -1,6 +1,8 @@
|
||||
package net.torvald.tsvm
|
||||
|
||||
import com.badlogic.gdx.Audio
|
||||
import com.badlogic.gdx.Gdx
|
||||
import com.badlogic.gdx.Input.Buttons
|
||||
import com.badlogic.gdx.graphics.Color
|
||||
import com.badlogic.gdx.graphics.g2d.SpriteBatch
|
||||
import net.torvald.reflection.extortField
|
||||
@@ -9,8 +11,8 @@ import net.torvald.tsvm.EmulatorGuiToolkit.Theme.COL_ACTIVE3
|
||||
import net.torvald.tsvm.EmulatorGuiToolkit.Theme.COL_HIGHLIGHT2
|
||||
import net.torvald.tsvm.EmulatorGuiToolkit.Theme.COL_WELL
|
||||
import net.torvald.tsvm.VMEmuExecutableWrapper.Companion.FONT
|
||||
import net.torvald.tsvm.VMEmuExecutableWrapper.Companion.TINY
|
||||
import net.torvald.tsvm.peripheral.AudioAdapter
|
||||
import java.lang.Math.pow
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.floor
|
||||
@@ -21,19 +23,70 @@ import kotlin.math.roundToInt
|
||||
*/
|
||||
class AudioMenu(parent: VMEmuExecutable, x: Int, y: Int, w: Int, h: Int) : EmuMenu(parent, x, y, w, h) {
|
||||
|
||||
// Per-playhead view mode: 0=detailed pattern, 1=abridged pattern (stub), 2=super-abridged (stub), 3=cuesheet detail
|
||||
private val scopeMode = IntArray(4)
|
||||
|
||||
override fun show() {
|
||||
}
|
||||
|
||||
override fun hide() {
|
||||
}
|
||||
|
||||
private var guiClickLatched = arrayOf(false, false, false, false, false, false, false, false)
|
||||
|
||||
override fun update() {
|
||||
if (Gdx.input.isButtonPressed(Buttons.LEFT)) {
|
||||
if (!guiClickLatched[Buttons.LEFT]) {
|
||||
val mx = Gdx.input.x - x
|
||||
val my = Gdx.input.y - y
|
||||
|
||||
if (mx in 117..629) {
|
||||
for (i in 0..3) {
|
||||
val syTop = h - 7 - 115 * i - 8 * FONT.H
|
||||
val syBot = h - 3 - 115 * i
|
||||
if (my in syTop..syBot) {
|
||||
scopeMode[3 - i] = (scopeMode[3 - i] + 1) % 4
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
guiClickLatched[Buttons.LEFT] = true
|
||||
}
|
||||
}
|
||||
else {
|
||||
guiClickLatched[Buttons.LEFT] = false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private val COL_SOUNDSCOPE_BACK = Color(0x081c08ff.toInt())
|
||||
private val COL_SOUNDSCOPE_FORE = Color(0x80f782ff.toInt())
|
||||
private val COL_TRACKER_ROW = Color(0x103010ff.toInt())
|
||||
private val STR_PLAY = "\u00D2\u00D3"
|
||||
|
||||
// Pattern field colours (loosely following MilkyTracker scheme)
|
||||
private val COL_NOTE = Color(1f, 1f, 1f, 1f) // white
|
||||
private val COL_INST = Color(0x6BB5FFff.toInt()) // sky blue
|
||||
private val COL_VOL = Color(0x80FF50ff.toInt()) // lime
|
||||
private val COL_PAN = Color(0xFFC040ff.toInt()) // amber
|
||||
private val COL_EFF = Color(0xFF50FFff.toInt()) // magenta
|
||||
private val COL_EFFARG = Color(0xFFAF7Fff.toInt()) // apricot
|
||||
|
||||
// Voice colours for cue-sheet view — 10-colour palette cycling across 20 voices
|
||||
private val COL_VOICE_PALETTE = arrayOf(
|
||||
Color(0xC0C0C0ff.toInt()), // 0: silver
|
||||
Color(0xFF8080ff.toInt()), // 1: salmon
|
||||
Color(0xFFBF60ff.toInt()), // 2: tangerine
|
||||
Color(0xFFFF70ff.toInt()), // 3: yellow
|
||||
Color(0x80FF80ff.toInt()), // 4: lime
|
||||
Color(0x60EEEEff.toInt()), // 5: aqua
|
||||
Color(0x80A0FFff.toInt()), // 6: periwinkle
|
||||
Color(0xD080FFff.toInt()), // 7: orchid
|
||||
Color(0xFF80C0ff.toInt()), // 8: pink
|
||||
Color(0xA0D0A0ff.toInt()), // 9: sage
|
||||
)
|
||||
|
||||
|
||||
override fun render(batch: SpriteBatch) {
|
||||
|
||||
@@ -111,7 +164,7 @@ class AudioMenu(parent: VMEmuExecutable, x: Int, y: Int, w: Int, h: Int) : EmuMe
|
||||
FONT.draw(batch, "Tickrate", x, y + 6*FONT.H)
|
||||
|
||||
batch.color = COL_ACTIVE3
|
||||
FONT.drawRalign(batch, "${ahead.position}", x + 84, y + 2*FONT.H)
|
||||
FONT.drawRalign(batch, "${ahead.trackerState?.cuePos}:${ahead.trackerState?.rowIndex?.toString(16)?.uppercase()?.padStart(2,'0')}", x + 84, y + 2*FONT.H)
|
||||
FONT.drawRalign(batch, "${ahead.masterVolume}", x + 84, y + 3*FONT.H)
|
||||
FONT.drawRalign(batch, "${ahead.masterPan}", x + 84, y + 4*FONT.H)
|
||||
FONT.drawRalign(batch, "${ahead.bpm}", x + 84, y + 5*FONT.H)
|
||||
@@ -122,9 +175,20 @@ class AudioMenu(parent: VMEmuExecutable, x: Int, y: Int, w: Int, h: Int) : EmuMe
|
||||
|
||||
fun Int.u16Tos16() = if (this > 32767) this - 65536 else this
|
||||
|
||||
private fun readCuePat12(audio: AudioAdapter, ci: Int, vi: Int): Int {
|
||||
val byteGroup = vi / 2
|
||||
val shift = if (vi % 2 == 0) 4 else 0
|
||||
val lo = (audio.mmio_read(32768L + ci * 32 + byteGroup ).toUint() ushr shift) and 0xF
|
||||
val mid = (audio.mmio_read(32768L + ci * 32 + 10 + byteGroup).toUint() ushr shift) and 0xF
|
||||
val hi = (audio.mmio_read(32768L + ci * 32 + 20 + byteGroup).toUint() ushr shift) and 0xF
|
||||
return (hi shl 8) or (mid shl 4) or lo
|
||||
}
|
||||
|
||||
private fun bipolarCeil(d: Double) = (if (d >= 0.0) ceil(d) else floor(d)).toInt()
|
||||
private fun bipolarFloor(d: Double) = (if (d >= 0.0) floor(d) else ceil(d)).toInt()
|
||||
|
||||
private val VOX_PER_VIEW = arrayOf(5,8,16)
|
||||
|
||||
private fun drawSoundscope(audio: AudioAdapter, ahead: AudioAdapter.Playhead, batch: SpriteBatch, index: Int, x: Float, y: Float) {
|
||||
val gdxadev = ahead.audioDevice
|
||||
val bytes = gdxadev.extortField<ByteArray>("bytes")
|
||||
@@ -175,7 +239,191 @@ class AudioMenu(parent: VMEmuExecutable, x: Int, y: Int, w: Int, h: Int) : EmuMe
|
||||
|
||||
}
|
||||
else {
|
||||
// Tracker pattern visualiser.
|
||||
// Modes: 0=detailed pattern, 1=abridged (stub), 2=super-abridged (stub), 3=cuesheet detail
|
||||
val ts = ahead.trackerState
|
||||
if (ts == null) {
|
||||
batch.color = COL_SOUNDSCOPE_FORE
|
||||
FONT.draw(batch, "No tracker state", x, y + 4)
|
||||
} else {
|
||||
val cuePos = ts.cuePos
|
||||
val rowIdx = ts.rowIndex
|
||||
val ROWS = 17
|
||||
val PTN_MAX_ROWS = 63
|
||||
|
||||
when (scopeMode[index]) {
|
||||
|
||||
// ── Mode 3: Cue-sheet detail ─────────────────────────────────────
|
||||
3 -> {
|
||||
// Layout per row: >NNN|p00p01…p19|INS
|
||||
// Voice pattern numbers are colour-coded; no spaces (colour provides separation).
|
||||
val cueFirst = (cuePos - ROWS / 2).coerceAtLeast(0).coerceAtMost(1023 - ROWS + 1)
|
||||
for (r in 0 until ROWS) {
|
||||
val ci = cueFirst + r
|
||||
if (ci > 1023) break
|
||||
val here = ci == cuePos
|
||||
val ry = y + 4 + r * TINY.H
|
||||
|
||||
if (here) {
|
||||
batch.color = COL_TRACKER_ROW
|
||||
batch.fillRect(x, ry, 512, TINY.H)
|
||||
}
|
||||
|
||||
var cx = x
|
||||
// cursor + cue number
|
||||
batch.color = if (here) Color.WHITE else COL_SOUNDSCOPE_FORE
|
||||
TINY.draw(batch, "${if (here) ">" else " "}${ci.toString(16).padStart(3, '0').uppercase()}|", cx, ry)
|
||||
cx += 5 * TINY.W
|
||||
|
||||
// voice pattern numbers
|
||||
for (vi in 0 until 20) {
|
||||
if (vi > 0) { cx += TINY.W }
|
||||
val pat = readCuePat12(audio, ci, vi)
|
||||
val patStr = if (pat == 0xFFF) "---"
|
||||
else pat.toString(16).padStart(3, '0').uppercase()
|
||||
batch.color = if (here) Color.WHITE else COL_VOICE_PALETTE[vi % COL_VOICE_PALETTE.size]
|
||||
TINY.draw(batch, patStr, cx, ry)
|
||||
cx += 3 * TINY.W
|
||||
}
|
||||
|
||||
// instruction
|
||||
val instrByte = audio.mmio_read(32768L + ci * 32 + 30).toUint()
|
||||
val instrStr3 = when {
|
||||
instrByte == 0x00 -> " " // no-op
|
||||
instrByte == 0x01 -> "HALT"
|
||||
instrByte and 0x80 != 0 -> "BACK ${(instrByte and 0x7F).toString(16).padStart(2, '0').uppercase()}"
|
||||
instrByte and 0xF0 == 0x10 -> "FWRD ${(instrByte and 0x0F).toString(16).uppercase()}"
|
||||
else -> "?${instrByte.toString(16).padStart(2, '0').uppercase()}"
|
||||
}
|
||||
batch.color = if (here) Color.WHITE else COL_SOUNDSCOPE_FORE
|
||||
TINY.draw(batch, "|$instrStr3", cx, ry)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Mode 0: Detailed pattern with colour-coded fields ────────────
|
||||
// ── Mode 1: Abridged pattern with colour-coded fields ────────────
|
||||
// ── Mode 2: Super-abridged pattern with colour-coded fields ────────────
|
||||
0, 1, 2 -> {
|
||||
val cueW = 4 * TINY.W
|
||||
val sepW = TINY.W
|
||||
// val patX = x + cueW + sepW
|
||||
val patX = x
|
||||
val VOICES = VOX_PER_VIEW[scopeMode[index]]
|
||||
|
||||
// Abridged cue sheet (left column, 8 entries centred on current cue)
|
||||
/*val cueFirst = (cuePos - ROWS / 2).coerceAtLeast(0).coerceAtMost(1023 - ROWS + 1)
|
||||
for (r in 0 until ROWS) {
|
||||
val ci = cueFirst + r
|
||||
if (ci > 1023) break
|
||||
val here = ci == cuePos
|
||||
batch.color = if (here) Color.WHITE else COL_SOUNDSCOPE_FORE
|
||||
TINY.draw(batch,
|
||||
"${if (here) ">" else " "}${ci.toString(16).padStart(3, '0').uppercase()}",
|
||||
x, y + 4 + r * TINY.H)
|
||||
}
|
||||
|
||||
// Vertical separator
|
||||
batch.color = COL_SOUNDSCOPE_FORE
|
||||
for (r in 0 until ROWS) TINY.draw(batch, "|", x + cueW, y + 4 + r * TINY.H)
|
||||
*/
|
||||
|
||||
// Pattern index for each voice in current cue
|
||||
val cuePats = IntArray(VOICES) { vi -> readCuePat12(audio, cuePos, vi) }
|
||||
|
||||
// Pattern rows (right area, 8 rows centred on current row)
|
||||
// Layout: > rr NOTE in E.Vo E.Pn Eff ffff [voice1 …]
|
||||
// 1 2 4 2 4 4 2 4
|
||||
val rowFirst = (rowIdx - ROWS / 2).coerceAtLeast(0).coerceAtMost(PTN_MAX_ROWS - ROWS + 1)
|
||||
for (r in 0 until ROWS) {
|
||||
val ri = rowFirst + r
|
||||
if (ri > PTN_MAX_ROWS) break
|
||||
val here = ri == rowIdx
|
||||
val ry = y + 4 + r * TINY.H
|
||||
|
||||
if (here) {
|
||||
batch.color = COL_TRACKER_ROW
|
||||
batch.fillRect(patX, ry, 512 - cueW - sepW, TINY.H)
|
||||
}
|
||||
|
||||
var cx = patX
|
||||
|
||||
// cursor + row number (drawn once per row)
|
||||
batch.color = if (here) Color.WHITE else COL_SOUNDSCOPE_FORE
|
||||
TINY.draw(batch, if (here) ">" else " ", cx, ry)
|
||||
cx += TINY.W
|
||||
TINY.draw(batch, ri.toString().padStart(2, '0').uppercase(), cx, ry)
|
||||
cx += 2 * TINY.W
|
||||
|
||||
for (vi in 0 until VOICES) {
|
||||
val pat12 = cuePats[vi]
|
||||
if (pat12 == 0xFFF) {
|
||||
// disabled voice — dimmed placeholder, same width as a live voice
|
||||
batch.color = COL_SOUNDSCOPE_FORE
|
||||
TINY.draw(batch, "(NO PATTERN DATA OR REACHED THE END OF THE SONG) ", cx, ry)
|
||||
} else {
|
||||
val localPat = pat12 and 0xFF
|
||||
val base = if (localPat < 128) 786432L + localPat * 512 + ri * 8
|
||||
else 851968L + (localPat - 128) * 512 + ri * 8
|
||||
|
||||
val noteLo = audio.peek(base + 0).toUint()
|
||||
val noteHi = audio.peek(base + 1).toUint()
|
||||
val noteVal = noteLo or (noteHi shl 8)
|
||||
val instr = audio.peek(base + 2).toUint()
|
||||
val volByte = audio.peek(base + 3).toUint()
|
||||
val panByte = audio.peek(base + 4).toUint()
|
||||
val eff = audio.peek(base + 5).toUint()
|
||||
val eaLo = audio.peek(base + 6).toUint()
|
||||
val eaHi = audio.peek(base + 7).toUint()
|
||||
|
||||
val vol = volByte and 63
|
||||
val volEff = (volByte ushr 6) and 3
|
||||
val pan = panByte and 63
|
||||
val panEff = (panByte ushr 6) and 3
|
||||
val effArg = eaLo or (eaHi shl 8)
|
||||
|
||||
val noteStr = when (noteVal) {
|
||||
0xFFFF -> "@@@@"
|
||||
0x0000 -> "===="
|
||||
0xFFFE -> "^^^^"
|
||||
else -> noteVal.toString(16).uppercase().padStart(4, '0')
|
||||
}
|
||||
|
||||
// note
|
||||
batch.color = if (here) Color.WHITE else COL_NOTE
|
||||
TINY.draw(batch, noteStr, cx, ry)
|
||||
cx += 4 * TINY.W
|
||||
// instrument
|
||||
batch.color = if (here) Color.WHITE else COL_INST
|
||||
TINY.draw(batch, instr.toString(16).padStart(2, '0').uppercase(), cx, ry)
|
||||
cx += 2 * TINY.W
|
||||
if (scopeMode[index] == 0) {
|
||||
// volume
|
||||
batch.color = if (here) Color.WHITE else COL_VOL
|
||||
TINY.draw(batch, "$volEff.${vol.toString().padStart(2, '0')}", cx, ry)
|
||||
cx += 4 * TINY.W
|
||||
}
|
||||
// pan
|
||||
if (scopeMode[index] == 0) {
|
||||
batch.color = if (here) Color.WHITE else COL_PAN
|
||||
TINY.draw(batch, "$panEff.${pan.toString().padStart(2, '0')}", cx, ry)
|
||||
cx += 4 * TINY.W
|
||||
}
|
||||
if (scopeMode[index] < 2) {
|
||||
// effect opcode
|
||||
batch.color = if (here) Color.WHITE else COL_EFF
|
||||
TINY.draw(batch, eff.toString(16).padStart(2, '0').uppercase(), cx, ry)
|
||||
cx += 2 * TINY.W
|
||||
// effect argument
|
||||
batch.color = if (here) Color.WHITE else COL_EFFARG
|
||||
TINY.draw(batch, effArg.toString(16).padStart(4, '0').uppercase(), cx, ry)
|
||||
cx += 4 * TINY.W
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -184,4 +432,4 @@ class AudioMenu(parent: VMEmuExecutable, x: Int, y: Int, w: Int, h: Int) : EmuMe
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import com.badlogic.gdx.utils.JsonValue
|
||||
import com.badlogic.gdx.utils.JsonWriter
|
||||
import net.torvald.terrarum.DefaultGL32Shaders
|
||||
import net.torvald.terrarum.FlippingSpriteBatch
|
||||
import net.torvald.terrarum.imagefont.PatternView
|
||||
import net.torvald.terrarum.imagefont.TinyAlphNum
|
||||
import net.torvald.terrarum.utils.JsonFetcher
|
||||
import net.torvald.tsvm.VMEmuExecutableWrapper.Companion.FONT
|
||||
@@ -25,10 +26,12 @@ class VMEmuExecutableWrapper(val windowWidth: Int, val windowHeight: Int, var pa
|
||||
companion object {
|
||||
lateinit var SQTEX: Texture; private set
|
||||
lateinit var FONT: TinyAlphNum; private set
|
||||
lateinit var TINY: PatternView; private set
|
||||
}
|
||||
|
||||
override fun create() {
|
||||
FONT = TinyAlphNum
|
||||
TINY = PatternView
|
||||
SQTEX = Texture(Gdx.files.internal("net/torvald/tsvm/sq.tga"))
|
||||
executable = VMEmuExecutable(windowWidth, windowHeight, panelsX, panelsY, diskPathRoot)
|
||||
executable.create()
|
||||
@@ -54,6 +57,8 @@ class VMEmuExecutableWrapper(val windowWidth: Int, val windowHeight: Int, var pa
|
||||
// println("App Dispose")
|
||||
executable.dispose()
|
||||
SQTEX.dispose()
|
||||
FONT.dispose()
|
||||
TINY.dispose()
|
||||
exitProcess(0)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user