Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ChatTrigger: Completely rework the criteria and parameter systems #122

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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