Skip to content

Commit fbd4078

Browse files
committed
Fix sign multi line detection
1 parent 01eacb4 commit fbd4078

4 files changed

Lines changed: 189 additions & 88 deletions

File tree

paper/src/main/java/io/wdsj/asw/bukkit/AdvancedSensitiveWords.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,7 @@ private void checkForUpdatesAsync() {
267267
return;
268268
}
269269
getScheduler().runTaskAsynchronously(() -> {
270-
LOGGER.info("Checking for update...");
270+
LOGGER.info("Checking for updates...");
271271
Updater.UpdateResult result = Updater.checkNow();
272272
updateResult = result;
273273
if (result.isUpdateAvailable()) {

paper/src/main/kotlin/io/wdsj/asw/bukkit/listener/SignListener.kt

Lines changed: 133 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import io.wdsj.asw.bukkit.util.context.SignContextEntry
2121
import io.wdsj.asw.bukkit.util.context.SignContextTarget
2222
import io.wdsj.asw.bukkit.util.message.MessageUtils
2323
import net.kyori.adventure.text.Component
24+
import net.kyori.adventure.text.TextReplacementConfig
2425
import net.kyori.adventure.text.event.ClickEvent
2526
import net.kyori.adventure.text.event.HoverEvent
2627
import net.kyori.adventure.text.format.NamedTextColor
@@ -56,9 +57,21 @@ class SignListener(private val configuration: PaperConfigurationService) : Liste
5657

5758
val startTime = System.currentTimeMillis()
5859
val lineScan = censorSingleLines(event, player, options)
60+
val multiLineViolation = if (lineScan.violation == null || !isCancelMode(options)) {
61+
censorMultiLine(event, options)
62+
} else {
63+
null
64+
}
65+
val contextViolation = if (
66+
!isCancelMode(options) || (lineScan.violation == null && multiLineViolation == null)
67+
) {
68+
censorContext(event, player, options)
69+
} else {
70+
null
71+
}
5972
val violation = lineScan.violation
60-
?: censorMultiLine(event, lineScan, options)
61-
?: censorContext(event, player, options)
73+
?: multiLineViolation
74+
?: contextViolation
6275
?: return
6376

6477
if (isCancelMode(options) && !violation.context &&
@@ -111,20 +124,15 @@ class SignListener(private val configuration: PaperConfigurationService) : Liste
111124

112125
private fun censorSingleLines(event: SignChangeEvent, player: Player, options: PlayerOptionView): SignLineScan {
113126
var violation: SignViolation? = null
114-
val cleanLineIndexes = mutableListOf<Int>()
115-
val cleanLineContent = StringBuilder()
116127

117128
for (lineIndex in event.lines().indices) {
118129
val originalComponent = event.line(lineIndex) ?: continue
119-
val originalMessage = preprocess(MessageUtils.plainText(originalComponent))
130+
val scanComponent = preprocess(originalComponent)
131+
val originalMessage = MessageUtils.plainText(scanComponent)
120132
val censoredWords = AdvancedSensitiveWords.findAllSensitive(originalMessage)
121133
SensitiveFilterEvents.post(event.isAsynchronous, ModuleType.SIGN, player, originalMessage, censoredWords)
122134

123135
if (censoredWords.isEmpty()) {
124-
if (originalMessage.trim().isNotEmpty()) {
125-
cleanLineIndexes.add(lineIndex)
126-
cleanLineContent.append(originalMessage)
127-
}
128136
continue
129137
}
130138

@@ -134,27 +142,41 @@ class SignListener(private val configuration: PaperConfigurationService) : Liste
134142
continue
135143
}
136144
val processedMessage = AdvancedSensitiveWords.replaceSensitive(originalMessage)
137-
event.line(lineIndex, MessageUtils.replaceLiteral(originalComponent, originalMessage, processedMessage))
145+
event.line(lineIndex, replaceWholeLine(scanComponent, originalMessage, processedMessage))
138146
}
139147

140-
return SignLineScan(violation, cleanLineIndexes, cleanLineContent.toString())
148+
return SignLineScan(violation)
141149
}
142150

143-
private fun censorMultiLine(event: SignChangeEvent, lineScan: SignLineScan, options: PlayerOptionView): SignViolation? {
151+
private fun censorMultiLine(event: SignChangeEvent, options: PlayerOptionView): SignViolation? {
144152
if (!options.bool(PlayerOptions.SIGN_MULTI_LINE_CHECK, PluginSettings.SIGN_MULTI_LINE_CHECK)) return null
145-
if (lineScan.cleanLineIndexes.isEmpty()) return null
146153

147-
val originalContent = lineScan.cleanLineContent
154+
val lines = event.lines().indices.mapNotNull { lineIndex ->
155+
val component = event.line(lineIndex) ?: return@mapNotNull null
156+
val scanComponent = preprocess(component)
157+
val content = MessageUtils.plainText(scanComponent)
158+
if (content.isBlank()) return@mapNotNull null
159+
SignLineContent(lineIndex, scanComponent, content)
160+
}
161+
if (lines.size < 2) return null
162+
163+
val originalContent = lines.joinToString("") { it.content }
148164
val censoredWords = AdvancedSensitiveWords.findAllSensitive(originalContent)
149165
SensitiveFilterEvents.post(event.isAsynchronous, ModuleType.SIGN, event.player, originalContent, censoredWords)
150166
if (censoredWords.isEmpty()) return null
151167

152168
if (isCancelMode(options)) {
153169
event.isCancelled = true
154170
} else {
155-
val processedMessage = AdvancedSensitiveWords.replaceSensitive(originalContent)
156-
for (lineIndex in lineScan.cleanLineIndexes) {
157-
event.line(lineIndex, MessageUtils.plainTextComponent(processedMessage))
171+
val processedLines = resolveSegmentReplacements(
172+
lines.map { it.content.length },
173+
originalContent,
174+
)
175+
lines.forEachIndexed { index, line ->
176+
event.line(
177+
line.index,
178+
replaceWholeLine(line.component, line.content, processedLines[index]),
179+
)
158180
}
159181
}
160182

@@ -181,7 +203,7 @@ class SignListener(private val configuration: PaperConfigurationService) : Liste
181203
}
182204

183205
private fun contextEntry(event: SignChangeEvent): SignContextEntry {
184-
val lines = event.lines().map { preprocess(MessageUtils.plainText(it)) }
206+
val lines = event.lines().map { MessageUtils.plainText(preprocess(it)) }
185207
return SignContextEntry(
186208
content = lines.joinToString(""),
187209
target = SignContextTarget(
@@ -191,7 +213,7 @@ class SignListener(private val configuration: PaperConfigurationService) : Liste
191213
event.block.z,
192214
event.side,
193215
),
194-
lineLengths = lines.map(String::length),
216+
lineContents = lines,
195217
)
196218
}
197219

@@ -219,81 +241,98 @@ class SignListener(private val configuration: PaperConfigurationService) : Liste
219241
}
220242
}
221243

222-
private fun applyEventReplacement(event: SignChangeEvent, entry: SignContextEntry, replacement: String) {
223-
splitLines(entry.lineLengths, replacement).forEachIndexed { index, line ->
224-
event.line(index, Component.text(line))
244+
private fun applyEventReplacement(event: SignChangeEvent, entry: SignContextEntry, replacement: List<String>) {
245+
replacement.forEachIndexed { index, line ->
246+
val component = preprocess(event.line(index) ?: Component.empty())
247+
val originalLine = entry.lineContents.getOrElse(index) { "" }
248+
event.line(index, replaceWholeLine(component, originalLine, line))
225249
}
226250
}
227251

228-
private fun scheduleSignMutation(entry: SignContextEntry, replacement: String?) {
252+
private fun scheduleSignMutation(entry: SignContextEntry, replacement: List<String>?) {
229253
val world = Bukkit.getWorld(entry.target.worldId) ?: return
230254
val location = Location(world, entry.target.x.toDouble(), entry.target.y.toDouble(), entry.target.z.toDouble())
231255
AdvancedSensitiveWords.getScheduler().runTaskLater(location, Runnable {
232256
val sign = location.block.state as? Sign ?: return@Runnable
233257
val signSide = sign.getSide(entry.target.side)
234-
val currentContent = (0 until 4).joinToString("") { line ->
235-
preprocess(MessageUtils.plainText(signSide.line(line)))
236-
}
258+
val currentLines = (0 until 4).map { line -> preprocess(signSide.line(line)) }
259+
val currentContent = currentLines.joinToString("") { MessageUtils.plainText(it) }
237260
if (currentContent != entry.content) return@Runnable
238261

239-
val lines = replacement?.let { splitLines(entry.lineLengths, it) } ?: List(4) { "" }
240-
lines.forEachIndexed { index, line -> signSide.line(index, Component.text(line)) }
262+
val lines = replacement ?: List(4) { "" }
263+
lines.forEachIndexed { index, line ->
264+
val component = if (replacement == null) {
265+
Component.empty()
266+
} else {
267+
replaceWholeLine(currentLines[index], entry.lineContents[index], line)
268+
}
269+
signSide.line(index, component)
270+
}
241271
sign.update(false, false)
242272
}, 1L)
243273
}
244274

275+
private fun resolveSegmentReplacements(segmentLengths: List<Int>, context: String): List<String> {
276+
return SignTextLayout.replaceSegments(segmentLengths, context, replacementSpans(context))
277+
}
278+
245279
private fun resolveContext(entries: List<SignContextEntry>, context: String): ContextResolution {
246-
val starts = IntArray(entries.size)
247-
for (index in 1 until entries.size) {
248-
starts[index] = starts[index - 1] + entries[index - 1].content.length
280+
val entryLengths = IntArray(entries.size) { entries[it].content.length }
281+
val entryStarts = segmentStarts(entryLengths)
282+
val lineReferences = buildList {
283+
entries.forEachIndexed { entryIndex, entry ->
284+
entry.lineContents.forEachIndexed { lineIndex, content ->
285+
if (content.isNotEmpty()) {
286+
add(ContextLineReference(entryIndex, lineIndex, content))
287+
}
288+
}
289+
}
290+
}
291+
val spans = replacementSpans(context)
292+
val resolvedLines = SignTextLayout.replaceSegments(
293+
lineReferences.map { it.content.length },
294+
context,
295+
spans,
296+
)
297+
val replacements = entries.associateWith {
298+
MutableList(it.lineContents.size) { "" }
299+
}
300+
lineReferences.forEachIndexed { index, reference ->
301+
replacements.getValue(entries[reference.entryIndex])[reference.lineIndex] = resolvedLines[index]
249302
}
250303

251-
val replacements = Array(entries.size) { StringBuilder() }
252304
val affectedEntries = linkedSetOf<SignContextEntry>()
305+
spans.forEach { span ->
306+
markAffectedEntries(affectedEntries, entries, entryStarts, span.start, span.end)
307+
}
308+
309+
return ContextResolution(
310+
replacements.mapValues { it.value.toList() },
311+
affectedEntries,
312+
)
313+
}
314+
315+
private fun replacementSpans(context: String): List<SignReplacementSpan> {
316+
val spans = mutableListOf<SignReplacementSpan>()
253317
val results = AdvancedSensitiveWords.findAllSensitiveRaw(context)
254318
.sortedWith(compareBy<IWordResult> { it.startIndex() }.thenByDescending { it.endIndex() })
255319
var cursor = 0
256320
for (result in results) {
257321
val start = result.startIndex().coerceIn(0, context.length)
258322
val end = result.endIndex().coerceIn(start, context.length)
259323
if (start < cursor || start == end) continue
260-
261-
appendUnchangedContext(replacements, entries, starts, context, cursor, start)
262-
replacements[entryIndexAt(starts, start)].append(replacementFor(context, result))
263-
markAffectedEntries(affectedEntries, entries, starts, start, end)
324+
spans.add(SignReplacementSpan(start, end, replacementFor(context, start, end)))
264325
cursor = end
265326
}
266-
appendUnchangedContext(replacements, entries, starts, context, cursor, context.length)
267-
268-
return ContextResolution(
269-
entries.indices.associate { index -> entries[index] to replacements[index].toString() },
270-
affectedEntries,
271-
)
327+
return spans
272328
}
273329

274-
private fun appendUnchangedContext(
275-
replacements: Array<StringBuilder>,
276-
entries: List<SignContextEntry>,
277-
starts: IntArray,
278-
context: String,
279-
start: Int,
280-
end: Int,
281-
) {
282-
var cursor = start
283-
while (cursor < end) {
284-
val entryIndex = entryIndexAt(starts, cursor)
285-
val entryEnd = starts[entryIndex] + entries[entryIndex].content.length
286-
val segmentEnd = minOf(end, entryEnd)
287-
replacements[entryIndex].append(context, cursor, segmentEnd)
288-
cursor = segmentEnd
330+
private fun segmentStarts(lengths: IntArray): IntArray {
331+
val starts = IntArray(lengths.size)
332+
for (index in 1 until lengths.size) {
333+
starts[index] = starts[index - 1] + lengths[index - 1]
289334
}
290-
}
291-
292-
private fun entryIndexAt(starts: IntArray, index: Int): Int {
293-
for (entryIndex in starts.indices.reversed()) {
294-
if (index >= starts[entryIndex]) return entryIndex
295-
}
296-
return 0
335+
return starts
297336
}
298337

299338
private fun markAffectedEntries(
@@ -312,36 +351,34 @@ class SignListener(private val configuration: PaperConfigurationService) : Liste
312351
}
313352
}
314353

315-
private fun replacementFor(context: String, result: IWordResult): String {
316-
val sensitiveWord = context.substring(result.startIndex(), result.endIndex())
354+
private fun replacementFor(context: String, start: Int, end: Int): String {
355+
val sensitiveWord = context.substring(start, end)
317356
configuration.get(PluginSettings.DEFINED_REPLACEMENT).forEach { definition ->
318357
val separator = definition.indexOf('|')
319358
if (separator <= 0 || definition.indexOf('|', separator + 1) >= 0) return@forEach
320359
if (definition.substring(0, separator) == sensitiveWord) {
321360
return definition.substring(separator + 1)
322361
}
323362
}
324-
return configuration.get(PluginSettings.REPLACEMENT).repeat(result.endIndex() - result.startIndex())
363+
return configuration.get(PluginSettings.REPLACEMENT).repeat(end - start)
325364
}
326365

327-
private fun splitLines(lineLengths: List<Int>, content: String): List<String> {
328-
val lines = MutableList(4) { "" }
329-
var offset = 0
330-
for (lineIndex in lines.indices) {
331-
val expectedLength = lineLengths.getOrElse(lineIndex) { 0 }
332-
val end = minOf(content.length, offset + expectedLength)
333-
lines[lineIndex] = content.substring(offset, end)
334-
offset = end
335-
}
336-
if (offset < content.length) {
337-
lines[3] += content.substring(offset)
338-
}
339-
return lines
366+
private fun replaceWholeLine(component: Component, originalText: String, replacement: String): Component {
367+
if (originalText == replacement) return component
368+
369+
val replaced = MessageUtils.replaceLiteral(component, originalText, replacement)
370+
if (MessageUtils.plainText(replaced) == replacement) return replaced
371+
return Component.text(replacement).style(component.style())
340372
}
341373

342-
private fun preprocess(text: String): String {
343-
if (!configuration.get(PluginSettings.PRE_PROCESS)) return text
344-
return text.replace(Utils.preProcessRegex.toRegex(), "")
374+
private fun preprocess(component: Component): Component {
375+
if (!configuration.get(PluginSettings.PRE_PROCESS)) return component
376+
377+
val replacementConfig = TextReplacementConfig.builder()
378+
.match(Utils.preProcessRegex.toPattern())
379+
.replacement("")
380+
.build()
381+
return component.replaceText(replacementConfig)
345382
}
346383

347384
private fun isCancelMode(options: PlayerOptionView): Boolean {
@@ -350,8 +387,18 @@ class SignListener(private val configuration: PaperConfigurationService) : Liste
350387

351388
private data class SignLineScan(
352389
val violation: SignViolation?,
353-
val cleanLineIndexes: List<Int>,
354-
val cleanLineContent: String,
390+
)
391+
392+
private data class SignLineContent(
393+
val index: Int,
394+
val component: Component,
395+
val content: String,
396+
)
397+
398+
private data class ContextLineReference(
399+
val entryIndex: Int,
400+
val lineIndex: Int,
401+
val content: String,
355402
)
356403

357404
private data class SignViolation(
@@ -361,7 +408,7 @@ class SignListener(private val configuration: PaperConfigurationService) : Liste
361408
)
362409

363410
private data class ContextResolution(
364-
val replacements: Map<SignContextEntry, String>,
411+
val replacements: Map<SignContextEntry, List<String>>,
365412
val affectedEntries: Set<SignContextEntry>,
366413
)
367414
}

paper/src/main/kotlin/io/wdsj/asw/bukkit/util/context/SignContext.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ object SignContext {
4747
data class SignContextEntry(
4848
val content: String,
4949
val target: SignContextTarget,
50-
val lineLengths: List<Int>,
50+
val lineContents: List<String>,
5151
val time: Long = System.currentTimeMillis(),
5252
)
5353

0 commit comments

Comments
 (0)