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