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.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.BlockStats import net.torvald.terrarum.blockstats.MinimapComposer 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.GameItem import net.torvald.terrarum.gameitems.mouseInInteractableRange 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.LightmapRenderer.LIGHTMAP_OVERRENDER 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) val worldgenThreadExecutor = ThreadExecutor() } 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) { if (mouseInInteractableRange(actor) { 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( (Toolkit.drawWidth - it.width) / 4, (App.scr.height - it.height) / 4 // what the fuck? ) it.setAsOpen() } true }) 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) { mouseInInteractableRange(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 private var oldPlayerX = 0.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 but not on every update measureDebugTime("Ingame.FillUpWiresBuffer*") { if (WORLD_UPDATE_TIMER % 2 == 1) { fillUpWiresBuffer() } } oldCamX = WorldCamera.x oldPlayerX = actorNowPlaying?.hitbox?.canonicalX ?: 0.0 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() - LIGHTMAP_OVERRENDER val for_y_end = for_y_start + BlocksDrawer.tilesInVertical + 2*LIGHTMAP_OVERRENDER val for_x_start = (WorldCamera.x.toFloat() / TILE_SIZE).floorInt() - LIGHTMAP_OVERRENDER val for_x_end = for_x_start + BlocksDrawer.tilesInHorizontal + 2*LIGHTMAP_OVERRENDER 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]!!.effectWhileEquipped(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() } }