aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLinnea Gräf <nea@nea.moe>2023-12-08 14:22:52 +0100
committerGitHub <noreply@github.com>2023-12-08 14:22:52 +0100
commit57acdfea63a58203cebda34ef7e16ed6fcc1bf1d (patch)
treeacd9786f82a304a5ecddb36ae6f7f1cdc752a297
parent247656e0d4cd1733028009cecfab98c26953b795 (diff)
downloadskyhanni-57acdfea63a58203cebda34ef7e16ed6fcc1bf1d.tar.gz
skyhanni-57acdfea63a58203cebda34ef7e16ed6fcc1bf1d.tar.bz2
skyhanni-57acdfea63a58203cebda34ef7e16ed6fcc1bf1d.zip
Add RepoPatterns (#715)
Added RepoPatterns. #715
-rw-r--r--.github/workflows/generate-constants.yaml62
-rw-r--r--build.gradle.kts36
-rw-r--r--gradle/libs.versions.toml4
-rw-r--r--settings.gradle.kts4
-rw-r--r--src/main/java/at/hannibal2/skyhanni/SkyHanniMod.kt5
-rw-r--r--src/main/java/at/hannibal2/skyhanni/config/ConfigManager.kt2
-rw-r--r--src/main/java/at/hannibal2/skyhanni/config/commands/Commands.kt2
-rw-r--r--src/main/java/at/hannibal2/skyhanni/config/features/dev/DevConfig.java5
-rw-r--r--src/main/java/at/hannibal2/skyhanni/config/features/dev/RepoPatternConfig.java23
-rw-r--r--src/main/java/at/hannibal2/skyhanni/events/LorenzEvent.kt13
-rw-r--r--src/main/java/at/hannibal2/skyhanni/events/PreInitFinished.kt3
-rw-r--r--src/main/java/at/hannibal2/skyhanni/features/inventory/HarpFeatures.kt9
-rw-r--r--src/main/java/at/hannibal2/skyhanni/mixins/hooks/GuiPlayerTabOverlayHook.kt6
-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
-rw-r--r--src/main/resources/assets/skyhanni/gui/regexes.xml25
21 files changed, 640 insertions, 5 deletions
diff --git a/.github/workflows/generate-constants.yaml b/.github/workflows/generate-constants.yaml
new file mode 100644
index 000000000..c5f667bad
--- /dev/null
+++ b/.github/workflows/generate-constants.yaml
@@ -0,0 +1,62 @@
+# Read the Javadoc of RepoPatternDump for more info
+
+name: RepoPattern
+
+env:
+ data_repo: nea89o/SkyHanni-REPO
+
+on:
+ push:
+ workflow_dispatch:
+
+permissions: { }
+
+jobs:
+ regexes:
+ runs-on: ubuntu-latest
+ name: "Generate regexes"
+ steps:
+ - uses: actions/checkout@v3
+ - name: Set up JDK 17
+ uses: actions/setup-java@v3
+ with:
+ java-version: 17
+ distribution: temurin
+ cache: gradle
+ - name: Setup gradle
+ uses: gradle/gradle-build-action@v2
+ - name: Generate Repo Patterns using Gradle
+ run: |
+ ./gradlew generateRepoPatterns --stacktrace
+ - uses: actions/upload-artifact@v3
+ name: Upload generated repo regexes
+ with:
+ name: Repo Regexes
+ path: build/regexes/constants.json
+ publish-regexes:
+ runs-on: ubuntu-latest
+ needs: regexes
+ name: "Publish regexes"
+ if: ${{ 'push' == github.event_name && 'beta' == github.ref_name }}
+ steps:
+ - uses: actions/checkout@v3
+ with:
+ repository: ${{ env.data_repo }}
+ branch: main
+ - uses: actions/download-artifact@v3
+ name: Upload generated repo regexes
+ with:
+ name: Repo Regexes
+ - name: Commit generated regex
+ run: |
+ mkdir -p constants/
+ mv constants.json constants/regexes.json
+ git config user.name 'github-actions[bot]'
+ git config user.email 'github-action@users.noreply.github.com'
+ git add constants/regexes.json
+ git commit -m "Update regexes based on https://github.com/hannibal002/Skyhanni/commit/$GITHUB_SHA"
+ - name: Publish new repository
+ run: |
+ git config --unset-all http.https://github.com/.extraheader
+ git remote add restream https://user:${{secrets.REPO_PAT}}@github.com/${{env.data_repo}}
+ git push restream HEAD:main
diff --git a/build.gradle.kts b/build.gradle.kts
index 694ef15b2..b46db017d 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,4 +1,5 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+import java.io.ByteArrayOutputStream
plugins {
idea
@@ -13,6 +14,16 @@ plugins {
group = "at.hannibal2.skyhanni"
version = "0.22.Beta.8"
+val gitHash by lazy {
+ val baos = ByteArrayOutputStream()
+ exec {
+ standardOutput = baos
+ commandLine("git", "rev-parse", "--short", "HEAD")
+ isIgnoreExitValue = true
+ }
+ baos.toByteArray().decodeToString().trim()
+}
+
// Toolchains:
java {
toolchain.languageVersion.set(JavaLanguageVersion.of(8))
@@ -51,6 +62,11 @@ val devenvMod: Configuration by configurations.creating {
isVisible = false
}
+val headlessLwjgl by configurations.creating {
+ isTransitive = false
+ isVisible = false
+}
+
dependencies {
minecraft("com.mojang:minecraft:1.8.9")
mappings("de.oceanlabs.mcp:mcp_stable:22-1.8.9")
@@ -63,7 +79,9 @@ dependencies {
exclude(module = "gson")
because("Different version conflicts with Minecraft's Log4j")
}
+ compileOnly(libs.jbAnnotations)
+ headlessLwjgl(libs.headlessLwjgl)
shadowImpl("org.spongepowered:mixin:0.7.11-SNAPSHOT") {
isTransitive = false
@@ -149,6 +167,24 @@ tasks.processResources {
}
}
+val generateRepoPatterns by tasks.creating(JavaExec::class) {
+ javaLauncher.set(javaToolchains.launcherFor(java.toolchain))
+ mainClass.set("net.fabricmc.devlaunchinjector.Main")
+ workingDir(project.file("run"))
+ classpath(sourceSets.main.map { it.runtimeClasspath }, sourceSets.main.map { it.output })
+ jvmArgs(
+ "-Dfabric.dli.config=${project.file(".gradle/loom-cache/launch.cfg").absolutePath}",
+ "-Dfabric.dli.env=client",
+ "-Dfabric.dli.main=net.minecraft.launchwrapper.Launch",
+ "-Dorg.lwjgl.opengl.Display.allowSoftwareOpenGL=true",
+ "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5006",
+ "-javaagent:${headlessLwjgl.singleFile.absolutePath}"
+ )
+ val outputFile = project.file("build/regexes/constants.json")
+ environment("SKYHANNI_DUMP_REGEXES", "${gitHash}:${outputFile.absolutePath}")
+ environment("SKYHANNI_DUMP_REGEXES_EXIT", "true")
+}
+
tasks.compileJava {
dependsOn(tasks.processResources)
}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index e29f8e72b..ce1a16908 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,7 +1,11 @@
[versions]
libautoupdate = "1.0.3"
moulconfig = "2.5.0"
+headlessLwjgl = "1.7.2"
+jbAnnotations = "24.1.0"
[libraries]
moulconfig = { module = "org.notenoughupdates.moulconfig:legacy", version.ref = "moulconfig" }
libautoupdate = { module = "moe.nea:libautoupdate", version.ref = "libautoupdate" }
+headlessLwjgl = { module = "com.github.3arthqu4ke.HeadlessMc:headlessmc-lwjgl", version.ref = "headlessLwjgl" }
+jbAnnotations = { module = "org.jetbrains:annotations", version.ref = "jbAnnotations" }
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 31ba0a0a8..616c8c8a9 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -18,4 +18,8 @@ pluginManagement {
}
}
+plugins {
+ id("org.gradle.toolchains.foojay-resolver-convention") version("0.6.0")
+}
+
rootProject.name = "SkyHanni"
diff --git a/src/main/java/at/hannibal2/skyhanni/SkyHanniMod.kt b/src/main/java/at/hannibal2/skyhanni/SkyHanniMod.kt
index 3aaa809d2..7db9976e8 100644
--- a/src/main/java/at/hannibal2/skyhanni/SkyHanniMod.kt
+++ b/src/main/java/at/hannibal2/skyhanni/SkyHanniMod.kt
@@ -56,6 +56,9 @@ import at.hannibal2.skyhanni.features.bingo.BingoAPI
import at.hannibal2.skyhanni.features.bingo.CompactBingoChat
import at.hannibal2.skyhanni.features.bingo.MinionCraftHelper
import at.hannibal2.skyhanni.features.bingo.card.BingoCardDisplay
+import at.hannibal2.skyhanni.utils.repopatterns.RepoPattern
+import at.hannibal2.skyhanni.utils.repopatterns.RepoPatternManager
+import at.hannibal2.skyhanni.events.PreInitFinished
import at.hannibal2.skyhanni.features.bingo.card.BingoCardReader
import at.hannibal2.skyhanni.features.bingo.card.BingoCardTips
import at.hannibal2.skyhanni.features.bingo.card.nextstephelper.BingoNextStepHelper
@@ -660,6 +663,7 @@ class SkyHanniMod {
loadModule(DungeonFinderFeatures())
loadModule(PabloHelper())
loadModule(FishingBaitWarnings())
+ loadModule(RepoPatternManager)
loadModule(PestSpawn())
loadModule(PestSpawnTimer)
loadModule(PestFinder())
@@ -680,6 +684,7 @@ class SkyHanniMod {
loadModule(TestShowSlotNumber())
loadModule(SkyHanniDebugsAndTests)
loadModule(HotSwapDetection)
+ PreInitFinished().postAndCatch()
}
@Mod.EventHandler
diff --git a/src/main/java/at/hannibal2/skyhanni/config/ConfigManager.kt b/src/main/java/at/hannibal2/skyhanni/config/ConfigManager.kt
index 04e01b565..b69c90a59 100644
--- a/src/main/java/at/hannibal2/skyhanni/config/ConfigManager.kt
+++ b/src/main/java/at/hannibal2/skyhanni/config/ConfigManager.kt
@@ -7,6 +7,7 @@ import at.hannibal2.skyhanni.data.jsonobjects.local.JacobContestsJson
import at.hannibal2.skyhanni.data.jsonobjects.local.KnownFeaturesJson
import at.hannibal2.skyhanni.features.fishing.trophy.TrophyRarity
import at.hannibal2.skyhanni.features.misc.update.UpdateManager
+import at.hannibal2.skyhanni.utils.KotlinTypeAdapterFactory
import at.hannibal2.skyhanni.utils.LorenzLogger
import at.hannibal2.skyhanni.utils.LorenzRarity
import at.hannibal2.skyhanni.utils.LorenzUtils
@@ -49,6 +50,7 @@ class ConfigManager {
.excludeFieldsWithoutExposeAnnotation()
.serializeSpecialFloatingPointValues()
.registerTypeAdapterFactory(PropertyTypeAdapterFactory())
+ .registerTypeAdapterFactory(KotlinTypeAdapterFactory())
.registerTypeAdapter(UUID::class.java, object : TypeAdapter<UUID>() {
override fun write(out: JsonWriter, value: UUID) {
out.value(value.toString())
diff --git a/src/main/java/at/hannibal2/skyhanni/config/commands/Commands.kt b/src/main/java/at/hannibal2/skyhanni/config/commands/Commands.kt
index 1a0b41629..bd8cea0e7 100644
--- a/src/main/java/at/hannibal2/skyhanni/config/commands/Commands.kt
+++ b/src/main/java/at/hannibal2/skyhanni/config/commands/Commands.kt
@@ -55,6 +55,7 @@ import at.hannibal2.skyhanni.utils.APIUtil
import at.hannibal2.skyhanni.utils.LorenzUtils
import at.hannibal2.skyhanni.utils.SoundUtils
import at.hannibal2.skyhanni.utils.TabListData
+import at.hannibal2.skyhanni.utils.repopatterns.RepoPatternGui
import net.minecraft.client.Minecraft
import net.minecraft.command.ICommandSender
import net.minecraft.event.ClickEvent
@@ -265,6 +266,7 @@ object Commands {
}
private fun developersCodingHelp() {
+ registerCommand("shrepopatterns", "See where regexes are loaded from") { RepoPatternGui.open() }
registerCommand("shtest", "Unused test command.") { SkyHanniDebugsAndTests.testCommand(it) }
registerCommand("shdebugwaypoint", "Mark a waypoint on that location") { SkyHanniDebugsAndTests.waypoint(it) }
registerCommand("shdebugtablist", "Set your clipboard as a fake tab list.") { TabListData.toggleDebugCommand() }
diff --git a/src/main/java/at/hannibal2/skyhanni/config/features/dev/DevConfig.java b/src/main/java/at/hannibal2/skyhanni/config/features/dev/DevConfig.java
index 0c1fc01ed..6a2ee9708 100644
--- a/src/main/java/at/hannibal2/skyhanni/config/features/dev/DevConfig.java
+++ b/src/main/java/at/hannibal2/skyhanni/config/features/dev/DevConfig.java
@@ -30,6 +30,11 @@ public class DevConfig {
public DebugConfig debug = new DebugConfig();
@Expose
+ @ConfigOption(name = "RepoPattern", desc = "")
+ @Accordion
+ public RepoPatternConfig repoPattern = new RepoPatternConfig();
+
+ @Expose
@ConfigOption(name = "Slot Number", desc = "Show slot number in inventory while pressing this key.")
@ConfigEditorKeybind(defaultKey = Keyboard.KEY_NONE)
public int showSlotNumberKey = Keyboard.KEY_NONE;
diff --git a/src/main/java/at/hannibal2/skyhanni/config/features/dev/RepoPatternConfig.java b/src/main/java/at/hannibal2/skyhanni/config/features/dev/RepoPatternConfig.java
new file mode 100644
index 000000000..d4ac7d4d9
--- /dev/null
+++ b/src/main/java/at/hannibal2/skyhanni/config/features/dev/RepoPatternConfig.java
@@ -0,0 +1,23 @@
+package at.hannibal2.skyhanni.config.features.dev;
+
+import com.google.gson.annotations.Expose;
+import io.github.moulberry.moulconfig.annotations.ConfigEditorBoolean;
+import io.github.moulberry.moulconfig.annotations.ConfigOption;
+import io.github.moulberry.moulconfig.observer.Property;
+
+public class RepoPatternConfig {
+ @Expose
+ @ConfigOption(name = "Force Local Loading", desc = "Force loading local patterns.")
+ @ConfigEditorBoolean
+ public Property<Boolean> forceLocal = Property.of(false);
+
+ @Expose
+ @ConfigOption(name = "Tolerate Duplicate Usages", desc = "Don't crash when two or more code locations use the same RepoPattern key")
+ @ConfigEditorBoolean
+ public boolean tolerateDuplicateUsage = false;
+
+ @Expose
+ @ConfigOption(name = "Tolerate Late Registration", desc = "Don't crash when a RepoPattern is obtained after preinitialization.")
+ @ConfigEditorBoolean
+ public boolean tolerateLateRegistration = false;
+}
diff --git a/src/main/java/at/hannibal2/skyhanni/events/LorenzEvent.kt b/src/main/java/at/hannibal2/skyhanni/events/LorenzEvent.kt
index 766da4c14..767643755 100644
--- a/src/main/java/at/hannibal2/skyhanni/events/LorenzEvent.kt
+++ b/src/main/java/at/hannibal2/skyhanni/events/LorenzEvent.kt
@@ -1,6 +1,8 @@
package at.hannibal2.skyhanni.events
import at.hannibal2.skyhanni.data.EventCounter
+import at.hannibal2.skyhanni.mixins.hooks.getValue
+import at.hannibal2.skyhanni.mixins.hooks.setValue
import at.hannibal2.skyhanni.mixins.transformers.AccessorEventBus
import at.hannibal2.skyhanni.test.command.ErrorManager
import at.hannibal2.skyhanni.utils.LorenzUtils
@@ -16,6 +18,15 @@ abstract class LorenzEvent : Event() {
fun postAndCatch() = postAndCatchAndBlock {}
+ companion object {
+ var eventHandlerDepth by object : ThreadLocal<Int>() {
+ override fun initialValue(): Int {
+ return 0
+ }
+ }
+ val isInGuardedEventHandler get() = eventHandlerDepth > 0
+ }
+
fun postAndCatchAndBlock(
printError: Boolean = true,
stopOnFirstError: Boolean = false,
@@ -25,6 +36,7 @@ abstract class LorenzEvent : Event() {
EventCounter.count(eventName)
val visibleErrors = 3
var errors = 0
+ eventHandlerDepth++
for (listener in getListeners()) {
try {
listener.invoke(this)
@@ -40,6 +52,7 @@ abstract class LorenzEvent : Event() {
if (stopOnFirstError) break
}
}
+ eventHandlerDepth--
if (errors > visibleErrors) {
val hiddenErrors = errors - visibleErrors
LorenzUtils.error("$hiddenErrors more errors in $eventName are hidden!")
diff --git a/src/main/java/at/hannibal2/skyhanni/events/PreInitFinished.kt b/src/main/java/at/hannibal2/skyhanni/events/PreInitFinished.kt
new file mode 100644
index 000000000..9b36f8826
--- /dev/null
+++ b/src/main/java/at/hannibal2/skyhanni/events/PreInitFinished.kt
@@ -0,0 +1,3 @@
+package at.hannibal2.skyhanni.events
+
+class PreInitFinished : LorenzEvent()
diff --git a/src/main/java/at/hannibal2/skyhanni/features/inventory/HarpFeatures.kt b/src/main/java/at/hannibal2/skyhanni/features/inventory/HarpFeatures.kt
index 0f8c3fe4a..ec5065d5f 100644
--- a/src/main/java/at/hannibal2/skyhanni/features/inventory/HarpFeatures.kt
+++ b/src/main/java/at/hannibal2/skyhanni/features/inventory/HarpFeatures.kt
@@ -8,6 +8,8 @@ import at.hannibal2.skyhanni.utils.KeyboardManager
import at.hannibal2.skyhanni.utils.KeyboardManager.isKeyHeld
import at.hannibal2.skyhanni.utils.LorenzUtils
import at.hannibal2.skyhanni.utils.SimpleTimeMark
+import at.hannibal2.skyhanni.utils.StringUtils.matches
+import at.hannibal2.skyhanni.utils.repopatterns.RepoPattern
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.inventory.GuiChest
import net.minecraft.item.Item
@@ -31,12 +33,15 @@ object HarpFeatures {
}
private val buttonColors = listOf('d', 'e', 'a', '2', '5', '9', 'b')
+ val inventoryTitleRegex by RepoPattern.pattern("harp.inventory", "^Harp.*")
+
+ private fun isHarpGui() = inventoryTitleRegex.matches(openInventoryName())
@SubscribeEvent
fun onGui(event: GuiScreenEvent) {
if (!LorenzUtils.inSkyBlock) return
if (!config.keybinds) return
- if (!openInventoryName().startsWith("Harp")) return
+ if (!isHarpGui()) return
val chest = event.gui as? GuiChest ?: return
for ((index, key) in KeyIterable.withIndex()) {
@@ -71,7 +76,7 @@ object HarpFeatures {
fun onRenderItemTip(event: RenderItemTipEvent) {
if (!LorenzUtils.inSkyBlock) return
if (!config.showNumbers) return
- if (!openInventoryName().startsWith("Harp")) return
+ if (!isHarpGui()) return
if (Item.getIdFromItem(event.stack.item) != 159) return // Stained hardened clay item id = 159
// Example: §9| §7Click! will select the 9
diff --git a/src/main/java/at/hannibal2/skyhanni/mixins/hooks/GuiPlayerTabOverlayHook.kt b/src/main/java/at/hannibal2/skyhanni/mixins/hooks/GuiPlayerTabOverlayHook.kt
index 158995e55..3496441fe 100644
--- a/src/main/java/at/hannibal2/skyhanni/mixins/hooks/GuiPlayerTabOverlayHook.kt
+++ b/src/main/java/at/hannibal2/skyhanni/mixins/hooks/GuiPlayerTabOverlayHook.kt
@@ -10,11 +10,11 @@ var tabListGuard by object : ThreadLocal<Boolean>() {
}
}
-private operator fun <T> ThreadLocal<T>.setValue(t: Any?, property: KProperty<*>, any: T) {
+operator fun <T> ThreadLocal<T>.setValue(t: Any?, property: KProperty<*>, any: T) {
this.set(any)
}
-private operator fun <T> ThreadLocal<T>.getValue(t: Any?, property: KProperty<*>): T {
+operator fun <T> ThreadLocal<T>.getValue(t: Any?, property: KProperty<*>): T {
return get()
}
@@ -27,4 +27,4 @@ fun getPlayerName(original: String, cir: CallbackInfoReturnable<String>) {
if (original != newText) {
cir.returnValue = newText
}
-} \ No newline at end of file
+}
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 }
+ }
+}
diff --git a/src/main/resources/assets/skyhanni/gui/regexes.xml b/src/main/resources/assets/skyhanni/gui/regexes.xml
new file mode 100644
index 000000000..7ba1feaef
--- /dev/null
+++ b/src/main/resources/assets/skyhanni/gui/regexes.xml
@@ -0,0 +1,25 @@
+<Root xmlns="http://notenoughupdates.org/moulconfig" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://notenoughupdates.org/moulconfig https://raw.githubusercontent.com/NotEnoughUpdates/MoulConfig/master/MoulConfig.xsd">
+ <Gui>
+ <Column>
+ <Row>
+ <Text text="Search: "/>
+ <TextField value="@search"/>
+ <Text text="@poll"/>
+ </Row>
+ <ScrollPanel width="360" height="266">
+ <Array data="@searchResults">
+ <Row>
+ <Hover lines="@keyW">
+ <Text text="@key" width="100"/>
+ </Hover>
+ <Hover lines="@hoverRegex">
+ <Text text="@regex" width="200"/>
+ </Hover>
+ <Text text="@overriden" width="50"/>
+ </Row>
+ </Array>
+ </ScrollPanel>
+ </Column>
+ </Gui>
+</Root>