Files
Terrarum/src/net/torvald/terrarum/Terrarum.kt

690 lines
21 KiB
Kotlin

package net.torvald.terrarum
import com.google.gson.JsonArray
import com.google.gson.JsonPrimitive
import net.torvald.imagefont.GameFontImpl
import net.torvald.terrarum.utils.JsonFetcher
import net.torvald.terrarum.utils.JsonWriter
import net.torvald.imagefont.TinyAlphNum
import net.torvald.terrarum.gamecontroller.mouseTileX
import net.torvald.terrarum.gamecontroller.mouseTileY
import net.torvald.terrarum.gameworld.toUint
import net.torvald.terrarum.langpack.Lang
import org.lwjgl.input.Controllers
import org.lwjgl.opengl.*
import org.newdawn.slick.*
import org.newdawn.slick.opengl.Texture
import org.newdawn.slick.state.StateBasedGame
import java.io.File
import java.io.IOException
import java.nio.ByteOrder
import java.text.SimpleDateFormat
import java.util.*
import java.util.logging.FileHandler
import java.util.logging.Level
import java.util.logging.Logger
import java.util.logging.SimpleFormatter
const val GAME_NAME = "Terrarum"
typealias Millisec = Int
/**
* Created by minjaesong on 15-12-30.
*/
object Terrarum : StateBasedGame(GAME_NAME) {
//////////////////////////////
// GLOBAL IMMUTABLE CONFIGS //
//////////////////////////////
var WIDTH = 1072
var HEIGHT = 742 // IMAX ratio
val RENDER_FPS = getConfigInt("displayfps")
val USE_VSYNC = getConfigBoolean("usevsync")
var VSYNC = USE_VSYNC
val VSYNC_TRIGGER_THRESHOLD = 56
val HALFW: Int
get() = WIDTH.ushr(1)
val HALFH: Int
get() = HEIGHT.ushr(1)
/**
* To be used with physics simulator
*/
val TARGET_FPS = 50
/**
* To be used with render, to achieve smooth frame drawing
* TARGET_INTERNAL_FPS > TARGET_FPS for smooth frame drawing
* Must choose a value so that (1000 / VAL) is still integer
*/
val TARGET_INTERNAL_FPS = 100
/**
* For the events depends on rendering frame (e.g. flicker on post-hit invincibility)
*/
var GLOBAL_RENDER_TIMER = Random().nextInt(1020) + 1
val sysLang: String
get() {
val lan = System.getProperty("user.language")
val country = System.getProperty("user.country")
return lan + country
}
lateinit var appgc: AppGameContainer
var ingame: StateInGame? = null
private val gameConfig = GameConfig()
val OSName = System.getProperty("os.name")
val OSVersion = System.getProperty("os.version")
lateinit var OperationSystem: String // all caps "WINDOWS, "OSX", "LINUX", "SOLARIS", "UNKNOWN"
private set
val isWin81: Boolean
get() = OperationSystem == "WINDOWS" && OSVersion.toDouble() >= 8.1
lateinit var defaultDir: String
private set
lateinit var defaultSaveDir: String
private set
val memInUse: Long
get() = (Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()) shr 20
val memTotal: Long
get() = Runtime.getRuntime().totalMemory() shr 20
val memXmx: Long
get() = Runtime.getRuntime().maxMemory() shr 20
val environment: RunningEnvironment
private val localeSimple = arrayOf("de", "en", "es", "it")
var gameLocale = "lateinit"
set(value) {
if (localeSimple.contains(value.substring(0..1)))
field = value.substring(0..1)
else
field = value
(fontGame as GameFontImpl).reload()
}
private val nullFont = object : Font {
override fun getHeight(str: String?) = 0
override fun drawString(x: Float, y: Float, text: String?) {}
override fun drawString(x: Float, y: Float, text: String?, col: Color?) {}
override fun drawString(x: Float, y: Float, text: String?, col: Color?, startIndex: Int, endIndex: Int) {}
override fun getWidth(str: String?) = 0
override fun getLineHeight() = 0
}
var fontGame: Font = nullFont
private set
var fontSmallNumbers: Font = nullFont
private set
var joypadLabelStart: Char = 0xE000.toChar() // lateinit
var joypadLableSelect: Char = 0xE000.toChar() // lateinit
var joypadLabelNinA: Char = 0xE000.toChar() // lateinit TODO
var joypadLabelNinB: Char = 0xE000.toChar() // lateinit TODO
var joypadLabelNinX: Char = 0xE000.toChar() // lateinit TODO
var joypadLabelNinY: Char = 0xE000.toChar() // lateinit TODO
var joypadLabelNinL: Char = 0xE000.toChar() // lateinit TODO
var joypadLabelNinR: Char = 0xE000.toChar() // lateinit TODO
var joypadLabelNinZL: Char = 0xE000.toChar() // lateinit TODO
var joypadLabelNinZR: Char = 0xE000.toChar() // lateinit TODO
val joypadLabelLEFT = 0xE068.toChar()
val joypadLabelDOWN = 0xE069.toChar()
val joypadLabelUP = 0xE06A.toChar()
val joypadLabelRIGHT = 0xE06B.toChar()
// 0x0 - 0xF: Game-related
// 0x10 - 0x1F: Config
// 0x100 and onward: unit tests for dev
val STATE_ID_SPLASH = 0x0
val STATE_ID_HOME = 0x1
val STATE_ID_GAME = 0x3
val STATE_ID_CONFIG_CALIBRATE = 0x11
val STATE_ID_TEST_FONT = 0x100
val STATE_ID_TEST_GFX = 0x101
val STATE_ID_TEST_TTY = 0x102
val STATE_ID_TEST_BLUR = 0x103
val STATE_ID_TEST_SHADER = 0x104
val STATE_ID_TEST_REFRESHRATE = 0x105
val STATE_ID_TEST_INPUT = 0x106
val STATE_ID_TEST_UI1 = 0x110
val STATE_ID_TOOL_NOISEGEN = 0x200
val STATE_ID_TOOL_RUMBLE_DIAGNOSIS = 0x201
var controller: org.lwjgl.input.Controller? = null
private set
val CONTROLLER_DEADZONE = 0.1f
/** Available CPU threads */
val THREADS = Runtime.getRuntime().availableProcessors()
/**
* If the game is multithreading.
* True if:
*
* THREADS >= 2 and config "multithread" is true
*/
val MULTITHREAD: Boolean
get() = THREADS >= 2 && getConfigBoolean("multithread")
private lateinit var configDir: String
/**
* 0xAA_BB_XXXX
* AA: Major version
* BB: Minor version
* XXXX: Revision (Repository commits)
*
* e.g. 0x02010034 can be translated as 2.1.52
*/
const val VERSION_RAW = 0x0002018E
const val VERSION_STRING: String =
"${VERSION_RAW.ushr(24)}.${VERSION_RAW.and(0xFF0000).ushr(16)}.${VERSION_RAW.and(0xFFFF)}"
const val NAME = "Terrarum"
var delta: Int = 0
// these properties goes into the GameContainer
var previousState: Int? = null // to be used with temporary states like StateMonitorCheck
val systemArch = System.getProperty("os.arch")
private val thirtyTwoBitArchs = arrayOf("i386", "i686", "ppc", "x86", "x86_32") // I know I should Write Once, Run Everywhere; but just in case :)
val is32Bit = thirtyTwoBitArchs.contains(systemArch)
lateinit var textureWhite: Image
lateinit var textureBlack: Image
init {
// just in case
println("[Terrarum] os.arch = $systemArch")
if (is32Bit) {
println("Java is running in 32 Bit")
}
joypadLabelStart = when (getConfigString("joypadlabelstyle")) {
"nwii" -> 0xE04B.toChar() // + mark
"logitech" -> 0xE05A.toChar() // number 10
else -> 0xE042.toChar() // |> mark (sonyps, msxb360, generic)
}
joypadLableSelect = when (getConfigString("joypadlabelstyle")) {
"nwii" -> 0xE04D.toChar() // - mark
"logitech" -> 0xE059.toChar() // number 9
"sonyps" -> 0xE043.toChar() // solid rectangle
"msxb360" -> 0xE041.toChar() // <| mark
else -> 0xE043.toChar() // solid rectangle
}
getDefaultDirectory()
createDirs()
val readFromDisk = readConfigJson()
if (!readFromDisk) readConfigJson()
environment = try {
Controllers.getController(0)
if (getConfigString("pcgamepadenv") == "console")
RunningEnvironment.CONSOLE
else
RunningEnvironment.PC
}
catch (e: IndexOutOfBoundsException) {
RunningEnvironment.PC
}
}
@Throws(SlickException::class)
override fun initStatesList(gc: GameContainer) {
textureWhite = Image("./assets/graphics/background_white.png")
textureBlack = Image("./assets/graphics/background_black.png")
fontGame = GameFontImpl()
fontSmallNumbers = TinyAlphNum()
gc.input.enableKeyRepeat()
// get locale from config
val gameLocaleFromConfig = gameConfig.getAsString("language") ?: sysLang
// if bad game locale were set, use system locale
if (gameLocaleFromConfig.length < 2)
gameLocale = sysLang
else
gameLocale = gameLocaleFromConfig
println("[Terrarum] Locale: " + gameLocale)
// search for real controller
// exclude controllers with name "Mouse", "keyboard"
val notControllerRegex = Regex("mouse|keyboard")
try {
// gc.input.controllerCount is unreliable
for (i in 0..255) {
val controllerInQuo = Controllers.getController(i)
println("Controller $i: ${controllerInQuo.name}")
// check the name
if (!controllerInQuo.name.toLowerCase().contains(notControllerRegex)) {
controller = controllerInQuo
println("Controller $i selected: ${controller!!.name}")
break
}
}
// test acquired controller
controller!!.getAxisValue(0)
}
catch (controllerDoesNotHaveAnyAxesException: java.lang.ArrayIndexOutOfBoundsException) {
controller = null
}
if (controller != null) {
for (c in 0..controller!!.axisCount - 1) {
controller!!.setDeadZone(c, CONTROLLER_DEADZONE)
}
}
// load languages
Lang
// load modules
ModMgr
gc.graphics.clear() // clean up any 'dust' in the buffer
//addState(StateVTTest())
//addState(StateGraphicComputerTest())
//addState(StateTestingLightning())
//addState(StateSplash())
//addState(StateMonitorCheck())
//addState(StateFontTester())
//addState(StateNoiseTexGen())
//addState(StateBlurTest())
//addState(StateShaderTest())
//addState(StateNoiseTester())
//addState(StateUITest())
//addState(StateControllerRumbleTest())
//addState(StateMidiInputTest())
//addState(StateNewRunesTest())
//addState(StateStutterTest())
ingame = StateInGame(); addState(ingame)
// foolproof
if (stateCount < 1) {
throw Error("Please add or un-comment addState statements")
}
}
private fun getDefaultDirectory() {
val OS = System.getProperty("os.name").toUpperCase()
if (OS.contains("WIN")) {
OperationSystem = "WINDOWS"
defaultDir = System.getenv("APPDATA") + "/Terrarum"
}
else if (OS.contains("OS X")) {
OperationSystem = "OSX"
defaultDir = System.getProperty("user.home") + "/Library/Application Support/Terrarum"
}
else if (OS.contains("NUX") || OS.contains("NIX")) {
OperationSystem = "LINUX"
defaultDir = System.getProperty("user.home") + "/.Terrarum"
}
else if (OS.contains("SUNOS")) {
OperationSystem = "SOLARIS"
defaultDir = System.getProperty("user.home") + "/.Terrarum"
}
else {
OperationSystem = "UNKNOWN"
defaultDir = System.getProperty("user.home") + "/.Terrarum"
}
defaultSaveDir = defaultDir + "/Saves"
configDir = defaultDir + "/config.json"
println("[Terrarum] os.name = $OSName")
println("[Terrarum] os.version = $OSVersion")
}
private fun createDirs() {
val dirs = arrayOf(File(defaultSaveDir))
dirs.forEach { if (!it.exists()) it.mkdirs() }
}
@Throws(IOException::class)
private fun createConfigJson() {
val configFile = File(configDir)
if (!configFile.exists() || configFile.length() == 0L) {
JsonWriter.writeToFile(DefaultConfig.fetch(), configDir)
}
}
private fun readConfigJson(): Boolean {
try {
// read from disk and build config from it
val jsonObject = JsonFetcher(configDir)
// make config
jsonObject.entrySet().forEach { entry -> gameConfig[entry.key] = entry.value }
return true
}
catch (e: IOException) {
// write default config to game dir. Call this method again to read config from it.
try {
createConfigJson()
}
catch (e1: IOException) {
e.printStackTrace()
}
return false
}
}
/**
* Return config from config set. If the config does not exist, default value will be returned.
* @param key
* *
* @return Config from config set or default config if it does not exist.
* *
* @throws NullPointerException if the specified config simply does not exist.
*/
fun getConfigInt(key: String): Int {
val cfg = getConfigMaster(key)
if (cfg is JsonPrimitive)
return cfg.asInt
else
return cfg as Int
}
/**
* Return config from config set. If the config does not exist, default value will be returned.
* @param key
* *
* @return Config from config set or default config if it does not exist.
* *
* @throws NullPointerException if the specified config simply does not exist.
*/
fun getConfigString(key: String): String {
val cfg = getConfigMaster(key)
if (cfg is JsonPrimitive)
return cfg.asString
else
return cfg as String
}
/**
* Return config from config set. If the config does not exist, default value will be returned.
* @param key
* *
* @return Config from config set or default config if it does not exist.
* *
* @throws NullPointerException if the specified config simply does not exist.
*/
fun getConfigBoolean(key: String): Boolean {
val cfg = getConfigMaster(key)
if (cfg is JsonPrimitive)
return cfg.asBoolean
else
return cfg as Boolean
}
fun getConfigIntArray(key: String): IntArray {
val cfg = getConfigMaster(key)
if (cfg is JsonArray) {
val jsonArray = cfg.asJsonArray
return IntArray(jsonArray.size(), { i -> jsonArray[i].asInt })
}
else
return cfg as IntArray
}
private fun getConfigMaster(key: String): Any {
var cfg: Any? = null
try {
cfg = gameConfig[key.toLowerCase()]!!
}
catch (e: NullPointerException) {
try {
cfg = DefaultConfig.fetch()[key.toLowerCase()]
}
catch (e1: NullPointerException) {
e.printStackTrace()
}
}
return cfg!!
}
fun setConfig(key: String, value: Any) {
gameConfig[key] = value
}
val currentSaveDir: File
get() {
val file = File(defaultSaveDir + "/test")
// failsafe?
if (!file.exists()) file.mkdir()
return file // TODO TEST CODE
}
// for external scripts (e.g. Groovy)
@JvmStatic fun getMouseTileX(): Int = appgc.mouseTileX
@JvmStatic fun getMouseTileY(): Int = appgc.mouseTileY
}
fun main(args: Array<String>) {
System.setProperty("java.library.path", "lib")
System.setProperty("org.lwjgl.librarypath", File("lib").absolutePath)
try {
Terrarum.appgc = AppGameContainer(Terrarum)
Terrarum.appgc.setDisplayMode(Terrarum.WIDTH, Terrarum.HEIGHT, false)
if (Terrarum.RENDER_FPS > 0) {
Terrarum.appgc.setTargetFrameRate(Terrarum.RENDER_FPS)
}
//Terrarum.appgc.setVSync(Terrarum.VSYNC)
Terrarum.appgc.setMaximumLogicUpdateInterval(1000 / Terrarum.TARGET_INTERNAL_FPS) // 10 ms
Terrarum.appgc.setMinimumLogicUpdateInterval(1000 / Terrarum.TARGET_INTERNAL_FPS - 1) // 9 ms
Terrarum.appgc.setMultiSample(0)
Terrarum.appgc.setShowFPS(false)
// game will run normally even if it is not focused
Terrarum.appgc.setUpdateOnlyWhenVisible(false)
Terrarum.appgc.alwaysRender = true
Terrarum.appgc.start()
}
catch (ex: Exception) {
val logger = Logger.getLogger(Terrarum::class.java.name)
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH-mm-ss")
val calendar = Calendar.getInstance()
val filepath = "${Terrarum.defaultDir}/crashlog-${dateFormat.format(calendar.time)}.txt"
val fileHandler = FileHandler(filepath)
logger.addHandler(fileHandler)
val formatter = SimpleFormatter()
fileHandler.formatter = formatter
//logger.info()
println("The game has crashed!")
println("Crash log were saved to $filepath.")
println("================================================================================")
logger.log(Level.SEVERE, null, ex)
}
}
///////////////////////////////////
// customised blending functions //
///////////////////////////////////
fun blendMul() {
// I must say: What the fuck is wrong with you, Slick2D? Your built-it blending is just fucking wrong.
GL11.glEnable(GL11.GL_BLEND)
GL11.glColorMask(true, true, true, true)
GL11.glBlendFunc(GL11.GL_DST_COLOR, GL11.GL_ONE_MINUS_SRC_ALPHA)
}
fun blendNormal() {
GL11.glEnable(GL11.GL_BLEND)
GL11.glColorMask(true, true, true, true)
//GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA)
// semitransparent textures working as intended with this,
// but needs further investigation in the case of:
// TODO test blend in the situation of semitransparent over semitransparent
GL14.glBlendFuncSeparate(
GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA, // blend func for RGB channels
GL11.GL_ONE, GL11.GL_ONE_MINUS_SRC_ALPHA // blend func for alpha channels
)
}
fun blendLightenOnly() {
GL11.glEnable(GL11.GL_BLEND)
GL11.glColorMask(true, true, true, false)
GL11.glBlendFunc(GL11.GL_ONE, GL11.GL_ONE)
GL14.glBlendEquation(GL14.GL_MAX)
}
fun blendAlphaMap() {
GL11.glDisable(GL11.GL_BLEND)
GL11.glColorMask(false, false, false, true)
}
fun blendScreen() {
GL11.glEnable(GL11.GL_BLEND)
GL11.glColorMask(true, true, true, true)
GL11.glBlendFunc(GL11.GL_ONE, GL11.GL_ONE_MINUS_SRC_COLOR)
}
fun blendDisable() {
GL11.glDisable(GL11.GL_BLEND)
}
object BlendMode {
const val SCREEN = "GL_BLEND screen"
const val MULTIPLY = "GL_BLEND multiply"
const val NORMAL = "GL_BLEND normal"
const val MAX = "GL_MAX"
fun resolve(mode: String) {
when (mode) {
SCREEN -> blendScreen()
MULTIPLY -> blendMul()
NORMAL -> blendNormal()
MAX -> blendLightenOnly()
else -> throw Error("Unknown blend mode: $mode")
}
}
}
enum class RunningEnvironment {
PC, CONSOLE, MOBILE
}
/** @return Intarray(R, G, B, A) */
fun Texture.getPixel(x: Int, y: Int): IntArray {
val textureWidth = this.textureWidth
val hasAlpha = this.hasAlpha()
val offset = (if (hasAlpha) 4 else 3) * (textureWidth * y + x) // 4: # of channels (RGBA)
if (ByteOrder.nativeOrder() == ByteOrder.LITTLE_ENDIAN) {
return intArrayOf(
this.textureData[offset].toUint(),
this.textureData[offset + 1].toUint(),
this.textureData[offset + 2].toUint(),
if (hasAlpha)
this.textureData[offset + 3].toUint()
else 255
)
}
else {
return intArrayOf(
this.textureData[offset + 2].toUint(),
this.textureData[offset + 1].toUint(),
this.textureData[offset].toUint(),
if (hasAlpha)
this.textureData[offset + 3].toUint()
else 255
)
}
}
/** @return Intarray(R, G, B, A) */
fun Image.getPixel(x: Int, y: Int) = this.texture.getPixel(x, y)
fun Color.toInt() = redByte.shl(16) or greenByte.shl(8) or blueByte
fun Color.to10bit() = redByte.shl(20) or greenByte.shl(10) or blueByte
infix fun Color.screen(other: Color) = Color(
1f - (1f - this.r) * (1f - other.r),
1f - (1f - this.g) * (1f - other.g),
1f - (1f - this.b) * (1f - other.b),
1f - (1f - this.a) * (1f - other.a)
)
infix fun Color.mul(other: Color) = Color( // don't turn into an operator!
this.r * other.r,
this.g * other.g,
this.b * other.b,
this.a * other.a
)
infix fun Color.minus(other: Color) = Color( // don't turn into an operator!
this.r - other.r,
this.g - other.g,
this.b - other.b,
this.a - other.a
)
fun Int.toHex() = this.toLong().and(0xFFFFFFFF).toString(16).padStart(8, '0').toUpperCase()
fun Long.toHex() = {
val sb = StringBuilder()
(0..16).forEach {
}
}