You mean that nasty piece of dialogue code they released 4 years ago? Who would want to emulate that?
Kotlin hates null. If you're gonna join the Kotlin community, I highly recommend straying away from purposefully using null. In fact, a lot of newer languages are getting rid of it, and for a good reason. Using null in Kotlin is similar to using improper naming conventions in Java, except worse with the potential of blowing up.
The let expression is 1 line, less syntax. It's similar to using optionals in Java, which would be the proper solution to this. But it's your code, so can do whatever you want with it. May be hard to wrap your mind around coding without null, but I'm sure down the line, you'll be preaching this same thing.
Thanks for the contribution. It's good to know Kotlin is slowly gaining popularity around here.
I've preached the same thing, and I agree with you. I don't really know why I used null in this case, just was explaining to you my choice.
This would be good if you could jump into different dialogues if needed eg. quests is a good one, this is also the only tutorial I have seen on r-s in ages thats decent lol so nice
This would be good if you could jump into different dialogues if needed eg. quests is a good one, this is also the only tutorial I have seen on r-s in ages thats decent lol so nice
Are you referring to sending singular dialogue? Or having an easy way to make a dialogue without having to make a new class? If so, doing so just needs this added in Dialogue.kt:
fun handleDeath(player: Player, killer: Player) {
// handle death stuff lol
Dialogue.build(player, {
npcMessage(0, "Hi my names Hans I just came to tell you how much of a loser you are for dying.")
playerMessage("Thanks man!")
npcMessage(0, "Anytime bro.")
npcMessage(0, "To emphasize how much of a noob you are, I'll make your dialogue uncloseable.", DialogueExpression.MANIC_FACE)
}, closeable = false) // the "closeable =" is not required here, but is put for readability.
Dialogue.build(killer, {
npcMessage(0, "Hey man, just wanted to tell you, thank you for killing that noob ${player.username}.")
playerMessage("Yeah no problem, anytime.")
})
}
The con to this code right now, however, is that in Java, things won't be as simple due to my lack of overloaded methods and usage of default parameters. Could be fixed, however.
Are you referring to sending singular dialogue? Or having an easy way to make a dialogue without having to make a new class? If so, doing so just needs this added in Dialogue.kt:
The con to this code right now, however, is that in Java, things won't be as simple due to my lack of overloaded methods and usage of default parameters. Could be fixed, however.
I mean being able to jump back into a certain stage of the dioalogue. I was testing it at the moment and that was the biggest issue I saw with it
eg. Dialogues going back to the options if you wanted it to. I can show you what I mean on discord if i'm maybe not wording it right
I mean being able to jump back into a certain stage of the dioalogue. I was testing it at the moment and that was the biggest issue I saw with it
eg. Dialogues going back to the options if you wanted it to. I can show you what I mean on discord if i'm maybe not wording it right
Ohh, right. I should look into that. Currently, the only possible way is making two seperate classes (a hassle) as shown in my example in my latest update comment.
Ohh, right. I should look into that. Currently, the only possible way is making two seperate classes (a hassle) as shown in my example in my latest update comment.
ya if you made that possible it would make this really nice hopefully in the future
How is this any better, this has more hard coding than the other systems? You can't really avoid it, but storing dialogue in a JSON / XML type of file is better than storing it in actual classes.
...what?
How is this more hardcoding..? You don't have to define any stages or anything writing dialogues with this. It's one of the least verbose methods of writing dialogues. Even Jagex's own dialogue system is actually fairly similar to this; theirs is heavily dependent on indentation though as opposed to wrapping in brackets.
Do you know what hard-coding is?
"Know thy self, know thy enemy. A thousand battles, a thousand victories." - Sun Tzu GitHub:https://github.com/Faris-Mckay
I took a look at how Ruse, more specifically Necrotic, did their dialogue and was utterly disgusted. There was a JSON file to handle dialogues and there was hardcoding EVERYWHERE. The sight led to making a new dialogue system. If any of you recall, I made a dialogue system 2 years ago, and the following system is thoroughly inspired from the comments in that thread.
I would like to thank Whis, netherfoam, and Kaleem for their input in that thread.
The following system uses Kotlin. It is worth noting, if you do not know, that Kotlin and Java are 100% interoperable. That is, if your source doesn't contain Kotlin, don't be afraid to add this as your first, and maybe even only (but I hope not), Kotlin code. Even I have done such a thing:
Yep, that's right, this is the only Kotlin in my source right now.
Implementing the System
Dialogue.kt
Code:
package com.ruse.world.content.dialogue
import com.ruse.world.entity.impl.player.Player
import java.util.*
abstract class Dialogue(val player: Player, val closeable: Boolean = true) {
val queue: Queue<DialogueMessage> = LinkedList<DialogueMessage>()
abstract fun execute()
protected fun playerMessage(message: String, expression: DialogueExpression = DialogueExpression.NORMAL, action: DialogueAction.() -> Unit = {}) {
queue.add(PlayerMessage(message, expression, action))
}
protected fun npcMessage(npcId: Int, message: String, expression: DialogueExpression = DialogueExpression.NORMAL, action: DialogueAction.() -> Unit = {}) {
queue.add(NPCMessage(message, npcId, expression, action))
}
protected fun itemMessage(itemId: Int, title: String = "", message: String, action: DialogueAction.() -> Unit = {}) {
queue.add(ItemMessage(itemId, title, message, action))
}
protected fun plainMessage(message: String, continueText: String = "Click here to continue", action: DialogueAction.() -> Unit = {}) {
queue.add(PlainMessage(message, continueText, noContinue = false, action = action))
}
protected fun plainMessageNoContinue(message: String, action: DialogueAction.() -> Unit = {}) {
queue.add(PlainMessage(message, noContinue = true, action = action))
}
protected fun optionMessage(vararg options: String,
option1: Dialogue.() -> Unit = {}, option2: Dialogue.() -> Unit = {},
option3: Dialogue.() -> Unit = {}, option4: Dialogue.() -> Unit = {},
option5: Dialogue.() -> Unit = {}, action: DialogueAction.() -> Unit = {}, title: String = "Choose an option") {
queue.add(OptionMessage(*options, option1 = option1, option2 = option2, option3 = option3, option4 = option4, option5 = option5, action = action, title = title))
}
protected fun closeMessage(action: DialogueAction.() -> Unit = {}) {
queue.add(CloseMessage(action))
}
}
Dialogue.kt is the abstract class that all dialogues will extend. Within it are helper functions to add to the queue new messages. There is a lot of flexibility in the system to allow for lots of things to be customizably changed, or just defaulted to their respective defaults. For example, an entity can choose to have an expression, or be defaulted to the DEFAULT expression. An item dialogue can have a title, or be defaulted to no title, as per usual. Every single message contains an action that can be commenced, such as giving an item to a player, or making them perform an emote.
One thing to note about option dialogue is specific is that each option branches off with a new function passed. This is important as then the system delays adding to the queue until a singular option is evoked by a button click. This also allows for option dialogues to default to closing by click, since the end of a queue signifies a closing of a dialogue. Due to this nature, one does not need to bother specifying endings to their dialogues.
Going back on flexibility, I provided a closeMessage() function that allows for customizable behavior for when a message closes. For example, ending the dialogue entirely could progress a quest. In the example I will provide, it will make the player cry. Along with customizability for the closing, you may also specify if the dialogue can even be closed by walking by the constructor.
DialogueMessage.kt
Code:
package com.ruse.world.content.dialogue
import com.ruse.model.definitions.NpcDefinition
import com.ruse.world.entity.impl.player.Player
abstract class DialogueMessage(val action: DialogueAction.() -> Unit) {
open val message: String = ""
abstract fun display(player: Player)
fun entityDisplay(frameIds: Array<Int>, expression: DialogueExpression?, title: String, player: Player, headDisplay: (Int) -> Unit) {
val split = message.splitDialogue(this)
val startingId = frameIds[split.size - 1]
val nameId = startingId - 1
val headId = startingId - 2
val interfaceId = startingId - 3
split.forEachIndexed { index, line -> player.packetSender.sendString(startingId + index, line) }
headDisplay(headId)
player.packetSender.sendString(nameId, title)
if (expression != null) {
player.packetSender.sendInterfaceAnimation(headId, expression.animation)
}
player.packetSender.sendChatboxInterface(interfaceId)
}
}
class PlayerMessage(override val message: String, private val expression: DialogueExpression, action: DialogueAction.() -> Unit) : DialogueMessage(action) {
private val frameIds = arrayOf(971, 976, 982, 989)
override fun display(player: Player) = entityDisplay(frameIds, expression, player.username, player) { headId ->
player.packetSender.sendPlayerHeadOnInterface(headId)
}
}
class NPCMessage(override val message: String, private val npcId: Int, private val expression: DialogueExpression, action: DialogueAction.() -> Unit) : DialogueMessage(action) {
private val frameIds = arrayOf(4885, 4890, 4896, 4903)
override fun display(player: Player) = entityDisplay(frameIds, expression, NpcDefinition.forId(npcId).name, player) { headId ->
player.packetSender.sendNpcHeadOnInterface(npcId, headId)
}
}
class ItemMessage(private val itemId: Int, private val title: String, override val message: String, action: DialogueAction.() -> Unit) : DialogueMessage(action) {
private val frameIds = arrayOf(4885, 4890, 4896, 4903)
override fun display(player: Player) = entityDisplay(frameIds, null, title, player) { headId ->
player.packetSender.sendInterfaceModel(headId, itemId, 150) // not sure if zoom should be 150
}
}
class PlainMessage(override val message: String, val continueText: String = "", val noContinue: Boolean, action: DialogueAction.() -> Unit) : DialogueMessage(action) {
private val frameIds = arrayOf(357, 360, 364, 369, 375)
private val frameIdsNoContinue = arrayOf(12789, 12791, 12794, 12798, 12803)
override fun display(player: Player) {
val split = message.splitDialogue(this)
val startingId = if (noContinue) frameIdsNoContinue[split.size - 1] else frameIds[split.size - 1]
val interfaceId = startingId - 1
split.forEachIndexed { index, line ->
player.packetSender.sendString(startingId + index, line)
}
if (!noContinue) {
player.packetSender.sendString(startingId + split.size, continueText)
}
player.packetSender.sendChatboxInterface(interfaceId)
}
}
/**
* An empty option lambda means that the dialogue will default to closing.
*/
class OptionMessage(vararg val options: String,
val option1: Dialogue.() -> Unit, val option2: Dialogue.() -> Unit,
val option3: Dialogue.() -> Unit, val option4: Dialogue.() -> Unit,
val option5: Dialogue.() -> Unit, action: DialogueAction.() -> Unit,
val title: String = "Choose an option") : DialogueMessage(action) {
val frameIds = arrayOf(13760, 2461, 2471, 2482, 2494)
override fun display(player: Player) {
val startingId = frameIds[options.size - 1]
val titleId = startingId - 1
val interfaceId = startingId - 2
player.packetSender.sendString(titleId, title)
options.forEachIndexed { index, option -> player.packetSender.sendString(startingId + index, option) }
player.packetSender.sendChatboxInterface(interfaceId)
}
}
/**
* If one wants a custom close action (for example, giving an item AFTER the dialogue is finished), then they must
* add a close message to the queue themselves. By default, a dialogue ends when the queue is empty, with no action
* being commenced.
*/
class CloseMessage(action: DialogueAction.() -> Unit = {}) : DialogueMessage(action) {
override fun display(player: Player) {
player.packetSender.sendInterfaceRemoval()
player.dialogueIterator = null
}
}
/**
* A custom made splitDialogue extension function to determine the splits in dialogue itself rather than having to go through
* trial and error to splitDialogue the lines of messages to fit the message frame. One can make new lines themselves
* by adding "\n" to the location.
*/
fun String.splitDialogue(dialogueMessageType: DialogueMessage): Array<String> {
val message = this
val messageSplit = ArrayList<String>()
var currentLine = ""
var chars = 0
val limit = if (dialogueMessageType is PlainMessage) 75 else 50
for (word in message.split(" ")) {
val length = word.length + 2
if (word.contains("\n")) {
chars = limit * (messageSplit.size + 1)
messageSplit.add("$currentLine ${word.replace("\n", "")}")
currentLine = ""
} else {
if (length + chars >= limit * (messageSplit.size + 1)) {
messageSplit.add(currentLine)
currentLine = word
} else {
currentLine += " $word"
}
chars += length
}
}
if (currentLine.isNotEmpty()) {
messageSplit.add(currentLine)
}
return messageSplit.toTypedArray()
}
A majority of these classes have already been discussed in the above paragraph. However, it is important to note the splitDialogue() function. This is why the title includes "Intelligent Message Splitting Included," for I haven't seen such a thing in a 317 dialogue system. This was inspired by higher revision's client-sided splitting of messages (though I'm not sure how they do it, it is worth looking into if splitDialogue() does not suffice your needs and turns faulty). Essentially, per word, it added the length to a variable, and if the line exceeds 50 with the new word, it make a line out of the previous words, excluding the current word, and makes the current word the start of a new line. This process continues till the dialogue has been completely split.
With this intelligence, a message can be written simply as:
Code:
playerMessage("Lorem ipsum dolor sit amet consectetur adipiscing elit. Integer dictum lorem ac condimentum egestas. Mauris molestie ligula vitae fermentum finibus. Lorem ipsum dolor sit")
Rather than:
Code:
playerMessage("Lorem ipsum dolor sit amet consectetur",
"adipiscing elit. Integer dictum lorem ac",
"condimentum egestas. Mauris molestie ligula vitae",
"fermentum finibus. Lorem ipsum dolor sit")
DialogueAction.kt
Code:
package com.ruse.world.content.dialogue
import com.ruse.model.Animation
import com.ruse.model.Item
class DialogueAction {
fun Dialogue.openNewDialogue(dialogue: Dialogue) {
DialogueExecutor.startDialogue(player, dialogue)
}
fun Dialogue.giveItem(item: Item) {
player.inventory.add(item)
}
fun Dialogue.giveItem(id: Int) {
giveItem(Item(id))
}
fun Dialogue.performAnimation(animation: Animation) {
player.performAnimation(animation)
}
fun Dialogue.performAnimation(id: Int) {
performAnimation(Animation(id))
}
}
DialogueAction will serve as a means of having predefined actions to choose from in our dialogue. The intention behind the class was that many dialogues require the same things out of a player and thus, following the same principle as the Dialogue.kt class above, should have helper functions to do those very actions.
This simply contains the expressions. This class already existed in Ruse. Credits to relax lawl for the expressions list.
DialogueExecutor.kt
Code:
package com.ruse.world.content.dialogue
import com.ruse.world.entity.impl.player.Player
object DialogueExecutor {
@JvmStatic
fun startDialogue(player: Player, dialogue: Dialogue) {
player.dialogueIterator = makeIterator(player, dialogue)
player.dialogueIterator.broadcastMessage()
}
private fun makeIterator(player: Player, dialogue: Dialogue): DialogueIterator {
return DialogueIterator(player, dialogue)
}
}
class DialogueIterator(private val player: Player, private val dialogue: Dialogue) {
private lateinit var currentMessage: DialogueMessage
private var lastMessage: DialogueMessage? = null
init {
dialogue.execute()
assignCurrentMessage()
}
fun broadcastMessage() {
currentMessage.display(player)
currentMessage.action(DialogueAction)
lastMessage = currentMessage
assignCurrentMessage()
}
private fun assignCurrentMessage() {
currentMessage = if (dialogue.queue.peek() != null) dialogue.queue.poll() else CloseMessage()
}
fun handleWalk() {
if (dialogue.closeable) player.packetSender.sendInterfaceRemoval()
}
fun forceCloseDialogue() {
currentMessage = CloseMessage()
broadcastMessage()
}
fun handleOptionClick(buttonId: Int) {
val optionMessage = run { if (lastMessage == null) currentMessage else lastMessage } as OptionMessage
val option1 = optionMessage.frameIds[optionMessage.options.size - 1]
when (buttonId) {
option1 -> optionMessage.option1(dialogue)
option1 + 1 -> optionMessage.option2(dialogue)
option1 + 2 -> optionMessage.option3(dialogue)
option1 + 3 -> optionMessage.option4(dialogue)
option1 + 4 -> optionMessage.option5(dialogue)
}
broadcastMessage()
}
}
It is worth noting that handleOptionClick() is the reason for the unique behavior of option dialogues, for only when the option is clicked is the function invoked. Another thing to note is here is how it is determined if a dialogue is to be closed or not (assignCurrentMessage()).
Integrating the System
There are four steps. One, replace the case for the dialogue opcode in DialoguePacketListener:
Code:
case DIALOGUE_OPCODE:
player.getDialogueIterator().broadcastMessage();
break;
Two, find where the following is in ButtonClickPacketListener:
You should now be ready to rock and roll with the new dialogue system. Example
For old traditions sakes (every dialogue system I've released, I've done this), let's use Hans as the culprit of our dialogue system again. Make a package in your dialogue package called impl, and add a file called Hans.kt. Here's an example of dialogue in Kotlin:
Code:
package com.ruse.world.content.dialogue.impl
import com.ruse.world.content.Emotes
import com.ruse.world.content.dialogue.Dialogue
import com.ruse.world.entity.impl.player.Player
class Hans(player: Player) : Dialogue(player) {
private val npcId = 0
override fun execute() {
playerMessage("Wow, this surprisingly works!") {
performAnimation(Emotes.EmoteData.DANCE.animation)
}
npcMessage(npcId, "Yeah man, I didn't think the day would come. But it has, alas, and I am just waiting for one more thing: option dialogues.")
optionMessage("Option 1", "Option 2", "Option 3",
option1 = {
playerMessage("Hey man, this is option 1")
npcMessage(npcId, "Seems to have worked.") // makes weird head, need to see
playerMessage("Yes indeed.")
itemMessage(4151, message = "To items! Take a whip!") {
giveItem(4151)
}
playerMessage("Thank you!")
plainMessage("You proceed to express your gratitude... in other ways.")
plainMessage("Now, let's test the custom close dialogue by making you cry.")
closeMessage { performAnimation(Emotes.EmoteData.CRY.animation) }
},
option2 = {
playerMessage("Hey dude, this is option 2")
npcMessage(npcId, "I believe this works.. too?")
playerMessage("Yes indeed, now we need to see if option3 closes the dialogue.")
}) // option 3 is not implemented to see if it will close the dialogue
}
}
Spoiler for Prettier syntax highlighting:
But Arham, doesn't the title say Kotlin/Java?
Yeah, well, I kinda sorta lied to you so you won't shoo away from "Kotlin." Yes, you can write your dialogues in Java, but they don't look as pretty:
Spoiler for Java Implementation:
Footnote: This java implementation showcases the dialogue system before multiple changes. I can't really be bothered to change it.
Code:
package com.ruse.world.content.dialogue.impl;
import com.ruse.model.Item;
import com.ruse.world.content.Emotes;
import com.ruse.world.content.dialogue.Dialogue;
import com.ruse.world.content.dialogue.DialogueExpression;
import com.ruse.world.entity.impl.player.Player;
import org.jetbrains.annotations.NotNull;
public class HansJava extends Dialogue {
private int npcId = 0;
@Override
public void execute(@NotNull Player player) {
playerMessage("Wow, this surprisingly works!", DialogueExpression.NORMAL, () -> {
player.performAnimation(Emotes.EmoteData.DANCE.animation);
return null;
});
npcMessage(npcId, "Yeah man, I didn't think the day would come. But it has, alas, and I am just waiting for one more thing: option dialogues.", DialogueExpression.NORMAL, () -> null);
optionMessage(new String[]{"Option 1", "Option 2", "Option 3"},
option1 -> {
playerMessage("Hey man, this is option 1", DialogueExpression.NORMAL, () -> null);
npcMessage(npcId, "Seems to have worked.", DialogueExpression.NORMAL, () -> null);
playerMessage("Yes indeed.", DialogueExpression.NORMAL, () -> null);
itemMessage(4151, "", "To items! Take a whip!", () -> {
player.getInventory().add(new Item(4151));
return null;
});
playerMessage("Thank you!", DialogueExpression.NORMAL, () -> null);
plainMessage("You proceed to express your gratitude... in other ways.", () -> null);
plainMessage("Now, let's test the custom close dialogue by making you cry.", () -> null);
closeMessage(() -> {
player.performAnimation(Emotes.EmoteData.CRY.animation);
return null;
});
return null;
},
option2 -> {
playerMessage("Hey dude, this is option 2", DialogueExpression.NORMAL, () -> null);
npcMessage(npcId, "I believe this works.. too?", DialogueExpression.NORMAL, () -> null);
playerMessage("Yes indeed, now we need to see if option3 closes the dialogue.", DialogueExpression.NORMAL, () -> null);
return null;
},
option3 -> null,
option4 -> null,
option5 -> null,
() -> null);
}
}
This is because a lot of my functions in Kotlin would be implemented in Java with overloaded methods, but instead I just used default parameters in Kotlin to achieve the same thing. Sorry, but come on, Kotlin is beautiful.
To run the above classes, I made a test command, likeso:
Code:
if (command[0].equalsIgnoreCase("testdialogue")) {
DialogueExecutor.startDialogue(player, new Hans(player));
}