mirror of
https://github.com/curioustorvald/Terrarum.git
synced 2026-03-07 12:21:52 +09:00
1904 lines
74 KiB
Kotlin
1904 lines
74 KiB
Kotlin
package net.torvald.terrarum.modulebasegame
|
|
|
|
import com.badlogic.gdx.Gdx
|
|
import com.badlogic.gdx.Input
|
|
import com.badlogic.gdx.graphics.Color
|
|
import com.badlogic.gdx.graphics.OrthographicCamera
|
|
import com.badlogic.gdx.graphics.g2d.SpriteBatch
|
|
import com.badlogic.gdx.graphics.glutils.ShapeRenderer
|
|
import net.torvald.terrarum.*
|
|
import net.torvald.terrarum.App.*
|
|
import net.torvald.terrarum.Terrarum.getPlayerSaveFiledesc
|
|
import net.torvald.terrarum.Terrarum.getWorldSaveFiledesc
|
|
import net.torvald.terrarum.TerrarumAppConfiguration.TILE_SIZE
|
|
import net.torvald.terrarum.TerrarumAppConfiguration.TILE_SIZED
|
|
import net.torvald.terrarum.blockproperties.BlockPropUtil
|
|
import net.torvald.terrarum.blockstats.MinimapComposer
|
|
import net.torvald.terrarum.blockstats.TileSurvey
|
|
import net.torvald.terrarum.concurrent.ThreadExecutor
|
|
import net.torvald.terrarum.console.AVTracker
|
|
import net.torvald.terrarum.console.ActorsList
|
|
import net.torvald.terrarum.console.Authenticator
|
|
import net.torvald.terrarum.gameactors.*
|
|
import net.torvald.terrarum.gamecontroller.IngameController
|
|
import net.torvald.terrarum.gamecontroller.KeyToggler
|
|
import net.torvald.terrarum.gamecontroller.TerrarumKeyboardEvent
|
|
import net.torvald.terrarum.gameitems.FixtureInteractionBlocked
|
|
import net.torvald.terrarum.gameitems.GameItem
|
|
import net.torvald.terrarum.gameitems.mouseInInteractableRange
|
|
import net.torvald.terrarum.gameparticles.ParticleBase
|
|
import net.torvald.terrarum.gameworld.GameWorld
|
|
import net.torvald.terrarum.gameworld.fmod
|
|
import net.torvald.terrarum.langpack.Lang
|
|
import net.torvald.terrarum.modulebasegame.gameactors.*
|
|
import net.torvald.terrarum.modulebasegame.gameactors.physicssolver.CollisionSolver
|
|
import net.torvald.terrarum.modulebasegame.gameitems.AxeCore
|
|
import net.torvald.terrarum.modulebasegame.gameitems.PickaxeCore
|
|
import net.torvald.terrarum.modulebasegame.gameworld.GameEconomy
|
|
import net.torvald.terrarum.modulebasegame.serialise.LoadSavegame
|
|
import net.torvald.terrarum.modulebasegame.serialise.ReadActor
|
|
import net.torvald.terrarum.modulebasegame.serialise.WriteSavegame
|
|
import net.torvald.terrarum.modulebasegame.ui.*
|
|
import net.torvald.terrarum.modulebasegame.ui.UIInventoryFull.Companion.gradEndCol
|
|
import net.torvald.terrarum.modulebasegame.worldgenerator.RoguelikeRandomiser
|
|
import net.torvald.terrarum.modulebasegame.worldgenerator.Worldgen
|
|
import net.torvald.terrarum.modulebasegame.worldgenerator.WorldgenParams
|
|
import net.torvald.terrarum.realestate.LandUtil
|
|
import net.torvald.terrarum.realestate.LandUtil.CHUNK_H
|
|
import net.torvald.terrarum.realestate.LandUtil.CHUNK_W
|
|
import net.torvald.terrarum.savegame.VDUtil
|
|
import net.torvald.terrarum.savegame.VirtualDisk
|
|
import net.torvald.terrarum.serialise.Common
|
|
import net.torvald.terrarum.ui.BasicDebugInfoWindow.Companion.toIntAndFrac
|
|
import net.torvald.terrarum.ui.Toolkit
|
|
import net.torvald.terrarum.ui.Toolkit.hdrawWidth
|
|
import net.torvald.terrarum.ui.UIAutosaveNotifier
|
|
import net.torvald.terrarum.ui.UICanvas
|
|
import net.torvald.terrarum.weather.WeatherMixer
|
|
import net.torvald.terrarum.worlddrawer.BlocksDrawer
|
|
import net.torvald.terrarum.worlddrawer.FeaturesDrawer
|
|
import net.torvald.terrarum.worlddrawer.WorldCamera
|
|
import net.torvald.unicode.EMDASH
|
|
import net.torvald.util.CircularArray
|
|
import org.khelekore.prtree.PRTree
|
|
import java.io.File
|
|
import java.util.*
|
|
import java.util.logging.Level
|
|
import kotlin.experimental.and
|
|
import kotlin.math.absoluteValue
|
|
import kotlin.math.min
|
|
import kotlin.math.pow
|
|
import kotlin.math.roundToInt
|
|
|
|
|
|
/**
|
|
* Ingame instance for the game Terrarum.
|
|
*
|
|
* Created by minjaesong on 2017-06-16.
|
|
*/
|
|
|
|
open class TerrarumIngame(batch: FlippingSpriteBatch) : IngameInstance(batch) {
|
|
|
|
var historicalFigureIDBucket: ArrayList<Int> = ArrayList<Int>()
|
|
|
|
|
|
/**
|
|
* list of Actors that is sorted by Actors' referenceID
|
|
*/
|
|
//val ACTORCONTAINER_INITIAL_SIZE = 64
|
|
val PARTICLES_MAX = App.getConfigInt("maxparticles")
|
|
val particlesContainer = CircularArray<ParticleBase>(PARTICLES_MAX, true)
|
|
|
|
// these are required because actors always change their position
|
|
private var visibleActorsRenderFarBehind: ArrayList<ActorWithBody> = ArrayList(1)
|
|
private var visibleActorsRenderBehind: ArrayList<ActorWithBody> = ArrayList(1)
|
|
private var visibleActorsRenderMiddle: ArrayList<ActorWithBody> = ArrayList(1)
|
|
private var visibleActorsRenderMidTop: ArrayList<ActorWithBody> = ArrayList(1)
|
|
private var visibleActorsRenderFront: ArrayList<ActorWithBody> = ArrayList(1)
|
|
private var visibleActorsRenderOverlay: ArrayList<ActorWithBody> = ArrayList(1)
|
|
|
|
//var screenZoom = 1.0f // definition moved to IngameInstance
|
|
//val ZOOM_MAXIMUM = 4.0f // definition moved to IngameInstance
|
|
//val ZOOM_MINIMUM = 0.5f // definition moved to IngameInstance
|
|
|
|
companion object {
|
|
/** Sets camera position so that (0,0) would be top-left of the screen, (width, height) be bottom-right. */
|
|
fun setCameraPosition(batch: SpriteBatch, camera: OrthographicCamera, newX: Float, newY: Float) {
|
|
camera.position.set((-newX + App.scr.halfw).roundToFloat(), (-newY + App.scr.halfh).roundToFloat(), 0f)
|
|
camera.update()
|
|
batch.projectionMatrix = camera.combined
|
|
}
|
|
fun setCameraPosition(batch: SpriteBatch, shape: ShapeRenderer, camera: OrthographicCamera, newX: Float, newY: Float) {
|
|
camera.setToOrtho(true, App.scr.wf, App.scr.hf)
|
|
camera.update()
|
|
camera.position.set((-newX + App.scr.halfw).roundToFloat(), (-newY + App.scr.halfh).roundToFloat(), 0f)
|
|
camera.update()
|
|
shape.projectionMatrix = camera.combined
|
|
|
|
camera.setToOrtho(true, App.scr.wf, App.scr.hf)
|
|
camera.update()
|
|
camera.position.set((-newX + App.scr.halfw).roundToFloat(), (-newY + App.scr.halfh).roundToFloat(), 0f)
|
|
camera.update()
|
|
batch.projectionMatrix = camera.combined
|
|
}
|
|
fun getCanonicalTitle() = App.GAME_NAME +
|
|
" $EMDASH F: ${Gdx.graphics.framesPerSecond}" +
|
|
if (App.IS_DEVELOPMENT_BUILD)
|
|
" (ΔF${Terrarum.updateRateStr})" +
|
|
" $EMDASH M: H${Terrarum.memJavaHeap}M / X${Terrarum.memXmx}M / U${Terrarum.memUnsafe}M"
|
|
else
|
|
""
|
|
|
|
val ACTOR_UPDATE_RANGE = 4096
|
|
|
|
fun distToActorSqr(world: GameWorld, a: ActorWithBody, p: ActorWithBody) =
|
|
min(// take min of normal position and wrapped (x < 0) position
|
|
min((a.hitbox.centeredX - p.hitbox.centeredX).sqr() +
|
|
(a.hitbox.centeredY - p.hitbox.centeredY).sqr(),
|
|
((a.hitbox.centeredX + world.width * TILE_SIZE) - p.hitbox.centeredX).sqr() +
|
|
(a.hitbox.centeredY - p.hitbox.centeredY).sqr()),
|
|
((a.hitbox.centeredX - world.width * TILE_SIZE) - p.hitbox.centeredX).sqr() +
|
|
(a.hitbox.centeredY - p.hitbox.centeredY).sqr()
|
|
)
|
|
fun distToCameraSqr(world: GameWorld, a: ActorWithBody) =
|
|
min(
|
|
min((a.hitbox.centeredX - WorldCamera.xCentre).sqr() +
|
|
(a.hitbox.centeredY - WorldCamera.yCentre).sqr(),
|
|
((a.hitbox.centeredX + world.width * TILE_SIZE) - WorldCamera.xCentre).sqr() +
|
|
(a.hitbox.centeredY - WorldCamera.yCentre).sqr()),
|
|
((a.hitbox.centeredX - world.width * TILE_SIZE) - WorldCamera.xCentre).sqr() +
|
|
(a.hitbox.centeredY - WorldCamera.yCentre).sqr()
|
|
)
|
|
|
|
/**
|
|
* Returns the minimum squared distance from the actor to any living WorldUpdater.
|
|
* Falls back to camera distance if no WorldUpdaters exist.
|
|
*/
|
|
fun distToNearestUpdaterSqr(world: GameWorld, actor: ActorWithBody): Double {
|
|
val worldUpdaters = Terrarum.ingame?.worldUpdaters
|
|
if (worldUpdaters.isNullOrEmpty()) {
|
|
return distToCameraSqr(world, actor)
|
|
}
|
|
return worldUpdaters.minOf { updater ->
|
|
if (updater.flagDespawn || updater.despawned) Double.MAX_VALUE
|
|
else distToActorSqr(world, actor, updater)
|
|
}
|
|
}
|
|
|
|
/** whether the actor is within update range of any WorldUpdater */
|
|
fun ActorWithBody.inUpdateRange(world: GameWorld) = distToNearestUpdaterSqr(world, this) <= ACTOR_UPDATE_RANGE.sqr()
|
|
|
|
/** whether the actor is within screen */
|
|
fun ActorWithBody.inScreen(world: GameWorld) =
|
|
|
|
// y
|
|
this.hitbox.endY >= WorldCamera.y && this.hitbox.startY <= WorldCamera.yEnd
|
|
|
|
&&
|
|
|
|
// x: camera is on the right side of the seam
|
|
((this.hitbox.endX - world.width >= WorldCamera.x && this.hitbox.startX - world.width <= WorldCamera.xEnd) ||
|
|
// x: camera in on the left side of the seam
|
|
(this.hitbox.endX + world.width >= WorldCamera.x && this.hitbox.startX + world.width <= WorldCamera.xEnd) ||
|
|
// x: neither
|
|
(this.hitbox.endX >= WorldCamera.x && this.hitbox.startX <= WorldCamera.xEnd))
|
|
|
|
val SIZE_SMALL = Point2i(CHUNK_W*66, CHUNK_H*60)
|
|
val SIZE_NORMAL = Point2i(CHUNK_W*100, CHUNK_H*60)
|
|
val SIZE_LARGE = Point2i(CHUNK_W*150, CHUNK_H*60)
|
|
val SIZE_HUGE = Point2i(CHUNK_W*250, CHUNK_H*60)
|
|
val SIZE_TEST = Point2i(CHUNK_W*11, CHUNK_H*7)
|
|
val NEW_WORLD_SIZE =
|
|
if (App.IS_DEVELOPMENT_BUILD)
|
|
arrayOf(SIZE_SMALL, SIZE_NORMAL, SIZE_LARGE, SIZE_HUGE, SIZE_TEST)
|
|
else
|
|
arrayOf(SIZE_SMALL, SIZE_NORMAL, SIZE_LARGE, SIZE_HUGE)
|
|
val WORLDPORTAL_NEW_WORLD_SIZE =
|
|
if (App.IS_DEVELOPMENT_BUILD)
|
|
arrayOf(SIZE_SMALL, SIZE_NORMAL, SIZE_LARGE, SIZE_HUGE, SIZE_TEST)
|
|
else
|
|
arrayOf(SIZE_SMALL, SIZE_NORMAL, SIZE_LARGE, SIZE_HUGE)
|
|
|
|
val worldgenThreadExecutor = ThreadExecutor()
|
|
}
|
|
|
|
|
|
|
|
init {
|
|
particlesContainer.overwritingPolicy = {
|
|
it.dispose()
|
|
}
|
|
|
|
gameUpdateGovernor = ConsistentUpdateRate
|
|
}
|
|
|
|
|
|
lateinit var uiBlur: UIFakeBlurOverlay
|
|
lateinit var uiPieMenu: UIQuickslotPie
|
|
lateinit var uiQuickBar: UIQuickslotBar
|
|
lateinit var uiInventoryPlayer: UIInventoryFull
|
|
|
|
internal val uiInventoryPlayerReady: Boolean get() = ::uiInventoryPlayer.isInitialized // this is some ugly hack but I didn't want to make uiInventoryPlayer nullable so bite me
|
|
|
|
/**
|
|
* This is a dedicated property for the fixtures' UI.
|
|
*
|
|
* When it's not null, the UI will be updated and rendered;
|
|
* when the UI is closed, it'll be replaced with a null value.
|
|
*
|
|
* This will not allow multiple fixture UIs from popping up (does not prevent them actually being open)
|
|
* because UI updating and rendering is whitelist-operated
|
|
*/
|
|
private var uiFixture: UICanvas? = null
|
|
set(value) {
|
|
printdbg(this, "uiFixture change: $uiFixture -> $value")
|
|
field?.setAsClose()
|
|
value?.let { uiFixturesHistory.add(it) }
|
|
field = value
|
|
}
|
|
|
|
var wearableDeviceUI: UICanvas? = null
|
|
set(value) {
|
|
field?.setAsClose()
|
|
value?.setAsOpen()
|
|
value?.setPosition(App.scr.tvSafeGraphicsWidth/2, App.scr.tvSafeActionHeight/2)
|
|
field = value
|
|
}
|
|
|
|
val getUIFixture = object : Id_UICanvasNullable { // quick workaround for the type erasure (you can't use lambda...)
|
|
override fun get(): UICanvas? {
|
|
return uiFixture
|
|
}
|
|
}
|
|
val getWearableDeviceUI = object : Id_UICanvasNullable { // quick workaround for the type erasure (you can't use lambda...)
|
|
override fun get(): UICanvas? {
|
|
return wearableDeviceUI
|
|
}
|
|
}
|
|
|
|
lateinit var uiVitalPrimary: UICanvas
|
|
lateinit var uiVitalSecondary: UICanvas
|
|
lateinit var uiVitalItem: UICanvas // itemcount/durability of held block or active ammo of held gun. As for the block, max value is 500.
|
|
|
|
private val uiFixturesHistory = HashSet<UICanvas>()
|
|
|
|
private lateinit var uiBasicInfo: UICanvas
|
|
private lateinit var uiWatchTierOne: UICanvas
|
|
lateinit var uiAutosaveNotifier: UIAutosaveNotifier
|
|
lateinit var uiCheatDetected: UICheatDetected
|
|
|
|
|
|
var particlesActive = 0
|
|
private set
|
|
|
|
var selectedWireRenderClass = ""
|
|
private var oldSelectedWireRenderClass = ""
|
|
|
|
|
|
|
|
private lateinit var ingameUpdateThread: ThreadIngameUpdate
|
|
private lateinit var updateThreadWrapper: Thread
|
|
//private val ingameDrawThread: ThreadIngameDraw // draw must be on the main thread
|
|
|
|
|
|
override var gameInitialised = false
|
|
internal set
|
|
override var gameFullyLoaded = false
|
|
internal set
|
|
|
|
override val musicStreamer = TerrarumMusicAndAmbientStreamer()
|
|
|
|
|
|
//////////////
|
|
// GDX code //
|
|
//////////////
|
|
|
|
|
|
lateinit var gameLoadMode: GameLoadMode
|
|
lateinit var gameLoadInfoPayload: Any
|
|
|
|
|
|
enum class GameLoadMode {
|
|
CREATE_NEW, LOAD_FROM
|
|
}
|
|
|
|
private val soundReflectiveMaterials = hashSetOf(
|
|
""
|
|
)
|
|
|
|
override fun show() {
|
|
//initViewPort(AppLoader.terrarumAppConfig.screenW, AppLoader.terrarumAppConfig.screenH)
|
|
|
|
// gameLoadMode and gameLoadInfoPayload must be set beforehand!!
|
|
|
|
when (gameLoadMode) {
|
|
GameLoadMode.CREATE_NEW -> enterCreateNewWorld(gameLoadInfoPayload as NewGameParams)
|
|
GameLoadMode.LOAD_FROM -> enterLoadFromSave(gameLoadInfoPayload as Codices)
|
|
}
|
|
|
|
IngameRenderer.setRenderedWorld(world)
|
|
blockMarkingActor.isVisible = true
|
|
|
|
App.audioMixer.reset()
|
|
|
|
super.show() // this function sets gameInitialised = true
|
|
|
|
TileSurvey.submitProposal(
|
|
TileSurvey.SurveyProposal(
|
|
"basegame.Ingame.audioReflection", 73, 73, 2, 4
|
|
) { world, x, y ->
|
|
val tileProp = BlockCodex[world.getTileFromTerrain(x, y)]
|
|
val wallProp = BlockCodex[world.getTileFromWall(x, y)]
|
|
val prop = if (tileProp.isSolid && !tileProp.isActorBlock) tileProp else wallProp
|
|
MaterialCodex[prop.material].sondrefl
|
|
}
|
|
)
|
|
|
|
TileSurvey.submitProposal(
|
|
TileSurvey.SurveyProposal(
|
|
"basegame.Ingame.openness", 73, 73, 2, 4
|
|
) { world, x, y ->
|
|
val tileProp = BlockCodex[world.getTileFromTerrain(x, y)]
|
|
val wallProp = BlockCodex[world.getTileFromWall(x, y)]
|
|
(!tileProp.isSolid && !wallProp.isSolid).toInt().toFloat()
|
|
}
|
|
)
|
|
|
|
loadedTime_t = App.getTIME_T()
|
|
|
|
MusicService.enterScene("ingame")
|
|
}
|
|
|
|
data class NewGameParams(
|
|
val player: IngamePlayer,
|
|
val newWorldParams: NewWorldParameters,
|
|
val callbackAfterLoad: (TerrarumIngame) -> Unit
|
|
|
|
)
|
|
|
|
data class NewWorldParameters(
|
|
val width: Int,
|
|
val height: Int,
|
|
val worldGenSeed: Long,
|
|
val savegameName: String
|
|
// other worldgen options
|
|
) {
|
|
init {
|
|
if (width % LandUtil.CHUNK_W != 0 || height % LandUtil.CHUNK_H != 0) {
|
|
throw IllegalArgumentException("World size is not a multiple of chunk size; World size: ($width, $height), Chunk size: (${LandUtil.CHUNK_W}, ${LandUtil.CHUNK_H})")
|
|
}
|
|
}
|
|
}
|
|
|
|
data class Codices(
|
|
val disk: VirtualDisk, // WORLD disk
|
|
val world: GameWorld,
|
|
// val meta: WriteMeta.WorldMeta,
|
|
// val block: BlockCodex,
|
|
// val item: ItemCodex,
|
|
// val wire: WireCodex,
|
|
// val material: MaterialCodex,
|
|
// val faction: FactionCodex,
|
|
// val apocryphas: Map<String, Any>,
|
|
val actors: List<ActorID>,
|
|
val player: IngamePlayer,
|
|
val worldGenver: Long,
|
|
val playerGenver: Long,
|
|
val callbackAfterLoad: (TerrarumIngame) -> Unit
|
|
)
|
|
|
|
private var loadCallback: ((TerrarumIngame) -> Unit)? = null
|
|
|
|
/**
|
|
* Init instance by loading saved world
|
|
*/
|
|
private fun enterLoadFromSave(codices: Codices) {
|
|
if (gameInitialised) {
|
|
printdbg(this, "loaded successfully.")
|
|
}
|
|
else {
|
|
printdbg(this, "Ingame setting things up from the savegame")
|
|
|
|
RoguelikeRandomiser.loadFromSave(this, codices.world.randSeeds[0], codices.world.randSeeds[1])
|
|
WeatherMixer.loadFromSave(this, codices.world.randSeeds[2], codices.world.randSeeds[3])
|
|
|
|
// Terrarum.itemCodex.loadFromSave(codices.item)
|
|
// Terrarum.apocryphas = HashMap(codices.apocryphas)
|
|
|
|
|
|
// feed info to the worldgen
|
|
Worldgen.attachMap(world, WorldgenParams.getParamsByVersion(codices.worldGenver, world.generatorSeed))
|
|
}
|
|
|
|
loadCallback = codices.callbackAfterLoad
|
|
worldGenVer = codices.worldGenver
|
|
}
|
|
|
|
/** Load rest of the game with GL context */
|
|
private fun postInitForLoadFromSave(codices: Codices) {
|
|
codices.actors.forEach {
|
|
try {
|
|
val actor = ReadActor(codices.disk, LoadSavegame.getFileReader(codices.disk, it.toLong()))
|
|
if (actor !is IngamePlayer) { // actor list should not contain IngamePlayers (see WriteWorld.preWrite) but just in case...
|
|
forceAddActor(actor)
|
|
}
|
|
}
|
|
catch (e: NullPointerException) {
|
|
System.err.println("Could not read the actor ${it} from the disk")
|
|
e.printStackTrace()
|
|
}
|
|
}
|
|
|
|
printdbg(this, "Player localhash: ${codices.player.localHashStr}, hasSprite: ${codices.player.sprite != null}")
|
|
|
|
// assign new random referenceID for player
|
|
codices.player.referenceID = Terrarum.generateUniqueReferenceID()
|
|
forceAddActor(codices.player)
|
|
|
|
|
|
// overwrite player's props with world's for multiplayer
|
|
// see comments on IngamePlayer.unauthorisedPlayerProps to know why this is necessary.
|
|
codices.player.backupPlayerProps(isMultiplayer) // backup first!
|
|
printdbg(this, "postInitForLoadFromSave")
|
|
printdbg(this, "Player UUID: ${codices.player.uuid}")
|
|
printdbg(this, world.playersLastStatus.keys)
|
|
printdbg(this, world.playersLastStatus[codices.player.uuid])
|
|
world.playersLastStatus[codices.player.uuid].let { // regardless of the null-ness, we still keep the backup, which WriteActor looks for it
|
|
// if the world has some saved values, use them
|
|
if (it != null) {
|
|
|
|
printdbg(this, "Found LastStatus mapping for Player ${codices.player.uuid}")
|
|
printdbg(this, "Changing XY Position ${codices.player.hitbox.canonVec} -> ${it.physics.position}")
|
|
|
|
codices.player.setPosition(it.physics.position)
|
|
if (isMultiplayer) {
|
|
printdbg(this, "Using world's ActorValue instead of player's")
|
|
codices.player.actorValue = it.actorValue!!
|
|
printdbg(this, "Using world's Inventory instead of player's")
|
|
codices.player.inventory = it.inventory!!
|
|
}
|
|
}
|
|
// if not, move player to the spawn point
|
|
else {
|
|
printdbg(this, "No mapping found")
|
|
printdbg(this, "Changing XY Position ${codices.player.hitbox.canonVec} -> (${world.spawnX * TILE_SIZED}, ${world.spawnY * TILE_SIZED})")
|
|
|
|
codices.player.setPosition((world.portalPoint ?: world.spawnPoint).toVector() * TILE_SIZED)
|
|
}
|
|
}
|
|
|
|
|
|
// try to unstuck the repositioned player
|
|
codices.player.tryUnstuck()
|
|
|
|
|
|
|
|
// by doing this, whatever the "possession" the player had will be broken by the game load
|
|
actorNowPlaying = codices.player
|
|
actorGamer = codices.player
|
|
|
|
SavegameMigrator.invoke(codices.worldGenver, codices.playerGenver, actorContainerActive)
|
|
|
|
printdbg(this, "postInitForLoadFromSave exit")
|
|
}
|
|
|
|
private val autosaveOnErrorAction = { e: Throwable -> uiAutosaveNotifier.setAsError() }
|
|
|
|
|
|
private fun postInitForNewGame() {
|
|
worldSavefileName = LoadSavegame.getWorldSavefileName(world)
|
|
playerSavefileName = LoadSavegame.getPlayerSavefileName(actorGamer)
|
|
|
|
worldDisk = VDUtil.createNewDisk(
|
|
1L shl 60,
|
|
worldName,
|
|
Common.CHARSET
|
|
)
|
|
|
|
playerDisk = VDUtil.readDiskArchive(App.savegamePlayers[actorGamer.uuid]!!.loadable().diskFile, Level.INFO)
|
|
|
|
// go to spawn position
|
|
printdbg(this, "World Spawn position: (${world.spawnX}, ${world.spawnY})")
|
|
actorGamer.setPosition(world.spawnPoint.toVector() * TILE_SIZED)
|
|
actorGamer.backupPlayerProps(isMultiplayer)
|
|
|
|
// make initial savefile
|
|
// we're not writing multiple files at one go because:
|
|
// 1. lighten the IO burden
|
|
// 2. cannot sync up the "counter" to determine whether both are finished
|
|
uiAutosaveNotifier.setAsOpen()
|
|
val saveTime_t = App.getTIME_T()
|
|
printdbg(this, "Immediate Save")
|
|
WriteSavegame.immediate(saveTime_t, WriteSavegame.SaveMode.PLAYER, playerDisk, getPlayerSaveFiledesc(playerSavefileName), this, true, autosaveOnErrorAction) {
|
|
printdbg(this, "immediate save callback from PLAYER")
|
|
|
|
makeSavegameBackupCopy(getPlayerSaveFiledesc(playerSavefileName))
|
|
|
|
WriteSavegame.immediate(saveTime_t, WriteSavegame.SaveMode.WORLD, worldDisk, getWorldSaveFiledesc(worldSavefileName), this, true, autosaveOnErrorAction) {
|
|
printdbg(this, "immediate save callback from WORLD")
|
|
|
|
makeSavegameBackupCopy(getWorldSaveFiledesc(worldSavefileName)) // don't put it on the postInit() or render(); must be called using callback
|
|
uiAutosaveNotifier.setAsClose()
|
|
|
|
App.savegameWorlds[world.worldIndex] = SavegameCollection.collectFromBaseFilename(File(worldsDir), worldSavefileName)
|
|
App.savegameWorldsName[world.worldIndex] = worldName
|
|
|
|
loadedTime_t = App.getTIME_T()
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Init instance by creating new world
|
|
*/
|
|
private fun enterCreateNewWorld(newGameParams: NewGameParams) {
|
|
|
|
val player = newGameParams.player
|
|
val worldParams = newGameParams.newWorldParams
|
|
|
|
printdbg(this, "Ingame called")
|
|
printStackTrace(this)
|
|
|
|
if (gameInitialised) {
|
|
printdbg(this, "loaded successfully.")
|
|
}
|
|
else {
|
|
App.getLoadScreen().addMessage("${App.GAME_NAME} ${App.getVERSION_STRING()}")
|
|
|
|
App.getLoadScreen().addMessage("Creating new world")
|
|
|
|
|
|
// init map as chosen size
|
|
val timeNow = App.getTIME_T()
|
|
world = GameWorld(worldParams.width, worldParams.height, timeNow, timeNow) // new game, so the creation time is right now
|
|
world.generatorSeed = worldParams.worldGenSeed
|
|
//gameworldIndices.add(world.worldIndex)
|
|
world.extraFields["basegame.economy"] = GameEconomy()
|
|
|
|
// generate terrain for the map
|
|
//WorldGenerator.attachMap(world)
|
|
//WorldGenerator.SEED = worldParams.worldGenSeed
|
|
//WorldGenerator.generateMap()
|
|
Worldgen.attachMap(world, WorldgenParams.getParamsByVersion(null, worldParams.worldGenSeed))
|
|
Worldgen.generateMap(App.getLoadScreen())
|
|
|
|
|
|
historicalFigureIDBucket = ArrayList<Int>()
|
|
|
|
worldName = worldParams.savegameName
|
|
|
|
|
|
world.worldCreator = UUID.fromString(player.uuid.toString())
|
|
|
|
printdbg(this, "new worldIndex: ${world.worldIndex}")
|
|
printdbg(this, "worldCurrentlyPlaying: ${player.worldCurrentlyPlaying}")
|
|
|
|
actorNowPlaying = player
|
|
actorGamer = player
|
|
forceAddActor(player)
|
|
|
|
WeatherMixer.internalReset(this)
|
|
|
|
UILoadGovernor.worldUUID = world.worldIndex
|
|
}
|
|
|
|
KeyToggler.forceSet(Input.Keys.Q, false)
|
|
|
|
loadCallback = newGameParams.callbackAfterLoad
|
|
worldGenVer = null
|
|
}
|
|
|
|
val ingameController = IngameController(this)
|
|
|
|
/** Load rest of the game with GL context */
|
|
fun postInit() {
|
|
actorNowPlaying!! // null check, just in case...
|
|
|
|
|
|
MegaRainGovernor // invoke MegaRain Governor
|
|
MinimapComposer // invoke MinimapComposer
|
|
|
|
|
|
|
|
// make controls work
|
|
Gdx.input.inputProcessor = ingameController
|
|
if (App.gamepad != null) {
|
|
ingameController.gamepad = App.gamepad
|
|
}
|
|
|
|
val drawWidth = Toolkit.drawWidth
|
|
|
|
|
|
|
|
// >- queue up game UIs that should pause the world -<
|
|
uiInventoryPlayer = UIInventoryFull()
|
|
uiInventoryPlayer.setPosition(0, 0)
|
|
|
|
// >- lesser UIs -<
|
|
// quick bar
|
|
uiQuickBar = UIQuickslotBar()
|
|
uiQuickBar.isVisible = true
|
|
uiQuickBar.setPosition((drawWidth - uiQuickBar.width) / 2, App.scr.tvSafeGraphicsHeight)
|
|
|
|
// pie menu
|
|
uiPieMenu = UIQuickslotPie()
|
|
uiPieMenu.setPosition(hdrawWidth, App.scr.halfh)
|
|
|
|
// vital metre
|
|
// fill in getter functions by
|
|
// (uiAliases[UI_QUICK_BAR]!!.UI as UIVitalMetre).vitalGetterMax = { some_function }
|
|
//uiVitalPrimary = UIVitalMetre(player, { 80f }, { 100f }, Color.red, 2, customPositioning = true)
|
|
//uiVitalPrimary.setAsAlwaysVisible()
|
|
//uiVitalSecondary = UIVitalMetre(player, { 73f }, { 100f }, Color(0x00dfff), 1) customPositioning = true)
|
|
//uiVitalSecondary.setAsAlwaysVisible()
|
|
//uiVitalItem = UIVitalMetre(player, { null }, { null }, Color(0xffcc00), 0, customPositioning = true)
|
|
//uiVitalItem.setAsAlwaysVisible()
|
|
|
|
// fake UI for blurring the background
|
|
uiBlur = UIFakeBlurOverlay(1f, true)
|
|
uiBlur.setPosition(0,0)
|
|
|
|
uiWatchTierOne = UIWatchLargeAnalogue()
|
|
// uiWatchTierOne = UIWatchLargeDigital()
|
|
uiWatchTierOne.setAsAlwaysVisible()
|
|
uiWatchTierOne.setPosition(
|
|
((drawWidth - App.scr.tvSafeActionWidth) - (uiQuickBar.posX + uiQuickBar.width) - uiWatchTierOne.width) / 2 + (uiQuickBar.posX + uiQuickBar.width),
|
|
App.scr.tvSafeGraphicsHeight + 8
|
|
)
|
|
|
|
// basic watch-style notification bar (temperature, new mail)
|
|
uiBasicInfo = UIBasicInfo()
|
|
uiBasicInfo.setAsAlwaysVisible()
|
|
uiBasicInfo.setPosition((uiQuickBar.posX - uiBasicInfo.width - App.scr.tvSafeActionWidth) / 2 + App.scr.tvSafeActionWidth, uiWatchTierOne.posY)
|
|
|
|
|
|
uiCheatDetected = UICheatDetected()
|
|
|
|
|
|
// batch-process uiAliases
|
|
// NOTE: UIs that should pause the game (e.g. Inventory) must have relevant codes ON THEIR SIDE
|
|
uiContainer.add(
|
|
// drawn first
|
|
//uiVitalPrimary,
|
|
//uiVitalSecondary,
|
|
//uiVitalItem,
|
|
noticelet,
|
|
|
|
uiBlur,
|
|
|
|
uiPieMenu,
|
|
uiQuickBar,
|
|
// uiBasicInfo, // temporarily commenting out: wouldn't make sense for v 0.3 release
|
|
uiWatchTierOne,
|
|
getWearableDeviceUI,
|
|
UIScreenZoom(),
|
|
uiAutosaveNotifier,
|
|
|
|
uiInventoryPlayer,
|
|
getUIFixture,
|
|
uiTooltip,
|
|
consoleHandler,
|
|
uiPaused,
|
|
uiCheatDetected,
|
|
// drawn last
|
|
)
|
|
|
|
|
|
ingameUpdateThread = ThreadIngameUpdate(this)
|
|
updateThreadWrapper = Thread(ingameUpdateThread, "Terrarum UpdateThread")
|
|
|
|
// add extra UIs from the other modules
|
|
ModMgr.GameExtraGuiLoader.guis.forEach {
|
|
uiContainer.add(it(this))
|
|
}
|
|
|
|
ModMgr.GameWatchdogLoader.watchdogs.forEach {
|
|
registerWatchdog(it.key, it.value)
|
|
}
|
|
|
|
// these need to appear on top of any others
|
|
uiContainer.add(notifier)
|
|
|
|
App.setDebugTime("Ingame.UpdateCounter", 0)
|
|
|
|
// some sketchy test code here
|
|
|
|
KeyToggler.forceSet(Input.Keys.F2, false)
|
|
|
|
|
|
if (loadCallback != null) {
|
|
try {
|
|
loadCallback!!(this)
|
|
}
|
|
catch (e: Throwable) {
|
|
e.printStackTrace()
|
|
}
|
|
loadCallback = null
|
|
}
|
|
}// END enter
|
|
|
|
|
|
|
|
private var worldPrimaryClickLatch = false
|
|
private var worldSecondaryClickLatch = false
|
|
|
|
private fun fireFixtureInteractEvent(fixture: FixtureBase, mwx: Double, mwy: Double) {
|
|
if (fixture.mouseUp) {
|
|
fixture.onInteract(mwx, mwy)
|
|
}
|
|
}
|
|
|
|
// left click: use held item, attack, pick up fixture if i'm holding a pickaxe or hammer (aka tool), do 'bare hand action' if holding nothing
|
|
override fun worldPrimaryClickStart(actor: ActorWithBody, delta: Float) {
|
|
//println("[Ingame] worldPrimaryClickStart $delta")
|
|
|
|
// prepare some variables
|
|
|
|
val itemOnGrip = ItemCodex[(actor as Pocketed).inventory.itemEquipped.get(GameItem.EquipPosition.HAND_GRIP)]
|
|
// bring up the UIs of the fixtures (e.g. crafting menu from a crafting table)
|
|
var uiOpened = false
|
|
val fixtureUnderMouse0: List<FixtureBase> = getActorsUnderMouse(Terrarum.mouseX, Terrarum.mouseY).filterIsInstance<FixtureBase>()
|
|
if (fixtureUnderMouse0.size > 1) {
|
|
App.printdbgerr(this, "Multiple fixtures at tile coord ${Terrarum.mouseX / TILE_SIZED}, ${Terrarum.mouseY / TILE_SIZED}: [${fixtureUnderMouse0.map { it.javaClass.simpleName }.joinToString()}]")
|
|
}
|
|
val fixtureUnderMouse = fixtureUnderMouse0.firstOrNull()
|
|
|
|
////////////////////////////////
|
|
|
|
mouseInInteractableRange(actor) { mwx, mwy, mtx, mty ->
|
|
// #1. interact with the fixture if NOT FixtureInteractionBlocked
|
|
// scan for the one with non-null UI.
|
|
// what if there's multiple of such fixtures? whatever, you are supposed to DISALLOW such situation.
|
|
if (fixtureUnderMouse != null && itemOnGrip !is FixtureInteractionBlocked) {
|
|
if (!worldPrimaryClickLatch) {
|
|
worldPrimaryClickLatch = true
|
|
fixtureUnderMouse.let { fixture ->
|
|
fixture.mainUI?.let { ui ->
|
|
uiOpened = true
|
|
|
|
// property 'uiFixture' is a dedicated property that the TerrarumIngame recognises.
|
|
// when it's not null, the UI will be updated and rendered
|
|
// when the UI is closed, it'll be replaced with a null value
|
|
uiFixture = ui
|
|
ui.setPosition(
|
|
(Toolkit.drawWidth - ui.width) / 4,
|
|
(App.scr.height - ui.height) / 4 // what the fuck?
|
|
)
|
|
ui.setAsOpen()
|
|
}
|
|
|
|
if (!uiOpened) {
|
|
fireFixtureInteractEvent(fixture, mwx, mwy)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// #2. If no fixture under mouse or FixtureInteractionBlocked, use the item
|
|
else if (itemOnGrip != null) {
|
|
// click filtering (latch stuff) is handled by IngameController (see inventoryCategoryAllowClickAndDrag)
|
|
// To disable click dragging for tool/block/etc., put `override val disallowToolDragging = true` to the item's code
|
|
val consumptionSuccessful = itemOnGrip.startPrimaryUse(actor, delta)
|
|
if (consumptionSuccessful > -1)
|
|
(actor as Pocketed).inventory.consumeItem(itemOnGrip, consumptionSuccessful)
|
|
|
|
worldPrimaryClickLatch = true
|
|
}
|
|
// #3. If not holding any item and can do barehandaction (size big enough that barehandactionminheight check passes), do it
|
|
else {
|
|
performBarehandAction(actor, delta, mwx, mwy, mtx, mty)
|
|
}
|
|
|
|
0L
|
|
}
|
|
}
|
|
|
|
override fun worldPrimaryClickEnd(actor: ActorWithBody, delta: Float) {
|
|
val canPerformBarehandAction = actor.scale * actor.baseHitboxH >=
|
|
(actor.actorValue.getAsDouble(AVKey.BAREHAND_MINHEIGHT) ?: 4294967296.0)
|
|
|
|
val itemOnGrip = (actor as Pocketed).inventory.itemEquipped.get(GameItem.EquipPosition.HAND_GRIP)
|
|
ItemCodex[itemOnGrip]?.endPrimaryUse(actor, delta)
|
|
|
|
if (canPerformBarehandAction) {
|
|
endPerformBarehandAction(actor)
|
|
}
|
|
|
|
worldPrimaryClickLatch = false
|
|
}
|
|
|
|
override fun worldSecondaryClickStart(actor: ActorWithBody, delta: Float) {
|
|
val itemOnGrip = ItemCodex[(actor as Pocketed).inventory.itemEquipped.get(GameItem.EquipPosition.HAND_GRIP)]
|
|
|
|
if (!worldSecondaryClickLatch) {
|
|
// #1. Try to pick up the fixture or dropped item first (if in range and there's something to pick up)
|
|
val pickupSuccessful = mouseInInteractableRange(actor) { mwx, mwy, mtx, mty ->
|
|
val actorsUnderMouse = getActorsUnderMouse(mwx, mwy)
|
|
val hasPickupableActor = actorsUnderMouse.any {
|
|
(it is FixtureBase && it.canBeDespawned && System.nanoTime() - it.spawnRequestedTime > 50000000) || // give freshly spawned fixtures 0.05 seconds of immunity
|
|
(it is DroppedItem && it.noAutoPickup)
|
|
}
|
|
if (hasPickupableActor) {
|
|
pickupFixtureOrDroppedItem(actor, delta, mwx, mwy, mtx, mty, assignToQuickslot = (itemOnGrip == null))
|
|
0L
|
|
}
|
|
else {
|
|
-1L
|
|
}
|
|
}
|
|
|
|
// #2. If pickup didn't happen, try to perform item's secondaryUse
|
|
if (pickupSuccessful < 0) {
|
|
val consumptionSuccessful = itemOnGrip?.startSecondaryUse(actor, delta) ?: -1
|
|
if (consumptionSuccessful > -1)
|
|
(actor as Pocketed).inventory.consumeItem(itemOnGrip!!, consumptionSuccessful)
|
|
}
|
|
|
|
worldSecondaryClickLatch = true
|
|
}
|
|
}
|
|
|
|
override fun worldSecondaryClickEnd(actor: ActorWithBody, delta: Float) {
|
|
val itemOnGrip = (actor as Pocketed).inventory.itemEquipped.get(GameItem.EquipPosition.HAND_GRIP)
|
|
ItemCodex[itemOnGrip]?.endSecondaryUse(actor, delta)
|
|
|
|
worldSecondaryClickLatch = false
|
|
}
|
|
|
|
|
|
|
|
private var firstTimeRun = true
|
|
|
|
///////////////
|
|
// prod code //
|
|
///////////////
|
|
private class ThreadIngameUpdate(val terrarumIngame: TerrarumIngame): Runnable {
|
|
override fun run() {
|
|
TODO()
|
|
}
|
|
}
|
|
|
|
internal var autosaveTimer: Second = 0f
|
|
|
|
override fun renderImpl(updateRate: Float) {
|
|
// Q&D solution for LoadScreen and Ingame, where while LoadScreen is working, Ingame now no longer has GL Context
|
|
// there's still things to load which needs GL context to be present
|
|
if (!gameFullyLoaded) {
|
|
|
|
uiAutosaveNotifier = UIAutosaveNotifier()
|
|
|
|
if (gameLoadMode == GameLoadMode.CREATE_NEW) {
|
|
postInitForNewGame()
|
|
}
|
|
else if (gameLoadMode == GameLoadMode.LOAD_FROM) {
|
|
postInitForLoadFromSave(gameLoadInfoPayload as Codices)
|
|
}
|
|
|
|
postInit()
|
|
|
|
gameUpdateGovernor.reset()
|
|
|
|
if (UILoadGovernor.previousSaveWasLoaded) {
|
|
sendNotification(listOf(Lang["GAME_PREV_SAVE_WAS_LOADED1"], Lang["GAME_PREV_SAVE_WAS_LOADED2"]))
|
|
}
|
|
|
|
gameFullyLoaded = true
|
|
}
|
|
else {
|
|
IngameRenderer.setRenderedWorld(world) // this doesn't slow down the game and prevents world-changing related bugs
|
|
}
|
|
|
|
super.renderImpl(updateRate)
|
|
|
|
ingameController.update()
|
|
|
|
|
|
// ASYNCHRONOUS UPDATE AND RENDER //
|
|
|
|
/** UPDATE CODE GOES HERE */
|
|
val dt = Gdx.graphics.deltaTime
|
|
if (!uiAutosaveNotifier.isVisible) {
|
|
autosaveTimer += dt
|
|
}
|
|
|
|
gameUpdateGovernor.update(dt, App.UPDATE_RATE, updateGame, renderGame)
|
|
|
|
|
|
val autosaveInterval = App.getConfigInt("autosaveinterval").coerceAtLeast(60000) / 1000f
|
|
if (autosaveTimer >= autosaveInterval) {
|
|
queueAutosave()
|
|
autosaveTimer -= autosaveInterval
|
|
}
|
|
|
|
}
|
|
|
|
private var worldWidth: Double = 0.0
|
|
private var oldCamX = 0
|
|
private var oldPlayerX = 0.0
|
|
|
|
private var deltaTeeCleared = false
|
|
|
|
private val terrarumWorldWatchdogs = TreeMap<String, TerrarumWorldWatchdog>()
|
|
|
|
fun registerWatchdog(identifier: String, watchdog: TerrarumWorldWatchdog) {
|
|
terrarumWorldWatchdogs[identifier] = watchdog
|
|
}
|
|
|
|
/**
|
|
* Ingame (world) related updates; UI update must go to renderGame()
|
|
*/
|
|
private val updateGame = { delta: Float ->
|
|
val world = this.world
|
|
worldWidth = world.width.toDouble() * TILE_SIZE
|
|
|
|
particlesActive = 0
|
|
|
|
|
|
// synchronised Ingame Input Updater
|
|
// will also queue up the block/wall/wire placed events
|
|
ingameController.update()
|
|
|
|
if ((!paused && !App.isScreenshotRequested()) || newWorldLoadedLatch) {
|
|
|
|
//hypothetical_input_capturing_function_if_you_finally_decided_to_forgo_gdx_input_processor_and_implement_your_own_to_synchronise_everything()
|
|
|
|
|
|
////////////////////////////
|
|
// camera-related updates //
|
|
////////////////////////////
|
|
FeaturesDrawer.update(delta)
|
|
|
|
///////////////////////////
|
|
// actor-related updates //
|
|
///////////////////////////
|
|
repossessActor()
|
|
|
|
// process actor addition requests
|
|
val addCueCpy = actorAdditionQueue.toList()
|
|
addCueCpy.forEach {
|
|
forceAddActor(it.first, it.second)
|
|
it.third(it.first)
|
|
}
|
|
actorAdditionQueue.removeAll(addCueCpy)
|
|
// determine whether the inactive actor should be activated
|
|
wakeDormantActors()
|
|
// update NOW; allow one last update for the actors flagged to despawn
|
|
updateActors(delta)
|
|
// determine whether the actor should keep being activated or be dormant
|
|
killOrKnockdownActors()
|
|
// process actor removal requests
|
|
val remCueCpy = actorRemovalQueue.toList()
|
|
remCueCpy.forEach {
|
|
forceRemoveActor(it.first, it.second)
|
|
it.third(it.first)
|
|
}
|
|
actorRemovalQueue.removeAll(remCueCpy)
|
|
// update particles
|
|
particlesContainer.toList().forEach { if (!it.flagDespawn) particlesActive++; it.update(delta) }
|
|
// TODO thread pool(?)
|
|
CollisionSolver.process()
|
|
|
|
|
|
///////////////////////////
|
|
// world-related updates //
|
|
///////////////////////////
|
|
actorsRTree = PRTree(actorMBRConverter, 24)
|
|
actorsRTree.load(actorContainerActive.filterIsInstance<ActorWithBody>())
|
|
|
|
BlockPropUtil.dynamicLumFuncTickClock()
|
|
world.updateWorldTime(delta)
|
|
measureDebugTime("WorldSimulator.update") {
|
|
WorldSimulator.invoke(actorNowPlaying, delta)
|
|
}
|
|
measureDebugTime("WeatherMixer.update") {
|
|
WeatherMixer.update(delta, actorNowPlaying, world)
|
|
}
|
|
measureDebugTime("BlockStats.update") {
|
|
TileSurvey.update()
|
|
}
|
|
// fill up visibleActorsRenderFront for wires but not on every update
|
|
measureDebugTime("Ingame.FillUpWiresBuffer*") {
|
|
if (WORLD_UPDATE_TIMER % 2 == 1L) {
|
|
fillUpWiresBuffer()
|
|
}
|
|
}
|
|
measureDebugTime("Ingame.FillUpWirePortsView*") {
|
|
if (WORLD_UPDATE_TIMER % 2 == 0L) {
|
|
val fixtures = INGAME.actorContainerActive.filterIsInstance<Electric>()
|
|
fillUpWirePortsView(fixtures)
|
|
}
|
|
}
|
|
terrarumWorldWatchdogs.entries.forEach {
|
|
measureDebugTime("Ingame.Watchdog.${it.key}*") {
|
|
if (WORLD_UPDATE_TIMER % it.value.runIntervalByTick.toLong() == 0L) {
|
|
it.value(world)
|
|
}
|
|
}
|
|
}
|
|
oldCamX = WorldCamera.x
|
|
oldPlayerX = actorNowPlaying?.hitbox?.canonicalX ?: 0.0
|
|
|
|
// update audio mixer
|
|
val ratio = (TileSurvey.getRatio("basegame.Ingame.audioReflection") ?: 0.0)
|
|
if (ratio >= 0.0) {
|
|
val ratio1 = ratio.coerceIn(0.0, 1.0)
|
|
App.audioMixer.convolveBusCave.volume = ratio1
|
|
App.audioMixer.convolveBusOpen.volume = 1.0 - ratio1
|
|
}
|
|
else {
|
|
val ratio1 = (ratio / MaterialCodex["AIIR"].sondrefl).absoluteValue.coerceIn(0.0, 1.0)
|
|
App.audioMixer.convolveBusOpen.volume = (1.0 - ratio1).pow(0.75)
|
|
App.audioMixer.convolveBusCave.volume = 0.0
|
|
}
|
|
val openness = (TileSurvey.getRatio("basegame.Ingame.openness") ?: 0.0).times(1.74).coerceIn(0.0, 1.0)
|
|
App.audioMixer.amb1plus2.volume = openness.pow(2.0 / 3.0)
|
|
|
|
|
|
|
|
actorNowPlaying?.let { if (WORLD_UPDATE_TIMER % 4 == 1L) updateWorldGenerator(actorNowPlaying!!) }
|
|
|
|
|
|
|
|
WORLD_UPDATE_TIMER += 1
|
|
|
|
// run benchmark if F2 is on
|
|
if (KeyToggler.isOn(Input.Keys.F2)) {
|
|
deltaTeeBenchmarks.appendHead(1f / Gdx.graphics.deltaTime)
|
|
|
|
if (deltaTeeCleared) deltaTeeCleared = false
|
|
}
|
|
else if (!deltaTeeCleared) {
|
|
deltaTeeCleared = true
|
|
deltaTeeBenchmarks.clear()
|
|
}
|
|
}
|
|
|
|
|
|
if ((!paused && !App.isScreenshotRequested()) || newWorldLoadedLatch) {
|
|
// completely consume block change queues because why not
|
|
terrainChangeQueue.clear()
|
|
wallChangeQueue.clear()
|
|
wireChangeQueue.clear()
|
|
|
|
oldSelectedWireRenderClass = selectedWireRenderClass
|
|
}
|
|
|
|
musicStreamer.update(this, delta)
|
|
MusicService.update(delta)
|
|
|
|
////////////////////////
|
|
// ui-related updates //
|
|
////////////////////////
|
|
// open/close fake blur UI according to what's opened
|
|
if (uiInventoryPlayer.isVisible ||
|
|
getUIFixture.get()?.isVisible == true || worldTransitionOngoing) {
|
|
uiBlur.setAsOpen()
|
|
}
|
|
else {
|
|
uiBlur.setAsClose()
|
|
}
|
|
|
|
// update debuggers using javax.swing //
|
|
if (Authenticator.b()) {
|
|
AVTracker.update()
|
|
ActorsList.update()
|
|
}
|
|
|
|
//println("paused = $paused")
|
|
|
|
if ((!paused && !App.isScreenshotRequested()) && newWorldLoadedLatch) newWorldLoadedLatch = false
|
|
|
|
|
|
if (doThingsAfterSave) {
|
|
saveRequested2 = false
|
|
doThingsAfterSave = false
|
|
saveCallback!!()
|
|
}
|
|
|
|
if (saveRequested2) {
|
|
saveRequested2 = false
|
|
doForceSave()
|
|
}
|
|
|
|
if (worldTransitionPauseRequested > 0) { // let a frame to update before locking (=pausing) entirely
|
|
worldTransitionPauseRequested -= 1
|
|
}
|
|
else if (worldTransitionPauseRequested == 0) {
|
|
paused = true
|
|
}
|
|
|
|
|
|
if (KeyToggler.isOn(Input.Keys.F10)) {
|
|
batch.inUse {
|
|
batch.color = Color.WHITE
|
|
App.fontSmallNumbers.draw(batch, "$ccY\u00DCupd $ccG${delta.toIntAndFrac(1)}", 2f, App.scr.height - 16f)
|
|
_dbgDeltaUpd = delta
|
|
}
|
|
}
|
|
}
|
|
|
|
private var _dbgDeltaUpd = 0f
|
|
|
|
|
|
private val renderGame = { frameDelta: Float ->
|
|
Gdx.graphics.setTitle(getCanonicalTitle())
|
|
|
|
WorldCamera.update(world, actorNowPlaying)
|
|
|
|
measureDebugTime("Ingame.FilterVisibleActors") {
|
|
filterVisibleActors()
|
|
}
|
|
|
|
if (uiInventoryPlayer == uiFixture) throw IllegalStateException("Do NOT use InventoryPlayer as a UIFixture, the engine cannot handle this situation XwX")
|
|
|
|
uiContainer.forEach {
|
|
|
|
// suppress inventory opening if fixture inventory is opened
|
|
if (uiFixture?.isClosed == false)
|
|
uiInventoryPlayer.handler.lockToggle()
|
|
else
|
|
uiInventoryPlayer.handler.unlockToggle()
|
|
|
|
|
|
when (it) {
|
|
is Id_UICanvasNullable -> it.get()?.update(Gdx.graphics.deltaTime)
|
|
is UICanvas -> it.update(Gdx.graphics.deltaTime)
|
|
}
|
|
}
|
|
//uiFixture?.update(Gdx.graphics.deltaTime)
|
|
// deal with the uiFixture being closed
|
|
if (uiFixture?.isClosed == true) { uiFixture = null }
|
|
|
|
IngameRenderer.invoke(
|
|
frameDelta,
|
|
paused,
|
|
screenZoom,
|
|
visibleActorsRenderFarBehind,
|
|
visibleActorsRenderBehind,
|
|
visibleActorsRenderMiddle,
|
|
visibleActorsRenderMidTop,
|
|
visibleActorsRenderFront,
|
|
visibleActorsRenderOverlay,
|
|
particlesContainer,
|
|
actorNowPlaying,
|
|
uiContainer// + uiFixture
|
|
)
|
|
|
|
// quick and dirty way to show
|
|
if (worldTransitionOngoing) {
|
|
batch.inUse {
|
|
batch.color = gradEndCol
|
|
Toolkit.fillArea(batch, 0, 0, App.scr.width, App.scr.height)
|
|
|
|
|
|
batch.color = Color.WHITE
|
|
val t = Lang["MENU_IO_SAVING"]
|
|
val circleSheet = CommonResourcePool.getAsTextureRegionPack("loading_circle_64")
|
|
Toolkit.drawTextCentered(batch, App.fontGame, t, Toolkit.drawWidth, 0, ((App.scr.height - circleSheet.tileH) / 2) - 40)
|
|
|
|
// -1..63
|
|
val index =
|
|
((WriteSavegame.saveProgress / WriteSavegame.saveProgressMax) * circleSheet.horizontalCount * circleSheet.verticalCount).roundToInt() - 1
|
|
if (index >= 0) {
|
|
val sx = index % circleSheet.horizontalCount
|
|
val sy = index / circleSheet.horizontalCount
|
|
// q&d fix for ArrayIndexOutOfBoundsException caused when saving huge world... wut?
|
|
if (sx in 0 until circleSheet.horizontalCount && sy in 0 until circleSheet.horizontalCount) {
|
|
batch.draw(
|
|
circleSheet.get(sx, sy),
|
|
((Toolkit.drawWidth - circleSheet.tileW) / 2).toFloat(),
|
|
((App.scr.height - circleSheet.tileH) / 2).toFloat()
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
if (KeyToggler.isOn(Input.Keys.F10)) {
|
|
batch.inUse {
|
|
batch.color = Color.WHITE
|
|
App.fontSmallNumbers.draw(batch, "$ccY\u00DCupd $ccG${_dbgDeltaUpd.toIntAndFrac(1)}", 2f, App.scr.height - 16f)
|
|
App.fontSmallNumbers.draw(batch, "$ccY\u00DCren $ccG${frameDelta.toIntAndFrac(1)}", 2f + 7*12, App.scr.height - 16f)
|
|
App.fontSmallNumbers.draw(batch, "$ccY\u00DCgdx $ccG${Gdx.graphics.deltaTime.toIntAndFrac(1)}", 2f + 7*24, App.scr.height - 16f)
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun Point2iMod(x: Int, y: Int) = Point2i(x fmod (world.width / CHUNK_W), y)
|
|
|
|
private fun updateWorldGenerator(actor: ActorWithBody) {
|
|
val pcx = (actor.intTilewiseHitbox.canonicalX.toInt() fmod world.width) / CHUNK_W
|
|
val pcy = (actor.intTilewiseHitbox.canonicalY.toInt() fmod world.width) / CHUNK_H
|
|
listOf(
|
|
Point2iMod(pcx - 1, pcy - 2), Point2iMod(pcx, pcy - 2), Point2iMod(pcx + 1, pcy - 2),
|
|
Point2iMod(pcx - 2, pcy - 1), Point2iMod(pcx - 1, pcy - 1), Point2iMod(pcx, pcy - 1), Point2iMod(pcx + 1, pcy - 1), Point2iMod(pcx + 2, pcy - 1),
|
|
Point2iMod(pcx - 2, pcy), Point2iMod(pcx - 1, pcy), Point2iMod(pcx + 1, pcy), Point2iMod(pcx + 2, pcy),
|
|
Point2iMod(pcx - 2, pcy + 1), Point2iMod(pcx - 1, pcy + 1), Point2iMod(pcx, pcy + 1), Point2iMod(pcx + 1, pcy + 1), Point2iMod(pcx + 2, pcy + 1),
|
|
Point2iMod(pcx - 1, pcy + 2), Point2iMod(pcx, pcy + 2), Point2iMod(pcx + 1, pcy + 2),
|
|
).filter { it.y in 0 until world.height }.filter { (cx, cy) ->
|
|
if (cy !in 0 until world.height / CHUNK_H) false
|
|
else (world.chunkFlags[cy][cx].and(0x7F) == 0.toByte())
|
|
}.forEach { (cx, cy) ->
|
|
Worldgen.generateChunkIngame(cx, cy) { cx, cy ->
|
|
listOf(0,1,2).forEach { layer ->
|
|
modified(layer, cx, cy)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private var worldTransitionOngoing = false
|
|
private var worldTransitionPauseRequested = -1
|
|
private var saveRequested2 = false
|
|
private var saveCallback: (() -> Unit)? = null
|
|
private var doThingsAfterSave = false
|
|
|
|
override fun requestForceSave(callback: () -> Unit) {
|
|
saveCallback = callback
|
|
worldTransitionOngoing = true
|
|
saveRequested2 = true
|
|
worldTransitionPauseRequested = 1
|
|
blockMarkingActor.isVisible = false
|
|
}
|
|
|
|
internal fun doForceSave() {
|
|
// TODO show appropriate UI
|
|
// uiBlur.setAsOpen()
|
|
|
|
saveTheGame({ // onSuccessful
|
|
System.gc()
|
|
autosaveTimer = 0f
|
|
|
|
// TODO hide appropriate UI
|
|
uiBlur.setAsClose()
|
|
doThingsAfterSave = true
|
|
}, { // onError
|
|
// TODO show failure message
|
|
|
|
// TODO hide appropriate UI
|
|
uiBlur.setAsClose()
|
|
})
|
|
}
|
|
|
|
override fun saveTheGame(onSuccessful: () -> Unit, onError: (Throwable) -> Unit) {
|
|
val saveTime_t = App.getTIME_T()
|
|
val playerSavefile = getPlayerSaveFiledesc(INGAME.playerSavefileName)
|
|
val worldSavefile = getWorldSaveFiledesc(INGAME.worldSavefileName)
|
|
|
|
|
|
INGAME.makeSavegameBackupCopy(playerSavefile)
|
|
WriteSavegame(saveTime_t, WriteSavegame.SaveMode.PLAYER, INGAME.playerDisk, playerSavefile, INGAME as TerrarumIngame, false, onError) {
|
|
|
|
INGAME.makeSavegameBackupCopy(worldSavefile)
|
|
WriteSavegame(saveTime_t, WriteSavegame.SaveMode.WORLD, INGAME.worldDisk, worldSavefile, INGAME as TerrarumIngame, false, onError) {
|
|
// callback:
|
|
// rebuild the disk skimmers
|
|
INGAME.actorContainerActive.filterIsInstance<IngamePlayer>().forEach {
|
|
printdbg(this, "Game Save callback -- rebuilding the disk skimmer for IngamePlayer ${it.actorValue.getAsString(AVKey.NAME)}")
|
|
// it.rebuildingDiskSkimmer?.rebuild()
|
|
}
|
|
|
|
// return to normal state
|
|
onSuccessful()
|
|
|
|
loadedTime_t = App.getTIME_T()
|
|
}
|
|
}
|
|
}
|
|
|
|
private val maxRenderableWires = ReferencingRanges.ACTORS_WIRES.last - ReferencingRanges.ACTORS_WIRES.first + 1
|
|
private val maxRenderablePorts = ReferencingRanges.ACTORS_WIRE_PORTS.last - ReferencingRanges.ACTORS_WIRE_PORTS.first + 1
|
|
private val wireActorsContainer = Array(maxRenderableWires) { WireActor(ReferencingRanges.ACTORS_WIRES.first + it).let {
|
|
forceAddActor(it)
|
|
/*^let*/ it
|
|
} }
|
|
private val wirePortActorsContainer = Array(maxRenderablePorts) { WirePortActor(ReferencingRanges.ACTORS_WIRE_PORTS.first + it).let {
|
|
forceAddActor(it)
|
|
/*^let*/ it
|
|
} }
|
|
|
|
private fun fillUpWiresBuffer() {
|
|
val for_y_start = (WorldCamera.y.toFloat() / TILE_SIZE).floorToInt() - 1
|
|
val for_y_end = for_y_start + BlocksDrawer.tilesInVertical + 2*1
|
|
|
|
val for_x_start = (WorldCamera.x.toFloat() / TILE_SIZE).floorToInt() - 1
|
|
val for_x_end = for_x_start + BlocksDrawer.tilesInHorizontal + 2*1
|
|
|
|
var wiringCounter = 0
|
|
for (y in for_y_start..for_y_end) {
|
|
for (x in for_x_start..for_x_end) {
|
|
if (wiringCounter >= maxRenderableWires) break
|
|
|
|
val (wires, nodes) = world.getAllWiresFrom(x, y)
|
|
|
|
wires?.forEach {
|
|
val wireActor = wireActorsContainer[wiringCounter]
|
|
|
|
wireActor.setWire(it, x, y, nodes!![it]!!.cnx)
|
|
|
|
if (WireCodex[it].renderClass == selectedWireRenderClass || selectedWireRenderClass == "wire_render_all") {
|
|
wireActor.renderOrder = Actor.RenderOrder.OVERLAY
|
|
}
|
|
else {
|
|
wireActor.renderOrder = Actor.RenderOrder.FAR_BEHIND
|
|
}
|
|
|
|
wireActor.isUpdate = true
|
|
wireActor.isVisible = true
|
|
wireActor.forceDormant = false
|
|
|
|
wiringCounter += 1
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
for (i in wiringCounter until maxRenderableWires) {
|
|
val wireActor = wireActorsContainer[i]
|
|
wireActor.isUpdate = false
|
|
wireActor.isVisible = false
|
|
wireActor.forceDormant = true
|
|
}
|
|
}
|
|
|
|
private fun fillUpWirePortsView(fixtures: List<Electric>) {
|
|
var portsCounter = 0
|
|
|
|
fixtures.forEach {
|
|
(it.wireSinkTypes.toList() + it.wireEmitterTypes.toList()).forEach { (boxIndex, type) ->
|
|
if (portsCounter < wirePortActorsContainer.size) {
|
|
val wireActor = wirePortActorsContainer[portsCounter]
|
|
val (wx, wy) = it.worldBlockPos!! + it.blockBoxIndexToPoint2i(boxIndex)
|
|
|
|
wireActor.setPort(type, wx, wy)
|
|
wireActor.isUpdate = true
|
|
wireActor.isVisible = true
|
|
wireActor.forceDormant = false
|
|
|
|
portsCounter += 1
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
for (i in portsCounter until maxRenderablePorts) {
|
|
val wireActor = wirePortActorsContainer[i]
|
|
wireActor.isUpdate = false
|
|
wireActor.isVisible = false
|
|
wireActor.forceDormant = true
|
|
}
|
|
}
|
|
|
|
private fun filterVisibleActors() {
|
|
visibleActorsRenderFarBehind.clear()
|
|
visibleActorsRenderBehind.clear()
|
|
visibleActorsRenderMiddle.clear()
|
|
visibleActorsRenderMidTop.clear()
|
|
visibleActorsRenderFront.clear()
|
|
visibleActorsRenderOverlay.clear()
|
|
|
|
actorContainerActive.forEach {
|
|
if (it is ActorWithBody)
|
|
actorToRenderQueue(it).add(it)
|
|
}
|
|
}
|
|
|
|
private fun repossessActor() {
|
|
// check if currently pocessed actor is removed from game
|
|
if (!theGameHasActor(actorNowPlaying)) {
|
|
// re-possess canonical player
|
|
if (theGameHasActor(actorGamer))
|
|
changePossession(actorGamer)
|
|
else
|
|
actorNowPlaying = null
|
|
}
|
|
}
|
|
|
|
internal fun changePossession(newActor: ActorHumanoid) {
|
|
if (!theGameHasActor(actorNowPlaying)) { throw NoSuchActorWithRefException(newActor) }
|
|
|
|
actorNowPlaying = newActor
|
|
}
|
|
|
|
internal fun changePossession(refid: Int) {
|
|
val actorToChange = getActorByID(refid)
|
|
|
|
if (actorToChange !is ActorHumanoid) {
|
|
throw Error("Unpossessable actor $refid: type expected ActorHumanoid, got ${actorToChange.javaClass.canonicalName}")
|
|
}
|
|
|
|
changePossession(getActorByID(refid) as ActorHumanoid)
|
|
}
|
|
|
|
fun wakeDormantActors() {
|
|
var actorContainerSize = actorContainerInactive.size
|
|
var i = 0
|
|
while (i < actorContainerSize) { // loop through actorContainerInactive
|
|
val actor = actorContainerInactive[i]
|
|
if (actor is ActorWithBody && (actor.chunkAnchoring || actor.inUpdateRange(world)) && !actor.forceDormant) {
|
|
activateDormantActor(actor) // duplicates are checked here
|
|
actorContainerSize -= 1
|
|
i-- // array removed 1 elem, so we also decrement counter by 1
|
|
}
|
|
i++
|
|
}
|
|
}
|
|
|
|
/**
|
|
* determine whether the actor should be active or dormant by its distance from the player.
|
|
* If the actor must be dormant, the target actor will be put to the list specifically for them.
|
|
* if the actor is not to be dormant, it will be just ignored.
|
|
*/
|
|
fun killOrKnockdownActors() {
|
|
var actorContainerSize = actorContainerActive.size
|
|
var i = 0
|
|
while (i < actorContainerSize) { // loop through actorContainerActive
|
|
val actor = actorContainerActive[i]
|
|
val actorIndex = i
|
|
// kill actors flagged to despawn
|
|
if (actor.despawned) {
|
|
queueActorRemoval(actor)
|
|
actorContainerSize -= 1
|
|
i-- // array removed 1 elem, so we also decrement counter by 1
|
|
}
|
|
// inactivate distant actors
|
|
else if (actor is ActorWithBody && (!actor.chunkAnchoring && !actor.inUpdateRange(world) || actor.forceDormant)) {
|
|
if (actor !is Projectile) { // if it's a projectile, don't inactivate it; just kill it.
|
|
actorContainerInactive.add(actor) // naïve add; duplicates are checked when the actor is re-activated
|
|
}
|
|
actorContainerActive.removeAt(actorIndex)
|
|
actorContainerSize -= 1
|
|
i-- // array removed 1 elem, so we also decrement counter by 1
|
|
}
|
|
i++
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update actors concurrently.
|
|
*
|
|
* NOTE: concurrency for actor updating is currently disabled because of it's poor performance
|
|
*/
|
|
fun updateActors(delta: Float) {
|
|
if (false) { // don't multithread this for now, it's SLOWER //if (Terrarum.MULTITHREAD && actorContainerActive.size > Terrarum.THREADS) {
|
|
/*ThreadExecutor.renew()
|
|
|
|
val actors = actorContainerActive.size.toFloat()
|
|
// set up indices
|
|
for (i in 0..App.THREAD_COUNT - 1) {
|
|
ThreadExecutor.submit(
|
|
ThreadActorUpdate(
|
|
actors.div(App.THREAD_COUNT).times(i).roundToInt(),
|
|
actors.div(App.THREAD_COUNT).times(i + 1).roundToInt() - 1
|
|
)
|
|
)
|
|
}
|
|
|
|
ThreadExecutor.join()
|
|
|
|
actorNowPlaying?.update(delta)*/
|
|
}
|
|
else {
|
|
// Pass 1: update contraptions first so riders get displaced before their own update
|
|
actorContainerActive.forEach {
|
|
if (it is PhysContraption && it != actorNowPlaying) {
|
|
it.update(delta)
|
|
}
|
|
}
|
|
|
|
// Pass 2: update all non-contraption actors with existing callbacks
|
|
actorContainerActive.forEach {
|
|
if (it !is PhysContraption && it != actorNowPlaying) {
|
|
it.update(delta)
|
|
|
|
if (it is Pocketed) {
|
|
it.inventory.forEach { inventoryEntry ->
|
|
ItemCodex[inventoryEntry.itm]!!.effectWhileInPocket(it as ActorWithBody, delta) // kind of an error checking because all Pocketed must be ActorWithBody
|
|
if (it.equipped(inventoryEntry.itm)) {
|
|
ItemCodex[inventoryEntry.itm]!!.effectWhileEquipped(it as ActorWithBody, delta)
|
|
}
|
|
}
|
|
}
|
|
|
|
if (it is CuedByTerrainChange) {
|
|
terrainChangeQueue.toList().forEach { cue ->
|
|
// printdbg(this, "Ingame actors terrainChangeCue: ${cue}")
|
|
if (cue != null) it.updateForTerrainChange(cue)
|
|
}
|
|
}
|
|
|
|
if (it is CuedByWallChange) {
|
|
wallChangeQueue.toList().forEach { cue ->
|
|
// printdbg(this, "Ingame actors wallChangeCue: ${cue}")
|
|
if (cue != null) it.updateForWallChange(cue)
|
|
}
|
|
}
|
|
|
|
if (it is CuedByWireChange) {
|
|
wireChangeQueue.toList().forEach { cue ->
|
|
// printdbg(this, "Ingame actors wireChangeCue: ${cue}")
|
|
if (cue != null) it.updateForWireChange(cue)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Pass 3: update player
|
|
actorNowPlaying?.update(delta)
|
|
//AmmoMeterProxy(player, uiVitalItem.UI as UIVitalMetre)
|
|
}
|
|
}
|
|
|
|
fun queueAutosave() {
|
|
val start = System.nanoTime()
|
|
|
|
uiAutosaveNotifier.setAsOpen()
|
|
|
|
val saveTime_t = App.getTIME_T()
|
|
val playerSavefile0 = getPlayerSaveFiledesc(INGAME.playerSavefileName)
|
|
val worldSavefile0 = getWorldSaveFiledesc(INGAME.worldSavefileName)
|
|
|
|
val playerSavefile = INGAME.makeSavegameBackupCopyAuto(playerSavefile0)
|
|
WriteSavegame(saveTime_t, WriteSavegame.SaveMode.PLAYER, INGAME.playerDisk, playerSavefile, INGAME as TerrarumIngame, true, autosaveOnErrorAction) {
|
|
|
|
val worldSavefile = INGAME.makeSavegameBackupCopyAuto(worldSavefile0)
|
|
WriteSavegame(saveTime_t, WriteSavegame.SaveMode.QUICK_WORLD, INGAME.worldDisk, worldSavefile, INGAME as TerrarumIngame, true, autosaveOnErrorAction) {
|
|
// callback:
|
|
// rebuild the disk skimmers
|
|
INGAME.actorContainerActive.filterIsInstance<IngamePlayer>().forEach {
|
|
printdbg(this, "Game Save callback -- rebuilding the disk skimmer for IngamePlayer ${it.actorValue.getAsString(AVKey.NAME)}")
|
|
// it.rebuildingDiskSkimmer?.rebuild()
|
|
}
|
|
|
|
// return to normal state
|
|
uiAutosaveNotifier.setAsClose()
|
|
autosaveTimer = 0f
|
|
|
|
val timeDiff = System.nanoTime() - start
|
|
debugTimers.put("Last Autosave Duration", timeDiff)
|
|
printdbg(this, "Last Autosave Duration: ${(timeDiff) / 1000000000} s")
|
|
|
|
|
|
loadedTime_t = App.getTIME_T()
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
|
|
fun Double.sqr() = this * this
|
|
fun Int.sqr() = this * this
|
|
fun min(vararg d: Double): Double {
|
|
var ret = Double.MAX_VALUE
|
|
d.forEach { if (it < ret) ret = it }
|
|
return ret
|
|
}
|
|
|
|
|
|
private val cameraWindowX = WorldCamera.x.toDouble()..WorldCamera.xEnd.toDouble()
|
|
private val cameraWindowY = WorldCamera.y.toDouble()..WorldCamera.yEnd.toDouble()
|
|
|
|
private fun actorToRenderQueue(actor: ActorWithBody): ArrayList<ActorWithBody> {
|
|
return when (actor.renderOrder) {
|
|
Actor.RenderOrder.FAR_BEHIND -> visibleActorsRenderFarBehind
|
|
Actor.RenderOrder.BEHIND -> visibleActorsRenderBehind
|
|
Actor.RenderOrder.MIDDLE -> visibleActorsRenderMiddle
|
|
Actor.RenderOrder.MIDTOP -> visibleActorsRenderMidTop
|
|
Actor.RenderOrder.FRONT -> visibleActorsRenderFront
|
|
Actor.RenderOrder.OVERLAY-> visibleActorsRenderOverlay
|
|
}
|
|
}
|
|
|
|
|
|
override fun forceRemoveActor(actor: Actor, caller: Throwable) {
|
|
arrayOf(actorContainerActive, actorContainerInactive).forEach { actorContainer ->
|
|
val indexToDelete = actorContainer.searchForIndex(actor.referenceID) { it.referenceID }
|
|
if (indexToDelete != null) {
|
|
// printdbg(this, "Removing actor $actor")
|
|
// printStackTrace(this)
|
|
|
|
actor.dispose()
|
|
actorContainer.removeAt(indexToDelete)
|
|
|
|
// indexToDelete >= 0 means that the actor certainly exists in the game
|
|
// which means we don't need to check if i >= 0 again
|
|
if (actor is ActorWithBody) {
|
|
actorToRenderQueue(actor).remove(actor)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun ArrayList<*>.binarySearch(actor: Actor) = this.binarySearch(actor.referenceID)
|
|
|
|
private fun ArrayList<*>.binarySearch(ID: Int): Int {
|
|
// code from collections/Collections.kt
|
|
var low = 0
|
|
var high = this.size - 1
|
|
|
|
while (low <= high) {
|
|
val mid = (low + high).ushr(1) // safe from overflows
|
|
|
|
val midVal = get(mid)!!
|
|
|
|
if (ID > midVal.hashCode())
|
|
low = mid + 1
|
|
else if (ID < midVal.hashCode())
|
|
high = mid - 1
|
|
else
|
|
return mid // key found
|
|
}
|
|
return -(low + 1) // key not found
|
|
}
|
|
|
|
/**
|
|
* Check for duplicates, append actor and sort the list
|
|
*/
|
|
override fun forceAddActor(actor: Actor?, caller: Throwable) {
|
|
if (actor == null) return
|
|
|
|
if (theGameHasActor(actor.referenceID)) {
|
|
throw ReferencedActorAlreadyExistsException(actor, caller)
|
|
}
|
|
else {
|
|
actorContainerActive.add(actor)
|
|
if (actor is ActorWithBody) actorToRenderQueue(actor).add(actor)
|
|
}
|
|
}
|
|
|
|
override fun inputStrobed(e: TerrarumKeyboardEvent) {
|
|
uiContainer.forEach { it?.inputStrobed(e) }
|
|
}
|
|
|
|
fun activateDormantActor(actor: Actor) {
|
|
if (!isInactive(actor.referenceID)) {
|
|
/*if (isActive(actor.referenceID))
|
|
throw Error("The actor $actor is already activated")
|
|
else
|
|
throw Error("The actor $actor already exists in the game")*/
|
|
return
|
|
}
|
|
else {
|
|
actorContainerInactive.remove(actor)
|
|
actorContainerActive.add(actor)
|
|
|
|
if (actor is ActorWithBody) {
|
|
actorToRenderQueue(actor).add(actor)
|
|
}
|
|
}
|
|
}
|
|
|
|
fun addParticle(particle: ParticleBase) {
|
|
particlesContainer.appendHead(particle)
|
|
}
|
|
|
|
private var barehandAxeInUse = false
|
|
private var barehandPickInUse = false
|
|
|
|
fun endPerformBarehandAction(actor: ActorWithBody) {
|
|
if (barehandAxeInUse) {
|
|
barehandAxeInUse = false
|
|
AxeCore.endPrimaryUse(actor, null)
|
|
}
|
|
if (barehandPickInUse) {
|
|
barehandPickInUse = false
|
|
PickaxeCore.endPrimaryUse(actor, null)
|
|
}
|
|
}
|
|
|
|
private fun getActorsAtVicinity(worldX: Double, worldY: Double, radius: Double): List<ActorWithBody> {
|
|
val outList = java.util.ArrayList<ActorWithBody>()
|
|
try {
|
|
actorsRTree.find(worldX - radius, worldY - radius, worldX + radius, worldY + radius, outList)
|
|
}
|
|
catch (e: NullPointerException) {
|
|
}
|
|
return outList
|
|
}
|
|
|
|
private fun getPunchSize(actor: ActorWithBody) = actor.scale * actor.actorValue.getAsDouble(AVKey.BAREHAND_BASE_DIGSIZE)!!
|
|
|
|
|
|
fun performBarehandAction(actor: ActorWithBody, delta: Float, mwx: Double, mwy: Double, mtx: Int, mty: Int) {
|
|
// for giant actors punching every structure pickaxe can dig out
|
|
val canAttackOrDig =
|
|
actor.scale * actor.baseHitboxH >= (actor.actorValue.getAsDouble(AVKey.BAREHAND_MINHEIGHT) ?: 4294967296.0)
|
|
|
|
// for players punching dirts or weaker blocks
|
|
val canDigSoftTileOnly =
|
|
actor is ActorHumanoid && (actor.baseHitboxH * actor.scale) >= 32f
|
|
|
|
|
|
val punchSize = getPunchSize(actor)
|
|
val punchBlockSize = punchSize.div(TILE_SIZED).floorToInt()
|
|
|
|
val mouseUnderPunchableTree = BlockCodex[world.getTileFromTerrain(mtx, mty)].hasAnyTagsOf("LEAVES", "TREESMALL")
|
|
|
|
|
|
// punch a small tree/shrub
|
|
if (mouseUnderPunchableTree) {
|
|
barehandAxeInUse = true
|
|
AxeCore.startPrimaryUse(actor, delta, null, mtx, mty, punchBlockSize.coerceAtLeast(1), punchBlockSize.coerceAtLeast(1), listOf("TREESMALL"))
|
|
}
|
|
// TODO attack a mob
|
|
// else if (mobsUnderHand.size > 0 && canAttackOrDig) {
|
|
// }
|
|
// else, punch a block
|
|
else if (canAttackOrDig) {
|
|
if (punchBlockSize > 0) {
|
|
barehandPickInUse = true
|
|
PickaxeCore.startPrimaryUse(actor, delta, null, mtx, mty, 1.0 / punchBlockSize, punchBlockSize, punchBlockSize)
|
|
}
|
|
}
|
|
else if (canDigSoftTileOnly) {
|
|
barehandPickInUse = true
|
|
val tileUnderCursor = INGAME.world.getTileFromTerrain(mtx, mty)
|
|
val tileprop = BlockCodex[tileUnderCursor]
|
|
if (tileprop.strength <= 12)
|
|
PickaxeCore.startPrimaryUse(actor, delta, null, mtx, mty, 1.0, 1, 1)
|
|
}
|
|
}
|
|
|
|
fun getActorsUnderMouse(mwx: Double, mwy: Double): List<ActorWithBody> {
|
|
val actorsUnderMouse: List<ActorWithBody> = getActorsAt(mwx, mwy).filter { it !is InternalActor }.sortedBy {
|
|
(mwx - it.hitbox.centeredX).sqr() + (mwy - it.hitbox.centeredY).sqr()
|
|
} // sorted by the distance from the mouse
|
|
return actorsUnderMouse
|
|
}
|
|
|
|
fun pickupFixtureOrDroppedItem(actor: ActorWithBody, delta: Float, mwx: Double, mwy: Double, mtx: Int, mty: Int, assignToQuickslot: Boolean = true) {
|
|
val actorsUnderMouse = getActorsUnderMouse(mwx, mwy)
|
|
|
|
val fixture = actorsUnderMouse.firstOrNull {
|
|
it is FixtureBase && it.canBeDespawned &&
|
|
System.nanoTime() - it.spawnRequestedTime > 500000000 // don't pick up the fixture if it was recently placed (0.5 seconds)
|
|
} as? FixtureBase
|
|
val droppedItemNoAutoPickup = actorsUnderMouse.firstOrNull {
|
|
it is DroppedItem && it.noAutoPickup
|
|
} as? DroppedItem
|
|
val mob = actorsUnderMouse.firstOrNull {
|
|
it !is FixtureBase && it.physProp.usePhysics && !it.physProp.immobileBody
|
|
} as? ActorWithBody
|
|
|
|
// pickup a fixture
|
|
if (fixture != null) {
|
|
val fixtureItem = fixture.itemise()
|
|
printdbg(this, "Fixture pickup at F${WORLD_UPDATE_TIMER}: ${fixture.javaClass.canonicalName} -> $fixtureItem")
|
|
// 0. hide tooltips
|
|
TooltipManager.tooltipShowing.clear()
|
|
setTooltipMessage(null)
|
|
if (!fixture.flagDespawn) {
|
|
// 1. put the fixture to the inventory
|
|
fixture.flagDespawn()
|
|
// 2. register this item(fixture) to the quickslot so that the player sprite would be actually lifting the fixture
|
|
if (actor is Pocketed) {
|
|
actor.inventory.add(fixtureItem)
|
|
|
|
if (assignToQuickslot) {
|
|
actor.equipItem(fixtureItem)
|
|
actor.inventory.setQuickslotItemAtSelected(fixtureItem)
|
|
// 2-1. unregister if other slot has the same item
|
|
for (k in 0..9) {
|
|
if (actor.inventory.getQuickslotItem(k)?.itm == fixtureItem && k != actor.actorValue.getAsInt(
|
|
AVKey.__PLAYER_QUICKSLOTSEL
|
|
)
|
|
) {
|
|
actor.inventory.setQuickslotItem(k, null)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// pickup a dropped item (no auto-pickup variants only)
|
|
else if (droppedItemNoAutoPickup != null) {
|
|
val droppedItemID = droppedItemNoAutoPickup.itemID
|
|
val droppedItemCount = droppedItemNoAutoPickup.itemCount
|
|
// 0. hide tooltips
|
|
TooltipManager.tooltipShowing.clear()
|
|
setTooltipMessage(null)
|
|
if (!droppedItemNoAutoPickup.flagDespawn) {
|
|
// 1. put the fixture to the inventory
|
|
droppedItemNoAutoPickup.flagDespawn()
|
|
// 2. register this item(fixture) to the quickslot so that the player sprite would be actually lifting the fixture
|
|
// BUUUUUUUT, only when the slot is already empty this time
|
|
if (actor is Pocketed) {
|
|
actor.inventory.add(droppedItemID, droppedItemCount)
|
|
|
|
if (assignToQuickslot && actor.inventory.getQuickslotItem(actor.actorValue.getAsInt(AVKey.__PLAYER_QUICKSLOTSEL)) == null) {
|
|
actor.equipItem(droppedItemID)
|
|
actor.inventory.setQuickslotItemAtSelected(droppedItemID)
|
|
// 2-1. unregister if other slot has the same item
|
|
for (k in 0..9) {
|
|
if (actor.inventory.getQuickslotItem(k)?.itm == droppedItemID && k != actor.actorValue.getAsInt(
|
|
AVKey.__PLAYER_QUICKSLOTSEL
|
|
)
|
|
) {
|
|
actor.inventory.setQuickslotItem(k, null)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if (mob != null) {
|
|
// TODO pickup a mob
|
|
}
|
|
}
|
|
|
|
override fun hide() {
|
|
uiContainer.forEach { it?.handler?.dispose() }
|
|
}
|
|
|
|
/**
|
|
* @param width same as AppLoader.terrarumAppConfig.screenW
|
|
* @param height same as AppLoader.terrarumAppConfig.screenH
|
|
* @see net.torvald.terrarum.Terrarum
|
|
*/
|
|
override fun resize(width: Int, height: Int) {
|
|
|
|
// FIXME debugger is pointing at this thing, not sure it actually caused memleak
|
|
//MegaRainGovernor.resize()
|
|
|
|
|
|
IngameRenderer.resize(App.scr.width, App.scr.height)
|
|
val drawWidth = Toolkit.drawWidth
|
|
|
|
|
|
if (gameFullyLoaded) {
|
|
// resize UIs
|
|
|
|
notifier.setPosition(
|
|
(Toolkit.drawWidth - notifier.width) / 2,
|
|
App.scr.height - notifier.height - App.scr.tvSafeGraphicsHeight
|
|
)
|
|
noticelet.setPosition(0, 0)
|
|
uiQuickBar.setPosition((drawWidth - uiQuickBar.width) / 2, App.scr.tvSafeGraphicsHeight)
|
|
|
|
// inventory
|
|
/*uiInventoryPlayer =
|
|
UIInventory(player,
|
|
width = 840,
|
|
height = AppLoader.terrarumAppConfig.screenH - 160,
|
|
categoryWidth = 210
|
|
)*/
|
|
|
|
|
|
// basic watch-style notification bar (temperature, new mail)
|
|
uiBasicInfo.setPosition(drawWidth - uiBasicInfo.width, 0)
|
|
uiWatchTierOne.setPosition(
|
|
((drawWidth - App.scr.tvSafeGraphicsWidth) - (uiQuickBar.posX + uiQuickBar.width) - uiWatchTierOne.width) / 2 + (uiQuickBar.posX + uiQuickBar.width),
|
|
App.scr.tvSafeGraphicsHeight + 8
|
|
)
|
|
}
|
|
|
|
|
|
printdbg(this, "Resize event")
|
|
}
|
|
|
|
override fun dispose() {
|
|
visibleActorsRenderFarBehind.forEach { it.dispose() }
|
|
visibleActorsRenderBehind.forEach { it.dispose() }
|
|
visibleActorsRenderMiddle.forEach { it.dispose() }
|
|
visibleActorsRenderMidTop.forEach { it.dispose() }
|
|
visibleActorsRenderFront.forEach { it.dispose() }
|
|
visibleActorsRenderOverlay.forEach { it.dispose() }
|
|
|
|
uiContainer.forEach {
|
|
it?.handler?.dispose()
|
|
it?.dispose()
|
|
}
|
|
uiFixturesHistory.forEach {
|
|
try {
|
|
it.handler.dispose()
|
|
it.dispose()
|
|
}
|
|
catch (e: IllegalArgumentException) {}
|
|
}
|
|
|
|
musicStreamer.dispose()
|
|
super.dispose()
|
|
}
|
|
}
|