aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/at/hannibal2/skyhanni/utils
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/at/hannibal2/skyhanni/utils')
-rw-r--r--src/main/java/at/hannibal2/skyhanni/utils/repopatterns/RepoPattern.kt98
-rw-r--r--src/main/java/at/hannibal2/skyhanni/utils/repopatterns/RepoPatternDump.kt30
-rw-r--r--src/main/java/at/hannibal2/skyhanni/utils/repopatterns/RepoPatternGroup.kt26
-rw-r--r--src/main/java/at/hannibal2/skyhanni/utils/repopatterns/RepoPatternGui.kt82
-rw-r--r--src/main/java/at/hannibal2/skyhanni/utils/repopatterns/RepoPatternImpl.kt49
-rw-r--r--src/main/java/at/hannibal2/skyhanni/utils/repopatterns/RepoPatternKeyOwner.kt8
-rw-r--r--src/main/java/at/hannibal2/skyhanni/utils/repopatterns/RepoPatternManager.kt153
7 files changed, 446 insertions, 0 deletions
diff --git a/src/main/java/at/hannibal2/skyhanni/utils/repopatterns/RepoPattern.kt b/src/main/java/at/hannibal2/skyhanni/utils/repopatterns/RepoPattern.kt
new file mode 100644
index 000000000..6af2cf2fe
--- /dev/null
+++ b/src/main/java/at/hannibal2/skyhanni/utils/repopatterns/RepoPattern.kt
@@ -0,0 +1,98 @@
+package at.hannibal2.skyhanni.utils.repopatterns
+
+import at.hannibal2.skyhanni.SkyHanniMod
+import org.intellij.lang.annotations.Language
+import java.util.regex.Pattern
+import kotlin.properties.ReadOnlyProperty
+import kotlin.reflect.KProperty
+
+/**
+ * RepoPattern is our innovative tool to cope with the fucking game updates Hypixel deems to be important enough to brick
+ * our regexes over for.
+ *
+ * ## Usage
+ *
+ * RepoPattern is only available in kotlin. If you must use a regex from java code that you anticipate might need updating
+ * in the future, please have a kotlin wrapper from which you pull the regex using a getter method of sorts.
+ *
+ * In order to use a RepoPattern, you need to obtain a reference to that repo pattern statically during pre init. This
+ * means you must either be loaded by [SkyHanniMod.loadModule] directly, or must be loaded during class or object
+ * initialization of an object that is pre init loaded. You will then have to bind that repo pattern to a field using
+ * kotlin delegation syntax:
+ *
+ * ```kt
+ * class SomeFeatureModule {
+ * // Initialize your regex
+ * val myRegey by /* notice the by here, instead of a = */ RepoPattern.of("someUniqueKey", "^[a-z]+")
+ * }
+ * ```
+ *
+ * If used like this, nothing will break. If you do want to live a little more daring, you can also keep the original
+ * reference around. If you do this, make sure that you only ever create one RepoPattern per key, and only ever use that
+ * RepoPattern instance bound to one field like so:
+ * ```kt
+ * class SomeFeatureModule {
+ * // Initialize your RepoPattern
+ * val meta = RepoPattern.of("someUniqueKey", "^[a-z]+")
+ * val pattern by meta // Creating a new RepoPattern.of in here for the same key would be illegal
+ * }
+ * ```
+ *
+ * When accessing the metaobject (the RepoPattern instance itself), then you afford yourself less protection at the cost
+ * of slightly more options.
+ */
+interface RepoPattern : ReadOnlyProperty<Any?, Pattern> {
+ /**
+ * Check whether [value] has been loaded remotely or from the fallback value at [defaultPattern]. In case this is
+ * accessed off-thread there are no guarantees for the correctness of this value in relation to any specific call
+ * to [value].
+ */
+ val isLoadedRemotely: Boolean
+
+ /**
+ * Check whether [value] was compiled from a value other than the [defaultPattern]. This is `false` even when
+ * loading remotely if the remote pattern matches the local one.
+ */
+ val wasOverridden: Boolean
+
+ /**
+ * The default pattern that is specified at compile time. This local pattern will be a fallback in case there is no
+ * remote pattern available or the remote pattern does not compile.
+ */
+ val defaultPattern: String
+
+ /**
+ * Key for this pattern. Used as an identifier when loading from the repo. Should be consistent accross versions.
+ */
+ val key: String
+
+ /**
+ * Should not be accessed directly. Instead, use delegation at one code location and share the regex from there.
+ * ```kt
+ * val actualValue by pattern
+ * ```
+ */
+ val value: Pattern
+
+ override fun getValue(thisRef: Any?, property: KProperty<*>): Pattern {
+ return value
+ }
+
+
+ companion object {
+ /**
+ * Obtain a reference to a [Pattern] backed by either a local regex, or a remote regex.
+ * Check the documentation of [RepoPattern] for more information.
+ */
+ fun pattern(key: String, @Language("RegExp") fallback: String): RepoPattern {
+ return RepoPatternManager.of(key, fallback)
+ }
+
+ /**
+ * Obtains a [RepoPatternGroup] to allow for easier defining [RepoPattern]s with common prefixes.
+ */
+ fun group(prefix: String): RepoPatternGroup {
+ return RepoPatternGroup(prefix)
+ }
+ }
+}
diff --git a/src/main/java/at/hannibal2/skyhanni/utils/repopatterns/RepoPatternDump.kt b/src/main/java/at/hannibal2/skyhanni/utils/repopatterns/RepoPatternDump.kt
new file mode 100644
index 000000000..6cacb9a53
--- /dev/null
+++ b/src/main/java/at/hannibal2/skyhanni/utils/repopatterns/RepoPatternDump.kt
@@ -0,0 +1,30 @@
+package at.hannibal2.skyhanni.utils.repopatterns
+
+import at.hannibal2.skyhanni.utils.KSerializable
+
+/**
+ * A class containing a dump of all regexes that are defined using [RepoPattern].
+ *
+ * # Generating a dump
+ * This dump is generated by the `.github/workflows/generate-constants.yaml` using the gradle task `generateRepoPatterns`.
+ * Said gradle task then launches Minecraft headless with environment variables set so that [RepoPatternManager.onPreInitFinished]
+ * calls [RepoPatternManager.dump] and closes afterward. The GitHub action then looks at the current branch and repo,
+ * and if it finds itself to be running on hannibal002/SkyHanni:beta, it commits that generated dump to the SkyHanni REPO.
+ *
+ * # Using the dump
+ * All clients upon launching will then look at that dump generated by the latest beta version, even if they themselves
+ * run an older SkyHanni version. The regexes generated by the dump will then take precedence over the ones found in the
+ * running JAR.
+ *
+ * # Setting up the `generate-constants.yaml` workflow
+ * The GitHub action workflow needs to be configured properly. For that it needs to have the
+ * `env.data_repo` key adjusted to be the live repo.
+ * It also needs a [repository GitHub action secret](https://github.com/nea89o/SkyHanni/settings/secrets/actions)
+ * called `REPO_PAT`, which contains a [personal access token](https://github.com/settings/tokens/new) with repo write
+ * access to the live repo.
+ */
+@KSerializable
+data class RepoPatternDump(
+ val sourceLabel: String = "anonymous",
+ val regexes: Map<String, String> = mapOf(),
+)
diff --git a/src/main/java/at/hannibal2/skyhanni/utils/repopatterns/RepoPatternGroup.kt b/src/main/java/at/hannibal2/skyhanni/utils/repopatterns/RepoPatternGroup.kt
new file mode 100644
index 000000000..326f447d5
--- /dev/null
+++ b/src/main/java/at/hannibal2/skyhanni/utils/repopatterns/RepoPatternGroup.kt
@@ -0,0 +1,26 @@
+package at.hannibal2.skyhanni.utils.repopatterns
+
+import org.intellij.lang.annotations.Language
+
+/**
+ * A utility class for allowing easier definitions of [RepoPattern]s with a common prefix.
+ */
+class RepoPatternGroup internal constructor(val prefix: String) {
+ init {
+ RepoPatternManager.verifyKeyShape(prefix)
+ }
+
+ /**
+ * Shortcut to [RepoPattern.pattern] prefixed with [prefix].
+ */
+ fun pattern(key: String, @Language("RegExp") fallback: String): RepoPattern {
+ return RepoPattern.pattern("$prefix.$key", fallback)
+ }
+
+ /**
+ * Shortcut to [RepoPattern.group] prefixed with [prefix].
+ */
+ fun group(subgroup: String): RepoPatternGroup {
+ return RepoPatternGroup("$prefix.$subgroup")
+ }
+}
diff --git a/src/main/java/at/hannibal2/skyhanni/utils/repopatterns/RepoPatternGui.kt b/src/main/java/at/hannibal2/skyhanni/utils/repopatterns/RepoPatternGui.kt
new file mode 100644
index 000000000..b70d54edd
--- /dev/null
+++ b/src/main/java/at/hannibal2/skyhanni/utils/repopatterns/RepoPatternGui.kt
@@ -0,0 +1,82 @@
+package at.hannibal2.skyhanni.utils.repopatterns
+
+import at.hannibal2.skyhanni.SkyHanniMod
+import io.github.moulberry.moulconfig.common.MyResourceLocation
+import io.github.moulberry.moulconfig.gui.GuiContext
+import io.github.moulberry.moulconfig.gui.GuiScreenElementWrapperNew
+import io.github.moulberry.moulconfig.observer.ObservableList
+import io.github.moulberry.moulconfig.xml.Bind
+import io.github.moulberry.moulconfig.xml.XMLUniverse
+
+/**
+ * Gui for analyzing [RepoPattern]s
+ */
+class RepoPatternGui private constructor() {
+ companion object {
+ /**
+ * Open the [RepoPatternGui]
+ */
+ fun open() {
+ SkyHanniMod.screenToOpen = GuiScreenElementWrapperNew(
+ GuiContext(
+ XMLUniverse.getDefaultUniverse()
+ .load(RepoPatternGui(), MyResourceLocation("skyhanni", "gui/regexes.xml"))
+ )
+ )
+ }
+ }
+
+ @field:Bind
+ var search: String = ""
+ var lastSearch = null as String?
+ val allKeys = RepoPatternManager.allPatterns.toList()
+ .sortedBy { it.key }
+ .map { RepoPatternInfo(it) }
+ var searchCache = ObservableList(mutableListOf<RepoPatternInfo>())
+
+
+ class RepoPatternInfo(
+ repoPatternImpl: RepoPatternImpl
+ ) {
+ @field:Bind
+ val key: String = repoPatternImpl.key
+
+ @field:Bind
+ val regex: String = repoPatternImpl.value.pattern()
+
+ @field:Bind
+ val hoverRegex: List<String> = if (repoPatternImpl.isLoadedRemotely) {
+ listOf(
+ "§aLoaded remotely",
+ "§7Remote: " + repoPatternImpl.compiledPattern.pattern(),
+ "§7Local: " + repoPatternImpl.defaultPattern,
+ )
+ } else {
+ listOf("§cLoaded locally", "§7Local: " + repoPatternImpl.defaultPattern)
+ }
+
+ @field:Bind
+ val keyW = listOf(key)
+
+ @field:Bind
+ val overriden: String =
+ if (repoPatternImpl.wasOverridden) "§9Overriden"
+ else if (repoPatternImpl.isLoadedRemotely) "§aRemote"
+ else "§cLocal"
+ }
+
+ @Bind
+ fun poll(): String {
+ if (search != lastSearch) {
+ searchCache.clear()
+ searchCache.addAll(allKeys.filter { search in it.key })
+ lastSearch = search
+ }
+ return ""
+ }
+
+ @Bind
+ fun searchResults(): ObservableList<RepoPatternInfo> {
+ return searchCache
+ }
+}
diff --git a/src/main/java/at/hannibal2/skyhanni/utils/repopatterns/RepoPatternImpl.kt b/src/main/java/at/hannibal2/skyhanni/utils/repopatterns/RepoPatternImpl.kt
new file mode 100644
index 000000000..89e9f99ec
--- /dev/null
+++ b/src/main/java/at/hannibal2/skyhanni/utils/repopatterns/RepoPatternImpl.kt
@@ -0,0 +1,49 @@
+package at.hannibal2.skyhanni.utils.repopatterns
+
+import java.util.regex.Pattern
+import kotlin.reflect.KProperty
+
+/**
+ * Internal class implementing [RepoPattern]. Obtain via [RepoPattern.pattern].
+ */
+class RepoPatternImpl(
+ override val defaultPattern: String,
+ override val key: String,
+) : RepoPattern {
+ var compiledPattern: Pattern = Pattern.compile(defaultPattern)
+ var wasLoadedRemotely = false
+ override var wasOverridden = false
+
+ /**
+ * Whether the pattern has obtained a lock on a code location and a key.
+ * Once set, no other code locations can access this repo pattern (and therefore the key).
+ * @see RepoPatternManager.checkExclusivity
+ */
+ var hasObtainedLock = false
+
+ override fun getValue(thisRef: Any?, property: KProperty<*>): Pattern {
+ verifyLock(thisRef, property)
+ return super.getValue(thisRef, property)
+ }
+
+ /**
+ * Try to lock the [key] to this key location.
+ * @see RepoPatternManager.checkExclusivity
+ */
+ fun verifyLock(thisRef: Any?, property: KProperty<*>) {
+ if (hasObtainedLock) return
+ hasObtainedLock = true
+ val owner = RepoPatternKeyOwner(thisRef?.javaClass, property)
+ RepoPatternManager.checkExclusivity(owner, key)
+ }
+
+
+ override val value: Pattern
+ get() {
+ return compiledPattern
+ }
+ override val isLoadedRemotely: Boolean
+ get() {
+ return wasLoadedRemotely
+ }
+}
diff --git a/src/main/java/at/hannibal2/skyhanni/utils/repopatterns/RepoPatternKeyOwner.kt b/src/main/java/at/hannibal2/skyhanni/utils/repopatterns/RepoPatternKeyOwner.kt
new file mode 100644
index 000000000..ccd98ff1c
--- /dev/null
+++ b/src/main/java/at/hannibal2/skyhanni/utils/repopatterns/RepoPatternKeyOwner.kt
@@ -0,0 +1,8 @@
+package at.hannibal2.skyhanni.utils.repopatterns
+
+import kotlin.reflect.KProperty
+
+data class RepoPatternKeyOwner(
+ val ownerClass: Class<*>?,
+ val property: KProperty<*>?,
+)
diff --git a/src/main/java/at/hannibal2/skyhanni/utils/repopatterns/RepoPatternManager.kt b/src/main/java/at/hannibal2/skyhanni/utils/repopatterns/RepoPatternManager.kt
new file mode 100644
index 000000000..9ebd6c145
--- /dev/null
+++ b/src/main/java/at/hannibal2/skyhanni/utils/repopatterns/RepoPatternManager.kt
@@ -0,0 +1,153 @@
+package at.hannibal2.skyhanni.utils.repopatterns
+
+import at.hannibal2.skyhanni.SkyHanniMod
+import at.hannibal2.skyhanni.config.ConfigManager
+import at.hannibal2.skyhanni.events.ConfigLoadEvent
+import at.hannibal2.skyhanni.events.LorenzEvent
+import at.hannibal2.skyhanni.events.PreInitFinished
+import at.hannibal2.skyhanni.events.RepositoryReloadEvent
+import at.hannibal2.skyhanni.utils.StringUtils.matches
+import net.minecraft.launchwrapper.Launch
+import net.minecraftforge.fml.common.FMLCommonHandler
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
+import java.io.File
+import java.util.regex.Pattern
+import java.util.regex.PatternSyntaxException
+
+/**
+ * Manages [RepoPattern]s.
+ */
+object RepoPatternManager {
+ val allPatterns: Collection<RepoPatternImpl> get() = usedKeys.values
+
+ /**
+ * Remote loading data that will be used to compile regexes from, once such a regex is needed.
+ */
+ private var regexes: RepoPatternDump? = null
+
+ /**
+ * Map containing the exclusive owner of a regex key
+ */
+ private var exclusivity: MutableMap<String, RepoPatternKeyOwner> = mutableMapOf()
+
+ /**
+ * Map containing all keys and their repo patterns. Used for filling in new regexes after an update, and for
+ * checking duplicate registrations.
+ */
+ private var usedKeys = mutableMapOf<String, RepoPatternImpl>()
+
+ private var wasPreinitialized = false
+ private val isInDevEnv = Launch.blackboard["fml.deobfuscatedEnvironment"] as Boolean
+ private val config get() = SkyHanniMod.feature.dev.repoPattern
+
+ /**
+ * Crash if in a development environment, or if inside a guarded event handler.
+ */
+ fun crash(reason: String) {
+ if (isInDevEnv || LorenzEvent.isInGuardedEventHandler)
+ throw RuntimeException(reason)
+ }
+
+ /**
+ * Check that the [owner] has exclusive right to the specified [key], and locks out other code parts from ever
+ * using that [key] again. Thread safe.
+ */
+ fun checkExclusivity(owner: RepoPatternKeyOwner, key: String) {
+ synchronized(exclusivity) {
+ val previousOwner = exclusivity.get(key)
+ if (previousOwner != owner && previousOwner != null) {
+ if (!config.tolerateDuplicateUsage)
+ crash("Non unique access to regex at \"$key\". First obtained by ${previousOwner.ownerClass} / ${previousOwner.property}, tried to use at ${owner.ownerClass} / ${owner.property}")
+ } else {
+ exclusivity[key] = owner
+ }
+ }
+ }
+
+ @SubscribeEvent
+ fun onRepoReload(event: RepositoryReloadEvent) {
+ regexes = null
+ regexes = event.getConstant<RepoPatternDump>("regexes")
+ reloadPatterns()
+ }
+
+
+ @SubscribeEvent
+ fun onConfigInit(event: ConfigLoadEvent) {
+ config.forceLocal.whenChanged { b, b2 -> reloadPatterns() }
+ }
+
+ /**
+ * Reload patterns in [usedKeys] from [regexes] or their fallbacks.
+ */
+ private fun reloadPatterns() {
+ val remotePatterns =
+ if (config.forceLocal.get()) mapOf()
+ else regexes?.regexes ?: mapOf()
+
+ for (it in usedKeys.values) {
+ val remotePattern = remotePatterns[it.key]
+ try {
+ if (remotePattern != null) {
+ it.compiledPattern = Pattern.compile(remotePattern)
+ it.wasLoadedRemotely = true
+ it.wasOverridden = remotePattern != it.defaultPattern
+ continue
+ }
+ } catch (e: PatternSyntaxException) {
+ SkyHanniMod.logger.error("Error while loading pattern from repo", e)
+ }
+ it.compiledPattern = Pattern.compile(it.defaultPattern)
+ it.wasLoadedRemotely = false
+ it.wasOverridden = false
+ }
+ }
+
+ val keyShape = Pattern.compile("^(?:[a-z0-9A-Z]+\\.)*[a-z0-9A-Z]+$")
+
+ /**
+ * Verify that a key has a valid shape or throw otherwise.
+ */
+ fun verifyKeyShape(key: String) {
+ require(keyShape.matches(key))
+ }
+
+ /**
+ * Dump all regexes labeled with the label into the file.
+ */
+ fun dump(sourceLabel: String, file: File) {
+ val data =
+ ConfigManager.gson.toJson(
+ RepoPatternDump(
+ sourceLabel,
+ usedKeys.values.associate { it.key to it.defaultPattern })
+ )
+ file.parentFile.mkdirs()
+ file.writeText(data)
+ }
+
+ @SubscribeEvent
+ fun onPreInitFinished(event: PreInitFinished) {
+ wasPreinitialized = true
+ val dumpDirective = System.getenv("SKYHANNI_DUMP_REGEXES")
+ if (dumpDirective.isNullOrBlank()) return
+ val (sourceLabel, path) = dumpDirective.split(":", limit = 2)
+ dump(sourceLabel, File(path))
+ if (System.getenv("SKYHANNI_DUMP_REGEXES_EXIT") != null) {
+ SkyHanniMod.logger.info("Exiting after dumping RepoPattern regex patterns to $path")
+ FMLCommonHandler.instance().exitJava(0, false)
+ }
+ }
+
+ fun of(key: String, fallback: String): RepoPattern {
+ verifyKeyShape(key)
+ if (wasPreinitialized && !config.tolerateLateRegistration) {
+ crash("Illegal late initialization of repo pattern. Repo pattern needs to be created during pre-initialization.")
+ }
+ if (key in usedKeys) {
+ exclusivity[key] = RepoPatternKeyOwner(null, null)
+ usedKeys[key]?.hasObtainedLock = false
+ }
+ return RepoPatternImpl(fallback, key).also { usedKeys[key] = it }
+ }
+}