diff options
Diffstat (limited to 'src/main/kotlin/moe/nea/firmament/features')
4 files changed, 308 insertions, 0 deletions
diff --git a/src/main/kotlin/moe/nea/firmament/features/FeatureManager.kt b/src/main/kotlin/moe/nea/firmament/features/FeatureManager.kt new file mode 100644 index 0000000..68205f4 --- /dev/null +++ b/src/main/kotlin/moe/nea/firmament/features/FeatureManager.kt @@ -0,0 +1,53 @@ +package moe.nea.firmament.features + +import kotlinx.serialization.Serializable +import kotlinx.serialization.serializer +import moe.nea.firmament.Firmament +import moe.nea.firmament.features.fishing.FishingWarning +import moe.nea.firmament.features.world.FairySouls +import moe.nea.firmament.util.data.DataHolder + +object FeatureManager : DataHolder<FeatureManager.Config>(serializer(), "features", ::Config) { + @Serializable + data class Config( + val enabledFeatures: MutableMap<String, Boolean> = mutableMapOf() + ) + + private val features = mutableMapOf<String, NEUFeature>() + + private var hasAutoloaded = false + + init { + autoload() + } + + fun autoload() { + synchronized(this) { + if (hasAutoloaded) return + loadFeature(FairySouls) + loadFeature(FishingWarning) + hasAutoloaded = true + } + } + + fun loadFeature(feature: NEUFeature) { + synchronized(features) { + if (feature.identifier in features) { + Firmament.logger.error("Double registering feature ${feature.identifier}. Ignoring second instance $feature") + return + } + features[feature.identifier] = feature + feature.onLoad() + } + } + + fun isEnabled(identifier: String): Boolean? = + data.enabledFeatures[identifier] + + + fun setEnabled(identifier: String, value: Boolean) { + data.enabledFeatures[identifier] = value + markDirty() + } + +} diff --git a/src/main/kotlin/moe/nea/firmament/features/NEUFeature.kt b/src/main/kotlin/moe/nea/firmament/features/NEUFeature.kt new file mode 100644 index 0000000..f231003 --- /dev/null +++ b/src/main/kotlin/moe/nea/firmament/features/NEUFeature.kt @@ -0,0 +1,18 @@ +package moe.nea.firmament.features + +import moe.nea.firmament.util.config.ManagedConfig + +interface NEUFeature { + val name: String + val identifier: String + val defaultEnabled: Boolean + get() = true + var isEnabled: Boolean + get() = FeatureManager.isEnabled(identifier) ?: defaultEnabled + set(value) { + FeatureManager.setEnabled(identifier, value) + } + val config: ManagedConfig? get() = null + fun onLoad() + +} diff --git a/src/main/kotlin/moe/nea/firmament/features/fishing/FishingWarning.kt b/src/main/kotlin/moe/nea/firmament/features/fishing/FishingWarning.kt new file mode 100644 index 0000000..cdeb24c --- /dev/null +++ b/src/main/kotlin/moe/nea/firmament/features/fishing/FishingWarning.kt @@ -0,0 +1,117 @@ +package moe.nea.firmament.features.fishing + +import kotlin.math.abs +import kotlin.math.absoluteValue +import kotlin.math.acos +import kotlin.math.asin +import kotlin.math.atan2 +import kotlin.math.sqrt +import kotlin.time.Duration.Companion.seconds +import net.minecraft.entity.projectile.FishingBobberEntity +import net.minecraft.particle.ParticleTypes +import net.minecraft.util.math.Vec3d +import moe.nea.firmament.events.ParticleSpawnEvent +import moe.nea.firmament.events.WorldReadyEvent +import moe.nea.firmament.events.WorldRenderLastEvent +import moe.nea.firmament.features.NEUFeature +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.TimeMark +import moe.nea.firmament.util.config.ManagedConfig +import moe.nea.firmament.util.render.RenderBlockContext.Companion.renderBlocks + +object FishingWarning : NEUFeature { + override val name: String + get() = "Fishing Warning" + override val identifier: String + get() = "fishing-warning" + + object TConfig : ManagedConfig("fishing-warning") { + // Display a warning when you are about to hook a fish + val displayWarning by toggle("display-warning") { false } + val highlightWakeChain by toggle("highlight-wake-chain") { false } + } + + override val config: ManagedConfig get() = TConfig + + + data class WakeChain( + val delta: Vec3d, + val momentum: Vec3d, + val lastContinued: TimeMark, + ) + + + val chains = mutableListOf<WakeChain>() + + private fun areAnglesClose(a: Double, b: Double, tolerance: Double): Boolean { + var dist = (a - b).absoluteValue + if (180 < dist) dist = 360 - dist; + return dist <= tolerance + } + + private fun calculateAngleFromOffsets(xOffset: Double, zOffset: Double): Double { + // See also: Vanilla 1.8.9 Fishing particle code. + var angleX = Math.toDegrees(acos(xOffset / 0.04)) + var angleZ = Math.toDegrees(asin(zOffset / 0.04)) + if (xOffset < 0) { + // Old: angleZ = 180 - angleZ; + angleZ = 180 - angleZ + } + if (zOffset < 0) { + angleX = 360 - angleX + } + angleX %= 360.0 + angleZ %= 360.0 + if (angleX < 0) angleX += 360.0 + if (angleZ < 0) angleZ += 360.0 + var dist = angleX - angleZ + if (dist < -180) dist += 360.0 + if (dist > 180) dist -= 360.0 + return angleZ + dist / 2 + } + + private fun toDegrees(d: Double) = d * 180 / Math.PI + + fun isHookPossible(hook: FishingBobberEntity, particlePos: Vec3d, angle1: Double, angle2: Double): Boolean { + val dx = particlePos.x - hook.pos.x + val dz = particlePos.z - hook.pos.z + val dist = sqrt(dx * dx + dz * dz) + + if (dist < 0.2) return true + val tolerance = toDegrees(atan2(0.03125, dist)) * 1.5 + var angleToHook = toDegrees(atan2(dx, dz)) % 360 + if (angleToHook < 0) angleToHook += 360 + return areAnglesClose(angle1, angleToHook, tolerance) || areAnglesClose(angle2, angleToHook, tolerance) + } + + val recentParticles = mutableListOf<Pair<Vec3d, TimeMark>>() + + private fun onParticleSpawn(event: ParticleSpawnEvent) { + if (event.particleEffect.type != ParticleTypes.FISHING) return + if (!(abs(event.offset.y - 0.01f) < 0.001f)) return + val hook = MC.player?.fishHook ?: return + val actualOffset = event.offset + val candidate1 = calculateAngleFromOffsets(actualOffset.x, -actualOffset.z) + val candidate2 = calculateAngleFromOffsets(-actualOffset.x, actualOffset.z) + + if (isHookPossible(hook, event.position, candidate1, candidate2)) { + recentParticles.add(Pair(event.position, TimeMark.now())) + } + } + + override fun onLoad() { + ParticleSpawnEvent.subscribe(::onParticleSpawn) + WorldReadyEvent.subscribe { + recentParticles.clear() + } + WorldRenderLastEvent.subscribe { + recentParticles.removeIf { it.second.passedTime() > 5.seconds } + renderBlocks(it.matrices, it.camera) { + color(0f, 0f, 1f, 1f) + recentParticles.forEach { + tinyBlock(it.first, 0.1F) + } + } + } + } +} diff --git a/src/main/kotlin/moe/nea/firmament/features/world/FairySouls.kt b/src/main/kotlin/moe/nea/firmament/features/world/FairySouls.kt new file mode 100644 index 0000000..f752963 --- /dev/null +++ b/src/main/kotlin/moe/nea/firmament/features/world/FairySouls.kt @@ -0,0 +1,120 @@ +package moe.nea.firmament.features.world + +import io.github.moulberry.repo.data.Coordinate +import kotlinx.serialization.Serializable +import kotlinx.serialization.serializer +import moe.nea.firmament.events.ServerChatLineReceivedEvent +import moe.nea.firmament.events.SkyblockServerUpdateEvent +import moe.nea.firmament.events.WorldRenderLastEvent +import moe.nea.firmament.features.NEUFeature +import moe.nea.firmament.repo.RepoManager +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.SBData +import moe.nea.firmament.util.blockPos +import moe.nea.firmament.util.config.ManagedConfig +import moe.nea.firmament.util.data.ProfileSpecificDataHolder +import moe.nea.firmament.util.render.RenderBlockContext.Companion.renderBlocks +import moe.nea.firmament.util.unformattedString + + +object FairySouls : NEUFeature { + + + @Serializable + data class Data( + val foundSouls: MutableMap<String, MutableSet<Int>> = mutableMapOf() + ) + + override val config: ManagedConfig + get() = TConfig + + object DConfig : ProfileSpecificDataHolder<Data>(serializer(), "found-fairysouls", ::Data) + + + object TConfig : ManagedConfig("fairy-souls") { + + val displaySouls by toggle("show") { false } + val resetSouls by button("reset") { + DConfig.data?.foundSouls?.clear() != null + updateMissingSouls() + } + } + + + override val name: String get() = "Fairy Souls" + override val identifier: String get() = "fairy-souls" + + val playerReach = 5 + val playerReachSquared = playerReach * playerReach + + var currentLocationName: String? = null + var currentLocationSouls: List<Coordinate> = emptyList() + var currentMissingSouls: List<Coordinate> = emptyList() + + fun updateMissingSouls() { + currentMissingSouls = emptyList() + val c = DConfig.data ?: return + val fi = c.foundSouls[currentLocationName] ?: setOf() + val cms = currentLocationSouls.toMutableList() + fi.asSequence().sortedDescending().filter { it in cms.indices }.forEach { cms.removeAt(it) } + currentMissingSouls = cms + } + + fun updateWorldSouls() { + currentLocationSouls = emptyList() + val loc = currentLocationName ?: return + currentLocationSouls = RepoManager.neuRepo.constants.fairySouls.soulLocations[loc] ?: return + } + + fun findNearestClickableSoul(): Coordinate? { + val player = MC.player ?: return null + val pos = player.pos + val location = SBData.skyblockLocation ?: return null + val soulLocations: List<Coordinate> = + RepoManager.neuRepo.constants.fairySouls.soulLocations[location] ?: return null + return soulLocations + .map { it to it.blockPos.getSquaredDistance(pos) } + .filter { it.second < playerReachSquared } + .minByOrNull { it.second } + ?.first + } + + private fun markNearestSoul() { + val nearestSoul = findNearestClickableSoul() ?: return + val c = DConfig.data ?: return + val loc = currentLocationName ?: return + val idx = currentLocationSouls.indexOf(nearestSoul) + c.foundSouls.computeIfAbsent(loc) { mutableSetOf() }.add(idx) + DConfig.markDirty() + updateMissingSouls() + } + + + override fun onLoad() { + SkyblockServerUpdateEvent.subscribe { + currentLocationName = it.newLocraw?.skyblockLocation + updateWorldSouls() + updateMissingSouls() + } + ServerChatLineReceivedEvent.subscribe { + when (it.text.unformattedString) { + "You have already found that Fairy Soul!" -> { + markNearestSoul() + } + + "SOUL! You found a Fairy Soul!" -> { + markNearestSoul() + } + } + } + WorldRenderLastEvent.subscribe { + if (!TConfig.displaySouls) return@subscribe + renderBlocks(it.matrices, it.camera) { + color(1F, 1F, 0F, 0.8F) + currentMissingSouls.forEach { + block(it.blockPos) + } + } + } + } +} |