package net.torvald.terrarum.modulebasegame import com.badlogic.gdx.Gdx import com.badlogic.gdx.Input import com.badlogic.gdx.graphics.Camera import com.badlogic.gdx.graphics.g2d.SpriteBatch import net.torvald.EMDASH import net.torvald.terrarum.* import net.torvald.terrarum.App.* 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.BlockStats import net.torvald.terrarum.blockstats.MinimapComposer 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.GameItem import net.torvald.terrarum.gameitems.inInteractableRange import net.torvald.terrarum.gameparticles.ParticleBase import net.torvald.terrarum.gameworld.GameWorld import net.torvald.terrarum.gameworld.WorldSimulator import net.torvald.terrarum.modulebasegame.gameactors.* import net.torvald.terrarum.modulebasegame.gameactors.physicssolver.CollisionSolver import net.torvald.terrarum.modulebasegame.gameitems.PickaxeCore import net.torvald.terrarum.modulebasegame.gameworld.GameEconomy import net.torvald.terrarum.modulebasegame.ui.* 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.savegame.VDUtil import net.torvald.terrarum.savegame.VirtualDisk import net.torvald.terrarum.serialise.Common import net.torvald.terrarum.serialise.LoadSavegame import net.torvald.terrarum.serialise.ReadActor import net.torvald.terrarum.serialise.WriteSavegame import net.torvald.terrarum.ui.Toolkit 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.util.CircularArray import org.khelekore.prtree.PRTree import java.util.* import java.util.concurrent.locks.ReentrantLock /** * Ingame instance for the game Terrarum. * * Created by minjaesong on 2017-06-16. */ open class TerrarumIngame(batch: SpriteBatch) : IngameInstance(batch) { var WORLD_UPDATE_TIMER = Random().nextInt(1020) + 1; private set 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) val uiContainer = UIContainer() // these are required because actors always change their position 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: Camera, newX: Float, newY: Float) { camera.position.set((-newX + App.scr.halfw).round(), (-newY + App.scr.halfh).round(), 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: J${Terrarum.memJavaHeap}M / N${Terrarum.memNativeHeap}M / U${Terrarum.memUnsafe}M / X${Terrarum.memXmx}M" else "" val ACTOR_UPDATE_RANGE = 4096 fun distToActorSqr(world: GameWorld, a: ActorWithBody, p: ActorWithBody) = minOf(// take min of normal position and wrapped (x < 0) position (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) = minOf( (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() ) /** whether the actor is within update range */ fun ActorWithBody.inUpdateRange(world: GameWorld) = distToCameraSqr(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(6030, 1800) val SIZE_NORMAL = Point2i(9000, 2250) val SIZE_LARGE = Point2i(13500, 2970) val SIZE_HUGE = Point2i(22500, 4500) val WORLDSIZE = arrayOf(SIZE_SMALL, SIZE_NORMAL, SIZE_LARGE, SIZE_HUGE) } init { } lateinit var uiBlur: UIFakeBlurOverlay lateinit var uiPieMenu: UIQuickslotPie lateinit var uiQuickBar: UIQuickslotBar lateinit var uiInventoryPlayer: UIInventoryFull /** * 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 uiCheatMotherfuckerNootNoot: 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 ////////////// // GDX code // ////////////// lateinit var gameLoadMode: GameLoadMode lateinit var gameLoadInfoPayload: Any enum class GameLoadMode { CREATE_NEW, LOAD_FROM } 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) super.show() // this function sets gameInitialised = true } data class NewGameParams( val player: IngamePlayer, val newWorldParams: NewWorldParameters ) 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 ) /** * 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(codices.world.randSeeds[0], codices.world.randSeeds[1]) WeatherMixer.loadFromSave(codices.world.randSeeds[2], codices.world.randSeeds[3]) // Terrarum.itemCodex.loadFromSave(codices.item) // Terrarum.apocryphas = HashMap(codices.apocryphas) } } /** 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... addNewActor(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(Actor.RenderOrder.MIDDLE) addNewActor(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) { codices.player.actorValue = it.actorValue!! 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.spawnX * TILE_SIZED, world.spawnY * TILE_SIZED) } } // by doing this, whatever the "possession" the player had will be broken by the game load actorNowPlaying = codices.player actorGamer = codices.player // don't put it on the postInit() or render(); postInitForNewGame calls this function on the savegamewriter's callback makeSavegameBackupCopy(getWorldSaveFiledesc(worldSavefileName)) makeSavegameBackupCopy(getPlayerSaveFiledesc(playerSavefileName)) } private fun postInitForNewGame() { worldSavefileName = LoadSavegame.getWorldSavefileName(savegameNickname, world) playerSavefileName = LoadSavegame.getPlayerSavefileName(actorGamer) worldDisk = VDUtil.createNewDisk( 1L shl 60, savegameNickname, Common.CHARSET ) playerDisk = VDUtil.createNewDisk( 1L shl 60, actorGamer.actorValue.getAsString(AVKey.NAME) ?: "", Common.CHARSET ) // go to spawn position printdbg(this, "World Spawn position: (${world.spawnX}, ${world.spawnY})") actorGamer.setPosition( world.spawnX * TILE_SIZED, world.spawnY * TILE_SIZED ) actorGamer.backupPlayerProps(isMultiplayer) val onError = { e: Throwable -> uiAutosaveNotifier.setAsError() } // 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() WriteSavegame.immediate(saveTime_t, WriteSavegame.SaveMode.PLAYER, playerDisk, getPlayerSaveFiledesc(playerSavefileName), this, true, onError) { makeSavegameBackupCopy(getPlayerSaveFiledesc(playerSavefileName)) WriteSavegame.immediate(saveTime_t, WriteSavegame.SaveMode.WORLD, worldDisk, getWorldSaveFiledesc(worldSavefileName), this, true, onError) { makeSavegameBackupCopy(getWorldSaveFiledesc(worldSavefileName)) // don't put it on the postInit() or render(); must be called using callback uiAutosaveNotifier.setAsClose() } } } /** * 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} version ${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(worldParams.worldGenSeed)) Worldgen.generateMap() historicalFigureIDBucket = ArrayList() savegameNickname = worldParams.savegameName world.worldCreator = UUID.fromString(player.uuid.toString()) printdbg(this, "new woridIndex: ${world.worldIndex}") printdbg(this, "worldCurrentlyPlaying: ${player.worldCurrentlyPlaying}") actorNowPlaying = player actorGamer = player addNewActor(player) } KeyToggler.forceSet(Input.Keys.Q, false) } 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 } // init console window // TODO test put it on the IngameInstance.(init) //consoleHandler = ConsoleWindow() //consoleHandler.setPosition(0, 0) 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(drawWidth / 2, 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 = UITierOneWatch() 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) uiCheatMotherfuckerNootNoot = 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, uiBlur, uiPieMenu, uiQuickBar, // uiBasicInfo, // temporarily commenting out: wouldn't make sense for v 0.3 release uiWatchTierOne, getWearableDeviceUI, UIScreenZoom(), uiAutosaveNotifier, uiInventoryPlayer, getUIFixture, uiTooltip, consoleHandler, uiCheatMotherfuckerNootNoot // drawn last ) ingameUpdateThread = ThreadIngameUpdate(this) updateThreadWrapper = Thread(ingameUpdateThread, "Terrarum UpdateThread") // these need to appear on top of any others uiContainer.add(notifier) App.setDebugTime("Ingame.UpdateCounter", 0) // some sketchy test code here }// END enter 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 canPerformBarehandAction = actor.scale * actor.baseHitboxH >= actor.actorValue.getAsDouble(AVKey.BAREHAND_MINHEIGHT) ?: 4294967296.0 // TODO actorsUnderMouse: support ROUNDWORLD val actorsUnderMouse: List = getActorsAt(Terrarum.mouseX, Terrarum.mouseY).filterIsInstance() if (actorsUnderMouse.size > 1) { App.printdbgerr(this, "Multiple fixtures at world coord ${Terrarum.mouseX}, ${Terrarum.mouseY}") } //////////////////////////////// // #1. Try to open a UI under the cursor // 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 (itemOnGrip?.inventoryCategory != GameItem.Category.TOOL) { // don't open the UI when player's holding a tool for (kk in actorsUnderMouse.indices) { actorsUnderMouse[kk].mainUI?.let { 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 = it it.setPosition(0, 0) it.setAsOpen() } break } } // #2. If there is no UI under and if I'm holding an item, use it // don't want to open the UI and use the item at the same time, would ya? if (!uiOpened && itemOnGrip != null) { val consumptionSuccessful = itemOnGrip.startPrimaryUse(actor, delta) if (consumptionSuccessful) (actor as Pocketed).inventory.consumeItem(itemOnGrip) } // #3. If I'm not holding any item and I can do barehandaction (size big enough that barehandactionminheight check passes), perform it else if (itemOnGrip == null && canPerformBarehandAction) { inInteractableRange(actor) { performBarehandAction(actor, delta) true } } } 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) { actor.actorValue.set(AVKey.__ACTION_TIMER, 0.0) } } // I have decided that left and right clicks must do the same thing, so no secondary use from now on. --Torvald on 2019-05-26 /*override fun worldSecondaryClickStart(delta: Float) { val itemOnGrip = actorNowPlaying?.inventory?.itemEquipped?.get(GameItem.EquipPosition.HAND_GRIP) val consumptionSuccessful = ItemCodex[itemOnGrip]?.startSecondaryUse(delta) ?: false if (consumptionSuccessful) actorNowPlaying?.inventory?.consumeItem(ItemCodex[itemOnGrip]!!) } override fun worldSecondaryClickEnd(delta: Float) { val itemOnGrip = actorNowPlaying?.inventory?.itemEquipped?.get(GameItem.EquipPosition.HAND_GRIP) ItemCodex[itemOnGrip]?.endSecondaryUse(delta) }*/ private var firstTimeRun = true /////////////// // prod code // /////////////// private class ThreadIngameUpdate(val terrarumIngame: TerrarumIngame): Runnable { override fun run() { TODO() } } private var updateAkku = 0f private var autosaveTimer = 0f override fun render(`_`: 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() gameFullyLoaded = true } ingameController.update() // define custom update rate val updateRate = App.UPDATE_RATE // if (KeyToggler.isOn(Input.Keys.APOSTROPHE)) 1f / 8f else App.UPDATE_RATE // ASYNCHRONOUS UPDATE AND RENDER // /** UPDATE CODE GOES HERE */ val dt = Gdx.graphics.deltaTime updateAkku += dt autosaveTimer += dt var i = 0L while (updateAkku >= updateRate) { measureDebugTime("Ingame.Update") { updateGame(updateRate) } updateAkku -= updateRate i += 1 } setDebugTime("Ingame.UpdateCounter", i) /** RENDER CODE GOES HERE */ measureDebugTime("Ingame.Render") { renderGame() } val autosaveInterval = App.getConfigInt("autosaveinterval") / 1000f if (autosaveTimer >= autosaveInterval) { queueAutosave() autosaveTimer -= autosaveInterval } } private var worldWidth: Double = 0.0 private var oldCamX = 0 /** * Ingame (world) related updates; UI update must go to renderGame() */ protected fun 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 || newWorldLoadedLatch) { //hypothetical_input_capturing_function_if_you_finally_decided_to_forgo_gdx_input_processor_and_implement_your_own_to_synchronise_everything() WorldSimulator.resetForThisFrame() //////////////////////////// // camera-related updates // //////////////////////////// FeaturesDrawer.update(delta) /////////////////////////// // actor-related updates // /////////////////////////// repossessActor() // determine whether the inactive actor should be activated wakeDormantActors() // determine whether the actor should keep being activated or be dormant killOrKnockdownActors() updateActors(delta) particlesContainer.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") { BlockStats.update() } // fill up visibleActorsRenderFront for wires, if: // 0. Camera wrapped // 1. new world has been loaded // 2. something is cued on the wire change queue // 3. wire renderclass changed if (Math.abs(WorldCamera.x - oldCamX) >= worldWidth * 0.85 || newWorldLoadedLatch || wireChangeQueue.isNotEmpty() || selectedWireRenderClass != oldSelectedWireRenderClass) { measureDebugTime("Ingame.FillUpWiresBuffer") { fillUpWiresBuffer() } } oldCamX = WorldCamera.x WORLD_UPDATE_TIMER += 1 } if (!paused || newWorldLoadedLatch) { // completely consume block change queues because why not terrainChangeQueue.clear() wallChangeQueue.clear() wireChangeQueue.clear() oldSelectedWireRenderClass = selectedWireRenderClass } //////////////////////// // ui-related updates // //////////////////////// //uiContainer.forEach { it.update(delta) } //debugWindow.update(delta) //notifier.update(delta) // open/close fake blur UI according to what's opened if (uiInventoryPlayer.isVisible || getUIFixture.get()?.isVisible == true) { uiBlur.setAsOpen() } else { uiBlur.setAsClose() } // update debuggers using javax.swing // if (Authenticator.b()) { AVTracker.update() ActorsList.update() } //println("paused = $paused") if (!paused && newWorldLoadedLatch) newWorldLoadedLatch = false } private fun renderGame() { Gdx.graphics.setTitle(getCanonicalTitle()) WorldCamera.update(world, actorNowPlaying) measureDebugTime("Ingame.FilterVisibleActors") { filterVisibleActors() } uiContainer.forEach { when (it) { is UICanvas -> it.update(Gdx.graphics.deltaTime) is Id_UICanvasNullable -> it.get()?.update(Gdx.graphics.deltaTime) } } //uiFixture?.update(Gdx.graphics.deltaTime) // deal with the uiFixture being closed if (uiFixture?.isClosed == true) { uiFixture = null } IngameRenderer.invoke( paused, screenZoom, visibleActorsRenderBehind, visibleActorsRenderMiddle, visibleActorsRenderMidTop, visibleActorsRenderFront, visibleActorsRenderOverlay, particlesContainer, actorNowPlaying, uiContainer// + uiFixture ) } private val maxRenderableWires = ReferencingRanges.ACTORS_WIRES.endInclusive - ReferencingRanges.ACTORS_WIRES.first + 1 private val wireActorsContainer = Array(maxRenderableWires) { WireActor(ReferencingRanges.ACTORS_WIRES.first + it).let { addNewActor(it) /*^let*/ it } } private fun fillUpWiresBuffer() { val for_y_start = (WorldCamera.y.toFloat() / TILE_SIZE).floorInt() val for_y_end = for_y_start + BlocksDrawer.tilesInVertical - 1 val for_x_start = (WorldCamera.x.toFloat() / TILE_SIZE).floorInt() val for_x_end = for_x_start + BlocksDrawer.tilesInHorizontal - 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 world.getAllWiresFrom(x, y)?.forEach { val wireActor = wireActorsContainer[wiringCounter] wireActor.setWire(it, x, y) if (WireCodex[it].renderClass == selectedWireRenderClass || selectedWireRenderClass == "wire_render_all") { wireActor.renderOrder = Actor.RenderOrder.OVERLAY } else { wireActor.renderOrder = Actor.RenderOrder.BEHIND } wireActor.isUpdate = true wireActor.isVisible = true wireActor.forceDormant = false wiringCounter += 1 } } } for (i in wiringCounter until maxRenderableWires) { wireActorsContainer[i].isUpdate = false wireActorsContainer[i].isVisible = false wireActorsContainer[i].forceDormant = true } } private fun filterVisibleActors() { 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 //WorldSimulator(actorNowPlaying, AppLoader.getSmoothDelta().toFloat()) } 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.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.flagDespawn) { removeActor(actor) actorContainerSize -= 1 i-- // array removed 1 elem, so we also decrement counter by 1 } // inactivate distant actors else if (actor is ActorWithBody && (!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 { actorContainerActive.forEach { if (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]!!.effectWhenEquipped(it as ActorWithBody, delta) } } } if (it is CuedByTerrainChange) { terrainChangeQueue.forEach { cue -> printdbg(this, "Ingame actors terrainChangeCue: ${cue}") it.updateForTerrainChange(cue) } } if (it is CuedByWallChange) { wallChangeQueue.forEach { cue -> printdbg(this, "Ingame actors wallChangeCue: ${cue}") it.updateForWallChange(cue) } } if (it is CuedByWireChange) { wireChangeQueue.forEach { cue -> printdbg(this, "Ingame actors wireChangeCue: ${cue}") it.updateForWireChange(cue) } } } } actorNowPlaying?.update(delta) //AmmoMeterProxy(player, uiVitalItem.UI as UIVitalMetre) } } fun queueAutosave() { val start = System.nanoTime() /*uiAutosaveNotifier.setAsOpen() makeSavegameBackupCopy() WriteSavegame.quick(savegameArchive, getSaveFileMain(), this, true) { uiAutosaveNotifier.setAsClose() debugTimers.put("Last Autosave Duration", System.nanoTime() - start) }*/ } 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.BEHIND -> visibleActorsRenderBehind Actor.RenderOrder.MIDDLE -> visibleActorsRenderMiddle Actor.RenderOrder.MIDTOP -> visibleActorsRenderMidTop Actor.RenderOrder.FRONT -> visibleActorsRenderFront Actor.RenderOrder.OVERLAY-> visibleActorsRenderOverlay } } override fun removeActor(ID: Int) = removeActor(getActorByID(ID)) /** * get index of the actor and delete by the index. * we can do this as the list is guaranteed to be sorted * and only contains unique values. * * Any values behind the index will be automatically pushed to front. * This is how remove function of [java.util.ArrayList] is defined. */ override fun removeActor(actor: Actor?) { if (actor == null) return // if (actor.referenceID == actorGamer.referenceID || actor.referenceID == 0x51621D) // do not delete this magic // throw ProtectedActorRemovalException("Player") forceRemoveActor(actor) } override fun forceRemoveActor(actor: Actor) { 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 addNewActor(actor: Actor?) { if (actor == null) return if (App.IS_DEVELOPMENT_BUILD && theGameHasActor(actor.referenceID)) { throw ReferencedActorAlreadyExistsException(actor) } else { if (actor.referenceID !in ReferencingRanges.ACTORS_WIRES && actor.referenceID !in ReferencingRanges.ACTORS_WIRES_HELPER) { printdbg(this, "Adding actor $actor") printStackTrace(this) } 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 (App.IS_DEVELOPMENT_BUILD && !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 fun insertionSortLastElemAV(arr: ArrayList) { // out-projection doesn't work, duh ReentrantLock().lock { var j = arr.lastIndex - 1 val x = arr.last() while (j >= 0 && arr[j] > x) { arr[j + 1] = arr[j] j -= 1 } arr[j + 1] = x } } fun performBarehandAction(actor: ActorWithBody, delta: Float) { // println("whack!") 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 } val punchSize = actor.scale * actor.actorValue.getAsDouble(AVKey.BAREHAND_BASE_DIGSIZE)!! // if there are attackable actor (todo) on the "actor punch hitbox (todo)", attack them (todo) val actorsUnderMouse: List = getActorsAtVicinity(Terrarum.mouseX, Terrarum.mouseY, punchSize / 2.0).filter { true } // else, punch a block val punchBlockSize = punchSize.div(TILE_SIZED).floorInt() if (punchBlockSize > 0) { PickaxeCore.startPrimaryUse(actor, delta, null, Terrarum.mouseTileX, Terrarum.mouseTileY, 1.0 / punchBlockSize, punchBlockSize, punchBlockSize, false) } } 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 (gameInitialised) { //LightmapRenderer.fireRecalculateEvent() } if (gameFullyLoaded) { // resize UIs notifier.setPosition( (drawWidth - notifier.width) / 2, App.scr.height - notifier.height) 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 ) } println("[Ingame] Resize event") } override fun 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) {} } super.dispose() } }