Anarchy helper

Watches chat and your health, marks nearby deaths, warns on low health, and remembers spots via a command. An example of a script that ties packets, a config, a command and notifications together.

name("AnarchyHelper")
description("Marks, warnings and notes for anarchy")

key(Key.H)

val warnHealth = slider("Warn at health", 8f, 1f, 20f, 1f)
val markDeaths = checkBox("Mark deaths", true)
val deathMinutes = slider("Mark lives, minutes", 2, 1, 10)
    .visibleWhen { markDeaths.value() }
val notifyChat = checkBox("Catch mentions", true)
val watchWords = input("Words, comma separated", "tp,tpa,help")

val notes = config("notes")
val sinceWarn = timer()

var lastHealth = 20f

onEnable {
    lastHealth = 20f
    sinceWarn.reset()
}

fun words(): List<String> =
    watchWords.value()
        .split(",")
        .map { it.trim().lowercase() }
        .filter { it.isNotEmpty() }

on<ClientTickEvent> {
    whenInGame {
        val health = player.health()
        if (health < warnHealth.value() && health < lastHealth && sinceWarn.passedAndReset(40)) {
            notify("health " + health.toInt(), NotifyKind.WARN)
        }
        lastHealth = health
    }
}

on<PacketReceiveEvent> { e ->
    if (!notifyChat.value()) return@on
    val text = when (val packet = e.packet()) {
        is S2CGameMessagePacket -> if (packet.overlay()) null else packet.content()
        is S2CProfilelessChatMessagePacket -> packet.message()
        is S2CChatMessagePacket -> packet.unsignedContent()
        else -> null
    } ?: return@on

    val lower = text.lowercase()
    if (words().none { lower.contains(it) }) return@on

    onClientThread {
        notify("chat: " + text.take(48), NotifyKind.ACCENT)
    }
}

on<EntityRemoveEvent> { e ->
    if (!markDeaths.value()) return@on
    val entity = e.entity()
    if (!entity.isPlayer() || entity.isSelf()) return@on
    val living = entity.asLiving() ?: return@on
    if (living.health() > 0f) return@on
    if (entity.distanceTo(player.position()) > 48.0) return@on

    client.waypoints().add(entity.name(), entity.position(), deathMinutes.intValue() * 20 * 60)
    chat.print("marked where " + entity.name() + " went down")
}

command("note") {
    usage("<add|go|list|del>")
    alias("n")

    sub("add") {
        usage("<name>")
        runs {
            val name = arg(0)
            val position = player.position()
            notes.put(name, position.x().toInt().toString() + " "
                + position.y().toInt() + " " + position.z().toInt())
            notes.save()
            reply("saved " + name)
        }
    }

    sub("go") {
        usage("<name>")
        completes(0) { notes.keys().toList() }
        runs {
            val value = notes.get(arg(0), "")
            if (value.isEmpty()) {
                replyError("no note called " + arg(0))
                return@runs
            }
            val parts = value.split(" ")
            client.waypoints().add(arg(0), Vec.of(
                parts[0].toDouble(), parts[1].toDouble(), parts[2].toDouble()
            ))
            reply("marked " + value)
        }
    }

    sub("del") {
        usage("<name>")
        completes(0) { notes.keys().toList() }
        runs {
            notes.remove(arg(0))
            notes.save()
            reply("removed " + arg(0))
        }
    }

    sub("list") {
        runs {
            if (notes.keys().isEmpty()) {
                reply("no notes")
                return@runs
            }
            reply("notes: " + notes.keys().joinToString(", "))
        }
    }
}

Things worth noticing

Packets are read on the network thread, notifications shown on the client one. Parsing the text can happen right away; anything touching the game is wrapped in onClientThread.

Three different packets for the same thing. A chat message arrives in different shapes depending on how the server sent it, which is why the when has three branches.

A warning with a cooldown. Without sinceWarn the notification would fire every tick while health is low. passedAndReset(40) limits it to once every two seconds, and only when health actually dropped.

A config instead of a variable. Notes have to survive a restart, so they live in config("notes") and are saved right after a change — the command runs rarely, so the disk is fine.

Completions come from the same data. completes(0) { notes.keys().toList() } means Tab offers exactly what is written down.

A setting that hides itself. deathMinutes means nothing when marks are off, so it has a visibleWhen.

Last updated