diff options
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> |