aboutsummaryrefslogtreecommitdiff
path: root/src/main/kotlin/moe/nea/firmament/features
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/kotlin/moe/nea/firmament/features')
-rw-r--r--src/main/kotlin/moe/nea/firmament/features/FeatureManager.kt53
-rw-r--r--src/main/kotlin/moe/nea/firmament/features/NEUFeature.kt18
-rw-r--r--src/main/kotlin/moe/nea/firmament/features/fishing/FishingWarning.kt117
-rw-r--r--src/main/kotlin/moe/nea/firmament/features/world/FairySouls.kt120
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)
+ }
+ }
+ }
+ }
+}