From 57acdfea63a58203cebda34ef7e16ed6fcc1bf1d Mon Sep 17 00:00:00 2001 From: Linnea Gräf Date: Fri, 8 Dec 2023 14:22:52 +0100 Subject: Add RepoPatterns (#715) Added RepoPatterns. #715 --- src/main/java/at/hannibal2/skyhanni/SkyHanniMod.kt | 5 + .../at/hannibal2/skyhanni/config/ConfigManager.kt | 2 + .../hannibal2/skyhanni/config/commands/Commands.kt | 2 + .../skyhanni/config/features/dev/DevConfig.java | 5 + .../config/features/dev/RepoPatternConfig.java | 23 ++++ .../at/hannibal2/skyhanni/events/LorenzEvent.kt | 13 ++ .../hannibal2/skyhanni/events/PreInitFinished.kt | 3 + .../skyhanni/features/inventory/HarpFeatures.kt | 9 +- .../mixins/hooks/GuiPlayerTabOverlayHook.kt | 6 +- .../skyhanni/utils/repopatterns/RepoPattern.kt | 98 +++++++++++++ .../skyhanni/utils/repopatterns/RepoPatternDump.kt | 30 ++++ .../utils/repopatterns/RepoPatternGroup.kt | 26 ++++ .../skyhanni/utils/repopatterns/RepoPatternGui.kt | 82 +++++++++++ .../skyhanni/utils/repopatterns/RepoPatternImpl.kt | 49 +++++++ .../utils/repopatterns/RepoPatternKeyOwner.kt | 8 ++ .../utils/repopatterns/RepoPatternManager.kt | 153 +++++++++++++++++++++ 16 files changed, 509 insertions(+), 5 deletions(-) create mode 100644 src/main/java/at/hannibal2/skyhanni/config/features/dev/RepoPatternConfig.java create mode 100644 src/main/java/at/hannibal2/skyhanni/events/PreInitFinished.kt create mode 100644 src/main/java/at/hannibal2/skyhanni/utils/repopatterns/RepoPattern.kt create mode 100644 src/main/java/at/hannibal2/skyhanni/utils/repopatterns/RepoPatternDump.kt create mode 100644 src/main/java/at/hannibal2/skyhanni/utils/repopatterns/RepoPatternGroup.kt create mode 100644 src/main/java/at/hannibal2/skyhanni/utils/repopatterns/RepoPatternGui.kt create mode 100644 src/main/java/at/hannibal2/skyhanni/utils/repopatterns/RepoPatternImpl.kt create mode 100644 src/main/java/at/hannibal2/skyhanni/utils/repopatterns/RepoPatternKeyOwner.kt create mode 100644 src/main/java/at/hannibal2/skyhanni/utils/repopatterns/RepoPatternManager.kt (limited to 'src/main/java/at') 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() { 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 @@ -29,6 +29,11 @@ public class DevConfig { @Accordion 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) 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 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() { + 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() { } } -private operator fun ThreadLocal.setValue(t: Any?, property: KProperty<*>, any: T) { +operator fun ThreadLocal.setValue(t: Any?, property: KProperty<*>, any: T) { this.set(any) } -private operator fun ThreadLocal.getValue(t: Any?, property: KProperty<*>): T { +operator fun ThreadLocal.getValue(t: Any?, property: KProperty<*>): T { return get() } @@ -27,4 +27,4 @@ fun getPlayerName(original: String, cir: CallbackInfoReturnable) { 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 { + /** + * 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 = 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()) + + + class RepoPatternInfo( + repoPatternImpl: RepoPatternImpl + ) { + @field:Bind + val key: String = repoPatternImpl.key + + @field:Bind + val regex: String = repoPatternImpl.value.pattern() + + @field:Bind + val hoverRegex: List = 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 { + 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 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 = 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() + + 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("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 } + } +} -- cgit