music plugin

This commit is contained in:
psyopcutie 2026-07-02 14:13:50 +02:00
parent ab2a28db8c
commit 413f3c30cd
6 changed files with 769 additions and 2 deletions

View file

@ -394,6 +394,43 @@ public class API {
MidiPlayer.playFadeOut(trackId, client.js5Archive6, volume);
}
/**
* Music hook used by the SmartAudio plugin.
*/
public interface AudioHandler {
boolean onMusicRequest(int trackId);
boolean onMusicEnd(int trackId);
default void onMusicSelect(int slot) {}
}
private static AudioHandler audioHandler = null;
public static void SetAudioHandler(AudioHandler handler) {
audioHandler = handler;
}
public static void ClearAudioHandler(AudioHandler handler) {
if (audioHandler == handler) {
audioHandler = null;
}
}
public static boolean MusicRequest(int trackId) {
return audioHandler == null || audioHandler.onMusicRequest(trackId);
}
public static boolean MusicEnd(int trackId) {
return audioHandler == null || audioHandler.onMusicEnd(trackId);
}
public static void MusicSelect(int slot) {
if (audioHandler != null) {
audioHandler.onMusicSelect(slot);
}
}
public static void SetLoginScreenMusicOnLoad(String song) {
client.TITLE_SONG = JagString.parse(song);
}

View file

