Skip to content

Commit

Permalink
ChatTrigger: Completely rework the criteria and parameter systems
Browse files Browse the repository at this point in the history
This removes the `Parameter` class and replaces it with 3 methods: `startsWith`, `contains`, and `endsWith`. These methods all take in criterias to make the API more understandable to users (in the past, I've noticed a lot of people attempting to do `setContains(msg)` instead of `setCriteria(msg).setContains()`).

setCriteria will work as normal, with a few slight tweaks.
- Now global flags will work if the criteria is passed as a Regex
- `.` or criteria variables will now detect `\n`.
- String criteria variables will now not be greedy. e.g. `setContains("<${a}>")` with a string of "<A> B>" will capture "A" in this string, instead of "A> B".
  • Loading branch information
camnwalter committed Apr 13, 2024
1 parent 5dc54f0 commit 4128ec4
Show file tree
Hide file tree
Showing 4 changed files with 113 additions and 182 deletions.
1 change: 1 addition & 0 deletions .idea/codeStyles/Project.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 3 additions & 14 deletions api/ctjs.api
Original file line number Diff line number Diff line change
Expand Up @@ -1936,21 +1936,10 @@ public class com/chattriggers/ctjs/api/triggers/CancellableEvent {

public final class com/chattriggers/ctjs/api/triggers/ChatTrigger : com/chattriggers/ctjs/api/triggers/Trigger {
public fun <init> (Ljava/lang/Object;Lcom/chattriggers/ctjs/api/triggers/ITriggerType;)V
public final fun addParameter (Ljava/lang/String;)Lcom/chattriggers/ctjs/api/triggers/ChatTrigger;
public final fun addParameters ([Ljava/lang/String;)Lcom/chattriggers/ctjs/api/triggers/ChatTrigger;
public final fun setCaseInsensitive ()Lcom/chattriggers/ctjs/api/triggers/ChatTrigger;
public final fun setChatCriteria (Ljava/lang/Object;)Lcom/chattriggers/ctjs/api/triggers/ChatTrigger;
public final fun setContains ()Lcom/chattriggers/ctjs/api/triggers/ChatTrigger;
public final fun contains ([Ljava/lang/Object;)Lcom/chattriggers/ctjs/api/triggers/ChatTrigger;
public final fun endsWith (Ljava/lang/Object;)Lcom/chattriggers/ctjs/api/triggers/ChatTrigger;
public final fun setCriteria (Ljava/lang/Object;)Lcom/chattriggers/ctjs/api/triggers/ChatTrigger;
public final fun setEnd ()Lcom/chattriggers/ctjs/api/triggers/ChatTrigger;
public final fun setExact ()Lcom/chattriggers/ctjs/api/triggers/ChatTrigger;
public final fun setFormatted ()V
public final fun setFormatted (Z)V
public static synthetic fun setFormatted$default (Lcom/chattriggers/ctjs/api/triggers/ChatTrigger;ZILjava/lang/Object;)V
public final fun setParameter (Ljava/lang/String;)Lcom/chattriggers/ctjs/api/triggers/ChatTrigger;
public final fun setParameters ([Ljava/lang/String;)Lcom/chattriggers/ctjs/api/triggers/ChatTrigger;
public final fun setStart ()Lcom/chattriggers/ctjs/api/triggers/ChatTrigger;
public final fun triggerIfCanceled (Z)Lcom/chattriggers/ctjs/api/triggers/ChatTrigger;
public final fun startsWith (Ljava/lang/Object;)Lcom/chattriggers/ctjs/api/triggers/ChatTrigger;
}

public final class com/chattriggers/ctjs/api/triggers/ChatTrigger$Event : com/chattriggers/ctjs/api/triggers/CancellableEvent {
Expand Down
263 changes: 101 additions & 162 deletions src/main/kotlin/com/chattriggers/ctjs/api/triggers/ChatTrigger.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,175 +4,122 @@ import com.chattriggers.ctjs.api.message.TextComponent
import org.mozilla.javascript.regexp.NativeRegExp

class ChatTrigger(method: Any, type: ITriggerType) : Trigger(method, type) {
private lateinit var chatCriteria: Any
private var formatted: Boolean = false
private var formattedForced = false
private var caseInsensitive: Boolean = false
private lateinit var criteriaPattern: Regex
private val parameters = mutableListOf<Parameter?>()
private var triggerIfCanceled: Boolean = true
private var usingCriteria = false
private var global = false

/**
* Sets if the chat trigger should run if the chat event has already been canceled.
* True by default.
* @param bool Boolean to set
* @return the trigger object for method chaining
*/
fun triggerIfCanceled(bool: Boolean) = apply { triggerIfCanceled = bool }
private var startsWith: Regex? = null
private val contains = mutableSetOf<Regex>()
private var endsWith: Regex? = null

/**
* Sets the chat criteria for [matchesChatCriteria].
* Arguments for the trigger's method can be passed in using ${variable}.
* Example: `setChatCriteria("<${name}> ${message}");`
* Use ${*} to match a chat message but ignore the pass through.
* @param chatCriteria the chat criteria to set
* @return the trigger object for method chaining
* Creates a regex string and flags according to the criteria
*/
fun setChatCriteria(chatCriteria: Any) = apply {
this.chatCriteria = chatCriteria
val flags = mutableSetOf<RegexOption>()
private fun createCriteria(criteria: Any): Pair<String, Set<RegexOption>> {
var source = ".+"
val flags = mutableSetOf<RegexOption>()

when (chatCriteria) {
when (criteria) {
is CharSequence -> {
if (!formattedForced)
formatted = Regex("[&\u00a7]") in chatCriteria
val chatCriteria = criteria.toString()

val replacedCriteria = Regex.escape(chatCriteria.toString().replace("\n", "->newLine<-"))
.replace(Regex("\\\$\\{[^*]+?}"), "\\\\E(.+)\\\\Q")
.replace(Regex("\\$\\{\\*?}"), "\\\\E(?:.+)\\\\Q")
if ("\n" in chatCriteria)
flags.add(RegexOption.DOT_MATCHES_ALL)

if (caseInsensitive)
flags.add(RegexOption.IGNORE_CASE)
val replacedCriteria = chatCriteria.replace("\n", "\\n")
.replace(Regex("""\$\{[^*]+?}"""), "(.+?)")
.replace(Regex("""\$\{\*?}"""), "(?:.+?)")

if ("" != chatCriteria)
source = replacedCriteria
}
is NativeRegExp -> {
if (chatCriteria["ignoreCase"] as Boolean || caseInsensitive)
if (criteria["ignoreCase"] as Boolean)
flags.add(RegexOption.IGNORE_CASE)

if (chatCriteria["multiline"] as Boolean)
if (criteria["multiline"] as Boolean)
flags.add(RegexOption.MULTILINE)

source = (chatCriteria["source"] as String).let {
if (criteria["dotAll"] as Boolean)
flags.add(RegexOption.DOT_MATCHES_ALL)

source = (criteria["source"] as String).let {
if ("" == it) ".+" else it
}

if (!formattedForced)
formatted = Regex("[&\u00a7]") in source
}
else -> throw IllegalArgumentException("Expected String or Regexp Object")
}

criteriaPattern = Regex(source, flags)
formatted = formatted or (Regex("[&\u00a7]") in source)
return source to flags
}

/**
* Alias for [setChatCriteria].
* @param chatCriteria the chat criteria to set
* Sets the chat criteria for this trigger.
* Arguments for the trigger's method can be passed in using ${variable}.
* Example: `setCriteria("<${name}> ${message}");`
* Use ${*} to match a chat message but ignore the pass through.
* @param criteria the chat criteria to set
* @return the trigger object for method chaining
*/
fun setCriteria(chatCriteria: Any) = setChatCriteria(chatCriteria)
fun setCriteria(criteria: Any) = apply {
check(startsWith == null && contains.isEmpty() && endsWith == null) {
"Can not use setCriteria() with any of startsWith(), contains(), or endsWith()"
}

/**
* Sets the chat parameter for [Parameter].
* Clears current parameter list.
* @param parameter the chat parameter to set
* @return the trigger object for method chaining
*/
fun setParameter(parameter: String) = apply {
parameters.clear()
addParameter(parameter)
}
usingCriteria = true
val (source, flags) = createCriteria(criteria)

/**
* Sets multiple chat parameters for [Parameter].
* Clears current parameter list.
* @param parameters the chat parameters to set
* @return the trigger object for method chaining
*/
fun setParameters(vararg parameters: String) = apply {
this.parameters.clear()
addParameters(*parameters)
}
if (criteria is NativeRegExp && criteria["global"] as Boolean)
global = true

/**
* Adds chat parameter for [Parameter].
* @param parameter the chat parameter to add
* @return the trigger object for method chaining
*/
fun addParameter(parameter: String) = apply {
parameters.add(Parameter.getParameterByName(parameter))
criteriaPattern = Regex("^${source}\$", flags)
}

/**
* Adds multiple chat parameters for [Parameter].
* @param parameters the chat parameters to add
* Sets the starting criteria for this trigger. In order for this trigger to run, the beginning of
* the chat message must match [criteria].
* Like [setCriteria], arguments can be passed in using ${variable}.
* @return the trigger object for method chaining
*/
fun addParameters(vararg parameters: String) = apply {
parameters.forEach(::addParameter)
}
fun startsWith(criteria: Any) = apply {
check(!usingCriteria) { "Can not use both setCriteria() and startsWith()" }

/**
* Adds the "start" parameter
* @return the trigger object for method chaining
*/
fun setStart() = apply {
setParameter("start")
val (source, flags) = createCriteria(criteria)
startsWith = Regex("^${source}", flags)
}

/**
* Adds the "contains" parameter
* Sets criteria this trigger must contain. In order for this trigger to run, the beginning of
* the chat message must contain **all** [criteria].
* Like [setCriteria], arguments can be passed in using ${variable}.
* @return the trigger object for method chaining
*/
fun setContains() = apply {
setParameter("contains")
}
fun contains(vararg criteria: Any) = apply {
check(!usingCriteria) { "Can not use both setCriteria() and contains()" }

/**
* Adds the "end" parameter
* @return the trigger object for method chaining
*/
fun setEnd() = apply {
setParameter("end")
}

/**
* Forces this trigger to be formatted or unformatted. If no argument is
* provided, it will be set to formatted. This method overrides the
* behavior of inferring the formatted status from the criteria.
*/
@JvmOverloads
fun setFormatted(formatted: Boolean = true) {
this.formatted = formatted
this.formattedForced = true
}

/**
* Makes the trigger match the entire chat message
* @return the trigger object for method chaining
*/
fun setExact() = apply {
parameters.clear()
for (criterion in criteria) {
val (source, flags) = createCriteria(criterion)
contains += Regex(source, flags)
}
}

/**
* Makes the chat criteria case insensitive
* Sets the ending criteria for this trigger. In order for this trigger to run, the end of
* the chat message must match [criteria].
* Like [setCriteria], arguments can be passed in using ${variable}.
* @return the trigger object for method chaining
*/
fun setCaseInsensitive() = apply {
caseInsensitive = true
fun endsWith(criteria: Any) = apply {
check(!usingCriteria) { "Can not use both setCriteria() and endsWith()" }

// Reparse criteria if setCriteria has already been called
if (::chatCriteria.isInitialized)
setCriteria(chatCriteria)
val (source, flags) = createCriteria(criteria)
endsWith = Regex("${source}\$", flags)
}

/**
* Argument 1 (String) The chat message received
* Argument 2 (ClientChatReceivedEvent) the chat event fired
* Argument 0 (Event) The chat message event
* @param args list of arguments as described
*/
override fun trigger(args: Array<out Any?>) {
Expand All @@ -181,15 +128,12 @@ class ChatTrigger(method: Any, type: ITriggerType) : Trigger(method, type) {
}

val chatEvent = args[0] as Event

if (!triggerIfCanceled && chatEvent.isCancelled()) return
if (chatEvent.isCancelled()) return

val chatMessage = getChatMessage(chatEvent.message)

val variables = getVariables(chatMessage) ?: return
variables.add(chatEvent)

callMethod(variables.toTypedArray())
callMethod((variables + chatEvent).toTypedArray())
}

// helper method to get the proper chat message based on the presence of color codes
Expand All @@ -198,64 +142,59 @@ class ChatTrigger(method: Any, type: ITriggerType) : Trigger(method, type) {
chatMessage.formattedText.replace("\u00a7", "&")
else chatMessage.unformattedText

// helper method to get the variables to pass through
private fun getVariables(chatMessage: String) =
if (::criteriaPattern.isInitialized)
matchesChatCriteria(chatMessage.replace("\n", "->newLine<-"))
else ArrayList()

/**
* A method to check whether a received chat message
* matches this trigger's definition criteria.
* Ex. "FalseHonesty joined Cops vs Crims" would match `${playername} joined ${gamejoined}`
* @param chat the chat message to compare against
* @return a list of the variables, in order or null if it doesn't match
*/
private fun matchesChatCriteria(chat: String): MutableList<Any>? {
val regex = criteriaPattern

if (parameters.isEmpty()) {
if (!(regex matches chat)) return null
} else {
parameters.forEach { parameter ->
val first = try {
regex.find(chat)?.groups?.get(0)
} catch (e: IndexOutOfBoundsException) {
return null
}
private fun getVariables(chat: String): List<String>? {
if (usingCriteria && ::criteriaPattern.isInitialized) {
if (global)
return criteriaPattern.findAll(chat).flatMap {
it.groupValues.drop(1)
}.toList().ifEmpty { null }

when (parameter) {
Parameter.CONTAINS -> if (first == null) return null
Parameter.START -> if (first == null || first.range.first != 0) return null
Parameter.END -> if (first?.range?.last != chat.length) return null
null -> if (!(regex matches chat)) return null
}
}
return criteriaPattern.find(chat)?.groupValues?.drop(1)
}

return regex.find(chat)?.groupValues?.drop(1)?.toMutableList()
}
val variables = mutableListOf<String>()
var start = 0
var end = chat.length

/**
* The parameter to match chat criteria to.
* Location parameters
* - contains
* - start
* - end
*/
private enum class Parameter(vararg names: String) {
CONTAINS("<c>", "<contains>", "c", "contains"),
START("<s>", "<start>", "s", "start"),
END("<e>", "<end>", "e", "end");
if (startsWith != null) {
val matcher = startsWith!!.find(chat) ?: return null
start = matcher.range.last + 1
matcher.groupValues.drop(1).let { variables += it }
}

var names: List<String> = names.asList()
if (endsWith != null) {
var subStart = chat.length - 1
var matcher: MatchResult? = null

companion object {
fun getParameterByName(name: String) =
entries.find { param ->
param.names.any { it.lowercase() == name }
while (subStart >= 0) {
matcher = endsWith!!.find(chat.substring(subStart))
if (matcher == null) {
subStart--
} else {
break
}
}

if (matcher == null) return null

end = subStart
matcher.groupValues.drop(1).let { variables += it }
}

for (contain in contains) {
contain.find(chat.substring(start, end))?.groupValues?.drop(1)?.let {
variables += it
} ?: return null
}

return variables
}

class Event(@JvmField val message: TextComponent) : CancellableEvent()
Expand Down
Loading

0 comments on commit 4128ec4

Please sign in to comment.