Files
Terrarum/src/net/torvald/terrarum/virtualcomputer/computer/TerrarumComputer.kt
Song Minjae 55f7a4ed46 working io.read from STDIN
Former-commit-id: b1168a53fd5fbd09c6a5a76506402560fc4e0fd7
Former-commit-id: a0d455da14a6eb7bf0127e136949273ec5a28628
2017-03-02 22:35:13 +09:00

538 lines
16 KiB
Kotlin

package net.torvald.terrarum.virtualcomputer.computer
import org.luaj.vm2.Globals
import org.luaj.vm2.LuaError
import org.luaj.vm2.LuaTable
import org.luaj.vm2.LuaValue
import org.luaj.vm2.lib.TwoArgFunction
import org.luaj.vm2.lib.ZeroArgFunction
import org.luaj.vm2.lib.jse.JsePlatform
import net.torvald.terrarum.KVHashMap
import net.torvald.terrarum.gameactors.roundInt
import net.torvald.terrarum.virtualcomputer.luaapi.*
import net.torvald.terrarum.virtualcomputer.peripheral.*
import net.torvald.terrarum.virtualcomputer.terminal.*
import net.torvald.terrarum.virtualcomputer.worldobject.ComputerPartsCodex
import org.lwjgl.BufferUtils
import org.lwjgl.openal.AL
import org.lwjgl.openal.AL10
import org.newdawn.slick.GameContainer
import org.newdawn.slick.Input
import java.io.*
import java.nio.ByteBuffer
import java.util.*
/**
* A part that makes "computer fixture" actually work
*
* @param avFixtureComputer : actor values for FixtureComputerBase
*
* @param term : terminal that is connected to the computer fixtures, null if not connected any.
* Created by minjaesong on 16-09-10.
*/
class TerrarumComputer(peripheralSlots: Int) {
var maxPeripherals: Int = peripheralSlots
private set
val DEBUG_UNLIMITED_MEM = false
val DEBUG = true
lateinit var luaJ_globals: Globals
private set
var stdout: PrintStream? = null
private set
var stderr: PrintStream? = null
private set
var stdin: InputStream? = null
private set
val processorCycle: Int // number of Lua statement to process per tick (1/100 s)
get() = ComputerPartsCodex.getProcessorCycles(computerValue.getAsInt("processor") ?: 0)
val memSize: Int // in bytes; max: 8 GB
get() {
if (DEBUG_UNLIMITED_MEM) return 16.shl(20)// 16 MB
var size = 0
for (i in 0..3)
size += ComputerPartsCodex.getRamSize(computerValue.getAsInt("memSlot$i")!!)
return size
}
val UUID = java.util.UUID.randomUUID().toString()
val computerValue = KVHashMap()
var isHalted = false
lateinit var input: Input
private set
lateinit var term: Teletype
private set
val peripheralTable = ArrayList<Peripheral>()
var stdinInput: Int = -1
private set
// os-related functions. These are called "machine" library-wise.
private val startupTimestamp: Long = System.currentTimeMillis()
/** Time elapsed since the power is on. */
val milliTime: Int
get() = (System.currentTimeMillis() - startupTimestamp).toInt()
init {
computerValue["memslot0"] = 4864 // -1 indicates mem slot is empty
computerValue["memslot1"] = -1 // put index of item here
computerValue["memslot2"] = -1 // ditto.
computerValue["memslot3"] = -1 // do.
computerValue["processor"] = -1 // do.
// as in "dev/hda"; refers hard disk drive (and no partitioning)
computerValue["hda"] = "uuid_testhda" // 'UUID rendered as String' or "none"
computerValue["hdb"] = "uuid_testhdb"
computerValue["hdc"] = "none"
computerValue["hdd"] = "none"
// as in "dev/fd1"; refers floppy disk drive
computerValue["fd1"] = "uuid_testfd1"
computerValue["fd2"] = "none"
computerValue["fd3"] = "none"
computerValue["fd4"] = "none"
// SCSI connected optical drive
computerValue["sda"] = "none"
// boot device
computerValue["boot"] = computerValue.getAsString("hda")!!
}
fun getPeripheral(tableName: String): Peripheral? {
peripheralTable.forEach {
if (it.tableName == tableName)
return it
}
return null
}
fun attachPeripheral(peri: Peripheral) {
if (peripheralTable.size < maxPeripherals) {
peripheralTable.add(peri)
peri.loadLib(luaJ_globals)
println("[TerrarumComputer] loading peripheral $peri")
}
else {
throw Error("No vacant peripheral slot")
}
}
fun detachPeripheral(peri: Peripheral) {
if (peripheralTable.contains(peri)) {
peripheralTable.remove(peri)
println("[TerrarumComputer] unloading peripheral $peri")
}
else {
throw IllegalArgumentException("Peripheral not exists: $peri")
}
}
fun attachTerminal(term: Teletype) {
this.term = term
initSandbox(term)
}
fun initSandbox(term: Teletype) {
luaJ_globals = JsePlatform.debugGlobals()
stdout = TerminalPrintStream(this)
stderr = TerminalPrintStream(this)
stdin = TerminalInputStream(this)
luaJ_globals.STDOUT = stdout
luaJ_globals.STDERR = stderr
luaJ_globals.STDIN = stdin
luaJ_globals["bit"] = luaJ_globals["bit32"]
// load libraries
Term(luaJ_globals, term)
Security(luaJ_globals)
Filesystem(luaJ_globals, this)
HostAccessProvider(luaJ_globals, this)
Input(luaJ_globals, this)
PcSpeakerDriver(luaJ_globals, this)
WorldInformationProvider(luaJ_globals)
// secure the sandbox
//luaJ_globals["io"] = LuaValue.NIL
// dubug should be sandboxed in BOOT.lua (use OpenComputers code)
//val sethook = luaJ_globals["debug"]["sethook"]
//luaJ_globals["debug"] = LuaValue.NIL
// ROM BASIC
val inputStream = javaClass.getResourceAsStream("/net/torvald/terrarum/virtualcomputer/assets/lua/BOOT.lua")
runCommand(InputStreamReader(inputStream), "=boot")
// computer-related global functions
luaJ_globals["totalMemory"] = LuaFunGetTotalMem(this)
luaJ_globals["computer"] = LuaTable()
// rest of the "computer" APIs should be implemented in BOOT.lua
// load every peripheral if we're in DEBUG
if (DEBUG) {
maxPeripherals = 32
attachPeripheral(PeripheralInternet(this))
attachPeripheral(PeripheralPSG(this))
// ...
}
}
fun update(gc: GameContainer, delta: Int) {
input = gc.input
if (currentExecutionThread.state == Thread.State.TERMINATED) {
threadRun = false
}
if (!isHalted) {
driveBeepQueueManager(delta)
}
}
fun keyPressed(key: Int, c: Char) {
stdinInput = c.toInt()
// wake thread
runnableRunCommand.resume()
synchronized(stdin!!) {
(stdin as java.lang.Object).notifyAll()
}
}
fun openStdin() {
stdinInput = -1
// sleep the thread
runnableRunCommand.pause()
}
lateinit var currentExecutionThread: Thread
private set
lateinit var runnableRunCommand: ThreadRunCommand
private set
private var threadRun = false
fun runCommand(line: String, env: String) {
if (!threadRun) {
runnableRunCommand = ThreadRunCommand(luaJ_globals, line, env)
currentExecutionThread = Thread(null, runnableRunCommand, "LuaJ Separated")
currentExecutionThread.start()
threadRun = true
}
}
fun runCommand(reader: Reader, filename: String) {
if (!threadRun) {
runnableRunCommand = ThreadRunCommand(luaJ_globals, reader, filename)
currentExecutionThread = Thread(null, runnableRunCommand, "LuaJ Separated")
currentExecutionThread.start()
threadRun = true
}
}
class ThreadRunCommand : Runnable {
private val mode: Int
private val arg1: Any
private val arg2: String
private val lua: Globals
@Volatile private var running = true
@Volatile private var paused = false
private val pauseLock = java.lang.Object()
constructor(luaInstance: Globals, line: String, env: String) {
mode = 0
arg1 = line
arg2 = env
lua = luaInstance
}
constructor(luaInstance: Globals, reader: Reader, filename: String) {
mode = 1
arg1 = reader
arg2 = filename
lua = luaInstance
}
override fun run() {
synchronized(pauseLock) {
if (!running) { // may have changed while waiting to
// synchronize on pauseLock
return
}
if (paused) {
try {
pauseLock.wait() // will cause this Thread to block until
// another thread calls pauseLock.notifyAll()
// Note that calling wait() will
// relinquish the synchronized lock that this
// thread holds on pauseLock so another thread
// can acquire the lock to call notifyAll()
// (link with explanation below this code)
}
catch (ex: InterruptedException) {
return
}
if (!running) { // running might have changed since we paused
return
}
}
}
try {
val chunk: LuaValue
if (mode == 0)
chunk = lua.load(arg1 as String, arg2)
else if (mode == 1)
chunk = lua.load(arg1 as Reader, arg2)
else
throw IllegalArgumentException("Unsupported mode: $mode")
chunk.call()
}
catch (e: LuaError) {
e.printStackTrace(System.err)
lua.STDERR.println("${SimpleTextTerminal.ASCII_DLE}${e.message}${SimpleTextTerminal.ASCII_DC4}")
}
}
fun stop() {
running = false
// you might also want to do this:
//interrupt()
}
fun pause() {
// you may want to throw an IllegalStateException if !running
paused = true
}
fun resume() {
synchronized(pauseLock) {
paused = false
pauseLock.notifyAll() // Unblocks thread
}
}
}
class LuaFunGetTotalMem(val computer: TerrarumComputer) : ZeroArgFunction() {
override fun call(): LuaValue {
return LuaValue.valueOf(computer.memSize)
}
}
class ComputerEmitTone(val computer: TerrarumComputer) : TwoArgFunction() {
override fun call(millisec: LuaValue, freq: LuaValue): LuaValue {
computer.playTone(millisec.checkint(), freq.checkdouble())
return LuaValue.NONE
}
}
///////////////////
// BEEPER DRIVER //
///////////////////
private val beepMaxLen = 10000
// let's regard it as a tracker...
private val beepQueue = ArrayList<Pair<Int, Double>>()
private var beepCursor = -1
private var beepQueueLineExecTimer = 0 // millisec
private var beepQueueFired = false
private fun driveBeepQueueManager(delta: Int) {
// start emitTone queue
if (beepQueue.size > 0 && beepCursor == -1) {
beepCursor = 0
}
// advance emitTone queue
if (beepCursor >= 0 && beepQueueLineExecTimer >= beepQueueGetLenOfPtn(beepCursor)) {
beepQueueLineExecTimer -= beepQueueGetLenOfPtn(beepCursor)
beepCursor += 1
beepQueueFired = false
}
// complete emitTone queue
if (beepCursor >= beepQueue.size) {
clearBeepQueue()
}
// actually play queue
if (beepCursor >= 0 && beepQueue.size > 0 && !beepQueueFired) {
playTone(beepQueue[beepCursor].first, beepQueue[beepCursor].second)
beepQueueFired = true
// delete sources that is finished. AL is limited to 256 sources. If you exceed it,
// we won't get any more sounds played.
AL10.alSourcei(oldBeepSource, AL10.AL_BUFFER, 0)
AL10.alDeleteSources(oldBeepSource)
AL10.alDeleteBuffers(oldBeepBuffer)
}
if (beepQueueFired) beepQueueLineExecTimer += delta
}
fun clearBeepQueue() {
beepQueue.clear()
beepCursor = -1
beepQueueLineExecTimer = 0
//AL.destroy()
if (DEBUG) println("[TerrarumComputer] !! Beep queue clear")
}
fun enqueueBeep(duration: Int, freq: Double) {
beepQueue.add(Pair(Math.min(duration, beepMaxLen), freq))
}
fun beepQueueGetLenOfPtn(ptnIndex: Int) = beepQueue[ptnIndex].first
////////////////////
// TONE GENERATOR //
////////////////////
private val sampleRate = 44100
private var beepSource: Int = -1
private var beepBuffer: Int = -1
private var oldBeepSource: Int = -1
private var oldBeepBuffer: Int = -1
var audioData: ByteBuffer? = null
/**
* @param duration : milliseconds
* @param rampUp
* @param rampDown
*
* ,---. (true, true) ,---- (true, false) ----. (false, true) ----- (false, false)
*/
private fun makeAudioData(duration: Int, freq: Double,
rampUp: Boolean = true, rampDown: Boolean = true): ByteBuffer {
val audioData = BufferUtils.createByteBuffer(duration.times(sampleRate).div(1000))
val realDuration = duration * sampleRate / 1000
val chopSize = freq / sampleRate
val amp = Math.max(4600.0 / freq, 1.0)
val nHarmonics = if (freq >= 22050.0) 1
else if (freq >= 11025.0) 2
else if (freq >= 5512.5) 3
else if (freq >= 2756.25) 4
else if (freq >= 1378.125) 5
else if (freq >= 689.0625) 6
else 7
val transitionThre = 974.47218
// TODO volume ramping?
if (freq == 0.0) {
for (x in 0..realDuration - 1) {
audioData.put(0x00.toByte())
}
}
else if (freq < transitionThre) { // chopper generator (for low freq)
for (x in 0..realDuration - 1) {
var sine: Double = amp * Math.cos(Math.PI * 2 * x * chopSize)
if (sine > 0.79) sine = 0.79
else if (sine < -0.79) sine = -0.79
audioData.put(
(0.5 + 0.5 * sine).times(0xFF).roundInt().toByte()
)
}
}
else { // harmonics generator (for high freq)
for (x in 0..realDuration - 1) {
var sine: Double = 0.0
for (k in 1..nHarmonics) { // mix only odd harmonics in order to make a squarewave
sine += Math.sin(Math.PI * 2 * (2*k - 1) * chopSize * x) / (2*k - 1)
}
audioData.put(
(0.5 + 0.5 * sine).times(0xFF).roundInt().toByte()
)
}
}
audioData.rewind()
return audioData
}
private fun playTone(leninmilli: Int, freq: Double) {
audioData = makeAudioData(leninmilli, freq)
if (!AL.isCreated()) AL.create()
// Clear error stack.
AL10.alGetError()
oldBeepBuffer = beepBuffer
beepBuffer = AL10.alGenBuffers()
checkALError()
try {
AL10.alBufferData(beepBuffer, AL10.AL_FORMAT_MONO8, audioData, sampleRate)
checkALError()
oldBeepSource = beepSource
beepSource = AL10.alGenSources()
checkALError()
try {
AL10.alSourceQueueBuffers(beepSource, beepBuffer)
checkALError()
AL10.alSource3f(beepSource, AL10.AL_POSITION, 0f, 0f, 1f)
AL10.alSourcef(beepSource, AL10.AL_REFERENCE_DISTANCE, 1f)
AL10.alSourcef(beepSource, AL10.AL_MAX_DISTANCE, 1f)
AL10.alSourcef(beepSource, AL10.AL_GAIN, 0.3f)
checkALError()
AL10.alSourcePlay(beepSource)
checkALError()
}
catch (e: ALException) {
AL10.alDeleteSources(beepSource)
}
}
catch (e: ALException) {
AL10.alDeleteSources(beepSource)
}
}
// Custom implementation of Util.checkALError() that uses our custom exception.
private fun checkALError() {
val errorCode = AL10.alGetError()
if (errorCode != AL10.AL_NO_ERROR) {
throw ALException(errorCode)
}
}
}