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 = ArrayList() /** * list of Actors that is sorted by Actors' referenceID */ //val ACTORCONTAINER_INITIAL_SIZE = 64 val PARTICLES_MAX = App.getConfigInt("maxparticles") val particlesContainer = CircularArray(PARTICLES_MAX, true) // these are required because actors always change their position private var visibleActorsRenderFarBehind: ArrayList = ArrayList(1) private var visibleActorsRenderBehind: ArrayList = ArrayList(1) private var visibleActorsRenderMiddle: ArrayList = ArrayList(1) private var visibleActorsRenderMidTop: ArrayList = ArrayList(1) private var visibleActorsRenderFront: ArrayList = ArrayList(1) private var visibleActorsRenderOverlay: ArrayList = 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() 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, val actors: List, 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() 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 = getActorsUnderMouse(Terrarum.mouseX, Terrarum.mouseY).filterIsInstance() 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() 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()) 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() 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().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) { 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().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 { 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 { val outList = java.util.ArrayList() 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 { val actorsUnderMouse: List = 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() } }