diff options
| author | nea <nea@nea.moe> | 2023-07-11 21:01:58 +0200 |
|---|---|---|
| committer | nea <nea@nea.moe> | 2023-07-11 21:01:58 +0200 |
| commit | 4d93f475aadc42c4bf83c3a0749af41659235c71 (patch) | |
| tree | c8e01710defe66e06c50fa962c72fdac66a35a1f | |
| parent | 4444fcca44d9a53c8162d69e0e9f19fd214c2f54 (diff) | |
| download | Firmament-4d93f475aadc42c4bf83c3a0749af41659235c71.tar.gz Firmament-4d93f475aadc42c4bf83c3a0749af41659235c71.tar.bz2 Firmament-4d93f475aadc42c4bf83c3a0749af41659235c71.zip | |
Bulk commit
39 files changed, 951 insertions, 40 deletions
@@ -1,7 +1,10 @@ +Prio 0 (Bugs): +- WorldReadyEvent buggy? -> out of date locraw + +Priority 1: - recipes - more recipe categories - dfu cache -- replace REI with custom renderer (if needed) - Storage Overlay - PV - NEU buttons diff --git a/build.gradle.kts b/build.gradle.kts index f944d2a..3b0b945 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,8 +4,9 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { java `maven-publish` - kotlin("jvm") version "1.8.10" - kotlin("plugin.serialization") version "1.8.10" + kotlin("jvm") version "1.8.20" + kotlin("plugin.serialization") version "1.8.20" + id("com.bnorm.power.kotlin-power-assert") version "0.13.0" id("dev.architectury.loom") version "1.1.336" id("com.github.johnrengelman.shadow") version "7.1.2" id("moe.nea.licenseextractificator") diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index aa5c2b2..8d64cb3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -16,6 +16,7 @@ qolify = "1.3.0-1.20" citresewn = "1.1.3+1.20" hotswap_agent = "1.4.2-SNAPSHOT" sodium = "mc1.20-0.4.10" +freecammod = "1.2.0-mc1.20" ncr = "Fabric-1.20-v2.2.0" mixinextras = "0.2.0-beta.9" @@ -43,12 +44,14 @@ qolify = { module = "maven.modrinth:qolify", version.ref = "qolify" } citresewn = { module = "maven.modrinth:cit-resewn", version.ref = "citresewn" } ncr = { module = "maven.modrinth:no-chat-reports", version.ref = "ncr" } sodium = { module = "maven.modrinth:sodium", version.ref = "sodium" } +freecammod = { module = "maven.modrinth:freecam", version.ref = "freecammod" } [bundles] dbus = ["dbus_java_core", "dbus_java_unixsocket"] runtime_required = ["architectury_fabric", "rei_fabric"] runtime_optional = [ "devauth", + "freecammod", "sodium", "qolify", "citresewn", diff --git a/src/main/java/moe/nea/firmament/mixins/MixinMouse.java b/src/main/java/moe/nea/firmament/mixins/MixinMouse.java new file mode 100644 index 0000000..19cdcd2 --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/MixinMouse.java @@ -0,0 +1,38 @@ +package moe.nea.firmament.mixins; + +import kotlin.Pair; +import moe.nea.firmament.features.inventory.SaveCursorPosition; +import net.minecraft.client.Mouse; +import org.objectweb.asm.Opcodes; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(Mouse.class) +public class MixinMouse { + @Shadow + private double x; + + @Shadow + private double y; + + @Inject(method = "lockCursor", at = @At(value = "FIELD", opcode = Opcodes.PUTFIELD, target = "Lnet/minecraft/client/Mouse;cursorLocked:Z")) + public void onLockCursor(CallbackInfo ci) { + SaveCursorPosition.saveCursorOriginal(x, y); + } + + @Inject(method = "lockCursor", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/util/Window;getHandle()J")) + public void onLockCursorAfter(CallbackInfo ci) { + SaveCursorPosition.saveCursorMiddle(x, y); + } + + @Inject(method = "unlockCursor", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/util/Window;getHandle()J")) + public void onUnlockCursor(CallbackInfo ci) { + Pair<Double, Double> cursorPosition = SaveCursorPosition.loadCursor(this.x, this.y); + if (cursorPosition == null) return; + this.x = cursorPosition.getFirst(); + this.y = cursorPosition.getSecond(); + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/MixinWorldRenderer.java b/src/main/java/moe/nea/firmament/mixins/MixinWorldRenderer.java index 2aebd8e..94bbef4 100644 --- a/src/main/java/moe/nea/firmament/mixins/MixinWorldRenderer.java +++ b/src/main/java/moe/nea/firmament/mixins/MixinWorldRenderer.java @@ -19,25 +19,29 @@ package moe.nea.firmament.mixins; import moe.nea.firmament.events.WorldRenderLastEvent; -import net.minecraft.client.render.Camera; -import net.minecraft.client.render.GameRenderer; -import net.minecraft.client.render.LightmapTextureManager; -import net.minecraft.client.render.WorldRenderer; +import net.minecraft.client.render.*; import net.minecraft.client.util.math.MatrixStack; import org.joml.Matrix4f; +import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; @Mixin(WorldRenderer.class) public class MixinWorldRenderer { + @Shadow + @Final + private BufferBuilderStorage bufferBuilders; + @Inject(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/WorldRenderer;renderChunkDebugInfo(Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumerProvider;Lnet/minecraft/client/render/Camera;)V", shift = At.Shift.BEFORE)) public void onWorldRenderLast(MatrixStack matrices, float tickDelta, long limitTime, boolean renderBlockOutline, Camera camera, GameRenderer gameRenderer, LightmapTextureManager lightmapTextureManager, Matrix4f positionMatrix, CallbackInfo ci) { var event = new WorldRenderLastEvent( matrices, tickDelta, renderBlockOutline, camera, gameRenderer, lightmapTextureManager, - positionMatrix + positionMatrix, + this.bufferBuilders.getEntityVertexConsumers() ); WorldRenderLastEvent.Companion.publish(event); } diff --git a/src/main/kotlin/moe/nea/firmament/Firmament.kt b/src/main/kotlin/moe/nea/firmament/Firmament.kt index 2e1cd05..46329f1 100644 --- a/src/main/kotlin/moe/nea/firmament/Firmament.kt +++ b/src/main/kotlin/moe/nea/firmament/Firmament.kt @@ -19,13 +19,15 @@ package moe.nea.firmament import com.mojang.brigadier.CommandDispatcher -import io.ktor.client.* -import io.ktor.client.plugins.* -import io.ktor.client.plugins.cache.* -import io.ktor.client.plugins.compression.* -import io.ktor.client.plugins.contentnegotiation.* -import io.ktor.client.plugins.logging.* -import io.ktor.serialization.kotlinx.json.* +import dev.architectury.event.events.client.ClientTickEvent +import io.ktor.client.HttpClient +import io.ktor.client.plugins.UserAgent +import io.ktor.client.plugins.cache.HttpCache +import io.ktor.client.plugins.compression.ContentEncoding +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.client.plugins.logging.LogLevel +import io.ktor.client.plugins.logging.Logging +import io.ktor.serialization.kotlinx.json.json import java.nio.file.Files import java.nio.file.Path import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback @@ -49,6 +51,7 @@ import net.minecraft.command.CommandRegistryAccess import net.minecraft.util.Identifier import moe.nea.firmament.commands.registerFirmamentCommand import moe.nea.firmament.dbus.FirmamentDbusObject +import moe.nea.firmament.events.TickEvent import moe.nea.firmament.features.FeatureManager import moe.nea.firmament.repo.HypixelStaticData import moe.nea.firmament.repo.RepoManager @@ -114,6 +117,10 @@ object Firmament { @JvmStatic fun onClientInitialize() { dbusConnection.requestBusName("moe.nea.firmament") + var tick = 0 + ClientTickEvent.CLIENT_POST.register(ClientTickEvent.Client { instance -> + TickEvent.publish(TickEvent(tick++)) + }) dbusConnection.exportObject(FirmamentDbusObject) IDataHolder.registerEvents() RepoManager.initialize() diff --git a/src/main/kotlin/moe/nea/firmament/commands/rome.kt b/src/main/kotlin/moe/nea/firmament/commands/rome.kt index 2237abd..11aaf17 100644 --- a/src/main/kotlin/moe/nea/firmament/commands/rome.kt +++ b/src/main/kotlin/moe/nea/firmament/commands/rome.kt @@ -22,6 +22,7 @@ import com.mojang.brigadier.CommandDispatcher import com.mojang.brigadier.arguments.StringArgumentType.string import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource import net.minecraft.text.Text +import moe.nea.firmament.features.inventory.storageoverlay.StorageOverlayScreen import moe.nea.firmament.features.world.FairySouls import moe.nea.firmament.gui.config.AllConfigsGui import moe.nea.firmament.gui.profileviewer.ProfileViewer @@ -30,6 +31,7 @@ import moe.nea.firmament.repo.RepoManager import moe.nea.firmament.util.FirmFormatters import moe.nea.firmament.util.MC import moe.nea.firmament.util.SBData +import moe.nea.firmament.util.ScreenUtil import moe.nea.firmament.util.SkyblockId import moe.nea.firmament.util.unformattedString @@ -40,6 +42,12 @@ fun firmamentCommand() = literal("firmament") { AllConfigsGui.showAllGuis() } } + thenLiteral("storage") { + thenExecute { + ScreenUtil.setScreenLater(StorageOverlayScreen()) + MC.player?.networkHandler?.sendChatCommand("ec") + } + } thenLiteral("repo") { thenLiteral("reload") { thenLiteral("fetch") { diff --git a/src/main/kotlin/moe/nea/firmament/events/TickEvent.kt b/src/main/kotlin/moe/nea/firmament/events/TickEvent.kt new file mode 100644 index 0000000..5d9b0ce --- /dev/null +++ b/src/main/kotlin/moe/nea/firmament/events/TickEvent.kt @@ -0,0 +1,5 @@ +package moe.nea.firmament.events + +data class TickEvent(val tickCount: Int) : FirmamentEvent() { + companion object : FirmamentEventBus<TickEvent>() +} diff --git a/src/main/kotlin/moe/nea/firmament/features/FeatureManager.kt b/src/main/kotlin/moe/nea/firmament/features/FeatureManager.kt index e465349..0f0a166 100644 --- a/src/main/kotlin/moe/nea/firmament/features/FeatureManager.kt +++ b/src/main/kotlin/moe/nea/firmament/features/FeatureManager.kt @@ -21,10 +21,13 @@ package moe.nea.firmament.features import kotlinx.serialization.Serializable import kotlinx.serialization.serializer import moe.nea.firmament.Firmament +import moe.nea.firmament.features.debug.DebugView import moe.nea.firmament.features.debug.DeveloperFeatures import moe.nea.firmament.features.fishing.FishingWarning import moe.nea.firmament.features.inventory.CraftingOverlay +import moe.nea.firmament.features.inventory.SaveCursorPosition import moe.nea.firmament.features.inventory.SlotLocking +import moe.nea.firmament.features.inventory.storageoverlay.StorageOverlay import moe.nea.firmament.features.world.FairySouls import moe.nea.firmament.util.data.DataHolder @@ -50,9 +53,13 @@ object FeatureManager : DataHolder<FeatureManager.Config>(serializer(), "feature loadFeature(FairySouls) loadFeature(FishingWarning) loadFeature(SlotLocking) + loadFeature(StorageOverlay) loadFeature(CraftingOverlay) - if (Firmament.DEBUG) + loadFeature(SaveCursorPosition) + if (Firmament.DEBUG) { loadFeature(DeveloperFeatures) + loadFeature(DebugView) + } hasAutoloaded = true } } diff --git a/src/main/kotlin/moe/nea/firmament/features/FirmamentFeature.kt b/src/main/kotlin/moe/nea/firmament/features/FirmamentFeature.kt index e394fb7..94b69f1 100644 --- a/src/main/kotlin/moe/nea/firmament/features/FirmamentFeature.kt +++ b/src/main/kotlin/moe/nea/firmament/features/FirmamentFeature.kt @@ -21,7 +21,6 @@ package moe.nea.firmament.features import moe.nea.firmament.gui.config.ManagedConfig interface FirmamentFeature { - val name: String val identifier: String val defaultEnabled: Boolean get() = true diff --git a/src/main/kotlin/moe/nea/firmament/features/debug/DebugView.kt b/src/main/kotlin/moe/nea/firmament/features/debug/DebugView.kt new file mode 100644 index 0000000..e8c85d8 --- /dev/null +++ b/src/main/kotlin/moe/nea/firmament/features/debug/DebugView.kt @@ -0,0 +1,53 @@ +package moe.nea.firmament.features.debug + +import io.github.cottonmc.cotton.gui.client.CottonHud +import io.github.cottonmc.cotton.gui.widget.WBox +import io.github.cottonmc.cotton.gui.widget.data.Axis +import java.util.Optional +import kotlin.time.Duration.Companion.seconds +import net.minecraft.scoreboard.Scoreboard +import net.minecraft.scoreboard.Team +import net.minecraft.text.StringVisitable +import net.minecraft.text.Style +import net.minecraft.text.Text +import net.minecraft.util.Formatting +import moe.nea.firmament.Firmament +import moe.nea.firmament.events.TickEvent +import moe.nea.firmament.features.FirmamentFeature +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.TimeMark + +object DebugView : FirmamentFeature { + private data class StoredVariable<T>( + val obj: T, + val timer: TimeMark, + ) + + private val storedVariables: MutableMap<String, StoredVariable<*>> = mutableMapOf() + override val identifier: String + get() = "debug-view" + override val defaultEnabled: Boolean + get() = Firmament.DEBUG + + fun <T : Any?> showVariable(label: String, obj: T) { + synchronized(this) { + storedVariables[label] = StoredVariable(obj, TimeMark.now()) + } + } + + val debugWidget = WBox(Axis.VERTICAL) + + + override fun onLoad() { + TickEvent.subscribe { + synchronized(this) { + storedVariables.entries.removeIf { it.value.timer.passedTime() > 1.seconds } + if (storedVariables.isEmpty()) { + CottonHud.add(debugWidget, 20, 20) + } else { + CottonHud.remove(debugWidget) + } + } + } + } +} diff --git a/src/main/kotlin/moe/nea/firmament/features/debug/DeveloperFeatures.kt b/src/main/kotlin/moe/nea/firmament/features/debug/DeveloperFeatures.kt index e89ee6f..253bc0d 100644 --- a/src/main/kotlin/moe/nea/firmament/features/debug/DeveloperFeatures.kt +++ b/src/main/kotlin/moe/nea/firmament/features/debug/DeveloperFeatures.kt @@ -14,8 +14,6 @@ import moe.nea.firmament.util.TimeMark import moe.nea.firmament.util.iterate object DeveloperFeatures : FirmamentFeature { - override val name: String - get() = "developer" override val identifier: String get() = "developer" override val config: TConfig diff --git a/src/main/kotlin/moe/nea/firmament/features/debug/ObjectRenderer.kt b/src/main/kotlin/moe/nea/firmament/features/debug/ObjectRenderer.kt new file mode 100644 index 0000000..f5185cd --- /dev/null +++ b/src/main/kotlin/moe/nea/firmament/features/debug/ObjectRenderer.kt @@ -0,0 +1,52 @@ +package moe.nea.firmament.features.debug + +import io.github.cottonmc.cotton.gui.widget.WBox +import io.github.cottonmc.cotton.gui.widget.WLabel +import io.github.cottonmc.cotton.gui.widget.WWidget +import io.github.cottonmc.cotton.gui.widget.data.Axis +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import kotlin.reflect.KProperty1 +import net.minecraft.text.Text +import moe.nea.firmament.gui.WSpacer + +class ObjectRenderer(val box: WBox) { + var indent = 0 + + fun beginObject() { + indent++ + } + + fun endObject() { + indent-- + } + + fun emit(label: String, widget: WWidget) { + WSpacer(WBox(Axis.VERTICAL).also { + it.add(WWidget()) + it.add(widget) + }, indent * 18) + } + + fun <T : Any?> getDebuggingView(label: String, obj: T) { + if (obj == null) { + emit(label, WLabel(Text.literal("§cnull"))) + return + } + if (obj is String) { + emit(label, WLabel(Text.literal(Json.encodeToString(obj)))) + } + getObject(label, obj) + } + + fun <T : Any> getObject(label: String, obj: T) { + emit(label, WLabel(Text.literal(obj::class.simpleName ?: "<unknown>"))) + beginObject() + for (prop in obj::class.members.filterIsInstance<KProperty1<T, *>>()) { + val child = prop.get(obj) + getDebuggingView(prop.name, child) + } + endObject() + } + +} diff --git a/src/main/kotlin/moe/nea/firmament/features/fishing/FishingWarning.kt b/src/main/kotlin/moe/nea/firmament/features/fishing/FishingWarning.kt index fa8d779..4c20450 100644 --- a/src/main/kotlin/moe/nea/firmament/features/fishing/FishingWarning.kt +++ b/src/main/kotlin/moe/nea/firmament/features/fishing/FishingWarning.kt @@ -38,8 +38,6 @@ import moe.nea.firmament.util.TimeMark import moe.nea.firmament.util.render.RenderInWorldContext.Companion.renderInWorld object FishingWarning : FirmamentFeature { - override val name: String - get() = "Fishing Warning" override val identifier: String get() = "fishing-warning" @@ -137,7 +135,7 @@ object FishingWarning : FirmamentFeature { WorldRenderLastEvent.subscribe { recentParticles.removeIf { it.second.passedTime() > 5.seconds } recentCandidates.removeIf { it.timeMark.passedTime() > 5.seconds } - renderInWorld(it.matrices, it.camera) { + renderInWorld(it) { color(0f, 0f, 1f, 1f) recentParticles.forEach { tinyBlock(it.first, 0.1F) diff --git a/src/main/kotlin/moe/nea/firmament/features/inventory/CraftingOverlay.kt b/src/main/kotlin/moe/nea/firmament/features/inventory/CraftingOverlay.kt index 31d2b23..bb73513 100644 --- a/src/main/kotlin/moe/nea/firmament/features/inventory/CraftingOverlay.kt +++ b/src/main/kotlin/moe/nea/firmament/features/inventory/CraftingOverlay.kt @@ -26,8 +26,6 @@ object CraftingOverlay : FirmamentFeature { this.recipe = recipe } - override val name: String - get() = "Crafting Overlay" override val identifier: String get() = "crafting-overlay" diff --git a/src/main/kotlin/moe/nea/firmament/features/inventory/SaveCursorPosition.kt b/src/main/kotlin/moe/nea/firmament/features/inventory/SaveCursorPosition.kt new file mode 100644 index 0000000..577f19b --- /dev/null +++ b/src/main/kotlin/moe/nea/firmament/features/inventory/SaveCursorPosition.kt @@ -0,0 +1,67 @@ +package moe.nea.firmament.features.inventory + +import kotlin.math.absoluteValue +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds +import net.minecraft.client.util.InputUtil +import moe.nea.firmament.features.FirmamentFeature +import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.TimeMark +import moe.nea.firmament.util.assertNotNullOr + +object SaveCursorPosition : FirmamentFeature { + override val identifier: String + get() = "save-cursor-position" + + object TConfig : ManagedConfig(identifier) { + val enable by toggle("enable") { true } + val tolerance by duration("tolerance", 10.milliseconds, 5000.milliseconds) { 500.milliseconds } + } + + override val config: TConfig + get() = TConfig + + override fun onLoad() { + + } + + var savedPositionedP1: Pair<Double, Double>? = null + var savedPosition: SavedPosition? = null + + data class SavedPosition( + val middle: Pair<Double, Double>, + val cursor: Pair<Double, Double>, + val savedAt: TimeMark = TimeMark.now() + ) + + @JvmStatic + fun saveCursorOriginal(positionedX: Double, positionedY: Double) { + savedPositionedP1 = Pair(positionedX, positionedY) + } + + @JvmStatic + fun loadCursor(middleX: Double, middleY: Double): Pair<Double, Double>? { + val lastPosition = savedPosition?.takeIf { it.savedAt.passedTime() < 1.seconds } + savedPosition = null + if (lastPosition != null && + (lastPosition.middle.first - middleX).absoluteValue < 1 && + (lastPosition.middle.second - middleY).absoluteValue < 1 + ) { + InputUtil.setCursorParameters( + MC.window.handle, + InputUtil.GLFW_CURSOR_NORMAL, + lastPosition.cursor.first, + lastPosition.cursor.second + ) + return lastPosition.cursor + } + return null + } + + @JvmStatic + fun saveCursorMiddle(middleX: Double, middleY: Double) { + val cursorPos = assertNotNullOr(savedPositionedP1) { return } + savedPosition = SavedPosition(Pair(middleX, middleY), cursorPos) + } +} diff --git a/src/main/kotlin/moe/nea/firmament/features/inventory/SlotLocking.kt b/src/main/kotlin/moe/nea/firmament/features/inventory/SlotLocking.kt index eab0da0..a601489 100644 --- a/src/main/kotlin/moe/nea/firmament/features/inventory/SlotLocking.kt +++ b/src/main/kotlin/moe/nea/firmament/features/inventory/SlotLocking.kt @@ -33,8 +33,6 @@ import moe.nea.firmament.util.MC import moe.nea.firmament.util.data.ProfileSpecificDataHolder object SlotLocking : FirmamentFeature { - override val name: String - get() = "Slot Locking" override val identifier: String get() = "slot-locking" diff --git a/src/main/kotlin/moe/nea/firmament/features/inventory/storageoverlay/StorageBackingHandle.kt b/src/main/kotlin/moe/nea/firmament/features/inventory/storageoverlay/StorageBackingHandle.kt new file mode 100644 index 0000000..5802cb7 --- /dev/null +++ b/src/main/kotlin/moe/nea/firmament/features/inventory/storageoverlay/StorageBackingHandle.kt @@ -0,0 +1,56 @@ +package moe.nea.firmament.features.inventory.storageoverlay + +import net.minecraft.client.gui.screen.Screen +import net.minecraft.client.gui.screen.ingame.GenericContainerScreen +import net.minecraft.screen.GenericContainerScreenHandler +import moe.nea.firmament.util.ifMatches +import moe.nea.firmament.util.unformattedString + +/** + * A handle representing the state of the "server side" screens. + */ +sealed interface StorageBackingHandle { + + sealed interface HasBackingScreen { + val handler: GenericContainerScreenHandler + } + + /** + * No open "server side" screen. + */ + object None : StorageBackingHandle + + /** + * The main storage overview is open. Clicking on a slot will open that page. This page is accessible via `/storage` + */ + data class Overview(override val handler: GenericContainerScreenHandler) : StorageBackingHandle, HasBackingScreen + + /** + * An individual storage page is open. This may be a backpack or an enderchest page. This page is accessible via + * the [Overview] or via `/ec <index + 1>` for enderchest pages. + */ + data class Page(override val handler: GenericContainerScreenHandler, val storagePageSlot: StoragePageSlot) : + StorageBackingHandle, HasBackingScreen + + companion object { |