@ -3,6 +3,7 @@ package rt4;
import org.openrs2.deob.annotation.OriginalArg;
import org.openrs2.deob.annotation.OriginalMember;
import org.openrs2.deob.annotation.Pc;
import plugin.api.API;
import java.io.IOException;
@ -55,6 +56,9 @@ public class ClientProt {
return;
}
if (arg2 == 1) {
if (arg3 == ((187 << 16) + 1)) {
API.MusicSelect(arg1);
}
Protocol.outboundBuffer.p1isaac(155);
Protocol.outboundBuffer.p4(arg3);
Protocol.outboundBuffer.p2(arg1);

View file

@ -4,6 +4,7 @@ import org.openrs2.deob.annotation.OriginalArg;
import org.openrs2.deob.annotation.OriginalMember;
import org.openrs2.deob.annotation.Pc;
import plugin.PluginRepository;
import plugin.api.API;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
@ -2320,7 +2321,9 @@ public class Protocol {
if (id == 65535) {
id = -1;
}
MusicPlayer.playSong(id);
if (API.MusicRequest(id)) {
MusicPlayer.playSong(id);
}
opcode = -1;
return true;
} else if (opcode == ServerProt.MIDI_JINGLE) {

View file

@ -3,6 +3,7 @@ package rt4;
import org.openrs2.deob.annotation.OriginalArg;
import org.openrs2.deob.annotation.OriginalMember;
import org.openrs2.deob.annotation.Pc;
import plugin.api.API;
public class SoundPlayer {
@OriginalMember(owner = "client!qe", name = "t", descriptor = "[I")
@ -128,7 +129,11 @@ public class SoundPlayer {
}
MidiPlayer.jingle = false;
} else if (Preferences.musicVolume != 0 && MusicPlayer.groupId != -1 && !MidiPlayer.isPlaying()) {
sendTrackEndPacket();
if (API.MusicEnd(MusicPlayer.groupId)) {
sendTrackEndPacket();
} else {
MusicPlayer.groupId = -1;
}
}
}

View file

@ -0,0 +1,715 @@
package SmartAudio
import plugin.Plugin
import plugin.api.API
import rt4.Camera
import rt4.Component
import rt4.IntNode
import rt4.InterfaceList
import rt4.JagString
import rt4.MidiPlayer
import rt4.MusicPlayer
import rt4.PlayerList
import kotlin.random.Random
class plugin : Plugin() {
private var enabled = true
private var jinglesOnly = false
private var debug = true
private var latestTrack = -1
private var lastPlayedTrack = -1
private var playingMusicLocation: MusicLocation? = null
private var currentMusicLocation: MusicLocation? = null
private var manualMusicSelect = false
private var manualMusicPlaying = false
private var intervalMillis = 20 * 1000L
private var nextMusicAt = 0L
private var musicTextComponent: Component? = null
private var musicLabelsLoaded = false
private val MUSIC_PLAYER_TRACK_COMPONENT_ID = (187 shl 16) + 14
private val MUSIC_BUTTON_TO_SONG_ENUM = 1351
private val MUSIC_BUTTON_TO_NAME_ENUM = 1345
private val musicLabels = HashMap<Int, String>()
private val random = Random.Default
private val NEWBIE_MELODY = 62
private val handler = object : API.AudioHandler {
override fun onMusicRequest(trackId: Int): Boolean {
if (!enabled) {
return true
}
if (trackId == -1) {
latestTrack = -1
manualMusicSelect = false
manualMusicPlaying = false
return true
}
if (manualMusicSelect) {
manualMusicSelect = false
latestTrack = trackId
lastPlayedTrack = trackId
playingMusicLocation = null
manualMusicPlaying = true
nextMusicAt = System.currentTimeMillis() + intervalMillis
debugMessage("Allowed manual music selection $trackId.")
return true
}
if (playImmediately(trackId)) {
latestTrack = trackId
lastPlayedTrack = trackId
playingMusicLocation = null
currentMusicLocation = getCurrentMusicLocation()
manualMusicPlaying = false
nextMusicAt = System.currentTimeMillis() + intervalMillis
debugMessage("Playing ${getMusicName(trackId)} ($trackId) immediately.")
return true
}
latestTrack = trackId
if (checkMusicLocationChange()) {
return false
}
if (playingMusicLocation != null) {
return false
}
if (jinglesOnly) {
MusicPlayer.playSong(-1)
debugMessage("Blocked music $trackId because jingles-only mode is enabled.")
return false
}
requestPlayMusic(
reason = "request",
replacePlaying = currentMusicLocation == null && !manualMusicPlaying,
)
return false
}
override fun onMusicEnd(trackId: Int): Boolean {
if (trackId <= 0) {
debugMessage("Ignoring invalid music end: $trackId.")
return false
}
playingMusicLocation = null
manualMusicPlaying = false
nextMusicAt = System.currentTimeMillis() + intervalMillis
debugMessage("Music ended: $trackId. Next music in ${formatRemaining()}.")
return true
}
override fun onMusicSelect(slot: Int) {
manualMusicSelect = true
debugMessage("Manual music selection from slot $slot.")
}
}
override fun Init() {
API.SetAudioHandler(handler)
log("loaded")
}
override fun OnLogout() {
latestTrack = -1
lastPlayedTrack = -1
playingMusicLocation = null
currentMusicLocation = null
manualMusicSelect = false
manualMusicPlaying = false
}
override fun OnLogin() {
if (System.currentTimeMillis() < nextMusicAt) {
MidiPlayer.playFadeOut()
MusicPlayer.groupId = -1
debugMessage("Quiet time continues after login: ${formatRemaining()}.")
}
}
override fun OnPluginsReloaded(): Boolean {
API.ClearAudioHandler(handler)
return false
}
private fun requestPlayMusic(reason: String, replacePlaying: Boolean = false) {
if (!enabled || jinglesOnly) {
return
}
if (latestTrack == -1 || API.GetMusicVolume() <= 0) {
return
}
if (!replacePlaying && API.IsMusicPlaying() && lastPlayedTrack != -1) {
return
}
val now = System.currentTimeMillis()
if (now < nextMusicAt) {
return
}
val location = getCurrentMusicLocation()
if (location != null) {
playMusicForLocation(location, reason)
return
}
MusicPlayer.playSong(latestTrack)
lastPlayedTrack = latestTrack
playingMusicLocation = null
manualMusicPlaying = false
debugMessage("Playing music ${getMusicName(latestTrack)} ($latestTrack) from $reason.")
}
private fun playMusicForLocation(location: MusicLocation, reason: String) {
val trackToPlay = selectTrackFromLocation(location)
MusicPlayer.playSong(trackToPlay)
lastPlayedTrack = trackToPlay
playingMusicLocation = location
manualMusicPlaying = false
debugMessage("Playing music ${getMusicName(trackToPlay)} ($trackToPlay) from $reason.")
}
private fun selectTrackToPlay(): Int {
val location = getCurrentMusicLocation() ?: return latestTrack
if (location.tracks.isEmpty()) {
return latestTrack
}
val choices = location.tracks.filter { it != lastPlayedTrack }
val track = if (choices.isNotEmpty()) {
choices[random.nextInt(choices.size)]
} else {
location.tracks[random.nextInt(location.tracks.size)]
}
debugMessage("Using ${location.name} dynamic music: ${getMusicName(track)} ($track) instead of ${getMusicName(latestTrack)} ($latestTrack).")
return track
}
private fun selectTrackFromLocation(location: MusicLocation): Int {
if (location.tracks.isEmpty()) {
return latestTrack
}
val choices = location.tracks.filter { it != lastPlayedTrack }
return if (choices.isNotEmpty()) {
choices[random.nextInt(choices.size)]
} else {
location.tracks[random.nextInt(location.tracks.size)]
}
}
private fun printStatus() {
val regionId = getCurrentRegionId()
val locationName = getCurrentMusicLocation()?.name ?: "none"
log(
"enabled=$enabled jinglesOnly=$jinglesOnly playing=${API.IsMusicPlaying()} latest=$latestTrack last=$lastPlayedTrack next=${formatRemaining()} region=$regionId location=$locationName"
)
}
private fun formatRemaining(): String {
val remaining = nextMusicAt - System.currentTimeMillis()
if (remaining <= 0L) {
return "now"
}
return "${remaining / 1000L}s"
}
private fun getCurrentRegionId(): Int {
val self = PlayerList.self ?: return -1
val worldX = self.movementQueueX[0] + Camera.originX
val worldZ = self.movementQueueZ[0] + Camera.originZ
return ((worldX shr 6) shl 8) + (worldZ shr 6)
}
private fun getCurrentMusicLocation(): MusicLocation? {
val regionId = getCurrentRegionId()
return musicLocations.firstOrNull { regionId in it.regions }
}
private fun checkMusicLocationChange(): Boolean {
val newLocation = getCurrentMusicLocation()
if (newLocation == currentMusicLocation) {
return false
}
val oldLocation = currentMusicLocation
currentMusicLocation = newLocation
if (!enabled || manualMusicPlaying || lastPlayedTrack == -1) {
return false
}
debugMessage(
"Music location changed from ${oldLocation?.name ?: "none"} " +
"to ${newLocation?.name ?: "none"}."
)
if (System.currentTimeMillis() >= nextMusicAt) {
requestPlayMusic("region change", true)
} else {
MidiPlayer.playFadeOut()
MusicPlayer.groupId = -1
playingMusicLocation = null
}
return true
}
private fun playImmediately(trackId: Int): Boolean {
return trackId == NEWBIE_MELODY &&
lastPlayedTrack != NEWBIE_MELODY
}
override fun Draw(timeDelta: Long) {
checkMusicLocationChange()
}
override fun Update() {
requestPlayMusic("timer")
updateMusicTabText()
}
private fun updateMusicTabText() {
val component = musicTextComponent ?: return
if (lastPlayedTrack == -1 || !API.IsMusicPlaying()) {
return
}
val label = getMusicName(lastPlayedTrack)
if (component.text.toString() == label) {
return
}
component.text = JagString.parse(label)
InterfaceList.redraw(component)
}
private fun loadMusicLabels() {
if (musicLabelsLoaded) {
return
}
val songs = API.GetDataMap(MUSIC_BUTTON_TO_SONG_ENUM)
val names = API.GetDataMap(MUSIC_BUTTON_TO_NAME_ENUM)
var node = songs.table.head()
while (node != null) {
val buttonId = node.key.toInt()
val songId = (node as IntNode).value
val name = names.getString(buttonId).toString()
if (name.isNotBlank()) {
musicLabels[songId] = name
}
node = songs.table.next()
}
musicLabelsLoaded = true
debugMessage("Loaded ${musicLabels.size} music labels.")
}
private fun getMusicName(trackId: Int): String {
if (trackId == -1) {
return ""
}
loadMusicLabels()
return musicLabels[trackId] ?: "Track $trackId"
}
override fun ComponentDraw(componentIndex: Int, component: Component?, screenX: Int, screenY: Int) {
if (component?.id != MUSIC_PLAYER_TRACK_COMPONENT_ID) {
return
}
musicTextComponent = component
updateMusicTabText()
}
override fun ProcessCommand(commandStr: String?, args: Array<out String>?) {
if (!"::sm".equals(commandStr, ignoreCase = true) &&
!"::smartaudio".equals(commandStr, ignoreCase = true)) {
return
}
when (args?.getOrNull(0)?.toLowerCase()) {
null, "status" -> printStatus()
"on" -> {
enabled = true
log("SmartAudio enabled")
}
"off" -> {
enabled = false
log("SmartAudio disabled")
}
"debug" -> {
debug = !debug
log("SmartAudio debug ${if (debug) "on" else "off"}")
}
"jingles" -> {
jinglesOnly = !jinglesOnly
if (jinglesOnly) {
MusicPlayer.playSong(-1)
}
log("SmartAudio jingles-only ${if (jinglesOnly) "on" else "off"}")
}
"interval" -> {
val seconds = args?.getOrNull(1)?.toLongOrNull()
if (seconds == null || seconds < 0) {
log("Usage: ::sm interval <seconds>")
return
}
intervalMillis = seconds * 1000L
log("SmartAudio track-end interval set to ${seconds}s")
}
"play" -> {
nextMusicAt = 0L
requestPlayMusic("command")
}
"stop" -> {
manualMusicSelect = false
manualMusicPlaying = false
MusicPlayer.playSong(-1)
lastPlayedTrack = -1
nextMusicAt = System.currentTimeMillis() + intervalMillis
log("music stopped; next=${formatRemaining()}")
}
"next" -> {
manualMusicSelect = false
manualMusicPlaying = false
MusicPlayer.playSong(-1)
lastPlayedTrack = -1
nextMusicAt = 0L
requestPlayMusic("command")
}
"dynamic" -> {
val location = getCurrentMusicLocation()
if (location == null) {
log("No dynamic music location for region ${getCurrentRegionId()}")
return
}
log("${location.name}: ${location.tracks.joinToString { "${getMusicName(it)} ($it)" }}")
}
else -> log("SmartAudio commands: status, on, off, debug, jingles, interval, play, next, stop, dynamic")
}
}
private fun log(message: String) {
println("[SmartAudio] $message")
}
private fun debugMessage(message: String) {
if (debug) {
log(message)
}
}
/**
* Dynamic music regions.
*/
private class MusicLocation(
val name: String,
val regions: Set<Int>,
val tracks: IntArray
)
//MusicLocation(
//name = "name",
//regions = setOf(),
//tracks = intArrayOf()
//),
private val musicLocations = listOf(
/**
* Misthalin (add barb village to asgarnia)
*/
MusicLocation(
name = "Lumbridge",
regions = setOf(
12595, 12594, 12593,
12851, 12850, 12849,
),
tracks = intArrayOf(
76, // Harmony
2, // Autumn Voyage
64, // Book of Spells
145, // Yesteryear
163, // Flute Salad
327, // Dream
62 // Newbie Melody
)
),
MusicLocation(
name = "Draynor Village & Wizards' Tower",
regions = setOf(
12339, 12338, 12337
),
tracks = intArrayOf(
3, // Unknown Land
85, // Vision
151 // Start
)
),
MusicLocation(
name = "Al-Kharid & Kharidian Desert",
regions = setOf(
12591, 12590, 12589,
12848, 12847, 12846, 12845, 12844, 12843,
13107, 13106, 13105, 13104, 13103, 13102, 13101, 13100, 13099,
13363, 13362, 13361, 13360, 13359, 13358, 13357, 13356, 13355,
13617, 13616, 13615, 13614, 13613, 13612, 13611,
13872
),
tracks = intArrayOf(
122, // Shine
123, // Arabian 2
47, // Duel Arena
50, // Al Kharid
352, // Scarab
69, // Egypt
79, // The Desert
36, // Arabian
124, // Arabian 3
174, // Desert Voyage
267, // Sunburn
465, // Desert Heat
387, // Sphinx
351, // Dynasty
263, // Bandit Camp
377, // The Golem
383, // City of the Dead
447, // Over to Nardah
451, // Tune from the Dune
505, // Pharaoh's Tomb
)
),
MusicLocation(
name = "Varrock & Edgeville",
regions = setOf(
12342,
12598, 12597, 12596,
12854, 12853, 12852,
13110, 13109, 13108,
13366
),
tracks = intArrayOf(
125, // Garden
157, // Medieval
175, // Spirit
177, // Adventure
56, // Doorways
93, // Parade
98, // Forever
106, // Expanse
111, // Still Night
116, // Greatness
496 // The Trade Parade
)
),
MusicLocation(
name = "Digsite",
regions = setOf(
13365, 13364
),
tracks = intArrayOf(
20, // Lullaby
75 // Venture
)
),
/**
* Morytania
*/
MusicLocation(
name = "Morytania",
regions = setOf(
13623, 13622, 13621,
13620, 13619, 13618,
13879, 13878, 13877,
13876, 13875, 13874,
13873, 14135, 14134,
14133, 14132, 14131,
14130, 14129, 14391,
14390, 14388, 14387,
14386, 14385, 14647,
14646
),
tracks = intArrayOf(
48, // Morytania
61, // Village
84, // Dead Quiet
154, // Bone Dance
241, // Stagnant
244, // Waterlogged
245, // Natural
286, // Shadowland
288, // Deadlands
339, // The Terrible Tower
344, // Fenkenstrain's Refrain
353, // Shipwrecked
355, // The Other Side
380, // Dance of the Undead
501, // Distant Land
)
),
/**
* Wilderness
*/
MusicLocation(
name = "Wilderness",
regions = setOf(
11837, 11836, 11835, 11834, 11833, 11832, 11831,
12093, 12092, 12091, 12090, 12089, 12088, 12087,
12349, 12348, 12347, 12346, 12345, 12344, 12343,
12605, 12604, 12603, 12602, 12601, 12600, 12599,
12861, 12860, 12859, 12858, 12857, 12856, 12855,
13117, 13116, 13115, 13114, 13113, 13112, 13111,
13373, 13372, 13371, 13370, 13369, 13368, 13367
),
tracks = intArrayOf(
8, // Wildwood
10, // Moody
13, // Mage Arena
14, // Witching
34, // Wonder
37, // Deep Wildy
42, // Wilderness2
43, // Wilderness3
52, // Serene
56, // Doorways
66, // Legion
67, // Close Quarters
96, // Inspiration
120, // Shining
121, // Forbidden
159, // Gaol
160, // Army of Darkness
169, // Crystal Sword
176, // Undercurrent
179, // Underground
182, // Dangerous
183, // Troubled
326, // Dark
329, // Regal
331, // Scape Sad
332, // Scape Wild
334, // Pirates of Peril
337, // Faithless
435, // Wilderness
449, // Wild Isle
475, // Wild Side
476, // Dead Can Dance
586, // Everlasting Fire
)
),
/**
* Asgarnia
*/
MusicLocation(
name = "Rimmington & Mudskipper Point",
regions = setOf(
11570,
11826, 11825, 11824,
12081
),
tracks = intArrayOf(
12, // Long Way Home
105, // Tomorrow
138, // Emperor
180, // Attention
515 // Mudskipper Melody
)
),
MusicLocation(
name = "Falador",
regions = setOf(
11572, 11571,
11828, 11827,
12084, 12083
),
tracks = intArrayOf(
15, // Workshop
49, // Wander
72, // Fanfare
107, // Miles Away
127, // Nightfall
186 // Arrival
)
),
MusicLocation(
name = "Barbarian/Goblin Village & Ice Mountain",
regions = setOf(
11830, 11829,
12086, 12085,
12341
),
tracks = intArrayOf(
54, // Scape Soft
102, // Alone
113, // Lightness
141, // Barbarianism
310, // Dwarf Theme
313, // Goblin Village
)
),
/**
* Islands/other
*/
MusicLocation(
name = "Mos Le'Harmless",
regions = setOf(
14639, 14638, 14637,
14895, 14894,
),
tracks = intArrayOf(
530, // In the Brine
631 // Life's a Beach!
)
),
MusicLocation(
name = "Mos Le'Harmless (Trouble Brewing)",
regions = setOf(
15151, 15150
),
tracks = intArrayOf(
610, // Distillery Hilarity
611, // Trouble Brewing
)
),
)
}

View file

@ -0,0 +1,3 @@
AUTHOR='Edith'
DESCRIPTION='Smart audio player'
VERSION=1.0