From 6886eead8462dcfa2d8c38f8cd77376d6253e083 Mon Sep 17 00:00:00 2001 From: GregHib Date: Sun, 25 Jan 2026 00:12:50 +0000 Subject: [PATCH 001/101] Align owns, carries and equips item check helpers --- .../world/gregs/voidps/engine/inv/Inventories.kt | 8 ++++++-- .../area/asgarnia/falador/MakeoverMage.kt | 4 ++-- .../content/area/asgarnia/falador/SirVyvin.kt | 4 ++-- .../content/area/asgarnia/falador/SquireAsrol.kt | 6 +++--- .../area/asgarnia/port_sarim/PortSarim.kt | 4 ++-- .../content/area/asgarnia/port_sarim/Thurgo.kt | 8 ++++---- .../content/area/kandarin/ardougne/Alrena.kt | 4 ++-- .../content/area/kandarin/ardougne/Edmond.kt | 6 +++--- .../kandarin/ardougne/west_ardougne/Bravek.kt | 6 +++--- .../kandarin/ardougne/west_ardougne/Mourner.kt | 4 ++-- .../ardougne/west_ardougne/WestArdougne.kt | 4 ++-- .../content/area/kandarin/catherby/Harry.kt | 10 +++++----- .../area/kharidian_desert/al_kharid/Ellis.kt | 4 ++-- .../kharidian_desert/sophanem/GuardianMummy.kt | 6 +++--- .../area/misthalin/barbarian_village/Dororan.kt | 4 ++-- .../area/misthalin/barbarian_village/Gudrun.kt | 6 +++--- .../StrongholdOfSecurityRewards.kt | 4 ++-- .../area/misthalin/draynor_village/Aggie.kt | 4 ++-- .../area/misthalin/draynor_village/Ned.kt | 4 ++-- .../content/area/misthalin/edgeville/Jeffery.kt | 4 ++-- .../area/misthalin/lumbridge/castle/Cook.kt | 16 ++++++++-------- .../misthalin/lumbridge/church/FatherAereck.kt | 4 ++-- .../misthalin/lumbridge/farm/GillieGroats.kt | 4 ++-- .../misthalin/lumbridge/windmill/MillieMiller.kt | 4 ++-- .../content/area/misthalin/varrock/Baraek.kt | 4 ++-- .../misthalin/varrock/palace/CaptainRovin.kt | 4 ++-- .../area/misthalin/varrock/palace/SirPrysin.kt | 8 ++++---- .../area/misthalin/wizards_tower/Sedridor.kt | 4 ++-- .../god_wars_dungeon/ArmadylPillar.kt | 4 ++-- .../content/bot/skill/fishing/FishingBot.kt | 4 ++-- game/src/main/kotlin/content/entity/npc/Sheep.kt | 4 ++-- .../main/kotlin/content/entity/obj/MilkCow.kt | 8 ++++---- game/src/main/kotlin/content/entity/obj/Mill.kt | 6 +++--- .../kotlin/content/entity/player/bank/Bank.kt | 6 +++--- .../quest/free/cooks_assistant/CooksAssistant.kt | 8 ++++---- .../quest/free/gunnars_ground/GunnarsGround.kt | 4 ++-- .../quest/free/rune_mysteries/RuneMysteries.kt | 8 ++++---- .../free/the_knights_sword/TheKnightsSword.kt | 6 +++--- .../free/the_restless_ghost/FatherUrhney.kt | 4 ++-- .../quest/member/plague_city/PlagueCity.kt | 10 +++++----- .../content/skill/constitution/drink/Potions.kt | 4 ++-- .../content/skill/crafting/SilverCasting.kt | 4 ++-- .../content/skill/farming/FarmingPatchPick.kt | 4 ++-- .../main/kotlin/content/skill/fishing/Fishing.kt | 6 +++--- .../main/kotlin/content/skill/mining/Pickaxe.kt | 4 ++-- .../content/skill/runecrafting/Runecrafting.kt | 4 ++-- .../kotlin/content/skill/woodcutting/Hatchet.kt | 4 ++-- 47 files changed, 129 insertions(+), 125 deletions(-) diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/inv/Inventories.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/inv/Inventories.kt index 2a49d97c72..e8dc56dd06 100644 --- a/engine/src/main/kotlin/world/gregs/voidps/engine/inv/Inventories.kt +++ b/engine/src/main/kotlin/world/gregs/voidps/engine/inv/Inventories.kt @@ -126,6 +126,10 @@ val Player.equipment: Inventory val Player.beastOfBurden: Inventory get() = inventories.inventory("beast_of_burden") -fun Player.holdsItem(id: String) = inventory.contains(id) || equipment.contains(id) +fun Player.carriesItem(id: String) = inventory.contains(id) || equips(id) -fun Player.holdsItem(id: String, amount: Int) = inventory.contains(id, amount) || equipment.contains(id, amount) +fun Player.carriesItem(id: String, amount: Int) = inventory.contains(id, amount) || equips(id, amount) + +fun Player.equips(id: String) = equipment.contains(id) + +fun Player.equips(id: String, amount: Int) = equipment.contains(id, amount) diff --git a/game/src/main/kotlin/content/area/asgarnia/falador/MakeoverMage.kt b/game/src/main/kotlin/content/area/asgarnia/falador/MakeoverMage.kt index a96139f1cb..c63fbc6b8d 100644 --- a/game/src/main/kotlin/content/area/asgarnia/falador/MakeoverMage.kt +++ b/game/src/main/kotlin/content/area/asgarnia/falador/MakeoverMage.kt @@ -13,7 +13,7 @@ import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.entity.character.player.chat.notEnough import world.gregs.voidps.engine.entity.character.player.flagAppearance import world.gregs.voidps.engine.entity.character.player.male -import world.gregs.voidps.engine.inv.holdsItem +import world.gregs.voidps.engine.inv.carriesItem import world.gregs.voidps.engine.inv.inventory import world.gregs.voidps.engine.inv.transact.TransactionError import world.gregs.voidps.engine.inv.transact.operation.AddItem.add @@ -157,7 +157,7 @@ class MakeoverMage(val enums: EnumDefinitions) : Script { fun ChoiceOption.amulet(): Unit = option("Cool amulet! Can I have one?") { val cost = 100 npc("No problem, but please remember that the amulet I will sell you is only a copy of my own. It contains no magical powers and, as such, will only cost you $cost coins.") - if (!holdsItem("coins", cost)) { + if (!carriesItem("coins", cost)) { player("Oh, I don't have enough money for that.") return@option } diff --git a/game/src/main/kotlin/content/area/asgarnia/falador/SirVyvin.kt b/game/src/main/kotlin/content/area/asgarnia/falador/SirVyvin.kt index d96e117eb6..297a1e4245 100644 --- a/game/src/main/kotlin/content/area/asgarnia/falador/SirVyvin.kt +++ b/game/src/main/kotlin/content/area/asgarnia/falador/SirVyvin.kt @@ -19,7 +19,7 @@ import world.gregs.voidps.engine.entity.character.sound import world.gregs.voidps.engine.entity.item.floor.FloorItems import world.gregs.voidps.engine.entity.obj.replace import world.gregs.voidps.engine.inv.add -import world.gregs.voidps.engine.inv.holdsItem +import world.gregs.voidps.engine.inv.carriesItem import world.gregs.voidps.engine.inv.inventory import world.gregs.voidps.engine.timer.toTicks import java.util.concurrent.TimeUnit @@ -48,7 +48,7 @@ class SirVyvin( npc("HEY! Just WHAT do you THINK you are DOING??? STAY OUT of MY cupboard!") return@objectOperate } - if (holdsItem("portrait")) { + if (carriesItem("portrait")) { statement("There is just a load of junk in here.") } else { statement("You find a small portrait in here which you take.") diff --git a/game/src/main/kotlin/content/area/asgarnia/falador/SquireAsrol.kt b/game/src/main/kotlin/content/area/asgarnia/falador/SquireAsrol.kt index e9b0413006..5f5d14aa3a 100644 --- a/game/src/main/kotlin/content/area/asgarnia/falador/SquireAsrol.kt +++ b/game/src/main/kotlin/content/area/asgarnia/falador/SquireAsrol.kt @@ -14,7 +14,7 @@ import world.gregs.voidps.engine.entity.character.player.combatLevel import world.gregs.voidps.engine.entity.character.player.skill.Skill import world.gregs.voidps.engine.event.AuditLog import world.gregs.voidps.engine.inv.equipment -import world.gregs.voidps.engine.inv.holdsItem +import world.gregs.voidps.engine.inv.carriesItem import world.gregs.voidps.engine.inv.inventory import world.gregs.voidps.engine.inv.remove import world.gregs.voidps.engine.queue.softQueue @@ -57,7 +57,7 @@ class SquireAsrol : Script { suspend fun Player.checkPicture() { npc("So how are you doing getting a sword?") - if (holdsItem("portrait")) { + if (carriesItem("portrait")) { player("I have the picture. I'll just take it to the dwarf now!") npc("Please hurry!") return @@ -72,7 +72,7 @@ class SquireAsrol : Script { npc("So can you un-equip it and hand it over to me now please?") return } - if (holdsItem("blurite_sword")) { + if (carriesItem("blurite_sword")) { player("I have retrieved your sword for you.") npc("Thank you, thank you, thank you! I was seriously worried I would have to own up to Sir Vyvin!") statement("You give the sword to the squire.") diff --git a/game/src/main/kotlin/content/area/asgarnia/port_sarim/PortSarim.kt b/game/src/main/kotlin/content/area/asgarnia/port_sarim/PortSarim.kt index 1825de7c18..f153a3bde8 100644 --- a/game/src/main/kotlin/content/area/asgarnia/port_sarim/PortSarim.kt +++ b/game/src/main/kotlin/content/area/asgarnia/port_sarim/PortSarim.kt @@ -10,14 +10,14 @@ import world.gregs.voidps.engine.entity.character.player.skill.exp.exp import world.gregs.voidps.engine.entity.character.player.skill.level.Level import world.gregs.voidps.engine.entity.character.sound import world.gregs.voidps.engine.entity.obj.GameObject -import world.gregs.voidps.engine.inv.holdsItem +import world.gregs.voidps.engine.inv.carriesItem import world.gregs.voidps.engine.queue.weakQueue class PortSarim : Script { init { takeable("white_apron_port_sarim") { - if (holdsItem("white_apron")) { + if (carriesItem("white_apron")) { message("You already have one of those.") null } else { diff --git a/game/src/main/kotlin/content/area/asgarnia/port_sarim/Thurgo.kt b/game/src/main/kotlin/content/area/asgarnia/port_sarim/Thurgo.kt index de52b1a84d..3560238b44 100644 --- a/game/src/main/kotlin/content/area/asgarnia/port_sarim/Thurgo.kt +++ b/game/src/main/kotlin/content/area/asgarnia/port_sarim/Thurgo.kt @@ -7,7 +7,7 @@ import world.gregs.voidps.engine.Script import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.entity.item.Item import world.gregs.voidps.engine.inv.contains -import world.gregs.voidps.engine.inv.holdsItem +import world.gregs.voidps.engine.inv.carriesItem import world.gregs.voidps.engine.inv.inventory import world.gregs.voidps.engine.inv.remove import world.gregs.voidps.engine.inv.transact.operation.AddItem.add @@ -42,7 +42,7 @@ class Thurgo : Script { suspend fun Player.menuReplacementSword() { choice { - if (holdsItem("blurite_sword")) { + if (carriesItem("blurite_sword")) { madeSword() } else { replacementSword() @@ -119,7 +119,7 @@ class Thurgo : Script { fun ChoiceOption.aboutSword() = option("About that sword...") { npc("Have you got a picture of the sword for me yet?") - if (!holdsItem("portrait")) { + if (!carriesItem("portrait")) { player("Sorry, not yet.") npc("Well, come back when you do.") return@option @@ -145,7 +145,7 @@ class Thurgo : Script { } fun ChoiceOption.redberryPie(player: Player) { - if (!player.holdsItem("redberry_pie")) { + if (!player.carriesItem("redberry_pie")) { return } option("Would you like a redberry pie?") { diff --git a/game/src/main/kotlin/content/area/kandarin/ardougne/Alrena.kt b/game/src/main/kotlin/content/area/kandarin/ardougne/Alrena.kt index 3f9a26bdb4..436945c3e6 100644 --- a/game/src/main/kotlin/content/area/kandarin/ardougne/Alrena.kt +++ b/game/src/main/kotlin/content/area/kandarin/ardougne/Alrena.kt @@ -10,7 +10,7 @@ import content.quest.quest import world.gregs.voidps.engine.Script import world.gregs.voidps.engine.entity.character.npc.NPC import world.gregs.voidps.engine.entity.character.player.Player -import world.gregs.voidps.engine.inv.holdsItem +import world.gregs.voidps.engine.inv.carriesItem import world.gregs.voidps.engine.inv.inventory import world.gregs.voidps.engine.inv.replace @@ -40,7 +40,7 @@ class Alrena : Script { suspend fun Player.started(target: NPC) { player("Hello, Edmond has asked me to help find your daughter.") npc("Yes he told me. I've begun making your special gas mask, but I need some dwellberries to finish it.") - if (holdsItem("dwellberries")) { + if (carriesItem("dwellberries")) { player("Yes I've got some here.") item("dwellberries", 600, "You give the dwellberries to Alrena.") target.anim("human_herbing_grind") diff --git a/game/src/main/kotlin/content/area/kandarin/ardougne/Edmond.kt b/game/src/main/kotlin/content/area/kandarin/ardougne/Edmond.kt index ffe50c7c42..b2656fd8b2 100644 --- a/game/src/main/kotlin/content/area/kandarin/ardougne/Edmond.kt +++ b/game/src/main/kotlin/content/area/kandarin/ardougne/Edmond.kt @@ -26,7 +26,7 @@ import world.gregs.voidps.engine.entity.item.floor.FloorItems import world.gregs.voidps.engine.entity.obj.GameObjects import world.gregs.voidps.engine.entity.obj.ObjectShape import world.gregs.voidps.engine.inv.add -import world.gregs.voidps.engine.inv.holdsItem +import world.gregs.voidps.engine.inv.carriesItem import world.gregs.voidps.engine.inv.inventory import world.gregs.voidps.type.Direction import world.gregs.voidps.type.Region @@ -90,7 +90,7 @@ class Edmond : Script { suspend fun Player.started() { player("Hello Edmond.") npc("Have you got the dwellberries yet?") - if (holdsItem("dwellberries")) { + if (carriesItem("dwellberries")) { player("Yes I've got some here.") npc("Take them to my wife Alrena, she's inside.") } else { @@ -152,7 +152,7 @@ class Edmond : Script { suspend fun Player.spoken() { player("Hello.") npc("Have you found Elena yet?") - if (holdsItem("picture_plague_city")) { + if (carriesItem("picture_plague_city")) { player("Not yet, it's a big city over there.") npc("I hope it's not too late.") } else { diff --git a/game/src/main/kotlin/content/area/kandarin/ardougne/west_ardougne/Bravek.kt b/game/src/main/kotlin/content/area/kandarin/ardougne/west_ardougne/Bravek.kt index c7b1dcef11..fcb293b9fa 100644 --- a/game/src/main/kotlin/content/area/kandarin/ardougne/west_ardougne/Bravek.kt +++ b/game/src/main/kotlin/content/area/kandarin/ardougne/west_ardougne/Bravek.kt @@ -12,7 +12,7 @@ import world.gregs.voidps.engine.Script import world.gregs.voidps.engine.entity.character.npc.NPC import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.inv.add -import world.gregs.voidps.engine.inv.holdsItem +import world.gregs.voidps.engine.inv.carriesItem import world.gregs.voidps.engine.inv.inventory import world.gregs.voidps.engine.inv.remove import world.gregs.voidps.engine.queue.softQueue @@ -88,7 +88,7 @@ class Bravek : Script { suspend fun Player.hasCurePaper(target: NPC) { npc("Uurgh! My head still hurts too much to think straight. Oh for one of Trudi's hangover cures!") - if (holdsItem("hangover_cure")) { + if (carriesItem("hangover_cure")) { player("Try this.") inventory.remove("hangover_cure") set("plague_city", "gave_cure") @@ -105,7 +105,7 @@ class Bravek : Script { suspend fun Player.gaveCure() { npc("Thanks again for the hangover cure.") - if (holdsItem("warrant")) { + if (carriesItem("warrant")) { player("Not a problem, happy to help out.") npc("I'm just having a little drop of whisky, then I'll feel really good.") } else { diff --git a/game/src/main/kotlin/content/area/kandarin/ardougne/west_ardougne/Mourner.kt b/game/src/main/kotlin/content/area/kandarin/ardougne/west_ardougne/Mourner.kt index a86b047669..21370b58cc 100644 --- a/game/src/main/kotlin/content/area/kandarin/ardougne/west_ardougne/Mourner.kt +++ b/game/src/main/kotlin/content/area/kandarin/ardougne/west_ardougne/Mourner.kt @@ -9,7 +9,7 @@ import content.entity.player.dialogue.type.statement import world.gregs.voidps.engine.Script import world.gregs.voidps.engine.entity.character.npc.NPCs import world.gregs.voidps.engine.entity.obj.GameObjects -import world.gregs.voidps.engine.inv.holdsItem +import world.gregs.voidps.engine.inv.carriesItem import world.gregs.voidps.type.Direction import world.gregs.voidps.type.Tile import world.gregs.voidps.type.equals @@ -18,7 +18,7 @@ class Mourner : Script { init { npcOperate("Talk-to", "mourner_elena_guard_vis") { (target) -> - if (holdsItem("warrant")) { + if (carriesItem("warrant")) { player("I have a warrant from Bravek to enter here.") npc("This is highly irregular. Please wait...") val otherGuard = NPCs.find(if (target.tile.equals(2539, 3273)) Tile(2534, 3273) else Tile(2539, 3273), "mourner_elena_guard_vis") diff --git a/game/src/main/kotlin/content/area/kandarin/ardougne/west_ardougne/WestArdougne.kt b/game/src/main/kotlin/content/area/kandarin/ardougne/west_ardougne/WestArdougne.kt index b93c9ed2d8..995ca34d7d 100644 --- a/game/src/main/kotlin/content/area/kandarin/ardougne/west_ardougne/WestArdougne.kt +++ b/game/src/main/kotlin/content/area/kandarin/ardougne/west_ardougne/WestArdougne.kt @@ -14,7 +14,7 @@ import world.gregs.voidps.engine.entity.item.floor.FloorItems import world.gregs.voidps.engine.entity.obj.remove import world.gregs.voidps.engine.entity.obj.replace import world.gregs.voidps.engine.inv.add -import world.gregs.voidps.engine.inv.holdsItem +import world.gregs.voidps.engine.inv.carriesItem import world.gregs.voidps.engine.inv.inventory import world.gregs.voidps.engine.inv.remove import world.gregs.voidps.engine.queue.softQueue @@ -84,7 +84,7 @@ class WestArdougne : Script { return@objectOperate } npc("ted_rehnison", "Go away. We don't want any.") - if (holdsItem("book_turnip_growing_for_beginners")) { + if (carriesItem("book_turnip_growing_for_beginners")) { player("I'm a friend of Jethick's, I have come to return a book he borrowed.") npc("ted_rehnison", "Oh... Why didn't you say, come in then.") enterDoor(target, delay = 2) diff --git a/game/src/main/kotlin/content/area/kandarin/catherby/Harry.kt b/game/src/main/kotlin/content/area/kandarin/catherby/Harry.kt index f53fc71e72..259a7cb4c9 100644 --- a/game/src/main/kotlin/content/area/kandarin/catherby/Harry.kt +++ b/game/src/main/kotlin/content/area/kandarin/catherby/Harry.kt @@ -5,7 +5,7 @@ import content.entity.player.dialogue.* import content.entity.player.dialogue.type.* import world.gregs.voidps.engine.Script import world.gregs.voidps.engine.inv.add -import world.gregs.voidps.engine.inv.holdsItem +import world.gregs.voidps.engine.inv.carriesItem import world.gregs.voidps.engine.inv.inventory import world.gregs.voidps.engine.inv.remove @@ -20,18 +20,18 @@ class Harry : Script { openShop("harrys_fishing_shop") } - if (holdsItem("fishbowl_water") || holdsItem("fishbowl_seaweed")) { + if (carriesItem("fishbowl_water") || carriesItem("fishbowl_seaweed")) { option("Can I get a fish for this bowl?") { player("Can I get a fish for this bowl?") when { - holdsItem("fishbowl_water") -> { + carriesItem("fishbowl_water") -> { npc("Sorry, you need to put some seaweed into the bowl first") player("Seaweed?") npc("Yes, the fish seem to like it. Come and see me when you have put some in the bowl.") } - holdsItem("fishbowl_seaweed") -> { + carriesItem("fishbowl_seaweed") -> { npc("Yes, you can. I can see that you have a nicely filled fishbowl there to use, and you can catch a fish from my aquarium if you want. You will need a special net to do this though - I sell them for 10 gold.") choice { option("I'll take it.") { @@ -56,7 +56,7 @@ class Harry : Script { // TODO: add fishing spot for tiny_net & get pet_fish to work like it is in real runescape - if (holdsItem("fishbowl_water") || holdsItem("fishbowl_seaweed") || holdsItem("fishbowl")) { + if (carriesItem("fishbowl_water") || carriesItem("fishbowl_seaweed") || carriesItem("fishbowl")) { option("Do you have any fish food?") { player("Do you have any fish food?") npc("Sorry, I'm all out. I used up the last of it feeding the fish in the aquarium. I have some empty boxes, though - they have the ingredients written on the back.") diff --git a/game/src/main/kotlin/content/area/kharidian_desert/al_kharid/Ellis.kt b/game/src/main/kotlin/content/area/kharidian_desert/al_kharid/Ellis.kt index b9fd7f2b5a..ff1f205281 100644 --- a/game/src/main/kotlin/content/area/kharidian_desert/al_kharid/Ellis.kt +++ b/game/src/main/kotlin/content/area/kharidian_desert/al_kharid/Ellis.kt @@ -20,7 +20,7 @@ import world.gregs.voidps.engine.data.definition.ItemDefinitions import world.gregs.voidps.engine.data.definition.data.Tanning import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.entity.character.player.male -import world.gregs.voidps.engine.inv.holdsItem +import world.gregs.voidps.engine.inv.carriesItem import world.gregs.voidps.engine.inv.inventory import world.gregs.voidps.engine.inv.transact.operation.RemoveItem.remove import world.gregs.voidps.engine.inv.transact.operation.ReplaceItem.replace @@ -86,7 +86,7 @@ class Ellis : Script { fun tan(player: Player, type: String, amount: Int) { val item = type.removeSuffix("_1") - if (!player.holdsItem(item)) { + if (!player.carriesItem(item)) { player.message("You don't have any ${item.toLowerSpaceCase()} to tan.") return } diff --git a/game/src/main/kotlin/content/area/kharidian_desert/sophanem/GuardianMummy.kt b/game/src/main/kotlin/content/area/kharidian_desert/sophanem/GuardianMummy.kt index 8b74efe5e5..ffeff04a27 100644 --- a/game/src/main/kotlin/content/area/kharidian_desert/sophanem/GuardianMummy.kt +++ b/game/src/main/kotlin/content/area/kharidian_desert/sophanem/GuardianMummy.kt @@ -12,7 +12,7 @@ import world.gregs.voidps.engine.Script import world.gregs.voidps.engine.client.message import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.inv.Inventory -import world.gregs.voidps.engine.inv.holdsItem +import world.gregs.voidps.engine.inv.carriesItem import world.gregs.voidps.engine.inv.inventory import world.gregs.voidps.engine.inv.replace import world.gregs.voidps.engine.inv.transact.TransactionError @@ -27,7 +27,7 @@ class GuardianMummy : Script { init { npcOperate("Talk-to", "guardian_mummy") { - if (holdsItem("pharaohs_sceptre")) { + if (carriesItem("pharaohs_sceptre")) { sceptreRecharging() return@npcOperate } @@ -75,7 +75,7 @@ class GuardianMummy : Script { iKnowWhatImDoing() } option("I want to charge or remove charges from my sceptre.") { - if (holdsItem("pharaohs_sceptre")) { + if (carriesItem("pharaohs_sceptre")) { sceptreRecharging() } else { sceptreDischarging() diff --git a/game/src/main/kotlin/content/area/misthalin/barbarian_village/Dororan.kt b/game/src/main/kotlin/content/area/misthalin/barbarian_village/Dororan.kt index 730ec9e9e8..5bebda0b96 100644 --- a/game/src/main/kotlin/content/area/misthalin/barbarian_village/Dororan.kt +++ b/game/src/main/kotlin/content/area/misthalin/barbarian_village/Dororan.kt @@ -12,7 +12,7 @@ import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.entity.character.player.chat.noInterest import world.gregs.voidps.engine.entity.character.player.skill.Skill import world.gregs.voidps.engine.inv.add -import world.gregs.voidps.engine.inv.holdsItem +import world.gregs.voidps.engine.inv.carriesItem import world.gregs.voidps.engine.inv.inventory import world.gregs.voidps.engine.inv.replace import world.gregs.voidps.engine.queue.softQueue @@ -975,7 +975,7 @@ class Dororan : Script { engrave() return } - if (holdsItem("ring_from_jeffery")) { + if (carriesItem("ring_from_jeffery")) { player("I have one right here.") item("ring_from_jeffery", 600, "You show Dororan the ring from Jeffery.") npc("Thank you! That's exactly what I need!") diff --git a/game/src/main/kotlin/content/area/misthalin/barbarian_village/Gudrun.kt b/game/src/main/kotlin/content/area/misthalin/barbarian_village/Gudrun.kt index 2a26219159..1b6c47d301 100644 --- a/game/src/main/kotlin/content/area/misthalin/barbarian_village/Gudrun.kt +++ b/game/src/main/kotlin/content/area/misthalin/barbarian_village/Gudrun.kt @@ -22,7 +22,7 @@ import world.gregs.voidps.engine.entity.obj.GameObjects import world.gregs.voidps.engine.entity.obj.ObjectShape import world.gregs.voidps.engine.event.AuditLog import world.gregs.voidps.engine.inv.add -import world.gregs.voidps.engine.inv.holdsItem +import world.gregs.voidps.engine.inv.carriesItem import world.gregs.voidps.engine.inv.inventory import world.gregs.voidps.engine.inv.remove import world.gregs.voidps.engine.queue.softQueue @@ -266,7 +266,7 @@ class Gudrun : Script { } suspend fun Player.poem() { - if (holdsItem("gunnars_ground")) { + if (carriesItem("gunnars_ground")) { npc("What have you got there?") player("Another gift from your mysterious suitor.") npc("A scroll?") @@ -342,7 +342,7 @@ class Gudrun : Script { npc("Sorry about that, stranger. Did you want something?.") player("Are you Gudrun?") npc("Yes.") - if (holdsItem("dororans_engraved_ring")) { + if (carriesItem("dororans_engraved_ring")) { player("This is for you.") anim("hand_over_item") item("dororans_engraved_ring", 400, "You show Gudrun the ring.") diff --git a/game/src/main/kotlin/content/area/misthalin/barbarian_village/stronghold_of_security/StrongholdOfSecurityRewards.kt b/game/src/main/kotlin/content/area/misthalin/barbarian_village/stronghold_of_security/StrongholdOfSecurityRewards.kt index 9df591d2eb..39f4ddf5f1 100644 --- a/game/src/main/kotlin/content/area/misthalin/barbarian_village/stronghold_of_security/StrongholdOfSecurityRewards.kt +++ b/game/src/main/kotlin/content/area/misthalin/barbarian_village/stronghold_of_security/StrongholdOfSecurityRewards.kt @@ -13,7 +13,7 @@ import world.gregs.voidps.engine.entity.character.jingle import world.gregs.voidps.engine.entity.character.player.skill.Skill import world.gregs.voidps.engine.entity.character.sound import world.gregs.voidps.engine.inv.add -import world.gregs.voidps.engine.inv.holdsItem +import world.gregs.voidps.engine.inv.carriesItem import world.gregs.voidps.engine.inv.inventory import world.gregs.voidps.engine.inv.replace @@ -116,7 +116,7 @@ class StrongholdOfSecurityRewards : Script { objectOperate("Search", "stronghold_dead_explorer") { anim("pick_pocket") sound("pick") - if (holdsItem("stronghold_notes")) { + if (carriesItem("stronghold_notes")) { message("You don't find anything.") return@objectOperate } diff --git a/game/src/main/kotlin/content/area/misthalin/draynor_village/Aggie.kt b/game/src/main/kotlin/content/area/misthalin/draynor_village/Aggie.kt index 78052069f3..f1ec65903f 100644 --- a/game/src/main/kotlin/content/area/misthalin/draynor_village/Aggie.kt +++ b/game/src/main/kotlin/content/area/misthalin/draynor_village/Aggie.kt @@ -10,7 +10,7 @@ import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.entity.character.sound import world.gregs.voidps.engine.entity.item.floor.FloorItems import world.gregs.voidps.engine.inv.add -import world.gregs.voidps.engine.inv.holdsItem +import world.gregs.voidps.engine.inv.carriesItem import world.gregs.voidps.engine.inv.inventory import world.gregs.voidps.engine.inv.remove import world.gregs.voidps.engine.inv.transact.operation.AddItem.add @@ -31,7 +31,7 @@ class Aggie : Script { "leela", "equipment", "joe_one_beer", "joe_two_beers", "joe_three_beers", "tie_up_lady_keli" -> { option("Talk about Prince Ali Rescue.") { player("Could you think of a way to make skin paste?") - if (holdsItem("ashes") && holdsItem("pot_of_flour") && holdsItem("bucket_of_water") && holdsItem("redberries")) { + if (carriesItem("ashes") && carriesItem("pot_of_flour") && carriesItem("bucket_of_water") && carriesItem("redberries")) { npc("Yes I can. I see you already have the ingredients. Would you like me to mix some for you now?") choice { option("Yes please. Mix me some skin paste.") { diff --git a/game/src/main/kotlin/content/area/misthalin/draynor_village/Ned.kt b/game/src/main/kotlin/content/area/misthalin/draynor_village/Ned.kt index 3f8e6108d2..6f772340e1 100644 --- a/game/src/main/kotlin/content/area/misthalin/draynor_village/Ned.kt +++ b/game/src/main/kotlin/content/area/misthalin/draynor_village/Ned.kt @@ -8,7 +8,7 @@ import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.entity.character.player.male import world.gregs.voidps.engine.entity.item.floor.FloorItems import world.gregs.voidps.engine.inv.add -import world.gregs.voidps.engine.inv.holdsItem +import world.gregs.voidps.engine.inv.carriesItem import world.gregs.voidps.engine.inv.inventory import world.gregs.voidps.engine.inv.remove import world.gregs.voidps.engine.inv.transact.operation.AddItem.add @@ -34,7 +34,7 @@ class Ned : Script { fun ChoiceOption.wig() { option("How about some sort of wig?") { npc("Well... that's an interesting thought. Yes, I think I could do something. Give me three balls of wool and I might be able to do it.") - if (!holdsItem("ball_of_wool", 3)) { + if (!carriesItem("ball_of_wool", 3)) { player("Great, I will get some. I think a wig would be useful.") return@option } diff --git a/game/src/main/kotlin/content/area/misthalin/edgeville/Jeffery.kt b/game/src/main/kotlin/content/area/misthalin/edgeville/Jeffery.kt index 33b68f547b..683883cc04 100644 --- a/game/src/main/kotlin/content/area/misthalin/edgeville/Jeffery.kt +++ b/game/src/main/kotlin/content/area/misthalin/edgeville/Jeffery.kt @@ -11,7 +11,7 @@ import world.gregs.voidps.engine.Script import world.gregs.voidps.engine.entity.character.npc.NPC import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.inv.equipment -import world.gregs.voidps.engine.inv.holdsItem +import world.gregs.voidps.engine.inv.carriesItem import world.gregs.voidps.engine.inv.inventory import world.gregs.voidps.engine.inv.replace @@ -37,7 +37,7 @@ class Jeffery : Script { choice { option("I was hoping you would trade me a gold ring.") { npc("Trade you? Trade you for what?") - if (holdsItem("love_poem")) { + if (carriesItem("love_poem")) { choice { option("This splendid love poem.") { lovePoem(target) diff --git a/game/src/main/kotlin/content/area/misthalin/lumbridge/castle/Cook.kt b/game/src/main/kotlin/content/area/misthalin/lumbridge/castle/Cook.kt index ef648396c2..aceb495f29 100644 --- a/game/src/main/kotlin/content/area/misthalin/lumbridge/castle/Cook.kt +++ b/game/src/main/kotlin/content/area/misthalin/lumbridge/castle/Cook.kt @@ -12,7 +12,7 @@ import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.entity.character.player.skill.Skill import world.gregs.voidps.engine.event.AuditLog import world.gregs.voidps.engine.inv.add -import world.gregs.voidps.engine.inv.holdsItem +import world.gregs.voidps.engine.inv.carriesItem import world.gregs.voidps.engine.inv.inventory import world.gregs.voidps.engine.inv.remove @@ -47,33 +47,33 @@ class Cook : Script { suspend fun Player.started() { npc("how are you getting on with finding the ingredients?") - if (holdsItem("top_quality_milk")) { + if (carriesItem("top_quality_milk")) { item("top_quality_milk", 500, "You give the top-quality milk to the cook.") inventory.remove("top_quality_milk") set("cooks_assistant_milk", 1) player("Here's some top-quality milk.") } - if (holdsItem("extra_fine_flour")) { + if (carriesItem("extra_fine_flour")) { item("extra_fine_flour", 500, "You give the extra fine flour to the cook.") inventory.remove("extra_fine_flour") set("cooks_assistant_flour", 1) player("Here's the extra fine flour.") } - if (holdsItem("super_large_egg")) { + if (carriesItem("super_large_egg")) { item("super_large_egg", 500, "You give the super large egg to the cook.") inventory.remove("super_large_egg") set("cooks_assistant_egg", 1) player("Here's a super large egg.") } - if (holdsItem("egg") && (get("cooks_assistant_egg", 0) == 0)) { + if (carriesItem("egg") && (get("cooks_assistant_egg", 0) == 0)) { player("I've this egg.") npc("No, I need a super large egg. You'll probably find one near the local chickens.") } - if (holdsItem("pot_of_flour") && (get("cooks_assistant_flour", 0) == 0)) { + if (carriesItem("pot_of_flour") && (get("cooks_assistant_flour", 0) == 0)) { player("I've this flour.") npc("That's not fine enough. I imagine if you speak with Millie at the mill to the north she'll help you out.") } - if (holdsItem("bucket_of_milk") && (get("cooks_assistant_milk", 0) == 0)) { + if (carriesItem("bucket_of_milk") && (get("cooks_assistant_milk", 0) == 0)) { player("I've this milk.") npc("Not bad, but not good enough. There's a milk maid that looks after the cows to the north-east. She might have some advice.") } @@ -185,7 +185,7 @@ class Cook : Script { npc("It's called the Cook-o-Matic 25 and it uses a combination of state-of-the-art temperature regulation and magic.") player("Will it mean my food will burn less often?") npc("As long as the food is fairly easy to cook in the first place!") - if (holdsItem("cook_o_matic_manual")) { + if (carriesItem("cook_o_matic_manual")) { npc("The manual you have in your inventory should tell you more.") } else if (inventory.isFull()) { npc("I'd give you the manual, but you don't have room to take it. Ask me again when you have some space.") diff --git a/game/src/main/kotlin/content/area/misthalin/lumbridge/church/FatherAereck.kt b/game/src/main/kotlin/content/area/misthalin/lumbridge/church/FatherAereck.kt index 357cd70a65..3bcabfcc52 100644 --- a/game/src/main/kotlin/content/area/misthalin/lumbridge/church/FatherAereck.kt +++ b/game/src/main/kotlin/content/area/misthalin/lumbridge/church/FatherAereck.kt @@ -8,7 +8,7 @@ import world.gregs.voidps.engine.Script import world.gregs.voidps.engine.client.ui.open import world.gregs.voidps.engine.data.Settings import world.gregs.voidps.engine.entity.character.player.Player -import world.gregs.voidps.engine.inv.holdsItem +import world.gregs.voidps.engine.inv.carriesItem import world.gregs.voidps.engine.inv.inventory import world.gregs.voidps.engine.inv.replace @@ -76,7 +76,7 @@ class FatherAereck : Script { } suspend fun Player.foundSkull() { - if (holdsItem("ghostspeak_amulet")) { + if (carriesItem("ghostspeak_amulet")) { npc("Have you got rid of the ghost yet?") player("I've finally found the ghost's skull!") npc("Great! Put it in the ghost's coffin and see what happens!") diff --git a/game/src/main/kotlin/content/area/misthalin/lumbridge/farm/GillieGroats.kt b/game/src/main/kotlin/content/area/misthalin/lumbridge/farm/GillieGroats.kt index a0888a0413..a9b906c4ad 100644 --- a/game/src/main/kotlin/content/area/misthalin/lumbridge/farm/GillieGroats.kt +++ b/game/src/main/kotlin/content/area/misthalin/lumbridge/farm/GillieGroats.kt @@ -9,7 +9,7 @@ import content.entity.player.dialogue.type.player import content.quest.quest import world.gregs.voidps.engine.Script import world.gregs.voidps.engine.entity.character.player.Player -import world.gregs.voidps.engine.inv.holdsItem +import world.gregs.voidps.engine.inv.carriesItem class GillieGroats : Script { @@ -17,7 +17,7 @@ class GillieGroats : Script { npcOperate("Talk-to", "gillie_groats") { npc("Hello, I'm Gillie the Milkmaid. What can I do for you?") choice { - if (quest("cooks_assistant") == "started" && !holdsItem("top_quality_milk")) { + if (quest("cooks_assistant") == "started" && !carriesItem("top_quality_milk")) { option("I'm after some Top-quality milk.") { topQualityMilk() } diff --git a/game/src/main/kotlin/content/area/misthalin/lumbridge/windmill/MillieMiller.kt b/game/src/main/kotlin/content/area/misthalin/lumbridge/windmill/MillieMiller.kt index 53fca2f548..93e194561a 100644 --- a/game/src/main/kotlin/content/area/misthalin/lumbridge/windmill/MillieMiller.kt +++ b/game/src/main/kotlin/content/area/misthalin/lumbridge/windmill/MillieMiller.kt @@ -7,7 +7,7 @@ import content.entity.player.dialogue.type.* import content.quest.quest import world.gregs.voidps.engine.Script import world.gregs.voidps.engine.entity.character.player.Player -import world.gregs.voidps.engine.inv.holdsItem +import world.gregs.voidps.engine.inv.carriesItem class MillieMiller : Script { @@ -20,7 +20,7 @@ class MillieMiller : Script { suspend fun Player.menu() { choice { - if (quest("cooks_assistant") == "started" && !holdsItem("extra_fine_flour")) { + if (quest("cooks_assistant") == "started" && !carriesItem("extra_fine_flour")) { option("I'm looking for extra fine flour.") { npc("What's wrong with ordinary flour?") player("Well, I'm no expert chef, but apparently it makes better cakes. This cake, you see, is for Duke Horacio.") diff --git a/game/src/main/kotlin/content/area/misthalin/varrock/Baraek.kt b/game/src/main/kotlin/content/area/misthalin/varrock/Baraek.kt index 66b2372049..3b3e6c1fe2 100644 --- a/game/src/main/kotlin/content/area/misthalin/varrock/Baraek.kt +++ b/game/src/main/kotlin/content/area/misthalin/varrock/Baraek.kt @@ -8,7 +8,7 @@ import content.entity.player.dialogue.type.player import world.gregs.voidps.engine.Script import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.inv.add -import world.gregs.voidps.engine.inv.holdsItem +import world.gregs.voidps.engine.inv.carriesItem import world.gregs.voidps.engine.inv.inventory import world.gregs.voidps.engine.inv.remove @@ -16,7 +16,7 @@ class Baraek : Script { init { npcOperate("Talk-to", "baraek") { - if (holdsItem("bear_fur")) { + if (carriesItem("bear_fur")) { choice { option("Can you sell me some furs?") { sellFur() diff --git a/game/src/main/kotlin/content/area/misthalin/varrock/palace/CaptainRovin.kt b/game/src/main/kotlin/content/area/misthalin/varrock/palace/CaptainRovin.kt index 963f69e27e..6a35c3cdc5 100644 --- a/game/src/main/kotlin/content/area/misthalin/varrock/palace/CaptainRovin.kt +++ b/game/src/main/kotlin/content/area/misthalin/varrock/palace/CaptainRovin.kt @@ -8,7 +8,7 @@ import content.quest.questCompleted import world.gregs.voidps.engine.Script import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.inv.add -import world.gregs.voidps.engine.inv.holdsItem +import world.gregs.voidps.engine.inv.carriesItem import world.gregs.voidps.engine.inv.inventory class CaptainRovin : Script { @@ -82,7 +82,7 @@ class CaptainRovin : Script { suspend fun Player.haveYouNotKilledIt() { npc("Yes, you said before, haven't you killed it yet?") player("I'm going to use the powerful sword Silverlight, which I believe you have one of the keys for?") - if (holdsItem("silverlight_key_captain_rovin")) { + if (carriesItem("silverlight_key_captain_rovin")) { npc("I already gave you my key. Check your pockets.") } else { npc("I already gave you my key. Maybe you left it somewhere. Have you checked your bank account?") diff --git a/game/src/main/kotlin/content/area/misthalin/varrock/palace/SirPrysin.kt b/game/src/main/kotlin/content/area/misthalin/varrock/palace/SirPrysin.kt index d2ca789b13..61f1453997 100644 --- a/game/src/main/kotlin/content/area/misthalin/varrock/palace/SirPrysin.kt +++ b/game/src/main/kotlin/content/area/misthalin/varrock/palace/SirPrysin.kt @@ -12,7 +12,7 @@ import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.entity.character.sound import world.gregs.voidps.engine.entity.obj.GameObjects import world.gregs.voidps.engine.inv.add -import world.gregs.voidps.engine.inv.holdsItem +import world.gregs.voidps.engine.inv.carriesItem import world.gregs.voidps.engine.inv.inventory import world.gregs.voidps.engine.inv.remove import world.gregs.voidps.type.Direction @@ -148,9 +148,9 @@ class SirPrysin : Script { suspend fun Player.keyProgressCheck(target: NPC) { npc("So how are you doing with getting the keys?") - val rovin = holdsItem("silverlight_key_captain_rovin") - val prysin = holdsItem("silverlight_key_sir_prysin") - val traiborn = holdsItem("silverlight_key_wizard_traiborn") + val rovin = carriesItem("silverlight_key_captain_rovin") + val prysin = carriesItem("silverlight_key_sir_prysin") + val traiborn = carriesItem("silverlight_key_wizard_traiborn") when { rovin && prysin && traiborn -> { giveSilverlight(target) diff --git a/game/src/main/kotlin/content/area/misthalin/wizards_tower/Sedridor.kt b/game/src/main/kotlin/content/area/misthalin/wizards_tower/Sedridor.kt index 2dd2e3f771..69d4af6ea7 100644 --- a/game/src/main/kotlin/content/area/misthalin/wizards_tower/Sedridor.kt +++ b/game/src/main/kotlin/content/area/misthalin/wizards_tower/Sedridor.kt @@ -17,7 +17,7 @@ import world.gregs.voidps.engine.entity.character.player.name import world.gregs.voidps.engine.entity.character.sound import world.gregs.voidps.engine.event.AuditLog import world.gregs.voidps.engine.inv.add -import world.gregs.voidps.engine.inv.holdsItem +import world.gregs.voidps.engine.inv.carriesItem import world.gregs.voidps.engine.inv.inventory import world.gregs.voidps.engine.inv.remove import world.gregs.voidps.engine.queue.softQueue @@ -166,7 +166,7 @@ class Sedridor : Script { npc("Ah, $name. How goes your quest? Have you delivered my research to Aubury yet?") player("Yes, I have. He gave me some notes to give to you.") npc("Wonderful! Let's have a look then.") - if (holdsItem("research_notes_rune_mysteries")) { + if (carriesItem("research_notes_rune_mysteries")) { item("research_notes_rune_mysteries", 600, "You hand the notes to Sedridor.") npc("Alright, let's see what Aubury has for us...") npc("Yes, this is it! The lost incantation!") diff --git a/game/src/main/kotlin/content/area/troll_country/god_wars_dungeon/ArmadylPillar.kt b/game/src/main/kotlin/content/area/troll_country/god_wars_dungeon/ArmadylPillar.kt index d54d61843e..fa69e6386e 100644 --- a/game/src/main/kotlin/content/area/troll_country/god_wars_dungeon/ArmadylPillar.kt +++ b/game/src/main/kotlin/content/area/troll_country/god_wars_dungeon/ArmadylPillar.kt @@ -12,7 +12,7 @@ import world.gregs.voidps.engine.entity.character.sound import world.gregs.voidps.engine.entity.obj.GameObjects import world.gregs.voidps.engine.inv.add import world.gregs.voidps.engine.inv.equipment -import world.gregs.voidps.engine.inv.holdsItem +import world.gregs.voidps.engine.inv.carriesItem import world.gregs.voidps.engine.inv.inventory import world.gregs.voidps.type.Direction import world.gregs.voidps.type.Tile @@ -49,7 +49,7 @@ class ArmadylPillar : Script { objectOperate("Search", "godwars_armadyl_crate") { val hasCrossbow = inventory.items.any { Weapon.crossbows.contains(it.id) } || equipment.items.any { Weapon.crossbows.contains(it.id) } - val hasGrapple = holdsItem("mithril_grapple") + val hasGrapple = carriesItem("mithril_grapple") if (!hasCrossbow || !hasGrapple) { if (inventory.add("bronze_crossbow", "mithril_grapple")) { item("bronze_crossbow", 400, "Inside the crate you find a bronze crossbow and a grappling hook.") diff --git a/game/src/main/kotlin/content/bot/skill/fishing/FishingBot.kt b/game/src/main/kotlin/content/bot/skill/fishing/FishingBot.kt index 941a79ae53..4b23c51b55 100644 --- a/game/src/main/kotlin/content/bot/skill/fishing/FishingBot.kt +++ b/game/src/main/kotlin/content/bot/skill/fishing/FishingBot.kt @@ -23,7 +23,7 @@ import world.gregs.voidps.engine.entity.character.npc.NPCs import world.gregs.voidps.engine.entity.character.player.skill.Skill import world.gregs.voidps.engine.entity.character.player.skill.level.Level.has import world.gregs.voidps.engine.entity.distanceTo -import world.gregs.voidps.engine.inv.holdsItem +import world.gregs.voidps.engine.inv.carriesItem import world.gregs.voidps.engine.inv.inventory import world.gregs.voidps.network.client.instruction.InteractNPC @@ -70,7 +70,7 @@ class FishingBot( suspend fun Bot.fish(map: AreaDefinition, option: String, bait: String, set: GearDefinition) { setupGear(set) goToArea(map) - while (player.inventory.spaces > 0 && (bait == "none" || player.holdsItem(bait))) { + while (player.inventory.spaces > 0 && (bait == "none" || player.carriesItem(bait))) { val spots = NPCs .filter { isAvailableSpot(map, it, option, bait) } .map { it to tile.distanceTo(it) } diff --git a/game/src/main/kotlin/content/entity/npc/Sheep.kt b/game/src/main/kotlin/content/entity/npc/Sheep.kt index c3c7ed82fa..5f6ec4ed35 100644 --- a/game/src/main/kotlin/content/entity/npc/Sheep.kt +++ b/game/src/main/kotlin/content/entity/npc/Sheep.kt @@ -15,7 +15,7 @@ import world.gregs.voidps.engine.entity.character.npc.NPC import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.entity.item.floor.FloorItems import world.gregs.voidps.engine.inv.add -import world.gregs.voidps.engine.inv.holdsItem +import world.gregs.voidps.engine.inv.carriesItem import world.gregs.voidps.engine.inv.inventory import world.gregs.voidps.engine.queue.softQueue import world.gregs.voidps.engine.timer.Timer @@ -64,7 +64,7 @@ class Sheep : Script { } private suspend fun Player.shear(target: NPC, colour: String) { - if (!holdsItem("shears")) { + if (!carriesItem("shears")) { message("You need a set of shears to do this.") return } diff --git a/game/src/main/kotlin/content/entity/obj/MilkCow.kt b/game/src/main/kotlin/content/entity/obj/MilkCow.kt index 8e335ade62..400f0cc7ea 100644 --- a/game/src/main/kotlin/content/entity/obj/MilkCow.kt +++ b/game/src/main/kotlin/content/entity/obj/MilkCow.kt @@ -11,7 +11,7 @@ import content.quest.quest import world.gregs.voidps.engine.Script import world.gregs.voidps.engine.client.message import world.gregs.voidps.engine.entity.character.sound -import world.gregs.voidps.engine.inv.holdsItem +import world.gregs.voidps.engine.inv.carriesItem import world.gregs.voidps.engine.inv.inventory import world.gregs.voidps.engine.inv.replace @@ -19,14 +19,14 @@ class MilkCow : Script { init { objectOperate("Milk", "prized_dairy_cow") { - if (!holdsItem("bucket")) { + if (!carriesItem("bucket")) { message("You'll need an empty bucket to collect the milk.") return@objectOperate } if (quest("cooks_assistant") != "started") { statement("If you're after ordinary milk, you should use an ordinary dairy cow.") } - if (holdsItem("top_quality_milk") || bank.contains("top_quality_milk")) { + if (carriesItem("top_quality_milk") || bank.contains("top_quality_milk")) { message("You've already got some top-quality milk; you should take it to the cook.") return@objectOperate } @@ -38,7 +38,7 @@ class MilkCow : Script { } objectOperate("Milk", "dairy_cow") { - if (holdsItem("bucket")) { + if (carriesItem("bucket")) { anim("milk_cow") sound("milk_cow") delay(5) diff --git a/game/src/main/kotlin/content/entity/obj/Mill.kt b/game/src/main/kotlin/content/entity/obj/Mill.kt index da5437971e..248d108f88 100644 --- a/game/src/main/kotlin/content/entity/obj/Mill.kt +++ b/game/src/main/kotlin/content/entity/obj/Mill.kt @@ -51,7 +51,7 @@ class Mill : Script { player("Hmm. I should probably ask that lady downstairs how I can make extra fine flour.") return@itemOnObjectOperate } - if (holdsItem("extra_fine_flour")) { + if (carriesItem("extra_fine_flour")) { message("It'd be best to take the extra fine flour you already have to the cook first.") return@itemOnObjectOperate } @@ -70,13 +70,13 @@ class Mill : Script { } objectOperate("Take-flour", "flour_bin") { - if (!holdsItem("empty_pot")) { + if (!carriesItem("empty_pot")) { message("You need an empty pot to hold the flour in.") return@objectOperate } if (quest("cooks_assistant") == "started" && get("cooks_assistant_talked_to_millie", 0) == 1) { inventory.remove("empty_pot") - if (holdsItem("extra_fine_flour") || bank.contains("extra_fine_flour")) { + if (carriesItem("extra_fine_flour") || bank.contains("extra_fine_flour")) { inventory.add("pot_of_flour") message("You fill a pot with flour from the bin.") } else { diff --git a/game/src/main/kotlin/content/entity/player/bank/Bank.kt b/game/src/main/kotlin/content/entity/player/bank/Bank.kt index 0249ff3d72..ea2b2ef16c 100644 --- a/game/src/main/kotlin/content/entity/player/bank/Bank.kt +++ b/game/src/main/kotlin/content/entity/player/bank/Bank.kt @@ -4,7 +4,7 @@ import world.gregs.voidps.engine.data.definition.ItemDefinitions import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.entity.item.Item import world.gregs.voidps.engine.inv.Inventory -import world.gregs.voidps.engine.inv.holdsItem +import world.gregs.voidps.engine.inv.carriesItem object Bank { private const val TAB_COUNT = 8 @@ -55,9 +55,9 @@ object Bank { val Player.bank: Inventory get() = inventories.inventory("bank") -fun Player.ownsItem(id: String) = holdsItem(id) || bank.contains(id) +fun Player.ownsItem(id: String) = carriesItem(id) || bank.contains(id) -fun Player.ownsItem(id: String, amount: Int) = holdsItem(id, amount) || bank.contains(id, amount) +fun Player.ownsItem(id: String, amount: Int) = carriesItem(id, amount) || bank.contains(id, amount) val Item.isNote: Boolean get() = def.notedTemplateId != -1 diff --git a/game/src/main/kotlin/content/quest/free/cooks_assistant/CooksAssistant.kt b/game/src/main/kotlin/content/quest/free/cooks_assistant/CooksAssistant.kt index cf236f8185..39c2e90563 100644 --- a/game/src/main/kotlin/content/quest/free/cooks_assistant/CooksAssistant.kt +++ b/game/src/main/kotlin/content/quest/free/cooks_assistant/CooksAssistant.kt @@ -4,7 +4,7 @@ import content.entity.player.bank.bank import content.quest.quest import content.quest.questJournal import world.gregs.voidps.engine.Script -import world.gregs.voidps.engine.inv.holdsItem +import world.gregs.voidps.engine.inv.carriesItem class CooksAssistant : Script { @@ -32,7 +32,7 @@ class CooksAssistant : Script { ) if (get("cooks_assistant_milk", 0) == 1) { list.add("I have given the cook a bucket of top-quality milk.") - } else if (holdsItem("top_quality_milk")) { + } else if (carriesItem("top_quality_milk")) { list.add("I have found a bucket of top-quality milk to give to the cook.") } else if (bank.contains("top_quality_milk")) { list.add("I have a bucket of top-quality milk to give to the cook. it's in my bank.") @@ -42,7 +42,7 @@ class CooksAssistant : Script { if (get("cooks_assistant_flour", 0) == 1) { list.add("I have given the cook a pot of extra fine flour.") - } else if (holdsItem("extra_fine_flour")) { + } else if (carriesItem("extra_fine_flour")) { list.add("I have found a pot of extra fine flour to give to the cook.") } else if (bank.contains("extra_fine_flour")) { list.add("I have a pot of extra fine flour to give to the cook. it's in my bank.") @@ -52,7 +52,7 @@ class CooksAssistant : Script { if (get("cooks_assistant_egg", 0) == 1) { list.add("I have given the cook a super large egg.") - } else if (holdsItem("super_large_egg")) { + } else if (carriesItem("super_large_egg")) { list.add("I have found a super large egg to give to the cook.") } else if (bank.contains("super_large_egg")) { list.add("I have a super large egg to give to the cook. it's in my bank.") diff --git a/game/src/main/kotlin/content/quest/free/gunnars_ground/GunnarsGround.kt b/game/src/main/kotlin/content/quest/free/gunnars_ground/GunnarsGround.kt index aa3c8f740d..979054ba8e 100644 --- a/game/src/main/kotlin/content/quest/free/gunnars_ground/GunnarsGround.kt +++ b/game/src/main/kotlin/content/quest/free/gunnars_ground/GunnarsGround.kt @@ -4,7 +4,7 @@ import content.quest.letterScroll import content.quest.quest import content.quest.questJournal import world.gregs.voidps.engine.Script -import world.gregs.voidps.engine.inv.holdsItem +import world.gregs.voidps.engine.inv.carriesItem class GunnarsGround : Script { @@ -285,7 +285,7 @@ class GunnarsGround : Script { "Gold ring he Specifically wants a ring from Jeffery in Edgeville.", "Items I need:", ) - if (holdsItem("ring_from_jeffery")) { + if (carriesItem("ring_from_jeffery")) { list.add("Ring from Jeffery") } else { list.add("Ring from Jeffery") diff --git a/game/src/main/kotlin/content/quest/free/rune_mysteries/RuneMysteries.kt b/game/src/main/kotlin/content/quest/free/rune_mysteries/RuneMysteries.kt index 12a095b207..8ef7166590 100644 --- a/game/src/main/kotlin/content/quest/free/rune_mysteries/RuneMysteries.kt +++ b/game/src/main/kotlin/content/quest/free/rune_mysteries/RuneMysteries.kt @@ -3,7 +3,7 @@ package content.quest.free.rune_mysteries import content.quest.quest import content.quest.questJournal import world.gregs.voidps.engine.Script -import world.gregs.voidps.engine.inv.holdsItem +import world.gregs.voidps.engine.inv.carriesItem class RuneMysteries : Script { @@ -41,7 +41,7 @@ class RuneMysteries : Script { "Lumbridge, across the bridge from Draynor Village.", ) - if (!holdsItem("air_talisman")) { + if (!carriesItem("air_talisman")) { list.add("If I lose the Strange Talisman , I'll need to ask Duke Horacio for") list.add("another.") } @@ -72,7 +72,7 @@ class RuneMysteries : Script { "Runecrafting. I can find him in his Rune Shop in south east", "Varrock.", ) - if (!holdsItem("research_package_rune_mysteries")) { + if (!carriesItem("research_package_rune_mysteries")) { list.add("If I lose the Package , I'll need to ask Sedridor for") list.add("another.") } @@ -110,7 +110,7 @@ class RuneMysteries : Script { "and asked me to take some Research Notes . back to him. I", "can find Sedridor in the basement of the Wizards' Tower.", ) - if (!holdsItem("research_notes_rune_mysteries")) { + if (!carriesItem("research_notes_rune_mysteries")) { list.add("If I lose the Research Notes I'll need to ask Aubury for") list.add("some more.") } diff --git a/game/src/main/kotlin/content/quest/free/the_knights_sword/TheKnightsSword.kt b/game/src/main/kotlin/content/quest/free/the_knights_sword/TheKnightsSword.kt index a61f9f0168..b7a973516a 100644 --- a/game/src/main/kotlin/content/quest/free/the_knights_sword/TheKnightsSword.kt +++ b/game/src/main/kotlin/content/quest/free/the_knights_sword/TheKnightsSword.kt @@ -4,7 +4,7 @@ import content.entity.player.bank.ownsItem import content.quest.quest import content.quest.questJournal import world.gregs.voidps.engine.Script -import world.gregs.voidps.engine.inv.holdsItem +import world.gregs.voidps.engine.inv.carriesItem class TheKnightsSword : Script { @@ -59,7 +59,7 @@ class TheKnightsSword : Script { "until I gave him a Redberry pie, which he gobbled up.", "Thurgo needed a picture of the sword to replace.", ) - if (holdsItem("portrait") || ownsItem("portrait")) { + if (carriesItem("portrait") || ownsItem("portrait")) { list.add("I now have a picture of the Knight's Sword - I should take it") list.add("to Thurgo so that he can duplicate it.") } else { @@ -79,7 +79,7 @@ class TheKnightsSword : Script { "start work on a replacement. I took him a portrait of it.", ) - if (holdsItem("blurite_sword") || ownsItem("blurite_sword")) { + if (carriesItem("blurite_sword") || ownsItem("blurite_sword")) { list.add("Thurgo has now smithed me a replica of Sir Vyvin's sword.") list.add("") list.add("I should return it to the Squire for my reward.") diff --git a/game/src/main/kotlin/content/quest/free/the_restless_ghost/FatherUrhney.kt b/game/src/main/kotlin/content/quest/free/the_restless_ghost/FatherUrhney.kt index 80671a0587..15e4581db7 100644 --- a/game/src/main/kotlin/content/quest/free/the_restless_ghost/FatherUrhney.kt +++ b/game/src/main/kotlin/content/quest/free/the_restless_ghost/FatherUrhney.kt @@ -9,7 +9,7 @@ import world.gregs.voidps.engine.client.message import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.entity.item.floor.FloorItems import world.gregs.voidps.engine.inv.add -import world.gregs.voidps.engine.inv.holdsItem +import world.gregs.voidps.engine.inv.carriesItem import world.gregs.voidps.engine.inv.inventory class FatherUrhney : Script { @@ -42,7 +42,7 @@ class FatherUrhney : Script { if (stage == "ghost" || stage == "mining_spot" || stage == "found_skull" || stage == "completed") { option("I've lost the Amulet of Ghostspeak.") { statement("Father Urhney sighs.") - if (holdsItem("ghostspeak_amulet")) { + if (carriesItem("ghostspeak_amulet")) { npc("What are you talking about? I can see you've got it with you!") return@option } diff --git a/game/src/main/kotlin/content/quest/member/plague_city/PlagueCity.kt b/game/src/main/kotlin/content/quest/member/plague_city/PlagueCity.kt index f276e06096..97fd19c42c 100644 --- a/game/src/main/kotlin/content/quest/member/plague_city/PlagueCity.kt +++ b/game/src/main/kotlin/content/quest/member/plague_city/PlagueCity.kt @@ -14,7 +14,7 @@ import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.entity.character.player.chat.noInterest import world.gregs.voidps.engine.entity.character.player.equip.equipped import world.gregs.voidps.engine.entity.character.sound -import world.gregs.voidps.engine.inv.holdsItem +import world.gregs.voidps.engine.inv.carriesItem import world.gregs.voidps.engine.inv.inventory import world.gregs.voidps.engine.inv.remove import world.gregs.voidps.engine.queue.queue @@ -53,7 +53,7 @@ class PlagueCity : Script { "Edmond told me that his wife, Alrena, can make me a gas", "Mask to protect myself from the plague.", ) - if (holdsItem("dwellberries")) { + if (carriesItem("dwellberries")) { list.add("I need to get some Dwellberries for Alrena so she can make") list.add("me a Gas Mask to protect myself from the Plague. According") list.add("to Edmond, I can find some in McGrubor's Wood, west of") @@ -200,7 +200,7 @@ class PlagueCity : Script { list.add("I entered West Ardougne and found Jethick, an old friend") list.add("of Edmond. He seemed willing to help me find Elena but") list.add("didn't know what she looked like.") - if (holdsItem("picture_plague_city")) { + if (carriesItem("picture_plague_city")) { list.add("I have a picture of her which might help. I should show it to Jethick.") } } else { @@ -235,13 +235,13 @@ class PlagueCity : Script { list.add("Rehnison Family. According to him, they live in a timber") list.add("house in the north of the city. He asked me to return a") list.add("book to them while I was there.") - if (!holdsItem("book_turnip_growing_for_beginners")) { + if (!carriesItem("book_turnip_growing_for_beginners")) { list.add("but I don't have it with me.") } } else { list.add("I entered West Ardougne and found Jethick, an old friend of") list.add("Edmond. He seemed willing to help me find Elena but didn't") - if (holdsItem("picture_plague_city")) { + if (carriesItem("picture_plague_city")) { list.add("know what she looked like. I have a picture of her which might") list.add("help. I should show it to Jethick.") } else { diff --git a/game/src/main/kotlin/content/skill/constitution/drink/Potions.kt b/game/src/main/kotlin/content/skill/constitution/drink/Potions.kt index 65f2633c31..ad8a7b8562 100644 --- a/game/src/main/kotlin/content/skill/constitution/drink/Potions.kt +++ b/game/src/main/kotlin/content/skill/constitution/drink/Potions.kt @@ -15,7 +15,7 @@ import world.gregs.voidps.engine.client.ui.chat.plural import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.entity.character.player.equip.equipped import world.gregs.voidps.engine.entity.character.player.skill.Skill -import world.gregs.voidps.engine.inv.holdsItem +import world.gregs.voidps.engine.inv.carriesItem import world.gregs.voidps.engine.inv.inventory import world.gregs.voidps.engine.inv.remove import world.gregs.voidps.engine.timer.toTicks @@ -41,7 +41,7 @@ class Potions : Script { } } - fun Player.hasHolyItem() = equipped(EquipSlot.Cape).id.startsWith("prayer_cape") || holdsItem("holy_wrench") + fun Player.hasHolyItem() = equipped(EquipSlot.Cape).id.startsWith("prayer_cape") || carriesItem("holy_wrench") fun Player.effects(potion: String) { when { diff --git a/game/src/main/kotlin/content/skill/crafting/SilverCasting.kt b/game/src/main/kotlin/content/skill/crafting/SilverCasting.kt index 4d1aa24816..80c15b079b 100644 --- a/game/src/main/kotlin/content/skill/crafting/SilverCasting.kt +++ b/game/src/main/kotlin/content/skill/crafting/SilverCasting.kt @@ -43,12 +43,12 @@ class SilverCasting : Script { val item = silver.item val quest = silver.quest interfaces.sendVisibility(id, mould.id, quest == null || quest(quest) != "unstarted") - val has = holdsItem(mould.id) + val has = carriesItem(mould.id) interfaces.sendText( id, "${mould.id}_text", if (has) { - val colour = if (holdsItem("silver_bar")) "green" else "orange" + val colour = if (carriesItem("silver_bar")) "green" else "orange" "<$colour>Make ${ItemDefinitions.get(item).name.toTitleCase()}" } else { "You need a ${silver.name ?: mould.def.name.lowercase()} to make this item." diff --git a/game/src/main/kotlin/content/skill/farming/FarmingPatchPick.kt b/game/src/main/kotlin/content/skill/farming/FarmingPatchPick.kt index 8ca78b32f7..5e6449996a 100644 --- a/game/src/main/kotlin/content/skill/farming/FarmingPatchPick.kt +++ b/game/src/main/kotlin/content/skill/farming/FarmingPatchPick.kt @@ -17,7 +17,7 @@ import world.gregs.voidps.engine.entity.character.sound import world.gregs.voidps.engine.entity.item.Item import world.gregs.voidps.engine.entity.obj.GameObject import world.gregs.voidps.engine.inv.add -import world.gregs.voidps.engine.inv.holdsItem +import world.gregs.voidps.engine.inv.carriesItem import world.gregs.voidps.engine.inv.inventory import world.gregs.voidps.engine.queue.weakQueue @@ -128,7 +128,7 @@ class FarmingPatchPick(val variableDefinitions: VariableDefinitions) : Script { } fun saveLife(player: Player, chance: IntRange, obj: GameObject): Boolean { - if (player.holdsItem("magic_secateurs") && !obj.id.startsWith("farming_belladonna") && !obj.id.startsWith("farming_cactus") && !obj.id.startsWith("farming_mushroom") && !obj.id.startsWith("farming_fruit_tree") && !obj.id.startsWith("farming_calquat") || (obj.id.startsWith("farming_flower") && obj.def(player).stringId.startsWith("limpwurt")) && !obj.id.endsWith("_stump")) { + if (player.carriesItem("magic_secateurs") && !obj.id.startsWith("farming_belladonna") && !obj.id.startsWith("farming_cactus") && !obj.id.startsWith("farming_mushroom") && !obj.id.startsWith("farming_fruit_tree") && !obj.id.startsWith("farming_calquat") || (obj.id.startsWith("farming_flower") && obj.def(player).stringId.startsWith("limpwurt")) && !obj.id.endsWith("_stump")) { return Level.success(player.levels.get(Skill.Farming), chance.first + (chance.first / 10)..chance.last + (chance.last / 10)) } return Level.success(player.levels.get(Skill.Farming), chance) diff --git a/game/src/main/kotlin/content/skill/fishing/Fishing.kt b/game/src/main/kotlin/content/skill/fishing/Fishing.kt index b800ecc697..e7592dfa6f 100644 --- a/game/src/main/kotlin/content/skill/fishing/Fishing.kt +++ b/game/src/main/kotlin/content/skill/fishing/Fishing.kt @@ -22,7 +22,7 @@ import world.gregs.voidps.engine.entity.character.player.skill.Skill import world.gregs.voidps.engine.entity.character.player.skill.level.Level.has import world.gregs.voidps.engine.entity.character.player.skill.level.Level.success import world.gregs.voidps.engine.inv.add -import world.gregs.voidps.engine.inv.holdsItem +import world.gregs.voidps.engine.inv.carriesItem import world.gregs.voidps.engine.inv.inventory import world.gregs.voidps.engine.inv.remove import world.gregs.voidps.engine.inv.transact.TransactionError @@ -78,13 +78,13 @@ class Fishing : Script { break } - val tackle = data.tackle.firstOrNull { tackle -> player.holdsItem(tackle) } + val tackle = data.tackle.firstOrNull { tackle -> player.carriesItem(tackle) } if (tackle == null) { player.message("You need a ${data.tackle.first().toTitleCase()} to catch these fish.") break@fishing } - val bait = data.bait.keys.firstOrNull { bait -> bait == "none" || player.holdsItem(bait) } + val bait = data.bait.keys.firstOrNull { bait -> bait == "none" || player.carriesItem(bait) } val catches = data.bait[bait] if (bait == null || catches == null) { player.message("You don't have any ${data.bait.keys.first().toTitleCase().plural(2)}.") diff --git a/game/src/main/kotlin/content/skill/mining/Pickaxe.kt b/game/src/main/kotlin/content/skill/mining/Pickaxe.kt index d45ee6c31e..cae2265009 100644 --- a/game/src/main/kotlin/content/skill/mining/Pickaxe.kt +++ b/game/src/main/kotlin/content/skill/mining/Pickaxe.kt @@ -4,7 +4,7 @@ import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.entity.character.player.skill.Skill import world.gregs.voidps.engine.entity.character.player.skill.level.Level.hasRequirementsToUse import world.gregs.voidps.engine.entity.item.Item -import world.gregs.voidps.engine.inv.holdsItem +import world.gregs.voidps.engine.inv.carriesItem object Pickaxe { private val pickaxes = listOf( @@ -20,5 +20,5 @@ object Pickaxe { Item("bronze_pickaxe"), ) - fun best(player: Player): Item? = pickaxes.firstOrNull { pickaxe -> player.hasRequirementsToUse(pickaxe, skills = setOf(Skill.Mining, Skill.Firemaking)) && player.holdsItem(pickaxe.id) } + fun best(player: Player): Item? = pickaxes.firstOrNull { pickaxe -> player.hasRequirementsToUse(pickaxe, skills = setOf(Skill.Mining, Skill.Firemaking)) && player.carriesItem(pickaxe.id) } } diff --git a/game/src/main/kotlin/content/skill/runecrafting/Runecrafting.kt b/game/src/main/kotlin/content/skill/runecrafting/Runecrafting.kt index 496159f305..c34d94fcf4 100644 --- a/game/src/main/kotlin/content/skill/runecrafting/Runecrafting.kt +++ b/game/src/main/kotlin/content/skill/runecrafting/Runecrafting.kt @@ -54,11 +54,11 @@ class Runecrafting : Script { } val combination = list[0] as String val xp = list[1] as Double - if (!holdsItem("pure_essence")) { + if (!carriesItem("pure_essence")) { message("You need pure essence to bind $combination runes.") return@itemOnObjectOperate } - if (!holdsItem("${element}_talisman") && !hasClock("magic_imbue")) { + if (!carriesItem("${element}_talisman") && !hasClock("magic_imbue")) { message("You need a $element talisman to bind $combination runes.") return@itemOnObjectOperate } diff --git a/game/src/main/kotlin/content/skill/woodcutting/Hatchet.kt b/game/src/main/kotlin/content/skill/woodcutting/Hatchet.kt index 740974d58c..cda0751c94 100644 --- a/game/src/main/kotlin/content/skill/woodcutting/Hatchet.kt +++ b/game/src/main/kotlin/content/skill/woodcutting/Hatchet.kt @@ -4,7 +4,7 @@ import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.entity.character.player.skill.Skill import world.gregs.voidps.engine.entity.character.player.skill.level.Level.hasRequirementsToUse import world.gregs.voidps.engine.entity.item.Item -import world.gregs.voidps.engine.inv.holdsItem +import world.gregs.voidps.engine.inv.carriesItem object Hatchet { private val hatchets = listOf( @@ -21,7 +21,7 @@ object Hatchet { Item("bronze_hatchet"), ) - fun best(player: Player): Item? = hatchets.firstOrNull { hasRequirements(player, it) && player.holdsItem(it.id) } + fun best(player: Player): Item? = hatchets.firstOrNull { hasRequirements(player, it) && player.carriesItem(it.id) } fun hasRequirements(player: Player, hatchet: Item, message: Boolean = false): Boolean = player.hasRequirementsToUse(hatchet, message, setOf(Skill.Firemaking, Skill.Woodcutting)) } From dee82f947c8a45a4cbadc389df7057ec5d10ae2b Mon Sep 17 00:00:00 2001 From: GregHib Date: Sun, 25 Jan 2026 20:22:25 +0000 Subject: [PATCH 002/101] Some ideas --- data/bot/test.bot.toml | 357 ++++++++++++++++++ .../content/bot/skill/mining/MiningBot.kt | 56 ++- 2 files changed, 411 insertions(+), 2 deletions(-) create mode 100644 data/bot/test.bot.toml diff --git a/data/bot/test.bot.toml b/data/bot/test.bot.toml new file mode 100644 index 0000000000..f550841b97 --- /dev/null +++ b/data/bot/test.bot.toml @@ -0,0 +1,357 @@ +[has_axe] +requirements = [ + { type = "carries", preset = "axe" }, + { type = "inventory_space", amount = 20 }, +] + +[bank_all_after_full] +plan = [ + { action = "wait_for_full_inv" }, + { action = "go_to", target = "bank" }, + { action = "interact_npc", option = "Bank", id = "banker*" }, + { action = "interface", option = "Deposit-All", id = "bank:inventory" }, +] + +[regular_trees] +type = "activity" +capacity = 4 +requirements = [ + { clone = "has_axe" }, + { type = "skill", id = "woodcutting", min = 1, max = 15 }, +] +plan = [ + { action = "go_to", target = "tree_area" }, + { action = "interact_obj", option = "Chop-down", id = "tree*" }, + { clone = "bank_all_after_full" }, +] +produces = [ + { item = "logs" } +] + + + +[actions.open_varrock_bank] +requires = [ + { location = "varrock_bank" } +] +action = { option = "Open-bank", npc = "varrock_banker" } +effect = [ + { interface = "bank" } +] + +[axe_shopping] +requirements = [ + { type = "inventory_space", amount = 1 }, +] +plan = [ + { action = "go_to", target = "axe_store" }, + { action = "interact_npc", option = "Trade", id = "axe_store_owner" }, +] + +[buy_bronze_axe] +type = "resolver" +requirements = [ + { type = "skill", id = "woodcutting", min = 5 }, + { type = "holds", id = "coins", amount = 25 }, + { clone = "axe_shopping" }, +] +plan = [ + { clone = "axe_shopping" }, + { action = "interface", option = "Buy-1", id = "axe_shop:inventory" }, +] +produces = [ + { item = "bronze_axe" } +] + +[buy_iron_axe] +type = "resolver" +requirements = [ + { type = "skill", id = "woodcutting", min = 10 }, + { type = "carries", id = "coins", amount = 150 }, + { clone = "axe_shopping" }, +] +plan = [ + { clone = "axe_shopping" }, + { action = "interface", option = "Buy-1", id = "axe_shop:inventory:iron_axe" }, +] +produces = [ + { item = "iron_axe" } +] + +# equips -> equipped +# carries -> on person +# owns -> in bank or on person + +[ring_of_duelling_equipped] +type = "teleport" +weight = 10 +requirements = [ + { type = "variable", id = "spellbook", value = "normal" }, + { type = "equips", id = "ring_of_dueling_*" } +] +plan = [ + { action = "interface", option = "Castle Wars", id = "equipment:inventory" }, +] +produces = [ + { location = "castle_wars_teleport" } +] + +[ring_of_duelling] +type = "teleport" +weight = 10 +requirements = [ + { type = "variable", id = "spellbook", value = "normal" }, + { type = "in_inventory", id = "ring_of_dueling_*" } +] +plan = [ + { action = "interface", option = "Rub", id = "inventory:inventory" }, + { action = "interface", option = "Select", id = "choice:line1" }, +] +produces = [ + { location = "castle_wars_teleport" } +] + + +[teleport_varrock] +type = "teleport" +weight = 10 +requirements = [ + { type = "variable", id = "spellbook", value = "normal" }, + { type = "carries", id = "fire_rune", amount = 1 }, + { type = "carries", id = "air_rune", amount = 3 }, + { type = "carries", id = "law_rune", amount = 1 }, +] +plan = [ + { action = "interface", option = "Cast", id = "normal_spellbook:varrock_teleport" }, +] +produces = [ + { location = "varrock_teleport" } +] + +[teleport_lumbridge] +type = "teleport" +weight = 0 +requirements = [ + { type = "variable", id = "spellbook", value = "normal" }, + { type = "variable", id = "lumbridge_cooldown", value = "normal" }, +] +plan = [ + { action = "interface", option = "Cast", id = "normal_spellbook:home_teleport" }, +] +produces = [ + { location = "lumbridge_teleport" } +] + +[go_to_bank] +req = ["inventory_full"] +action = { type = "go_to", target = "bank" } +context = "banking" + +[bank_all] +req = ["in_bank", "items_in_inventory"] +action = { type = "interact_npc", option = "Open bank", target = "banker*" } +context = "bank_open" + +[withdraw_coins] +req = ["coins_in_bank", "bank_open"] +action = { type = "interface", option = "Withdraw-100", target = "coins" } +context = "shopping" + +[go_to_axe_shop] +req = ["shopping", "1_inventory_space"] +context = "axe_shopping" + +[open_axe_shop] +req = ["axe_shopping"] +action = { type = "interact_npc", option = "Trade", target = "axe_shopkeeper" } +context = "axe_shop" + +[buy_axe_shop] +req = ["axe_shop"] +action = { type = "interact_npc", option = "Buy", target = "iron_axe" } +context = "axe" + +[buy_pickaxe_shop] +req = ["axe_shop"] +action = { type = "interact_npc", option = "Buy", target = "iron_pickaxe" } +context = "pickaxe" + + +[bank_deposit_items] +req = ["inventory_full"] +context = "banking" + +[bank_withdraw_items] +req = ["inventory_empty"] +context = "banking" + +[bank_withdraw_melee_gear] +req = [ + { context = "banking" }, +] +context = "melee_combat" + +[bank_withdraw_mage_gear] +req = [ + { context = "banking" }, +] +context = "magic_combat" + +[fight_cows] +req = [ + { context = "melee_combat" }, + { skill = "attack", min = 5 }, +] + +[fight_chickens] +req = [ + { context = "melee_combat" }, + { skill = "attack", min = 1 }, +] +context = "fight_chickens" + +# val context = Stack() // after complete pop and re-evaluate + +[bank_withdraw_ranged] +req = ["inventory_empty"] + + + + + + + + + + + + + +[woodcutting] +requires = { state = "idle" } +states = [ + { to = "find_tree" }, + { to = "buy_axe" }, + { to = "get_axe" }, + { to = "wait" }, +] + +[woodcutting.get_axe] +actions = [ + { go_to = "jims_farm" }, + { option = "Pick-up", item = "iron_axe" }, +] + +[woodcutting.buy_axe] +requires = [ + { carries = "coins", amount = 10 } +] +actions = [ + { go_to = "axe_store" }, + { option = "Trade", npc = "axe_store_owner" }, + { option = "Buy-1", interface = "shop:inventory:iron_axe" }, +] + +[woodcutting.find_tree] +requires = [ + { carries = "iron_axe" } +] +actions = [ + { go_to = "village_woods" }, + { option = "Chop-down", obj = "normal_tree" }, +] + + + +[walk_to_bank_trees] +actions = [ + { go_to = "nearest_bank" } +] +transitions = [ + { when = { in = "bank" }, then = "open_bank_trees" }, + { when = { timeout = 20 }, then = "idle" } +] + +[open_bank_trees] +actions = [ + { go_to = "nearest_bank" } +] +transitions = [ + { when = { in = "bank" }, then = "open_bank_logs" }, + { when = { timeout = 20 }, then = "idle" } +] + +[open_bank_logs] +actions = [ + { option = "Open-bank", npc = "banker" } +] +transitions = [ + { when = { open = "bank" }, then = "deposit_logs" }, + { when = { timeout = 20 }, then = "idle" } +] + +[go_to_trees] +transitions = [ + { when = { in = "tree_area" }, then = "find_tree" }, +] + +[find_tree] +actions = [ + { scan_for = "tree", radius = 5 } +] +transitions = [ + { when = { has_target = true }, then = "chop_tree" }, + { when = { location_not = "tree_area" }, then = "walk_to_trees" }, + { when = { timeout = 5 }, then = "idle" } +] + +[walk_to_trees] +actions = [ + { go_to = "tree_area" } +] +transitions = [ + { when = { in = "tree_area" }, then = "find_tree" }, + { when = { timeout = 20 }, then = "idle" }, +] + +[chop_tree] +actions = [ + { option = "Chop-down", entity = "target" } +] +transitions = [ + { when = { inventory_full = true }, then = "walk_to_bank_trees" }, + { when = { has_target = false }, then = "find_tree" }, + { when = { timeout = 30 }, then = "idle" } +] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/game/src/main/kotlin/content/bot/skill/mining/MiningBot.kt b/game/src/main/kotlin/content/bot/skill/mining/MiningBot.kt index d5c2310260..91548c53eb 100644 --- a/game/src/main/kotlin/content/bot/skill/mining/MiningBot.kt +++ b/game/src/main/kotlin/content/bot/skill/mining/MiningBot.kt @@ -9,6 +9,7 @@ import content.bot.skill.combat.setupGear import content.entity.death.weightedSample import net.pearx.kasechange.toLowerSpaceCase import world.gregs.voidps.engine.Script +import world.gregs.voidps.engine.client.instruction.handle.interactObject import world.gregs.voidps.engine.client.ui.chat.plural import world.gregs.voidps.engine.client.ui.chat.toIntRange import world.gregs.voidps.engine.data.definition.AreaDefinition @@ -19,7 +20,10 @@ import world.gregs.voidps.engine.entity.character.player.skill.level.Level.has import world.gregs.voidps.engine.entity.distanceTo import world.gregs.voidps.engine.entity.obj.GameObject import world.gregs.voidps.engine.inv.inventory -import world.gregs.voidps.network.client.instruction.InteractObject +import world.gregs.voidps.engine.suspend.Suspension +import kotlin.coroutines.Continuation +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine class MiningBot(val tasks: TaskManager) : Script { @@ -54,6 +58,46 @@ class MiningBot(val tasks: TaskManager) : Script { } } + interface State + + object Idle : State + object Running : State + + sealed class MiningState : State { + object MissingItem : MiningState() + object Depleted : MiningState() + object Level : MiningState() + object Success : MiningState() + object InvFull : MiningState() + } + + suspend fun wait(): T { + return suspendCoroutine { + suspensions[0] = it + } as T + } + + var Bot.state: State + get() = Idle + set(value) {} + + val states = arrayOfNulls(100) + val suspensions = arrayOfNulls>(100) + + fun tick() { + for (i in states.indices) { + when (val state = states[i]) { + null, Idle, Running -> continue + else -> { + val suspension = suspensions[i] + suspensions[i] = null + suspension?.resume(state) + } + } + } + + } + suspend fun Bot.mineRocks(map: AreaDefinition, type: String) { setupGear(Skill.Mining) goToArea(map) @@ -68,8 +112,16 @@ class MiningBot(val tasks: TaskManager) : Script { } continue } - player.instructions.send(InteractObject(rock.def.id, rock.tile.x, rock.tile.y, 1)) + state = Running + player.interactObject(rock, "Mine") await("mining") + when (wait()) { + MiningState.Depleted -> continue + MiningState.MissingItem -> setupGear(Skill.Mining) + MiningState.Level -> break // TODO cancel task + MiningState.Success -> continue + MiningState.InvFull -> mineRocks(map, type) + } } } From 6e081064206d36b64be2b970ecc347d2b00df5dd Mon Sep 17 00:00:00 2001 From: GregHib Date: Mon, 26 Jan 2026 12:40:29 +0000 Subject: [PATCH 003/101] Add 3k bot names --- data/bot/bot_names.txt | 3056 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 3056 insertions(+) create mode 100644 data/bot/bot_names.txt diff --git a/data/bot/bot_names.txt b/data/bot/bot_names.txt new file mode 100644 index 0000000000..5f07fb22fe --- /dev/null +++ b/data/bot/bot_names.txt @@ -0,0 +1,3056 @@ +Aang +AarrWoof +Abe +Abra +Abrams +AbyssAce +AccelAce +Accident +AccurateA +AceAdam +ActivePass +ActualAce +Adaki +Adam +Admiral Flag +AdmiralAce +Adorable +Aedrin +Aeon +Aerodactyl +Aether +Afiko +AfternoonA +Agent Zero +AgentZero +AgileeAce +Aidene +Aika +AimAssist +AimBotBob +AirBorne +Aisha +Aiwei +Alakazam +Alan +Alex +AlignedSkew +AllBrawn +Alles +AlligatorC +AlloAce +AlmostThere +AlosaurAce +AlpacaAl +Alpha +Alpha Wolf +AlphaTest +AlphaWolf1 +Alphamind +AlpineAce +AlwaysNot +Amak +Amber Light +Amethyst +Amit +Amon +AmplifiedA +Amy +AnacondaA +Anais +AncientMod +Anezka +Angel +Angel Wings +AngelfishA +AngoraAce +AngryAnvil +AngulatedA +AnkyloAce +AnnihilateA +AnniversarA +AnonymousA +Anselm +AntFly +Anzih +ApatosaurA +ApexHunter +Apollo +Appa +ApplePie +AprilFool +Aptitude +Aqua +Aquarius H2O +ArabianTort +Aragon +Arbok +Arby +ArcAngel +Arcane +Arcanine +Arche +ArcheoA +Archer +Archer Aim +Archer Mark +Archie +Archmother +Archpreist +Arctic +Arctic panda +ArcticAce +ArcticHippo +ArcticSeal +Ares +Argue +Aries Ram +ArnoldArno +ArowanaAce +Aroyo +Arpy +Arr +ArrowStorm +ArsonistA +Arthur +Articuno +Artillery +Arwen +Aryan +Ash +AshMaker +Ashe +Asher +AsphaltAce +Aspho +AssassinEdge +Assire +Astra +Astro +Astro Boy +Astrofire +Athena +Auckes +AudaciousA +AugustAce +Aura +Aureus +Aurora +Austin +AutoMatic +Avatar +AvenueAce +Awthi +Axe +Axe Throw +AxeMan +AxisAngel +AxleAce +Axy +Azn +Azula +Azulon +Azure Sky +Babylon +BackendBob +Bactrian +Baited +BakersDozn +Bakshi +BalanceBob +BalancedSkw +BalancedTip +Balin +Ballet +BananaBob +BananasB +Bandit Mask +Bao +Barbarian +Barbski +Bard Song +BarelyNo +Bark +Bark beetle +BaronBob +Baroness +BarraBob +BarryonixB +Bartok +BasalBill +Basalt +BaseLine99 +BashBob +BasicBob +BasiclyBob +Basilisk Eye +BaskinShrk +BasmatiB +BasssBob +BastionBoss +Battle Axe +Battlecraft +Baxter +Baylan +Bazz +BeaconBob +BeamingBob +Bearie +BeatBob +Bebop +Beedrill +BehemothBoss +Beifong +Belee +BellsHinge +Bellsprout +Bemoan +Ben +BendableStf +BengalBob +BentUnbent +BergBerger +Beri +Bernard +BerryBest +Berserker +Beta Strike +BetaBlaze +BetaTester +BettaBob +Bewail +Bian Whip +Bicker +Biervat +BigBrain +BigChungus +BigOof +Bilbo +Billy +Binary Code +BinaryBeast +BioChem +Birdie +Birthday99 +BiryaniB +BisqueBob +Black Sun +BlackBerry +BlackBird +BlackOps99 +Blade Dance +Blade Unit +BladeRunner +BlankBob +BlankFull +BlasterBob +Blastoise +Blaze +Blaze King +BlazeBoy +BlazeFury +Blessed +BlessedDmnd +BlistorBlue +BlitzKrieg +BlockheadB +Blonde +Blood Moon +Blood Wings +Blubber +Blue +Blue Falcon +BlueBarry +BlueBird99 +Blueberry +BluegillB +BluffBob +BluntBlade +Bo Staff +Bob +BobTail +BoilingBob +Bojio +BolaThrower +BoldBrian +Bolin +BoltBrian +BombSquad +Bomber Run +Bonaro +BoneheadB +BonfireB +BonkersBen +BonkersBob +Boom +BoostedBob +Boqin +Borderland +BorealBob +Boromir +Bosco +BottomBob +BoulengBoul +BoulevardB +BouncyRock +BoundUnbound +BowMaster +BowSprit +BowWow +BowfinBob +Bowie Knife +BoxTurtle +BoysenB +BrachioB +Bradley +Brady +BradyBrady +BrakeCheck +BranchBob +BrandedBob +BraveBen +BraveChickn +Brawl +BreachBob +Break +BreakerBob +BreamBob +Brebis +Breem +Breeze +BreezeBlue +Bregalad +Brick Wall +BrightDim +BrightEye +BrittleMall +Broadsword +BrokenHero +BrokenWheel +BrontoB +Bronze Medal +BrothBob +Brouhaha +BrownRice +Brutal +Brutalblaze +Buccaneer +BudgieBob +BuffPlease +BuffaloYak +Buffering +Bug Smasher +BugAbuser +BuildBob +Bulbasaur +BullFrog +BullShark +BullsEye99 +Bumble Fly +Bumi +BumpySmooth +BunBoy +BunchBanana +BunkerBeast +BunnySnail +BurmeseStar +BurnerBob +BurstBrian +BussinBob +Busy +ButteBill +Buttercup +Butterfly +Butterfree +Bydou +Byte Boss +ByteBandit +CPU King +Cacl +CadenceC +Caedes +Cafi +CaimanGator +Calculated +CalculatedL +CaliRoll +Calico +CalicoKid +CalmStorm +CamShaftC +CamelCarl +CanTorGiant +CanaryC +Cancer Crab +Candle +CannonKing +CantDie +Canteloupe +Canyon +CanyonCarl +Capricorn +Captain Hook +CaptainC +CarchCarl +CarettCaret +CarnoCarl +Carp +CarpCarl +Carpet +CaseCarl +CashmerC +Casual +CasualCarl +Cat Owl +CatNip +Caterpie +Caterpillar +CatfishC +Catgator +Cavalry Rush +Cavil +Cecil +Cele +Celebration +Celeste +Celestial +Centaur Run +CenterCore +CenteredOff +CentralC +Centurion +Cerberus Dog +Chain Whip +ChainBreaker +Chan +ChancellorC +Chansey +Chaos +ChaosOrder +CharCoal +ChargeChad +Charizard +Charmander +CharmedCurs +Charmeleon +Charmy +ChasmCharlie +ChassisC +CheatEngine +CheetahSloth +CheloChelyon +CherryChad +ChersChersi +ChiChampion +ChickenTurt +Chickpea +ChiefChad +Chikori +ChillCharli +ChillDay +ChillPill +ChillyChili +ChillyWilly +Chimera Mix +ChimeraCore +Chipette +ChopperChad +Chorab +ChowderC +Chris +Chronos +ChryChrys +Ciaran +CichlidCarl +Cicis +CinderSoul +CipherCode +Cipmunk +CircleLine +CirclingC +CircuitC +Cirtus +CitadelCore +Clanker +Clarko +ClaspedFist +ClawedCat +Claymore Hit +ClearCarl +Cleaver Chop +Clefable +Clefairy +ClementyneC +ClemmClemm +Cleric Heal +ClickHeads +ClientSide +CliffCarl +Clonas +Cloud +Cloud Nine +CloudArchC +CloudCompC +CloudNine +ClownCarl +Cloyster +ClutchKing +Cobalt Blue +CobraStrike +CockatooC +CodCarl +Code Master +CodeCrusher +ColdFeet +Collin +Colonel Rank +ColonelCarl +ColossusC +Comet Tail +CommanderC +CompileCarl +Complain +CompletelyNo +CompyCarl +ConcreteC +Conduit +ConquerorC +ConsommeC +ControlFreak +CooCooKid +CoolBeans +CoolCat +CoolDude +Copege +Copper Wire +CoreCarl +CoreyCore +Corn +CornerKing +CornishRex +Corporal Hit +Corsair Sail +CoryhtoCory +CourageC +CouscousC +CovertCore +CozyCharlie +CrackCarl +CrackerCarl +CragKing +Cranberry +CrankCase +CrappieCar +CrawlSprint +Crazy +CrazyCarl +CreatureFeat +CreviceC +Crimson Tide +CrimsonWolf +Crinfrid +CriticalC +CrocodileD +CrookedStrt +CrossHair +CrossbowKid +CrowLook +CrucialCal +CrusherC +Crutek +Cruz +Cry +CryoCrunch +Crypton +CrystalClear +CtrlAltWin +Cubone +CucumberC +Cue +CuoraCuora +CurrentCarl +Cursed Apple +CursedCharm +CurvedFlat +Cushi +Cutlass Raid +CutlassCap +CutterCole +Cuttlefish +Cyber Punk +Cyber Wolf +CyberNinja +CyberPunk +CyclemCycle +CycloneC +Cyclops Eye +Cynthia +DBAdmin +DabOnEm +Dagger Stab +DaggerDuke +Dagonet +DakotaRaptr +DaleDan +Dalton +Dalum +DamnedBlss +Dampen +Dandelion +Dando +DangerSafe +Dans +Dao Saber +Darae +DaringDave +Dark +Dark Viper +DarkLight99 +DarkOps +DarkPhoenix +Darkness +Darth +DashDot +Data +Data Miner +DataDemon +DateDave +Davina +Dawn +DawnDan +DayOff +DayOneD +DazzlingD +Dead Eye +DeadAlive +DeadCenter +Dean +Death Star +DeathDealer +Debate +DebugDan +DecemberD +Decimation +DecimatorD +DeepDan +DefaultDan +DefenderDX +Definitely5 +DeinoDemo +DeiroDeirc +DellDave +Delos +Delta Force +DeltaForce +Delusion +Demeter +DemoDay +DemoDisc +DemoExpert +DemolisherD +Demolition +Demon +Demon Spawn +Deni +DenseDave +DeployDave +DepthDave +Deputy Badge +DermoDermo +Dermoth +Desafe +DesertTort +DesktopDan +Destroyed +DestroyerD +DesyncDave +Detective Q +DevOpsDave +DevonRex +DewItNow +Dewgong +Diamond +Diamond Mind +DicerDan +Digi Mind +DigitalDoom +Diglett +DimBright +DimDave +DinosaurD +DinosaurKom +DiplodocusD +DipsoDipso +Dire +DirectDan +Disease +Disha +Dispute +DisscusDan +DitchDave +Ditto +Divine +DivineMortal +Dizzee +Dodrio +Doduo +DolphinDan +DominatorD +Donza +Doorman +DopeDan +DopeyDan +DorritosDew +DotDash +DoubtfulSure +Dove +DoveDan +DownUp99 +DraculaDrac +Dragon Fury +Dragon Moose +DragonFire +DragonIgua +Dragonair +Dragonite +Drakon +Dratini +Draugy +Dread +Dream +Dream Killer +Dreazd +Drewen +DriftKing +Drifter +DriveShaft +Droid +Dromedary +Drowzee +Druid Tree +DryWater +Dryme +Dubsi +Duck +DuckbillDan +Duelist Draw +Dugtrio +DukeDan +DullDan +DullShiny +DuskDave +Dust Devil +DustUp +Duva +Dwarf Mine +Dye +DyingLive +Dynamo +DynamoStatic +Dyns +EagleEye99 +EarlEd +EarlyAcces +EarlyBird +Earth +EasyEd +EasygoingE +Eceth +Echidna +Echo +Eclipse Day +EdgeNode +Edgecrusher +EdmontoE +EdmontoniE +Eeli +Eevee +Eggman +EgyptianEgy +EightEric +EightSix +EightTen +Eiko +Eimis +Ekans +ElbowEd +ElbowLeech +Elder +ElderBerry +Electabuzz +ElectricE +Electrode +Elendil +ElephantKoi +ElevenNine +Elf Arrow +Eligor +EliteEdge +Ella +Ellie +Elliot +Ellunes +ElongatdEl +Elrond +EmbEmydid +Ember +EmberEyes +Emerald +Emerald Eye +Emoer +Emote +Empha +EmptyEd +EmptyFilled +EmptySpark +Enaka +EndGame99 +EnergeticL +EnergizedE +Engineer Fix +Enigma +EnigmaEdge +Envy +Eomund +Eothain +Eowyn +EpsilonE +EradicatorE +Erasold +Erehk +Erste +Erulasteil +EscarpmentE +Eskem +Espeon +EssenceEd +EssentialE +Eternal +Ethereal +Eural +Evan +EvenUneven +EveningEd +Evu +ExactEd +ExecutorE +Exeggcute +Exeggutor +ExploitLord +ExplosiveE +EzClap +Ezpz +Ezra +F2PFrank +Fabi +Fade +Fairfax +Faith +FakeTruth +Falas +FalconSight +Fang +Fantm +Farfetch'd +Farid +Farmer Plow +FastFred +FastLane +FastSloth +Fathey +Faun +FearlessFred +Fearow +FebruaryF +FeebleMighty +FeelsBad +Felicia +Felix +Fencer Parry +Feran +FettucineF +FieldFare +FieryIce +FiestaF +FigFrank +Fighter Jet +FighterSwing +FilledEmpty +FinalBoss +Finally +Finch +Fire +Fire Storm +Fire ant +FireStarter +Firefly +FirmFloppy +FirmWobly +FirstClass +Fisher Hook +Fisherman +Fishie +Fishopotamus +FissureF +FiveFinn +FiveSeven +FiveThree +FlaatbackF +Flail Spin +FlailFury +Flakey +Flame +FlameKnight +Flamestar +FlareFrank +Flareon +Flash Bang +FlashFast +FlashFred +FlashPoint +FlatCurved +FlatRough +Flem +FlexiRigid +FlexibleRig +FloorFrank +FloppyFirm +FluffyFinn +Flury +FlyFrank +Flying Bison +FocusFrank +Fog Horn +FogCompF +FoolFrank +FootLong +Foox +ForcefulWeek +ForestFinn +Forie +ForstensF +FortressF +FortuneUnf +FosseFrank +FounderFred +FourFred +FourSix +FourTwo +FoxhoundFox +FpsCounter +Fracas +FragilTough +Frame +FrameDrop +Frank +FrankFred +FrankFurter +Frankenstein +Frankie +Fraser +FreeToPlay +FreeTrapped +FreebieF +FreewayF +FreezeFrame +FreshPrince +FridayFun +FriedRice +Friuti +Frodo +FrogLizard +FrogTurtle +FrontendF +Frost +Frost Bite +FrostBite +Frostbite +Frosty +FrostyFeet +FrozenFood +Fukiya Dart +FullBlank +FullStackF +FullTower +FullVoid +FundamentalF +FunnelCloud +FurBall +FurryFred +FuturePast +Fuzzy +GPU Beast +GachaGame +Gaia +Gainey +Galadreil +Galaxy Map +GalaxyBrain +GaleForce +Galid +GamePadGary +GamePreview +Gamma +Gamma Ray +GammaGhost +GamonGary +Gandalf +GapGary +Gar +GarGary +Gardeni +Garry +Gaspar +Gastly +GaugeGary +Gaur +Gaze +GearHead +GeckoSnake +Geist +Gemini Twin +General Star +GeneralG +Gengar +Genna +GeoGeoemy +GeocGeoche +Geodude +Geoff +Gerad +Geralt +GetGood +GetRekt +Gez +GharialCaim +Ghazan +Ghost Rider +Ghost spider +GhostOps +GhostReaper +GhostToast +Ghoul Feast +GhoulLiving +Giant Step +Gibbery +GigaGary +Gilbo +Gilly +Gimli +Giselle +GitGud +Gladiator +Glaive Sweep +GlareGary +Glass Canon +GleamTeam +GlenGary +GlitchKing +GlitterGuy +Gloom +GlossyMatte +GlowingGus +Gloz +GnarlyGary +Goat +Goblin Raid +GodMode99 +Golbat +Gold +Gold Rush +GoldFinch +Goldeen +GoldfishG +Golduck +Golem +GoliathGrim +Gollum +Gong +GoodFight +GoodGame99 +GoofyGary +GooseBerry +GophGopher +GopherTort +GorgeGus +GourdGary +GrandGary +GrapGrapt +Grape +GrapeGary +Graveler +Graves +GreatWhite +GreekGreek +Green +Green Arrow +GreenSea +GrenadeGuy +Grenadier +Grey Talon +GridGary +Gridley +Griffin Wing +GriffinGuard +Grimer +Gripe +Groan +GroundZero +Grounder +Grouse +GrouseGary +Growl +Growlithe +Grumble +Grunt +GuanacoGus +Guandao Spin +GuardianGod +GulchGreg +GullyGabe +GumboGary +Gun Club +GunSlinger +Gunbon +Gunner Aim +Gunslinger +Gupples +GuppyGary +Gus +GustGary +Gustav +GutsynGary +Guzma +Gwynrael +Gyarados +Gyatso +Hack Smith +HackerHal +HaddockH +Hades +HadrosaurH +Haggard +Hail Mary +Halberd Chop +HalberdHero +HalfWay +HalibutHal +Halp +Ham Slice +Hama +Hammer Time +HammerHead +HammerTime +Hampe +Hana +Hanbo Short +Hano +Hanson +HappySorrow +HardHardy +HardScope +HardSoft +HardlyYes +HareTurtle +Harmony +HatingLove +Haunter +Hawk +Hawk Eye +HawkVision +Hawkmoth +HawksbillH +Hawky +HaxorMan +Haze +Hazi +HeadHunter +HeartHank +HeartHarry +Heaven +HeavyFeather +Helicaze +Hellz +Hera +HereThere +HermanHerm +Hero +HeroZero +Hex Editor +HexHavoc +Hexen +Hexo +Hidden King +HiddenNeck +HierHiero +HighPingGod +HighlandH +HighwayH +HillHank +Hippo cow +Hippy +Hitmonchan +Hitmonlee +Hog monkey +Holiday +Holliday +HolyAxe +HolyBow +HolyCross +HolyDagger +HolyGrail +HolyMace +HolyShield +HolySword +HolyUnholy +HomHomopus +HomHompous +HomHomscutm +HomoHomo +HomopHomo2 +HomopusHomo +HonestHank +HoneyDew99 +HootnannyH +Hopium +Hoplite Wall +Horsea +HorsfieldsH +HotDog99 +HotHank +Hotkeys +Houndmaster +HourZero +Howard +Howl +Hubbub +HuionHank +Hullabaloo +HumanPlayer +HumpDay +Hunter Trap +HunterHank +Huntsman +HurricaneH +Hydra Head +HydraHead +HyperDrive +HyperHit +Hypno +IAmLegend +IballEye +Ice +Ice Dragon +IceBerg +IceCold +IceCold99 +IceCube +IceStorm +Iceberg +IgnitePro +Igor +IguanaDrgn +IguanadonI +IllMoves +Ilona +Immola +ImmortalDie +ImmortalMan +ImperialI +ImpotentPot +ImprssImprs +InOut99 +IncognitoI +IndiIndote +IndianStar +Infantry +InfernoIvan +Infernus +InputLag +InsaneIan +Inspector G +InstantIan +InvaderIvan +InvincibleI +Iroh +Iron Fist +Iron Will +IronFist +IronJaw +IronSky +Isacc +Isaknda +Iscan +Isidor +Isildur +IslandGiant +Ivy +Ivysaur +Izumi +JackJones +Jackato +Jacob +Jade Stone +Jaguar +Jaii +Jake +Jamboree +Jan +JanuaryJim +JasmineJ +Jawn +Jaws +Jay +JayBird +Jayce +Jaylen +Jayson +Jayvon +Jebaited +Jeffe +Jeimzu +Jennica +Jenny +Jere +Jeremie +Jeremy +Jerome +Jerry +Jessica +Jessie +JesterJack +JetStream +Jetsun +Jezebel +Jezura +Jian Sword +Jigglypuff +Jimmerik +Jinora +JitterBug +Jo Stick +Joan +Joao +Jobless +Joe +Joffrey +JoggingWalk +JokerJim +JoltJohn +Jolteon +Jon +Jonathan +Jord +Joris +Jos +Jose +JoyStickJoe +Judas +JuggernautJ +Juked +JulyJim +JumpySeat +JuneBug +JuneJay +JungleJim +Juntas +JustAbout +Jutte Guard +Jynx +Kabuto +Kabutops +Kadabra +Kadett +Kakuna +Kaladin +Kalten +Kalyt +Kama Hook +KanbanKen +Kangaskhan +Kappa +Kappa123 +KappaCrush +Karl +Karma +Karmy +Kat +Katana Slash +KatanaKing +Katara +KataraMai +Kate +Kathos +Katrin +Katya +KeenEye +Kelvin +KempRidley +Ken +Kennet +Kenny +Kerboros +Kerfuffle +KetchupKid +KeyKevin +KeyboardCat +Khamul +Khandro +Kickback +Kierzo +Killaz +Killer Whale +KillerKurt +Killzone +KindaSort +KindleKid +KingCool +KingKurt +KingKyle +Kingler +Kingly +Kinn +KinyKinyxs +Kirito +KittyKat +Kjell +Klas +KleinmanK +Klop +KnifeMaster +Knight +Knight Guard +KnightKen +KnollKen +Knor +Knuckles +Koalaotter +Koffing +KoiKen +Kolik +Kolton +Komfortable +KomodoDrag +Koray +Korra +Kort +Krabby +Kraken Deep +KrakenKill +Krieg +Krill +Krox +Kube Node +Kukri Slice +Kunai Throw +Kurtz +Kusarigama +Kyle +Kyoketsu +Kyoshi +Kyros +Kyurem +Kyza +LUL +Lady Geist +Lady Zodiac +LadyLiberty +LadyVictoria +LagSwitch +LaggingLou +LaidBackLou +LambdaLion +Lambert +Lament +LampeoL +Lance Charge +LanceLord +Lane +LaneLord +LapTopLuke +Lapras +LargeOof +Larry +Lash +Laskari +Lasota +LastStand +Lathow +Latty +LaunchDay +Layla +LazyLuke +LeafyGreen +LeatherBack +Leen +LeftRight99 +Legacy +Legionnaire +Legions +Legolas +Leito +Leo Lion +Leon +LeopardLeo +Leorex +LepidLepid +LethargeticE +Letho +LevelLuke +LevelUnlevl +LeviathanL +Liam +Libra Scale +Lickitung +Light +LightDark99 +LightningL +Lily +LimpStiff +LineCircle +LinguineL +Linus +Lionfish +LiteralLee +Liva +LivingDead +LizardBeetle +LizardToad +LlamaLuke +LoadingBar +Lob +Lockhart +LocoLou +LoggerHead +LogicLord +Loki +Lone Wolf +LonewolfLeo +Long Feng +LongHair +Longsword +LooseTight +LootBoxLou +LooterLuke +LopsidedSqr +LordLuke +Loredo +LostWhisper +Lothe +LoudMouse +Louis +LoveBird +LovingHate +LowFpsLord +LowerLevel +LowlandLou +Lucifer +LuckShot +LuckyThirtn +LuckyUnluck +Luiqis +Luiz +Luke +Lukey +LuminousL +Luna +Lunar Phase +Lynx Titan +Lyric +Macao +MacaroniM +MacawMax +Macaws +Mace Swing +MaceKnight +MachSpeed +Machamp +Machete Hack +Machoke +Machop +MackerelM +MadMike +Maddie +MadnessSane +Maelstrom +Maffers +Mage Fool +Magenta Mist +Magicnite +Magikarp +Magmar +Magnemite +Magnetic +Magneton +MaiMaia +Maikeru +MainCharac +MainMike +MaineCoon +Majke +Major Order +MajorMike +Makav +MakiMike +Mako +MakoMax +MalaMalacho +MalaMalacl +Malget +MalleableU +MambaBlack +MandarinM +Mankey +Manks +ManoManour +ManouMano +ManrikiChain +Mantis +ManualMax +ManxMax +MapTurtle +Marauder +MarauderM +MarchMad +Marcus +MargMargin +MarginatdM +Margot +Maria +MarkmanMax +Marksman Hit +Marowak +Marshal Law +MarshalM +MasterMax +MataMata +Mathas +Mathcore +Matoya +Matrix Neo +MatrixMage +MatteGlossy +Matthew +MaureMaure +Maverick +Max Power +MaxChill +MaxLevel99 +MayFlower +MaybeSo +MaybeYes +McCaine +McGinnis +MeadowM +MeasureM +Medic Bag +Medusa Gaze +Mega +MegaOof +MegaPunch +MegalodonM +MelanMelano +Melc +Melee +Melissa +MellowMike +Melo +Melon +MelonMan +Mendoza +MeowMix +Meowster +Meowth +MerchantGold +Merry +MesaMax +MeshMike +MetaStrike +Metal Head +Metapod +Meteor Hit +Meteor Link +MeterMike +Mew +Mewtwo +MicroATX +MicroRaptor +MidTierM +MidTower +MiddleWeek +MidnightM +Mighty +MightyFeeble +Miles +Miley +Milky Way +Mime +Mina +MineSweeper +Miner Pick +Ming +MiniPC +Minir +MinnowMike +Minotaur +MinuteMan +Mirage +Miriam +MissileMan +Mist Walker +Mistime +Misty +Mo +Moan +MoatMike +ModernOld +Moeldi +MohairMax +MoleRat +MollyMike +Molnar +Moltres +MomentMike +MondayMan +Monk Fist +Monk Spade +MonkaS +MonsterMash +Moon +Moon Beam +Moon Lord +Moon Walker +Morax +MoreMornie +Morena +Moril +MorningM +MortalDiv +MortarMan +MotorMouth +Mottle +MoundMike +MountainM +Mourn +MourningM +MouseAccel +MouseMatt +MovingStill +Moxie +MtnDewMe +MuMaster +MudTurtle +Muk +MummyAlive +MunchkinM +MuskTurtle +MuskieMax +MustardMan +MyBad +Myron +MysteryMan +Mystic +Naga +Naginata +Nanci +Narto +Nashala +Natalie +Natasha +Naura +Nazgul +NearlyNear +Nebula +Nebula Gas +Necron +NegavNegev +NeiR +Neits +Nellie +Nelly +Nelus +Neon +Neon Dash +Neon Knight +NerfThis +NetAdminN +NetCaster +NetNinja +NeverYes +NewHistory +Newbie +NewbieBob +Nexo +NiceShot +Nicholas +Nidoking +Nidoqueen +Nidoran♀ +Nidoran♂ +Nidorina +Nidorino +Night +Night Hawk +NightHawk +NightOwl99 +NigiriNed +Nims +NineEleven +NineNick +NineSeven +Ninetales +Ninja Star +Nite +Nitpick +Nitro Speed +NitroNova +Niwa +NoBrain +NoCapFr +NoScope360 +NoSkinNed +NoYes99 +NobleNed +Noch +NodosaursN +NonneOne +Noob +NoodleNed +NoonNed +Norilsk +Norsu +NotABot123 +NotAbsolute +Nova Blast +NovaBlast +NovemberN +NowThen +NuNinja +NuchaNucha +NucleusN +Nuke +NullFilled +NullKnight +Numa +Nunchaku +NuttyNed +OGPlayer +OMEGALUL +Oathkeeper +ObliviatorO +Oceanic +Oceanus +OctoberO +Octopus +OcultaOcul +Oddish +Odin +Odrin +OffCentered +OffOn99 +OftenNo +Ogre Crush +OkBoomer +Olcan +OldNews +Ole +OliveRidley +Omanyte +Omastar +Omega +Omega End +OmegaOps +OmicronOne +Omid +OnOff99 +OnPurpose +OneNone +OneThree +OneWill +Onix +Onyx +Onyx Black +OofSize +OpPleaseNo +OpenOscar +OpeningO +OrangeOscar +Orbit King +OrbitingO +Orc Smash +OrderChaos +Origami +OrzoOscar +OscarOllie +Oshme +Osku +Otio +OtterPenguin +Otto +OuachOuach +OutIn99 +Outer Planes +Outlaw Gun +Outplayed +OverPower +OwOSniper +OwlWatch +PachyPete +PachycephP +Pack Leader +PackerPatr +PaddlefishP +PaellaP +Paid +Paige +Pain Train +PaintedTurt +Paladin Oath +PaltPaltho +Paly +Pammi +PancakePan +Panics +Pano +Papu +ParaPete +Parab +Paradox +ParakeetP +Paras +ParasaurP +Parasect +ParkwayPat +ParrotBeak +ParrotPete +PartiallyYes +PartridgeP +PartyTime +PashmineP +PassiveActv +PastFutur +Pasta +PastaPete +PatchDay +Patheon +Patron +Pax +PayDay +PayToWin +Peabody +PeaceThreat +PeacefulWar +PeachPit +PeakPete +PearPete +Pearl +Pearl White +PedalPro +PeerToPeer +Pegasus Fly +Pehle +PennePete +Pentapus +PepeHands +Pepega +PerchPat +PerhapsNo +Persian +PersianPat +Phalanx +Phantom +Phantom Pain +PhantomFox +PhantomPhan +Phaser +PheasantP +PhiPhantom +Philippa +PhoPhil +Phoenix +Phoenix Rise +PhoenixDown +PiPirate +PickleRick +Pidgeot +Pidgeotto +Pidgey +PigNosedPig +PigeonPete +Pikachu +Pike Wall +PikeMan +PikePete +PilafPete +PillagerLoot +PillagerPat +PineApple +PingPong99 +Pingu +Pinsir +PintoPinto +Pippin +Pirate +Pirate Ship +Pisces Fish +PistolPete +Pistolero +PistonPete +PivotPoint +Pixel Art +PixelWarrior +Pixie +PlainPete +PlainsPete +Planet X +PlatePlate +PlateauPat +Platinum +PlatyPete +PliableStif +PlotArmor +PluggedIn +PlumPerfect +PlumbPete +PlumbTilted +Plunderer +PlundererP +PocketLoss +PogChamp99 +Polar Bear +PolarDog +PolarOrca +PolishdRough +Poliwag +Poliwhirl +Poliwrath +PolkockPete +Polycarp +Ponyta +Porio +Porygon +PossiblyYes +PotentImpot +PowerFist +PowerStrife +PoweredUp +PowerlessPow +PracticlyP +Praetorian +PrairieP +PranksterP +PreciseP +PredatorP +PrehistorP +Prie +PrimaryPat +PrimePete +PrimeTime +Primeape +PrinceFresh +PrincePete +Princess +PrincessP +Prism +Private Eye +Privateer +ProPunch +ProbablyNo +Prod +ProductOwn +ProfaneSacr +Professor +ProfoundP +Promiz +PropPro +ProtectorPro +PrunesPete +PsamPsamobb +PsammPsamm +PseuPseudem +PsiPower +Psyduck +PtarmiganP +PteroPete +Pudao Chop +PuffinSeal +Pugg +PullRates +Pulp +Pulsar Wave +PulsePete +PulsePoint +Pulu +PumpkinPie +PurePatrick +Purple Haze +Purrfect +PyroManiac +PythonPower +PyxiPyxis +QAQuinn +Qiang Poke +QuailQuinn +QuantumQuake +Quarrel +Quasar Jet +QueenQuin +QueenQuinn +Questi +Quibble +Quick Shot +QuickDraw +QuickQuinn +QuickScope +Quiet +QuietLion +Quinn +QuinoaQ +Quint +Quintis +Quteth +RacingSnail +RackMount +RadBrad +RadiantRay +Radovid +RagdollRay +Raibu +Raichu +Raid +Raider Camp +RaiderRex +Rail +Rain Maker +RainMaker +RaisinRon +Raketti +Ram Boost +RamenRon +Ranger Hat +Ranger Track +Ranx +RapidFire +RapidRon +Rapidash +Rapier Point +RapierRex +RaptorRon +RarelyYes +Raskal +Raslak +RaspBerry +Rastafa +Raticate +Rattata +RattleSnake +Raul +RavenGaze +RavineRon +RawRon +Ray +Razor Beast +Razor Edge +RealRyan +Realm +Reck +Recon Drone +Red Phoenix +RedEared +RedFootRed +RedWing +Rekt +RelaxedRon +ReleaseRon +Relic +RelishRob +Rellano +Rem +RenderRon +RespawnKing +RespawnNow +ReticleR +RetroRon +RevolvingR +RevvedUp +Rew +RhinoRhino +RhoRanger +Rhydon +Rhyhorn +RhythmRick +RiceRon +RidgeRider +Rido +RifleRanger +Rifleman +RiftRick +RigatoniR +RightLeft99 +RigidFlexy +RigidSoft +Ril +Riley +RipperRyan +RippleRick +Risen +RiskSecure +RisottoR +RoadRage +Roan +RobinRed +Robotnik +Roca +Roche +Rock Solid +RocketRider +Roger +Rogue +Rogue Sneak +Rogue007 +RogueKnight +Rolis +RollRon +RolloutRay +Rooda +Rope Dart +RopeDancer +Rose +RotatingR +RoughPolish +RoundSquare +Rowan +Roxer +RoyalRon +Royka +RubberBand +RubberBurn +Ruby +Ruby Red +Ruckus +Rumpus +RunningStop +Ruralli +RushHour +Ryza +SRankHero +SSSTier +Saber Strike +SaberSage +SabinSabin +SacraedSacr +Sacred +Sacred Blood +Sacred Dawn +Sacred Heart +Sacred Oath +Sacred Sword +Sacred Wings +SacredLegacy +SacredProf +SadJoy +Sadge +Saena +SafeDanger +Sage +SageSparrow +Sai Catch +Sailor Knot +Sakura +Sal +Sally +SalmonSam +Salve +SamuraiBlade +Samwise +Sand Storm +Sandshrew +Sandslash +Saney +SanityMad +Sanna +Sanny +Saph +Sapper Mine +SapperSam +SapphireSoul +Saruman +SashimiS +SatinySam +SaturdaySam +Sauade +Sauron +SauropeltaS +SavannaS +Sawyer +ScaldingSam +ScaleSteve +Scarak +ScaredBear +ScarelyNo +Scarlett +ScavengerS +Scimitar Arc +ScimitarS +ScopeDogg +ScopeKing +ScorchMark +Scorpio Tail +Scott +Scourge +Scout Map +Scrap +Scratch +ScreenTear +Scrimmage +ScrubLord +ScrumMastr +Scuffle +Scythe Cut +ScytheSage +Scyther +SeaSponge +SeaSquid +SeaTurtle +Seadra +Seaking +Seal +Seal lion +Sean +Seance +SearingSam +SecondSam +SecretSix +SecurRisky +Sedyana +Seel +Sellarity +Sena +SentinelSix +September9 +Sergeant Bar +Serpect +SerpentSoul +ServerSam +ServerSide +Seven +SevenFive +SevenNine +SevenSeth +ShadeShadow +ShadeShady +Shadow +Shadow Fox +ShadowBlade +ShadowOps +ShaftedSam +ShakyStable +ShakySteady +Shaman Mask +Shaper +Shark +SharpCloud +SharpShooter +Sharpshoot +ShatterSean +Shaw +Shawn +Shellder +ShellsSam +Shelly +Shelob +Sheng Biao +Sheriff Star +ShieldHero +ShifterS +ShimmerS +ShindigS +ShiningS +ShinyDull +Shir +Shiv +ShockWave +Shop +ShortHair +Shortsword +Shorty +Shuang Gou +Shuriken Fly +SiameseSam +SickSkills +Sid +SideNeck +SiebnSiebn +Sigil +Sigma +SigmaSix +SignSignatu +Signal +SignalSam +SignatuSign +Silas +Silence +Silent Kill +SilentKill +SilentScrem +SilkySteve +SillySam +Silver +SilverFang +SimmerStan +SimpleSam +Sinclair +SirSam +SixEight +SixFour +SixSam +SkewedAlign +SkidMark +Skilla +Skirmish +Skoll +Sky High +Sky Reaper +SkyHigh99 +Skype +SlackTaut +Slain +SlayQueen +SlayerSeth +SlicerSteve +SliderSlide +SlightlyYes +SlingShot +Sliske +SlopeSam +SlothCheetah +SlowPoke +Slowbro +Slowpoke +SmallGiant +SmasherSam +Smiles +SmokeyJoe +SmolBean +SmoothBrain +SmoothBumpy +SmoothSam +SnailBunny +SnailRacing +SnakeGecko +SnakyNecked +Snap +SnapperSnap +Snarl +SneakySnail +Sniffle +Sniper Scope +SniperElite +Snivel +Snorlax +Snort +Snow Fall +Snow raccoon +SnowFlake +Snowman +Snuffle +SnugSam +SoClose +SoMaybe +SoSad +Soad +Sob +SobaSteve +SoftHammer +SoftHard +SoftShellS +Softy +Sokka +Solar +Solar Flare +SolitaSolit +Solitude +SolutionS +SometimesNo +Sonic +SonicBoom +Sophics +Sorcerer Hat +SortKinda +Soul +Soul Eater +SoulStan +Soulless +SoupSam +Sozin +Space Cadet +SpaghettiS +Spark +SparkPlug +SparkleKid +SparrowS +Spartan 300 +Spat +Spear Thrust +SpearTip +Spearow +SpecklSpeck +SpecterHaunt +SpecterSpec +SpeedDemon +SpeedHack99 +SpeedySam +SphinxRiddle +SphinxSam +SpicyTuna +Spider Bat +SpiderTort +Spiderfly +SpinningS +SpinosarusS +SpinyShell +SpiritSpike +SpiritSpook +SplitSteve +SplitterS +SpoonfishS +SpotStripe +SprintCrawl +SprintStan +SpurThigh +Spy Glass +SpyMaster +Squabble +SquallSteve +SquareLopsid +SquareRound +SquashSteve +Squid +Squirtle +StablShaky +StablWobble +Stain +Stalhrim +StalkerStan +StandUpSam +Star +Star Gazer +Star Lord +StarStar +StarlingS +Starmie +StartPoint +StarterStan +Staryu +StaticDynmc +StaticS +SteadyShake +SteadyUnstdy +Stealth Fly +StealthMode +SteamRice +Steel Claw +Steel Heart +SteelClaw +SteelViper +StegoStan +Stellar +Stennis +Stephan +SteppeStan +StewSteve +StickShift +StickyDoor +StiffFlexi +StiffLimp +StiffPliable +StigStgmoch +StillMoving +StockStan +Stok +Stone Cold +Stony +StoppedRun +Storm +Storm Blade +StormCalm +StormCloud +StormRider +Stormy +StraightBent +StraightLean +StraightS +StraightTwst +StrawBerry +StreetKing +StripedSpot +StrongWeak +Stuey +SturgenSam +StutterStep +StygimolchS +Styloire +StylysSteve +StyracoS +Subtle +SuchomimusS +Suhly +Suki +SulcataSul +SummitSam +Sun +Sun Spot +SundayFun +SunfishSam +SunriseSam +SunsetStan +SuperSlam +Support Unit +SupremeS +SurfaceSam +Surge +SurgeSteve +SushiSam +Suspect +Sven +SwallowS +Swan +SwiftSteve +SwiftStrike +SwirlingSam +SwordSaint +Swordsman +SwordtailS +Syndicate +SyntaxSlayer +SysAdminSam +TRexRex +Tabby +TabbyTom +TabletTim +TaigaTed +Tails +Talies +TallAnt +Talon +TameWild +Tangela +TangerinT +Tank Shell +Tanto Blade +Tanto Sharp +TargetLock +TarmacTom +TauTerror +Tauros +Taurus Bull +TautRelaxed +TearerTom +Tech Noir +TechLead +Tekko Hand +Teleportin +TempestTom +TempoTom +TenEight +TenTony +TenTwelve +TenseChill +TentTortois +Tentacool +Tentacruel +TenuiTenui +Teps +Teran +Term +TerminatorT +TerraTerrp +TerrapinTur +Tessa +Tessen Fan +TestTim +TestuTestud +TettraTom +TexasTort +ThenNow +Theoden +ThereHere +TherizinoT +ThetaThree +Theump +ThickSkull +Thief Steal +ThirteenElv +Thor +Thorak +Thoron +ThreatPeace +ThreeFive +ThreeOne +ThreeTom +ThrottleT +Thru +Thulth +Thump +Thunder +Thunder God +ThunderHead +ThunderStrix +ThunderT +ThursdayT +TiedLoose +Tiff +Tiger Hook +Tiger monkey +TigerSeal +TigerShark +TightSlack +TilapiaTom +TiltedPlumb +Tinne +TinyBrain +TippedBal +TireFlip +TiredVigor +Titan Fall +TitanGrip +TitanTerror +ToadLizard +Toar +ToastyTom +TodayTomrw +Todes +Tom +TomorrowYes +Tonfa Block +Toni +TooBad +TooEz +TopSideTom +TopTier1 +Topaz +Toph +Topher +TorchBearer +Tornado +TornadoT +TotallyNot +TotallyReal +TouchPadTom +ToughFragile +TowerTom +Toxic Venom +ToyboyTom +TrachemTrac +TrackBallT +TrackerTom +TransMaster +TrappedFree +Trapper Net +Traut +Trav +TravanTrav +Travis +TreeTop99 +TrenchTitan +TrenchTony +Trevor +TrialTime +TriceraTop +TrickShot99 +TricksterT +TridentKing +TrimTrimacu +Triss +Trist +TrodonTroy +Troll Bridge +Trork +TroutTim +TrueLies +TrueTodd +TrueTom +TryAgain99 +Tryhard +Tryver +TubularTom +Tuesday2 +Tuga +Tugas +TunaTodd +TundraTom +Turbo Boost +TurboTed +TurboTitan +Turk +TurnPro +TurtleDove +TurtleFrog +TurtleHare +TurtleSeal +Tussle +TwelveTen +Twenty +TwilightTom +TwirlingTom +TwisterTed +TwoFour +TwoTim +TwoZero +TyphoonT +UdonUlf +Uiua +UltimateU +UltraKick +UltraOof +Umar +Unagi +UnbalancedB +UnbendablB +UnbentBent +UndeadLive +UnevenEven +UnfortuneF +Unholy +UnholyHoly +Unicorn Horn +UnknownX +UnlevelLevl +UnluckyLuck +UnluckyLuk +UnsteadyStdy +UpDown99 +UpdateTime +UplandUlf +UpperDeck +Uraby +Usagi +UtahraptrU +Uther +UwUWarrior +VacayVibes +Vaht +Valar +Valette +ValiantV +Valkyrie +ValleyVic +Valor +Vampire Bite +VampireSun +Vaporeon +Varyn +Vasco +Vector +VelociRapV +VelocityV +VelutiVelu +VelvetVic +Venator +VenomStrike +Venomoth +Venonat +Venusaur +VersionV +Ves +VetVince +VibeCheck +Victor +Victreebel +VicunaVic +VigorousTird +Viking Ship +Vileplume +Vindicta +Vine +Violet +Violet Storm +ViperVenom +Virgo Pure +Virtual Pro +VirtualVoid +VirtuallyV +Viscous +VitalVince +Void Hunter +VoidFull +VoidVader +VoidVince +VoidWalker +VoltageV +Voltorb +Vortex +VortexVic +VsyncOff +Vub +Vulpix +VultureView +Vyper +WackyWill +WacomWill +Wail +Waio +Wake +Wakizashi +WalkingJog +WallHackWil +WalleyeWill +War Machine +WarMachine +WarPeace +Warden +WardenWill +WardenWrath +Warlock Deal +WarmWill +WarpDrive +Wartortle +Water +WaterMelon +WaterfallW +Waves +WaveyWill +Wazzy +WeakStrong +Wednesday +Wednesday2 +Weedle +Weekday +Weekend99 +Weep +Weepinbell +Weezing +WellPlayed +Wendy +WerewolfDay +WerewolfHowl +Werr +Wes +WetFire +Whale +WhaleShark +WhaleWalrus +WhaleWill +WheelieKing +Whiff +Whimper +Whine +WhipMaster +WhirlWind +WhiskerWig +Whisky +White Tiger +WhiteRice +Whoops +WickedWill +WienerKing +Wigglytuff +Wild Card +WildCard99 +WildRice +WildTame +WildfireW +Willy +Wilt +Wind +Wind Breaker +Wind buffalo +WindStorm +WiredWill +Wisely +Witch Brew +Wizard Staff +WobblyStabl +WoblyFirm +Wobsy +WolfPack99 +WolfmanWolf +WoodFrog +WoodlandW +Woof +WorkDay +Wraith +Wraith Form +WraithWrath +Wrecked +WreckerWill +WyvernWing +Xelio +Xepher +Xephyr +Xeria +XeroXeroba +XiWarrior +XpPenXander +Xtone +Xuhe +Yamato +Yap +Yari Spear +Yarpen +YeetMaster +YellowBelly +YellowFoot +YellowtailY +Yelp +Yelpsie +Yenn +Yennefer +YesNo99 +YesterdayNo +Yickee +Ylaria +Yogo +Yoji +Yoram +YourDaddy +Yowl +YudonYuri +Yumi Bow +Yuno +Yurcher +YutytyrannY +Zac +Zach +Zamata +ZanyZack +ZapZack +Zapdos +Zapy +Zebra +Zell +Zenig +Zephyr +ZephyrZack +Zero +Zero Cool +ZeroChill +ZeroGrav +ZeroHero +ZeroHero99 +ZeroTwo +Zeroth +Zerxis +ZetaZero +Zeurpiet +Zhao +Ziffix +Zig +Zindra +Zip +Ziyte +Zodiac Sign +Zodo +Zohan +Zoltan +Zombie Brain +ZombieAlive +ZoomMaster +Zubat +ZucchiniZ +Zuko +Zulu +Zurm +Zush +Zweb +Zyvik \ No newline at end of file From cdc9141e78cca8208b9ed2d85a81cf929219ab15 Mon Sep 17 00:00:00 2001 From: GregHib Date: Mon, 26 Jan 2026 12:41:30 +0000 Subject: [PATCH 004/101] Add username length check to login --- .../src/main/kotlin/world/gregs/voidps/network/LoginServer.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/network/src/main/kotlin/world/gregs/voidps/network/LoginServer.kt b/network/src/main/kotlin/world/gregs/voidps/network/LoginServer.kt index d4b2ec25cd..130f409536 100644 --- a/network/src/main/kotlin/world/gregs/voidps/network/LoginServer.kt +++ b/network/src/main/kotlin/world/gregs/voidps/network/LoginServer.kt @@ -107,6 +107,10 @@ class LoginServer( write.finish(response) return false } + if (username.length > 12) { + write.finish(Response.INVALID_CREDENTIALS) + return false + } if (!online.add(username)) { write.finish(Response.ACCOUNT_ONLINE) return false From 5fe25a402e04097a68fdc52701a8fcf55e6c6c7a Mon Sep 17 00:00:00 2001 From: GregHib Date: Mon, 26 Jan 2026 18:20:35 +0000 Subject: [PATCH 005/101] Add bot name randomising --- data/{bot => }/bot_names.txt | 0 game/src/main/kotlin/content/bot/BotSpawns.kt | 21 ++++++++++++++++++- game/src/main/resources/game.properties | 11 +++++++++- 3 files changed, 30 insertions(+), 2 deletions(-) rename data/{bot => }/bot_names.txt (100%) diff --git a/data/bot/bot_names.txt b/data/bot_names.txt similarity index 100% rename from data/bot/bot_names.txt rename to data/bot_names.txt diff --git a/game/src/main/kotlin/content/bot/BotSpawns.kt b/game/src/main/kotlin/content/bot/BotSpawns.kt index 0bb94bf073..60c37df1a0 100644 --- a/game/src/main/kotlin/content/bot/BotSpawns.kt +++ b/game/src/main/kotlin/content/bot/BotSpawns.kt @@ -27,6 +27,7 @@ import world.gregs.voidps.network.client.DummyClient import world.gregs.voidps.network.login.protocol.visual.update.player.BodyColour import world.gregs.voidps.network.login.protocol.visual.update.player.BodyPart import world.gregs.voidps.type.random +import java.io.File import java.util.concurrent.TimeUnit import kotlin.coroutines.resume import kotlin.text.toIntOrNull @@ -39,6 +40,7 @@ class BotSpawns( ) : Script { val bots = mutableListOf() + val names = mutableListOf() var counter = 0 @@ -57,6 +59,9 @@ class BotSpawns( if (Settings["bots.count", 0] > 0) { World.timers.start("bot_spawn") } + if (!Settings["bots.numberedNames", false]) { + names.addAll(File(Settings["bots.names"]).readLines()) + } } settingsReload { @@ -114,7 +119,21 @@ class BotSpawns( fun spawn() { GlobalScope.launch(Contexts.Game) { - val name = "Bot ${++counter}" + counter++ + val name = if (Settings["bots.numberedNames", false]) { + "Bot $counter" + } else { + val prefix = Settings["bots.namePrefix", ""].trim('"') + val length = 12 - prefix.length + val short = names.filter { it.length < length } + var selected = short.randomOrNull(random) + if (selected == null) { + selected = names.removeAt(random.nextInt(names.size)) + } else { + names.remove(selected) + } + "${prefix}${selected}" + } val bot = Player(tile = Areas["lumbridge_teleport"].random(), accountName = name) bot.initBot() loader.connect(bot, if (Settings["development.bots.live", false]) DummyClient() else null) diff --git a/game/src/main/resources/game.properties b/game/src/main/resources/game.properties index a8249883eb..9cae89eeff 100644 --- a/game/src/main/resources/game.properties +++ b/game/src/main/resources/game.properties @@ -238,12 +238,21 @@ fightCave.startWave = 1 # The number of AI-controlled bots spawned on startup bots.count=10 -# Frequently between spawning bots on startup +# Frequency between spawning bots on startup bots.spawnSeconds=60 # What tasks to give AI-controlled bots with no tasks (options: nothing, randomWalk) bots.idle=randomWalk +# Use bot names instead of randomised +bots.numberedNames=false + +# Prefix at the start of bot's names (blank to hide) +bots.names=./data/bot_names.txt + +# Prefix at the start of bot's names (blank to hide) +bots.namePrefix="" + #=================================== # Storage & File System From fb8dc33cb98a2f38e9da233cecdcee47d859a490 Mon Sep 17 00:00:00 2001 From: GregHib Date: Mon, 26 Jan 2026 18:24:32 +0000 Subject: [PATCH 006/101] Fix bots spawning on startup --- game/src/main/kotlin/content/bot/BotSpawns.kt | 29 ++++++++++--------- game/src/main/resources/game.properties | 2 +- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/game/src/main/kotlin/content/bot/BotSpawns.kt b/game/src/main/kotlin/content/bot/BotSpawns.kt index 60c37df1a0..65840563d0 100644 --- a/game/src/main/kotlin/content/bot/BotSpawns.kt +++ b/game/src/main/kotlin/content/bot/BotSpawns.kt @@ -45,29 +45,22 @@ class BotSpawns( var counter = 0 init { - timerStart("bot_spawn") { TimeUnit.SECONDS.toTicks(Settings["bots.spawnSeconds", 60]) } + worldTimerStart("bot_spawn") { TimeUnit.SECONDS.toTicks(Settings["bots.spawnSeconds", 60]) } - timerTick("bot_spawn") { + worldTimerTick("bot_spawn") { if (counter > Settings["bots.count", 0]) { - return@timerTick Timer.CANCEL + return@worldTimerTick Timer.CANCEL } spawn() - return@timerTick Timer.CONTINUE + return@worldTimerTick Timer.CONTINUE } worldSpawn { - if (Settings["bots.count", 0] > 0) { - World.timers.start("bot_spawn") - } - if (!Settings["bots.numberedNames", false]) { - names.addAll(File(Settings["bots.names"]).readLines()) - } + loadSettings() } settingsReload { - if (Settings["bots.count", 0] > bots.size) { - World.timers.start("bot_spawn") - } + loadSettings() } adminCommand("bots", intArg("count", optional = true), desc = "Spawn (count) number of bots", handler = ::spawn) @@ -75,6 +68,16 @@ class BotSpawns( adminCommand("bot", stringArg("task", optional = true, autofill = tasks.names), desc = "Toggle yourself on/off as a bot player", handler = ::toggle) } + private fun loadSettings() { + if (Settings["bots.count", 0] > 0) { + World.timers.start("bot_spawn") + } + if (!Settings["bots.numberedNames", false]) { + names.clear() + names.addAll(File(Settings["bots.names"]).readLines()) + } + } + fun spawn(player: Player, args: List) { val count = args[0].toIntOrNull() ?: 1 GlobalScope.launch { diff --git a/game/src/main/resources/game.properties b/game/src/main/resources/game.properties index 9cae89eeff..5065cd0967 100644 --- a/game/src/main/resources/game.properties +++ b/game/src/main/resources/game.properties @@ -247,7 +247,7 @@ bots.idle=randomWalk # Use bot names instead of randomised bots.numberedNames=false -# Prefix at the start of bot's names (blank to hide) +# Location of a text file containing bot names to pick from bots.names=./data/bot_names.txt # Prefix at the start of bot's names (blank to hide) From b15f3f83ee7bf4ecdcfea6587e8369c7b61c2f4c Mon Sep 17 00:00:00 2001 From: GregHib Date: Mon, 26 Jan 2026 20:33:15 +0000 Subject: [PATCH 007/101] Add a basic bot manager system which assigns location-based tasks --- data/bot/test.bot.toml | 357 ------------------ data/bot/woodcutting.bots.toml | 150 ++++++++ data/dirs.txt | 3 +- game/src/main/kotlin/GameModules.kt | 2 + game/src/main/kotlin/GameTick.kt | 4 +- .../main/kotlin/content/bot/ActivitySlots.kt | 19 + game/src/main/kotlin/content/bot/Bot.kt | 31 ++ .../src/main/kotlin/content/bot/BotManager.kt | 104 +++++ .../kotlin/content/bot/action/Behaviour.kt | 9 + .../content/bot/action/BehaviourFrame.kt | 39 ++ .../content/bot/action/BehaviourState.kt | 9 + .../kotlin/content/bot/action/BotAction.kt | 38 ++ .../kotlin/content/bot/action/BotActivity.kt | 179 +++++++++ .../main/kotlin/content/bot/action/Reason.kt | 9 + .../content/bot/req/MandatoryRequirement.kt | 15 + .../kotlin/content/bot/req/Requirement.kt | 11 + .../content/bot/req/ResolvableRequirement.kt | 37 ++ game/src/main/resources/game.properties | 2 + .../kotlin/content/bot/ActivitySlotsTest.kt | 42 +++ .../test/kotlin/content/bot/BotManagerTest.kt | 233 ++++++++++++ 20 files changed, 934 insertions(+), 359 deletions(-) delete mode 100644 data/bot/test.bot.toml create mode 100644 data/bot/woodcutting.bots.toml create mode 100644 game/src/main/kotlin/content/bot/ActivitySlots.kt create mode 100644 game/src/main/kotlin/content/bot/BotManager.kt create mode 100644 game/src/main/kotlin/content/bot/action/Behaviour.kt create mode 100644 game/src/main/kotlin/content/bot/action/BehaviourFrame.kt create mode 100644 game/src/main/kotlin/content/bot/action/BehaviourState.kt create mode 100644 game/src/main/kotlin/content/bot/action/BotAction.kt create mode 100644 game/src/main/kotlin/content/bot/action/BotActivity.kt create mode 100644 game/src/main/kotlin/content/bot/action/Reason.kt create mode 100644 game/src/main/kotlin/content/bot/req/MandatoryRequirement.kt create mode 100644 game/src/main/kotlin/content/bot/req/Requirement.kt create mode 100644 game/src/main/kotlin/content/bot/req/ResolvableRequirement.kt create mode 100644 game/src/test/kotlin/content/bot/ActivitySlotsTest.kt create mode 100644 game/src/test/kotlin/content/bot/BotManagerTest.kt diff --git a/data/bot/test.bot.toml b/data/bot/test.bot.toml deleted file mode 100644 index f550841b97..0000000000 --- a/data/bot/test.bot.toml +++ /dev/null @@ -1,357 +0,0 @@ -[has_axe] -requirements = [ - { type = "carries", preset = "axe" }, - { type = "inventory_space", amount = 20 }, -] - -[bank_all_after_full] -plan = [ - { action = "wait_for_full_inv" }, - { action = "go_to", target = "bank" }, - { action = "interact_npc", option = "Bank", id = "banker*" }, - { action = "interface", option = "Deposit-All", id = "bank:inventory" }, -] - -[regular_trees] -type = "activity" -capacity = 4 -requirements = [ - { clone = "has_axe" }, - { type = "skill", id = "woodcutting", min = 1, max = 15 }, -] -plan = [ - { action = "go_to", target = "tree_area" }, - { action = "interact_obj", option = "Chop-down", id = "tree*" }, - { clone = "bank_all_after_full" }, -] -produces = [ - { item = "logs" } -] - - - -[actions.open_varrock_bank] -requires = [ - { location = "varrock_bank" } -] -action = { option = "Open-bank", npc = "varrock_banker" } -effect = [ - { interface = "bank" } -] - -[axe_shopping] -requirements = [ - { type = "inventory_space", amount = 1 }, -] -plan = [ - { action = "go_to", target = "axe_store" }, - { action = "interact_npc", option = "Trade", id = "axe_store_owner" }, -] - -[buy_bronze_axe] -type = "resolver" -requirements = [ - { type = "skill", id = "woodcutting", min = 5 }, - { type = "holds", id = "coins", amount = 25 }, - { clone = "axe_shopping" }, -] -plan = [ - { clone = "axe_shopping" }, - { action = "interface", option = "Buy-1", id = "axe_shop:inventory" }, -] -produces = [ - { item = "bronze_axe" } -] - -[buy_iron_axe] -type = "resolver" -requirements = [ - { type = "skill", id = "woodcutting", min = 10 }, - { type = "carries", id = "coins", amount = 150 }, - { clone = "axe_shopping" }, -] -plan = [ - { clone = "axe_shopping" }, - { action = "interface", option = "Buy-1", id = "axe_shop:inventory:iron_axe" }, -] -produces = [ - { item = "iron_axe" } -] - -# equips -> equipped -# carries -> on person -# owns -> in bank or on person - -[ring_of_duelling_equipped] -type = "teleport" -weight = 10 -requirements = [ - { type = "variable", id = "spellbook", value = "normal" }, - { type = "equips", id = "ring_of_dueling_*" } -] -plan = [ - { action = "interface", option = "Castle Wars", id = "equipment:inventory" }, -] -produces = [ - { location = "castle_wars_teleport" } -] - -[ring_of_duelling] -type = "teleport" -weight = 10 -requirements = [ - { type = "variable", id = "spellbook", value = "normal" }, - { type = "in_inventory", id = "ring_of_dueling_*" } -] -plan = [ - { action = "interface", option = "Rub", id = "inventory:inventory" }, - { action = "interface", option = "Select", id = "choice:line1" }, -] -produces = [ - { location = "castle_wars_teleport" } -] - - -[teleport_varrock] -type = "teleport" -weight = 10 -requirements = [ - { type = "variable", id = "spellbook", value = "normal" }, - { type = "carries", id = "fire_rune", amount = 1 }, - { type = "carries", id = "air_rune", amount = 3 }, - { type = "carries", id = "law_rune", amount = 1 }, -] -plan = [ - { action = "interface", option = "Cast", id = "normal_spellbook:varrock_teleport" }, -] -produces = [ - { location = "varrock_teleport" } -] - -[teleport_lumbridge] -type = "teleport" -weight = 0 -requirements = [ - { type = "variable", id = "spellbook", value = "normal" }, - { type = "variable", id = "lumbridge_cooldown", value = "normal" }, -] -plan = [ - { action = "interface", option = "Cast", id = "normal_spellbook:home_teleport" }, -] -produces = [ - { location = "lumbridge_teleport" } -] - -[go_to_bank] -req = ["inventory_full"] -action = { type = "go_to", target = "bank" } -context = "banking" - -[bank_all] -req = ["in_bank", "items_in_inventory"] -action = { type = "interact_npc", option = "Open bank", target = "banker*" } -context = "bank_open" - -[withdraw_coins] -req = ["coins_in_bank", "bank_open"] -action = { type = "interface", option = "Withdraw-100", target = "coins" } -context = "shopping" - -[go_to_axe_shop] -req = ["shopping", "1_inventory_space"] -context = "axe_shopping" - -[open_axe_shop] -req = ["axe_shopping"] -action = { type = "interact_npc", option = "Trade", target = "axe_shopkeeper" } -context = "axe_shop" - -[buy_axe_shop] -req = ["axe_shop"] -action = { type = "interact_npc", option = "Buy", target = "iron_axe" } -context = "axe" - -[buy_pickaxe_shop] -req = ["axe_shop"] -action = { type = "interact_npc", option = "Buy", target = "iron_pickaxe" } -context = "pickaxe" - - -[bank_deposit_items] -req = ["inventory_full"] -context = "banking" - -[bank_withdraw_items] -req = ["inventory_empty"] -context = "banking" - -[bank_withdraw_melee_gear] -req = [ - { context = "banking" }, -] -context = "melee_combat" - -[bank_withdraw_mage_gear] -req = [ - { context = "banking" }, -] -context = "magic_combat" - -[fight_cows] -req = [ - { context = "melee_combat" }, - { skill = "attack", min = 5 }, -] - -[fight_chickens] -req = [ - { context = "melee_combat" }, - { skill = "attack", min = 1 }, -] -context = "fight_chickens" - -# val context = Stack() // after complete pop and re-evaluate - -[bank_withdraw_ranged] -req = ["inventory_empty"] - - - - - - - - - - - - - -[woodcutting] -requires = { state = "idle" } -states = [ - { to = "find_tree" }, - { to = "buy_axe" }, - { to = "get_axe" }, - { to = "wait" }, -] - -[woodcutting.get_axe] -actions = [ - { go_to = "jims_farm" }, - { option = "Pick-up", item = "iron_axe" }, -] - -[woodcutting.buy_axe] -requires = [ - { carries = "coins", amount = 10 } -] -actions = [ - { go_to = "axe_store" }, - { option = "Trade", npc = "axe_store_owner" }, - { option = "Buy-1", interface = "shop:inventory:iron_axe" }, -] - -[woodcutting.find_tree] -requires = [ - { carries = "iron_axe" } -] -actions = [ - { go_to = "village_woods" }, - { option = "Chop-down", obj = "normal_tree" }, -] - - - -[walk_to_bank_trees] -actions = [ - { go_to = "nearest_bank" } -] -transitions = [ - { when = { in = "bank" }, then = "open_bank_trees" }, - { when = { timeout = 20 }, then = "idle" } -] - -[open_bank_trees] -actions = [ - { go_to = "nearest_bank" } -] -transitions = [ - { when = { in = "bank" }, then = "open_bank_logs" }, - { when = { timeout = 20 }, then = "idle" } -] - -[open_bank_logs] -actions = [ - { option = "Open-bank", npc = "banker" } -] -transitions = [ - { when = { open = "bank" }, then = "deposit_logs" }, - { when = { timeout = 20 }, then = "idle" } -] - -[go_to_trees] -transitions = [ - { when = { in = "tree_area" }, then = "find_tree" }, -] - -[find_tree] -actions = [ - { scan_for = "tree", radius = 5 } -] -transitions = [ - { when = { has_target = true }, then = "chop_tree" }, - { when = { location_not = "tree_area" }, then = "walk_to_trees" }, - { when = { timeout = 5 }, then = "idle" } -] - -[walk_to_trees] -actions = [ - { go_to = "tree_area" } -] -transitions = [ - { when = { in = "tree_area" }, then = "find_tree" }, - { when = { timeout = 20 }, then = "idle" }, -] - -[chop_tree] -actions = [ - { option = "Chop-down", entity = "target" } -] -transitions = [ - { when = { inventory_full = true }, then = "walk_to_bank_trees" }, - { when = { has_target = false }, then = "find_tree" }, - { when = { timeout = 30 }, then = "idle" } -] - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/data/bot/woodcutting.bots.toml b/data/bot/woodcutting.bots.toml new file mode 100644 index 0000000000..a53a58035c --- /dev/null +++ b/data/bot/woodcutting.bots.toml @@ -0,0 +1,150 @@ +[woodcutting_ready] +requires = [ + { carries = "bronze_hatchet,iron_hatchet,steel_hatchet" }, + { inventory_space = 20 }, +] + +[bank_all_after_full] +plan = [ + { wait_for = "full_inventory" }, + { go_to = "bank" }, + { option = "Bank-quickly", npc = "banker*" }, + { wait = 2, interface = "bank" }, + { option = "Deposit-All", interface = "bank:inventory" }, +] + +[chop_normal_trees] +type = "activity" +capacity = 4 +requires = [ + { clone = "woodcutting_ready" }, + { location = "normal_tree_area" }, + { skill = "woodcutting", min = 1, max = 15 }, +] +plan = [ + { option = "Chop-down", object = "tree*", retry_ticks = 5, retry_max = 10 }, + { clone = "bank_all_after_full" }, +] +produces = [ + { item = "logs" } +] + +[chop_oak_trees] +type = "activity" +capacity = 4 +requires = [ + { clone = "woodcutting_ready" }, + { location = "oak_tree_area" }, + { skill = "woodcutting", min = 15, max = 30 }, +] +plan = [ + { option = "Chop-down", object = "oak_tree", retry_ticks = 15, retry_max = 10 }, + { clone = "bank_all_after_full" }, +] +produces = [ + { item = "logs" } +] + + +[axe_shopping] +requires = [ + { location = "axe_store" }, + { inventory_space = 1 }, +] +plan = [ + { option = "Trade", npc = "axe_store_owner" }, +] + +[buy_bronze_axe] +type = "resolver" +requires = [ + { clone = "axe_shopping" }, + { skill = "woodcutting", min = 5 }, + { carries = "coins", amount = 25 }, +] +plan = [ + { clone = "axe_shopping" }, + { option = "Buy-1", interface = "axe_shop:inventory" }, +] +produces = [ + { item = "bronze_axe" } +] + +[buy_iron_axe] +type = "resolver" +requires = [ + { clone = "axe_shopping" }, + { skill = "woodcutting", min = 10 }, + { carries = "coins", amount = 150 }, +] +plan = [ + { clone = "axe_shopping" }, + { option = "Buy-1", interface = "axe_shop:inventory:iron_axe" }, +] +produces = [ + { item = "iron_axe" } +] + +# equips -> equipped +# carries -> on person +# owns -> in bank or on person + +[ring_of_duelling_equipped] +type = "teleport" +weight = 10 +requires = [ + { variable = "spellbook", value = "normal" }, + { equips = "ring_of_dueling_*" } +] +plan = [ + { option = "Castle Wars", interface = "equipment:inventory" }, +] +produces = [ + { location = "castle_wars_teleport" } +] + +[ring_of_duelling] +type = "teleport" +weight = 8 +requires = [ + { variable = "spellbook", value = "normal" }, + { carries = "ring_of_dueling_*" } +] +plan = [ + { option = "Rub", interface = "inventory:inventory" }, + { option = "Select", interface = "choice:line1" }, +] +produces = [ + { location = "castle_wars_teleport" } +] + + +[teleport_varrock] +type = "teleport" +weight = 10 +requires = [ + { variable = "spellbook", value = "normal" }, + { carries = "fire_rune", amount = 1 }, + { carries = "air_rune", amount = 3 }, + { carries = "law_rune", amount = 1 }, +] +plan = [ + { option = "Cast", interface = "normal_spellbook:varrock_teleport" }, +] +produces = [ + { location = "varrock_teleport" } +] + +[teleport_lumbridge] +type = "teleport" +weight = 5 +requires = [ + { variable = "spellbook", value = "normal" }, + { variable = "lumbridge_cooldown", value = "normal" }, +] +plan = [ + { option = "Cast", interface = "normal_spellbook:home_teleport" }, +] +produces = [ + { location = "lumbridge_teleport" } +] diff --git a/data/dirs.txt b/data/dirs.txt index 96dded11be..acf98a7940 100644 --- a/data/dirs.txt +++ b/data/dirs.txt @@ -6,4 +6,5 @@ entity minigame quest skill -social \ No newline at end of file +social +bot \ No newline at end of file diff --git a/game/src/main/kotlin/GameModules.kt b/game/src/main/kotlin/GameModules.kt index ea1ad74bd6..37dc868dff 100644 --- a/game/src/main/kotlin/GameModules.kt +++ b/game/src/main/kotlin/GameModules.kt @@ -1,3 +1,4 @@ +import content.bot.BotManager import content.bot.TaskManager import content.bot.interact.navigation.graph.NavigationGraph import content.bot.interact.path.Dijkstra @@ -22,6 +23,7 @@ import java.io.File fun gameModule(files: ConfigFiles) = module { single { ItemSpawns() } single { TaskManager() } + single { BotManager().load(files) } single { val size = get().size Dijkstra( diff --git a/game/src/main/kotlin/GameTick.kt b/game/src/main/kotlin/GameTick.kt index 91fe271601..abecd7965b 100644 --- a/game/src/main/kotlin/GameTick.kt +++ b/game/src/main/kotlin/GameTick.kt @@ -1,4 +1,5 @@ import com.github.michaelbull.logging.InlineLogger +import content.bot.BotManager import content.social.trade.exchange.GrandExchange import world.gregs.voidps.engine.client.instruction.InstructionHandlers import world.gregs.voidps.engine.client.instruction.InstructionTask @@ -42,6 +43,7 @@ fun getTickStages( sequential: Boolean = CharacterTask.DEBUG, handlers: InstructionHandlers = get(), dynamicZones: DynamicZones = get(), + botManager: BotManager = get(), ): List { val sequentialNpc: TaskIterator = SequentialIterator() val sequentialPlayer: TaskIterator = SequentialIterator() @@ -70,7 +72,7 @@ fun getTickStages( PlayerUpdateTask(), NPCUpdateTask(npcVisualEncoders()), ), - AiTick, + botManager, accountSave, SaveLogs(), ) diff --git a/game/src/main/kotlin/content/bot/ActivitySlots.kt b/game/src/main/kotlin/content/bot/ActivitySlots.kt new file mode 100644 index 0000000000..7cafa7ca67 --- /dev/null +++ b/game/src/main/kotlin/content/bot/ActivitySlots.kt @@ -0,0 +1,19 @@ +package content.bot + +import content.bot.action.BotActivity + +class ActivitySlots { + private val occupied = mutableMapOf() + + fun hasFree(activity: BotActivity): Boolean { + return occupied.getOrDefault(activity.id, 0) < activity.capacity + } + + fun occupy(activity: BotActivity) { + occupied[activity.id] = (occupied.getOrDefault(activity.id, 0) + 1).coerceAtMost(activity.capacity) + } + + fun release(activity: BotActivity) { + occupied[activity.id] = ((occupied[activity.id] ?: 1) - 1).coerceAtLeast(0) + } +} \ No newline at end of file diff --git a/game/src/main/kotlin/content/bot/Bot.kt b/game/src/main/kotlin/content/bot/Bot.kt index 718ea0b3a4..dab530d771 100644 --- a/game/src/main/kotlin/content/bot/Bot.kt +++ b/game/src/main/kotlin/content/bot/Bot.kt @@ -1,9 +1,40 @@ package content.bot +import content.bot.action.BehaviourFrame +import content.bot.action.BehaviourState +import content.bot.action.BotAction +import content.bot.action.BotActivity +import content.bot.action.Reason import world.gregs.voidps.engine.entity.character.Character import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.network.client.Instruction +import java.util.Stack data class Bot(val player: Player) : Character by player { var step: Instruction? = null + + val blocked: MutableSet = mutableSetOf() + var previous: BotActivity? = null + val frames = Stack() + + fun noTask() = frames.isEmpty() + + internal fun action(): BotAction = frames.peek().action() + + internal fun frame(): BehaviourFrame = frames.peek() + + internal fun reset() { + frames.clear() + } + + internal fun queue(frame: BehaviourFrame) { + frames.add(frame) + } + + fun stop() { + if (noTask()) { + return + } + frame().state = BehaviourState.Failed(Reason.Cancelled) + } } diff --git a/game/src/main/kotlin/content/bot/BotManager.kt b/game/src/main/kotlin/content/bot/BotManager.kt new file mode 100644 index 0000000000..8a4d57cb6c --- /dev/null +++ b/game/src/main/kotlin/content/bot/BotManager.kt @@ -0,0 +1,104 @@ +package content.bot + +import com.github.michaelbull.logging.InlineLogger +import content.bot.action.* +import world.gregs.voidps.engine.data.ConfigFiles +import world.gregs.voidps.engine.data.Settings +import world.gregs.voidps.type.random + +class BotManager( + private var activities: Map = emptyMap(), +) : Runnable { + val slots = ActivitySlots() + val bots = mutableListOf() + val logger = InlineLogger("BotManager") + + fun load(files: ConfigFiles): BotManager { + activities = loadActivities(files.list(Settings["bots.definitions"])) + return this + } + + override fun run() { + for (bot in bots) { + tick(bot) + } + } + + fun tick(bot: Bot) { + if (bot.noTask()) { + assignActivity(bot) + return + } + execute(bot) + } + + private fun hasRequirements(bot: Bot, activity: BotActivity): Boolean { + return slots.hasFree(activity) && !bot.blocked.contains(activity.id) && activity.requirements.all { it.satisfied(bot) } + } + + private fun assignActivity(bot: Bot) { + val activity = if (bot.previous != null && hasRequirements(bot, bot.previous!!)) { + bot.previous!! + } else { + activities.values + .filter { hasRequirements(bot, it) } + .randomOrNull(random) ?: return + } + logger.info { "Assigned bot: ${bot.player.accountName} task ${activity.id}." } + slots.occupy(activity) + bot.previous = activity + bot.queue(BehaviourFrame(activity)) + } + + private fun start(bot: Bot, behaviour: Behaviour, frame: BehaviourFrame) { + if (behaviour.requirements.any { !it.satisfied(bot) }) { + frame.fail(Reason.Requirements) + return + } + frame.start(bot) + } + + private fun execute(bot: Bot) { + val frame = bot.frame() + val behaviour = frame.behaviour + when (val state = frame.state) { + BehaviourState.Running -> return + BehaviourState.Pending -> start(bot, behaviour, frame) + BehaviourState.Success -> if (!frame.next()) { + bot.frames.pop() + if (behaviour is BotActivity) { + slots.release(behaviour) + } + } + is BehaviourState.Failed -> { + val action = frame.action() + if (action is BotAction.RetryableAction && action.retryMax > 0) { + frame.state = BehaviourState.Wait(action.retryTicks) + if (frame.retries++ < action.retryMax) { + return + } + } + bot.frames.pop() + if (behaviour is BotActivity) { + bot.blocked.add(behaviour.id) + slots.release(behaviour) + } + } + is BehaviourState.Wait -> { + if (--state.ticks <= 0) { + frame.state = BehaviourState.Pending + } + } + } + } + + private fun stop(bot: Bot) { + for (frame in bot.frames) { + if (frame.behaviour is BotActivity) { + slots.release(frame.behaviour) + } + } + bot.reset() + } + +} \ No newline at end of file diff --git a/game/src/main/kotlin/content/bot/action/Behaviour.kt b/game/src/main/kotlin/content/bot/action/Behaviour.kt new file mode 100644 index 0000000000..b7fca7f6f1 --- /dev/null +++ b/game/src/main/kotlin/content/bot/action/Behaviour.kt @@ -0,0 +1,9 @@ +package content.bot.action + +import content.bot.req.Requirement + +interface Behaviour { + val id: String + val requirements: List + val plan: List +} \ No newline at end of file diff --git a/game/src/main/kotlin/content/bot/action/BehaviourFrame.kt b/game/src/main/kotlin/content/bot/action/BehaviourFrame.kt new file mode 100644 index 0000000000..118f1b9ff9 --- /dev/null +++ b/game/src/main/kotlin/content/bot/action/BehaviourFrame.kt @@ -0,0 +1,39 @@ +package content.bot.action + +import content.bot.Bot + +data class BehaviourFrame( + val behaviour: Behaviour, + var state: BehaviourState = BehaviourState.Pending, + var index: Int = 0, + var retries: Int = 0, + var blocked: MutableSet = mutableSetOf(), +) { + + fun action(): BotAction = behaviour.plan[index] + + fun completed() = index >= behaviour.plan.size + + fun start(bot: Bot) { + val action = action() + action.start(bot) + state = BehaviourState.Running + } + + fun next(): Boolean { + index++ + state = BehaviourState.Pending + return index < behaviour.plan.size + } + + fun fail(reason: Reason) { + state = BehaviourState.Failed(reason) + } + + fun success() { + if (state is BehaviourState.Failed) { + return + } + state = BehaviourState.Success + } +} \ No newline at end of file diff --git a/game/src/main/kotlin/content/bot/action/BehaviourState.kt b/game/src/main/kotlin/content/bot/action/BehaviourState.kt new file mode 100644 index 0000000000..48840324c7 --- /dev/null +++ b/game/src/main/kotlin/content/bot/action/BehaviourState.kt @@ -0,0 +1,9 @@ +package content.bot.action + +sealed interface BehaviourState { + object Pending : BehaviourState + object Running : BehaviourState + object Success : BehaviourState + data class Failed(val reason: Reason) : BehaviourState + data class Wait(var ticks: Int) : BehaviourState +} \ No newline at end of file diff --git a/game/src/main/kotlin/content/bot/action/BotAction.kt b/game/src/main/kotlin/content/bot/action/BotAction.kt new file mode 100644 index 0000000000..8ab5856d99 --- /dev/null +++ b/game/src/main/kotlin/content/bot/action/BotAction.kt @@ -0,0 +1,38 @@ +package content.bot.action + +import content.bot.Bot + +sealed interface BotAction { + open fun start(bot: Bot) { + // TODO here's where code goes, either handle this way or put in an event handler for each type + } + + sealed class RetryableAction : BotAction { + abstract val retryTicks: Int + abstract val retryMax: Int + } + + data class GoTo(val target: String) : BotAction + data class Clone(val id: String) : BotAction + + data class Wait(val ticks: Int) : BotAction + + data class InteractNpc( + val option: String, + val id: String, + override val retryTicks: Int = 0, + override val retryMax: Int = 0, + val radius: Int = 10, + ) : RetryableAction() + + data class InteractObject( + val option: String, + val id: String, + override val retryTicks: Int = 0, + override val retryMax: Int = 0, + val radius: Int = 10, + ) : RetryableAction() + + data class InterfaceOption(val option: String, val id: String) : BotAction + data class WaitFullInventory(val timeout: Int) : BotAction +} \ No newline at end of file diff --git a/game/src/main/kotlin/content/bot/action/BotActivity.kt b/game/src/main/kotlin/content/bot/action/BotActivity.kt new file mode 100644 index 0000000000..69ecbdfc8f --- /dev/null +++ b/game/src/main/kotlin/content/bot/action/BotActivity.kt @@ -0,0 +1,179 @@ +package content.bot.action + +import content.bot.req.CloneRequirement +import content.bot.req.RequiresInvSpace +import content.bot.req.RequiresCarriedItem +import content.bot.req.MandatoryRequirement +import content.bot.req.Requirement +import content.bot.req.RequiresEquippedItem +import content.bot.req.RequiresLocation +import content.bot.req.RequiresOwnedItem +import content.bot.req.RequiresSkill +import content.bot.req.RequiresVariable +import world.gregs.config.Config +import world.gregs.config.ConfigReader +import world.gregs.voidps.engine.timedLoad + +data class BotActivity( + override val id: String, + val capacity: Int, + override val requirements: List = emptyList(), + override val plan: List = emptyList(), +) : Behaviour + +fun loadActivities(paths: List): Map { + val activities = mutableMapOf() + timedLoad("bot activity") { + val clones = mutableMapOf() + for (path in paths) { + Config.fileReader(path) { + while (nextSection()) { + val id = section() + var capacity = 0 + var type = "activity" + var weight = 0 + var actions: List = emptyList() + var requirements: List = emptyList() + while (nextPair()) { + when (val key = key()) { + "requires" -> requirements = requirements() + "plan" -> actions = actions() + "produces" -> produces() + "capacity" -> capacity = int() + "type" -> type = string() + "weight" -> weight = int() + else -> throw IllegalArgumentException("Unexpected key: '$key' ${exception()}") + } + } + val clone = requirements.filterIsInstance().firstOrNull() + if (clone != null) { + clones[id] = clone.id + } + activities[id] = BotActivity(id, capacity, requirements, plan = actions) + } + } + } + for (activity in activities.values) { + for (index in activity.plan.indices.reversed()) { + val action = activity.plan[index] + if (action is BotAction.Clone) { + val list = activities[action.id]?.plan ?: throw IllegalArgumentException("Unable to find activity to clone '${action.id}'.") + val actions = activity.plan as MutableList + actions.removeAt(index) + actions.addAll(index, list) + } + } + for ((id, cloneId) in clones) { + val activity = activities[id] ?: continue + val clone = activities[cloneId] ?: continue + val requirements = activity.requirements as MutableList + requirements.removeIf { it is CloneRequirement && it.id == cloneId } + requirements.addAll(clone.requirements) + } + } + activities.size + } + return activities +} + +private fun ConfigReader.produces() { + while (nextElement()) { + while (nextEntry()) { + val key = key() + val value = value() +// println("$key = $value") + } + } +} + +private fun ConfigReader.requirements(): List { + val list = mutableListOf() + while (nextElement()) { + var type = "" + var id = "" + var value: Any? = null + var min = 1 + var max = 1 + while (nextEntry()) { + when (val key = key()) { + "skill", "carries", "equips", "owns", "variable", "clone", "location" -> { + type = key + id = string() + } + "amount" -> { + min = int() + max = min + } + "min" -> min = int() + "max" -> max = int() + "inventory_space" -> { + type = key + min = int() + } + "value" -> value = value() + } + } + val requirement = when (type) { + "skill" -> RequiresSkill(id, min, max) + "carries" -> RequiresCarriedItem(id, min) + "owns" -> RequiresOwnedItem(id, min) + "equips" -> RequiresEquippedItem(id, min) + "variable" -> RequiresVariable(id, value) + "clone" -> CloneRequirement(id) + "inventory_space" -> RequiresInvSpace(min) + "location" -> RequiresLocation(id) + "holds" -> throw IllegalArgumentException("Unknown requirement type 'holds'; did you mean 'carries' or 'equips'? ${exception()}.") + else -> throw IllegalArgumentException("Unknown requirement type: $type ${exception()}") + } + list.add(requirement) + } + return list +} + +private fun ConfigReader.actions(): List { + val list = mutableListOf() + while (nextElement()) { + var type = "" + var id = "" + var option = "" + var retryTicks = 0 + var retryMax = 0 + var timeout = 0 + var ticks = 0 + var radius = 0 + while (nextEntry()) { + when (val key = key()) { + "go_to", "wait_for", "interface", "npc", "object", "clone" -> { + type = key + id = string() + } + "wait" -> { + type = key + ticks = int() + } + "radius" -> radius = int() + "target", "id" -> id = string() + "retry_ticks" -> retryTicks = int() + "retry_max" -> retryMax = int() + "option" -> option = string() + "timeout" -> timeout = int() + else -> throw IllegalArgumentException("Unknown action key: $key ${exception()}") + } + } + val action = when (type) { + "go_to" -> BotAction.GoTo(id) + "wait" -> BotAction.Wait(ticks) + "npc" -> BotAction.InteractNpc(id, option, retryTicks, retryMax, radius) + "object" -> BotAction.InteractObject(id, option, retryTicks, retryMax, radius) + "interface" -> BotAction.InterfaceOption(option, id) + "clone" -> BotAction.Clone(id) + "wait_for" -> when (id) { + "full_inventory" -> BotAction.WaitFullInventory(timeout) + else -> throw IllegalArgumentException("Unknown wait_for action: $id ${exception()}") + } + else -> throw IllegalArgumentException("Unknown action type: $type ${exception()}") + } + list.add(action) + } + return list +} \ No newline at end of file diff --git a/game/src/main/kotlin/content/bot/action/Reason.kt b/game/src/main/kotlin/content/bot/action/Reason.kt new file mode 100644 index 0000000000..fdbb4a6e09 --- /dev/null +++ b/game/src/main/kotlin/content/bot/action/Reason.kt @@ -0,0 +1,9 @@ +package content.bot.action + +interface Reason { + object Cancelled : HardReason + object Requirements : HardReason +} +interface SoftReason : Reason +interface HardReason : Reason + diff --git a/game/src/main/kotlin/content/bot/req/MandatoryRequirement.kt b/game/src/main/kotlin/content/bot/req/MandatoryRequirement.kt new file mode 100644 index 0000000000..aee71d6983 --- /dev/null +++ b/game/src/main/kotlin/content/bot/req/MandatoryRequirement.kt @@ -0,0 +1,15 @@ +package content.bot.req + +sealed interface MandatoryRequirement : Requirement + +data class RequiresSkill( + val id: String, + val min: Int = 1, + val max: Int = 120 +) : MandatoryRequirement + + +data class RequiresVariable( + val id: String, + val value: Any? = null +) : MandatoryRequirement diff --git a/game/src/main/kotlin/content/bot/req/Requirement.kt b/game/src/main/kotlin/content/bot/req/Requirement.kt new file mode 100644 index 0000000000..22e35e3a78 --- /dev/null +++ b/game/src/main/kotlin/content/bot/req/Requirement.kt @@ -0,0 +1,11 @@ +package content.bot.req + +import content.bot.Bot + +sealed interface Requirement { + fun satisfied(bot: Bot): Boolean = false +} + +data class CloneRequirement( + val id: String, +) : Requirement \ No newline at end of file diff --git a/game/src/main/kotlin/content/bot/req/ResolvableRequirement.kt b/game/src/main/kotlin/content/bot/req/ResolvableRequirement.kt new file mode 100644 index 0000000000..c75ebcd8b1 --- /dev/null +++ b/game/src/main/kotlin/content/bot/req/ResolvableRequirement.kt @@ -0,0 +1,37 @@ +package content.bot.req + +import content.bot.Bot + +sealed interface ResolvableRequirement : Requirement + +sealed interface ItemRequirement : ResolvableRequirement { + val id: String + val amount: Int +} + +data class RequiresEquippedItem( + override val id: String, + override val amount: Int = 1, +) : ItemRequirement { + fun check(bot: Bot) { + // bot.inventory.has(id, amount) + } +} + +data class RequiresCarriedItem( + override val id: String, + override val amount: Int = 1, +) : ItemRequirement + +data class RequiresOwnedItem( + override val id: String, + override val amount: Int = 1, +) : ItemRequirement + +data class RequiresInvSpace( + val amount: Int, +) : ResolvableRequirement + +data class RequiresLocation( + val id: String, +) : ResolvableRequirement \ No newline at end of file diff --git a/game/src/main/resources/game.properties b/game/src/main/resources/game.properties index 5065cd0967..d55737fe87 100644 --- a/game/src/main/resources/game.properties +++ b/game/src/main/resources/game.properties @@ -253,6 +253,8 @@ bots.names=./data/bot_names.txt # Prefix at the start of bot's names (blank to hide) bots.namePrefix="" +# File ending for bot definitions +bots.definitions=bots.toml #=================================== # Storage & File System diff --git a/game/src/test/kotlin/content/bot/ActivitySlotsTest.kt b/game/src/test/kotlin/content/bot/ActivitySlotsTest.kt new file mode 100644 index 0000000000..83e778244b --- /dev/null +++ b/game/src/test/kotlin/content/bot/ActivitySlotsTest.kt @@ -0,0 +1,42 @@ +package content.bot + +import content.bot.action.BotActivity +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +class ActivitySlotsTest { + + lateinit var slots: ActivitySlots + + @BeforeEach + fun setup() { + slots = ActivitySlots() + } + + @Test + fun `Unoccupied slots are free`() { + val activity = BotActivity("test", 2) + + assertTrue(slots.hasFree(activity)) + } + + @Test + fun `Occupied slots aren't free`() { + val activity = BotActivity("test", 1) + + slots.occupy(activity) + + assertFalse(slots.hasFree(activity)) + } + + @Test + fun `Released slots are free`() { + val activity = BotActivity("test", 1) + + slots.occupy(activity) + slots.release(activity) + + assertTrue(slots.hasFree(activity)) + } +} \ No newline at end of file diff --git a/game/src/test/kotlin/content/bot/BotManagerTest.kt b/game/src/test/kotlin/content/bot/BotManagerTest.kt new file mode 100644 index 0000000000..464df46420 --- /dev/null +++ b/game/src/test/kotlin/content/bot/BotManagerTest.kt @@ -0,0 +1,233 @@ +package content.bot + +import content.bot.action.BehaviourFrame +import content.bot.action.BehaviourState +import content.bot.action.BotAction +import content.bot.action.BotActivity +import content.bot.action.Reason +import content.bot.req.MandatoryRequirement +import content.bot.req.RequiresSkill +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import world.gregs.voidps.engine.entity.character.player.Player + +class BotManagerTest { + + fun testBot(name: String = "bot") = Bot(Player(accountName = name)) + + @Test + fun `Idle bot gets assigned an activity`() { + val activity = testActivity( + id = "woodcutting", + plan = listOf(BotAction.Wait(1)) + ) + val manager = BotManager(mapOf(activity.id to activity)) + val bot = testBot() + + manager.tick(bot) + + assertEquals(1, bot.frames.size) + assertEquals(activity, bot.previous) + } + + @Test + fun `Activity capacity is respected`() { + val activity = testActivity( + id = "mine", + plan = listOf(BotAction.Wait(1)) + ) + val manager = BotManager(mapOf(activity.id to activity)) + + val bot1 = testBot("bot1") + val bot2 = testBot("bot2") + + manager.tick(bot1) + manager.tick(bot2) + + assertEquals(1, bot1.frames.size) + assertTrue(bot2.frames.isEmpty()) + } + + @Test + fun `Pending frame starts running`() { + val activity = testActivity( + id = "walk", + plan = listOf(BotAction.Wait(1)) + ) + val manager = BotManager(mapOf(activity.id to activity)) + val bot = testBot() + + manager.tick(bot) + manager.tick(bot) + + assertEquals(BehaviourState.Running, bot.frame().state) + } + + @Test + fun `Success advances frame index`() { + val activity = testActivity( + id = "task", + plan = listOf( + BotAction.Wait(1), + BotAction.Wait(1) + ) + ) + val frame = BehaviourFrame(activity) + frame.start(testBot()) + frame.success() + + + val advanced = frame.next() + + + assertTrue(advanced) + assertEquals(1, frame.index) + assertEquals(BehaviourState.Pending, frame.state) + } + + @Test + fun `Retryable action retries before failing`() { + val action = BotAction.InteractNpc( + option = "Talk", + id = "npc", + retryTicks = 2, + retryMax = 2 + ) + + val activity = testActivity( + id = "talk", + plan = listOf(action) + ) + + val manager = BotManager(mapOf(activity.id to activity)) + val bot = testBot() + + manager.tick(bot) + manager.tick(bot) + + val frame = bot.frame() + repeat(3) { + frame.fail(Reason.Requirements) + manager.tick(bot) + assertTrue(frame.state is BehaviourState.Wait) + manager.tick(bot) // Tick 1 + manager.tick(bot) // Tick 2 + manager.tick(bot) // Pending + } + + // after retries exhausted → popped + assertTrue(bot.frames.isEmpty()) + assertTrue("talk" in bot.blocked) + } + + @Test + fun `Activity slot released on success`() { + val activity = testActivity( + id = "cook", + plan = listOf(BotAction.Wait(1)) + ) + + val manager = BotManager(mapOf(activity.id to activity)) + val bot = testBot() + + manager.tick(bot) + manager.tick(bot) + + bot.frame().success() + manager.tick(bot) + assertTrue(manager.slots.hasFree(activity)) + assertEquals(0, bot.frames.size) + } + + @Test + fun `Completed activity most likely to be reassigned on success`() { + val activity = testActivity( + id = "cook", + plan = listOf(BotAction.Wait(1)) + ) + val test = testActivity( + id = "test", + plan = listOf(BotAction.Wait(1)) + ) + + val activities = mutableMapOf(activity.id to activity, test.id to test) + val manager = BotManager(activities) + val bot = testBot() + + bot.previous = activity + + manager.tick(bot) + + assertFalse(manager.slots.hasFree(activity)) + assertEquals(activity, bot.previous) + assertEquals(activity, bot.frames.peek().behaviour) + assertEquals(1, bot.frames.size) + } + + @Test + fun `Activity slot released on failure`() { + val activity = testActivity( + id = "smith", + plan = listOf(BotAction.Wait(1)) + ) + + val manager = BotManager(mapOf(activity.id to activity)) + val bot = testBot() + + manager.tick(bot) + manager.tick(bot) + + bot.frame().fail(Reason.Cancelled) + manager.tick(bot) + + assertTrue(bot.frames.isEmpty()) + } + + @Test + fun `Failed activity is blocked`() { + val activity = testActivity( + id = "fish", + plan = listOf(BotAction.Wait(1)) + ) + + val manager = BotManager(mapOf(activity.id to activity)) + val bot = testBot() + + manager.tick(bot) + manager.tick(bot) + bot.frame().fail(Reason.Requirements) + manager.tick(bot) + manager.tick(bot) + + assertTrue(bot.frames.isEmpty()) + assertTrue("fish" in bot.blocked) + } + + @Test + fun `Behaviour without requirements isn't started`() { + val activity = testActivity( + id = "test", + requirements = listOf( + RequiresSkill("attack", 99, 99) + ), + plan = listOf(BotAction.Wait(4)) + ) + + val manager = BotManager(mapOf(activity.id to activity)) + val bot = testBot() + bot.frames.add(BehaviourFrame(activity)) + + manager.tick(bot) + assertTrue(bot.frame().state is BehaviourState.Failed) + manager.tick(bot) + + assertTrue(bot.frames.isEmpty()) + assertTrue("test" in bot.blocked) + } + + fun testActivity( + id: String, + requirements: List = emptyList(), + plan: List + ) = BotActivity(id, 1, requirements, plan) +} \ No newline at end of file From b214ff36ec9b6ce85f9a8ad1b2d1280c5cefda2f Mon Sep 17 00:00:00 2001 From: GregHib Date: Tue, 27 Jan 2026 14:20:47 +0000 Subject: [PATCH 008/101] Add behaviour templating --- data/bot/woodcutting.bots.toml | 82 ++-- .../content/bot/action/BehaviourFragment.kt | 152 ++++++++ .../kotlin/content/bot/action/BotAction.kt | 3 +- .../kotlin/content/bot/action/BotActivity.kt | 184 +++++++-- .../kotlin/content/bot/req/Requirement.kt | 7 +- .../content/bot/req/ResolvableRequirement.kt | 7 + game/src/test/kotlin/WorldTest.kt | 2 +- .../bot/action/BehaviourFragmentTest.kt | 367 ++++++++++++++++++ 8 files changed, 722 insertions(+), 82 deletions(-) create mode 100644 game/src/main/kotlin/content/bot/action/BehaviourFragment.kt create mode 100644 game/src/test/kotlin/content/bot/action/BehaviourFragmentTest.kt diff --git a/data/bot/woodcutting.bots.toml b/data/bot/woodcutting.bots.toml index a53a58035c..22ba6ed31b 100644 --- a/data/bot/woodcutting.bots.toml +++ b/data/bot/woodcutting.bots.toml @@ -1,99 +1,70 @@ -[woodcutting_ready] +[woodcutting_template] requires = [ - { carries = "bronze_hatchet,iron_hatchet,steel_hatchet" }, + { carries = "$hatchet" }, + { location = "$location" }, { inventory_space = 20 }, ] - -[bank_all_after_full] plan = [ - { wait_for = "full_inventory" }, - { go_to = "bank" }, - { option = "Bank-quickly", npc = "banker*" }, - { wait = 2, interface = "bank" }, - { option = "Deposit-All", interface = "bank:inventory" }, + { option = "Chop-down", object = "$tree", retry_ticks = "$wait", retry_max = 10 }, ] -[chop_normal_trees] -type = "activity" +[normal_trees] +template = "woodcutting_template" capacity = 4 +fields = { hatchet = "bronze_hatchet,iron_hatchet", location = "normal_trees", tree = "tree*", wait = 10 } requires = [ - { clone = "woodcutting_ready" }, - { location = "normal_tree_area" }, { skill = "woodcutting", min = 1, max = 15 }, ] -plan = [ - { option = "Chop-down", object = "tree*", retry_ticks = 5, retry_max = 10 }, - { clone = "bank_all_after_full" }, -] produces = [ { item = "logs" } ] -[chop_oak_trees] -type = "activity" +[oak_trees] +template = "woodcutting_template" capacity = 4 +fields = { hatchet = "steel_hatchet", location = "oak_trees", tree = "oak_tree", wait = 10 } requires = [ - { clone = "woodcutting_ready" }, - { location = "oak_tree_area" }, { skill = "woodcutting", min = 15, max = 30 }, ] -plan = [ - { option = "Chop-down", object = "oak_tree", retry_ticks = 15, retry_max = 10 }, - { clone = "bank_all_after_full" }, -] produces = [ - { item = "logs" } + { item = "oak_logs" } ] -[axe_shopping] +[buy_from_shop] requires = [ - { location = "axe_store" }, + { carries = "coins", amount = "$cost" }, + { location = "$shop_location" }, { inventory_space = 1 }, ] plan = [ - { option = "Trade", npc = "axe_store_owner" }, + { option = "Trade", npc = "$shopkeeper" }, + { option = "Buy-1", interface = "shop:inventory:$item" }, +] +produces = [ + { item = "$item" } ] -[buy_bronze_axe] +[buy_iron_hatchet] type = "resolver" +template = "buy_from_shop" +fields = { cost = 25, shop_location = "axe_shop", shopkeeper = "bob", item = "iron_hatchet" } requires = [ - { clone = "axe_shopping" }, - { skill = "woodcutting", min = 5 }, - { carries = "coins", amount = 25 }, -] -plan = [ - { clone = "axe_shopping" }, - { option = "Buy-1", interface = "axe_shop:inventory" }, -] -produces = [ - { item = "bronze_axe" } + { skill = "woodcutting", min = 1 }, ] -[buy_iron_axe] +[buy_steel_hatchet] type = "resolver" +template = "buy_from_shop" +fields = { cost = 50, shop_location = "axe_shop", shopkeeper = "bob", item = "steel_hatchet" } requires = [ - { clone = "axe_shopping" }, { skill = "woodcutting", min = 10 }, - { carries = "coins", amount = 150 }, ] -plan = [ - { clone = "axe_shopping" }, - { option = "Buy-1", interface = "axe_shop:inventory:iron_axe" }, -] -produces = [ - { item = "iron_axe" } -] - -# equips -> equipped -# carries -> on person -# owns -> in bank or on person [ring_of_duelling_equipped] type = "teleport" weight = 10 requires = [ - { variable = "spellbook", value = "normal" }, { equips = "ring_of_dueling_*" } ] plan = [ @@ -118,7 +89,6 @@ produces = [ { location = "castle_wars_teleport" } ] - [teleport_varrock] type = "teleport" weight = 10 diff --git a/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt b/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt new file mode 100644 index 0000000000..453c9df454 --- /dev/null +++ b/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt @@ -0,0 +1,152 @@ +package content.bot.action + +import content.bot.req.CloneRequirement +import content.bot.req.Requirement +import content.bot.req.RequiresCarriedItem +import content.bot.req.RequiresEquippedItem +import content.bot.req.RequiresInvSpace +import content.bot.req.RequiresLocation +import content.bot.req.RequiresOwnedItem +import content.bot.req.RequiresReference +import content.bot.req.RequiresSkill +import content.bot.req.RequiresTile +import content.bot.req.RequiresVariable + +data class BehaviourFragment( + override val id: String, + val capacity: Int, + var template: String, + override val requirements: List = emptyList(), + override val plan: List = emptyList(), + val fields: Map = emptyMap(), +) : Behaviour { + fun resolveActions(template: BotActivity, actions: MutableList) { + for (action in template.plan) { + val resolved = when (action) { + is BotAction.Reference -> when (val copy = action.action) { + is BotAction.GoTo -> BotAction.GoTo(resolve(action.references["go_to"], copy.target)) + is BotAction.InterfaceOption -> BotAction.InterfaceOption( + id = resolve(action.references["interface"], copy.id), + option = resolve(action.references["option"], copy.option), + ) + is BotAction.InteractNpc -> BotAction.InteractNpc( + option = resolve(action.references["option"], copy.option), + id = resolve(action.references["npc"], copy.id), + retryTicks = resolve(action.references["retry_ticks"], copy.retryTicks), + retryMax = resolve(action.references["retry_max"], copy.retryMax), + radius = resolve(action.references["radius"], copy.radius), + ) + is BotAction.InteractObject -> BotAction.InteractObject( + option = resolve(action.references["option"], copy.option), + id = resolve(action.references["object"], copy.id), + retryTicks = resolve(action.references["retry_ticks"], copy.retryTicks), + retryMax = resolve(action.references["retry_max"], copy.retryMax), + radius = resolve(action.references["radius"], copy.radius), + ) + is BotAction.Wait -> BotAction.Wait(resolve(action.references["wait"], copy.ticks)) + is BotAction.WaitFullInventory -> BotAction.WaitFullInventory(resolve(action.references["timeout"], copy.timeout)) + is BotAction.Clone, is BotAction.Reference -> throw IllegalArgumentException("Invalid reference action type: ${action.action::class.simpleName}.") + } + is BotAction.Clone -> throw IllegalArgumentException("Unresolved clone action in template ${id}.") + else -> action + } + actions.add(resolved) + } + } + + fun resolveRequirements(template: BotActivity, requirements: MutableList) { + for (req in template.requirements) { + val resolved = when (req) { + is RequiresReference -> when (val requirement = req.requirement) { + is RequiresSkill -> RequiresSkill( + id = resolve(req.references["skill"], requirement.id), + min = resolve(req.references["min"], requirement.min), + max = resolve(req.references["max"], requirement.max), + ) + is RequiresVariable -> RequiresVariable( + id = resolve(req.references["variable"], requirement.id), + value = resolve(req.references["value"], requirement.value), + ) + is RequiresCarriedItem -> RequiresCarriedItem( + id = resolve(req.references["carries"], requirement.id), + amount = resolve(req.references["amount"], requirement.amount), + ) + is RequiresEquippedItem -> RequiresEquippedItem( + id = resolve(req.references["equips"], requirement.id), + amount = resolve(req.references["amount"], requirement.amount), + ) + is RequiresOwnedItem -> RequiresOwnedItem( + id = resolve(req.references["owns"], requirement.id), + amount = resolve(req.references["amount"], requirement.amount), + ) + is RequiresInvSpace -> RequiresInvSpace( + amount = resolve(req.references["inventory_space"], requirement.amount), + ) + is RequiresLocation -> RequiresLocation( + id = resolve(req.references["location"], requirement.id), + ) + is RequiresTile -> RequiresTile( + x = resolve(req.references["x"], requirement.x), + y = resolve(req.references["y"], requirement.y), + level = resolve(req.references["level"], requirement.level), + radius = resolve(req.references["radius"], requirement.radius), + ) + is CloneRequirement, is RequiresReference -> throw IllegalArgumentException("Invalid requirement type: ${req.requirement::class.simpleName}.") + } + is CloneRequirement -> throw IllegalArgumentException("Unresolved clone requirement in template ${id}.") + else -> req + } + requirements.add(resolved) + } + } + + private fun String.key(): String { + if (startsWith('$')) { + return substring(1) + } + val index = indexOf($$"${") + if (index == -1) { + return substringAfter('$') + } + val end = indexOf('}', index + 2) + return substring(index + 2, end) + } + + private fun String.reference(): String { + if (startsWith('$')) { + return this + } + val index = indexOf($$"${") + if (index == -1) { + return "\$${substringAfter('$')}" + } + val end = indexOf('}', index) + 1 + return substring(index, end) + } + + private fun resolve(reference: String?, default: Int): Int { + return if (reference != null) { + fields[reference.key()] as? Int ?: throw IllegalArgumentException("Unable to find field '$reference' in ${id}.") + } else { + default + } + } + + private fun resolve(reference: String?, default: String): String { + return if (reference != null) { + val key = reference.key() + val value = fields[key] as? String ?: throw IllegalArgumentException("Unable to find field '$reference' in ${id}.") + reference.replace(reference.reference(), value) + } else { + default + } + } + + private fun resolve(reference: String?, default: Any?): Any? { + return if (reference != null) { + fields[reference.key()] ?: throw IllegalArgumentException("Unable to find field '$reference' in ${id}.") + } else { + default + } + } +} \ No newline at end of file diff --git a/game/src/main/kotlin/content/bot/action/BotAction.kt b/game/src/main/kotlin/content/bot/action/BotAction.kt index 8ab5856d99..bcbc8c1889 100644 --- a/game/src/main/kotlin/content/bot/action/BotAction.kt +++ b/game/src/main/kotlin/content/bot/action/BotAction.kt @@ -14,6 +14,7 @@ sealed interface BotAction { data class GoTo(val target: String) : BotAction data class Clone(val id: String) : BotAction + data class Reference(val action: BotAction, val references: Map) : BotAction data class Wait(val ticks: Int) : BotAction @@ -33,6 +34,6 @@ sealed interface BotAction { val radius: Int = 10, ) : RetryableAction() - data class InterfaceOption(val option: String, val id: String) : BotAction + data class InterfaceOption(val id: String, val option: String) : BotAction data class WaitFullInventory(val timeout: Int) : BotAction } \ No newline at end of file diff --git a/game/src/main/kotlin/content/bot/action/BotActivity.kt b/game/src/main/kotlin/content/bot/action/BotActivity.kt index 69ecbdfc8f..acd2129f4f 100644 --- a/game/src/main/kotlin/content/bot/action/BotActivity.kt +++ b/game/src/main/kotlin/content/bot/action/BotActivity.kt @@ -1,14 +1,15 @@ package content.bot.action import content.bot.req.CloneRequirement +import content.bot.req.RequiresReference import content.bot.req.RequiresInvSpace import content.bot.req.RequiresCarriedItem -import content.bot.req.MandatoryRequirement import content.bot.req.Requirement import content.bot.req.RequiresEquippedItem import content.bot.req.RequiresLocation import content.bot.req.RequiresOwnedItem import content.bot.req.RequiresSkill +import content.bot.req.RequiresTile import content.bot.req.RequiresVariable import world.gregs.config.Config import world.gregs.config.ConfigReader @@ -23,6 +24,7 @@ data class BotActivity( fun loadActivities(paths: List): Map { val activities = mutableMapOf() + val fragments = mutableMapOf() timedLoad("bot activity") { val clones = mutableMapOf() for (path in paths) { @@ -30,10 +32,12 @@ fun loadActivities(paths: List): Map { while (nextSection()) { val id = section() var capacity = 0 + var template: String? = null var type = "activity" var weight = 0 var actions: List = emptyList() var requirements: List = emptyList() + var fields: Map = emptyMap() while (nextPair()) { when (val key = key()) { "requires" -> requirements = requirements() @@ -41,7 +45,9 @@ fun loadActivities(paths: List): Map { "produces" -> produces() "capacity" -> capacity = int() "type" -> type = string() + "template" -> template = string() "weight" -> weight = int() + "fields" -> fields = fields() else -> throw IllegalArgumentException("Unexpected key: '$key' ${exception()}") } } @@ -49,11 +55,16 @@ fun loadActivities(paths: List): Map { if (clone != null) { clones[id] = clone.id } - activities[id] = BotActivity(id, capacity, requirements, plan = actions) + if (template != null) { + fragments[id] = BehaviourFragment(id, capacity, template, requirements, plan = actions, fields = fields) + } else { + activities[id] = BotActivity(id, capacity, requirements, plan = actions) + } } } } - for (activity in activities.values) { + // Resolve cloning first + for (activity in activities.values + fragments.values) { for (index in activity.plan.indices.reversed()) { val action = activity.plan[index] if (action is BotAction.Clone) { @@ -71,6 +82,26 @@ fun loadActivities(paths: List): Map { requirements.addAll(clone.requirements) } } + // Fragments are partially filled behaviours with template + fields + // This code resolves those fields into actual values taken from the template. + val templates = mutableSetOf() + for ((id, fragment) in fragments) { + val template = activities[fragment.template] ?: throw IllegalArgumentException("Unable to find template '${fragment.template}' for activity '$id'.") + templates.add(fragment.template) + + val requirements = mutableListOf() + requirements.addAll(fragment.requirements) + fragment.resolveRequirements(template, requirements) + + val actions = mutableListOf() + actions.addAll(fragment.plan) + fragment.resolveActions(template, actions) + activities[id] = BotActivity(id, fragment.capacity, requirements, actions) + } + // Templates aren't selectable activities + for (template in templates) { + activities.remove(template) + } activities.size } return activities @@ -86,6 +117,16 @@ private fun ConfigReader.produces() { } } +private fun ConfigReader.fields(): Map { + val map = mutableMapOf() + while (nextEntry()) { + val key = key() + val value = value() + map[key] = value + } + return map +} + private fun ConfigReader.requirements(): List { val list = mutableListOf() while (nextElement()) { @@ -94,26 +135,81 @@ private fun ConfigReader.requirements(): List { var value: Any? = null var min = 1 var max = 1 + var x = 0 + var y = 0 + var level = 0 + val references = mutableMapOf() while (nextEntry()) { when (val key = key()) { "skill", "carries", "equips", "owns", "variable", "clone", "location" -> { type = key id = string() + if (id.contains('$')) { + references[key] = id + } } - "amount" -> { - min = int() - max = min + "amount" -> when (val value = value()) { + is Int -> { + min = value + max = value + } + is String if value.contains('$') -> references[key] = value + else -> throw IllegalArgumentException("Invalid '$key' value: $value ${exception()}") + } + "min" -> when (val value = value()) { + is Int -> min = value + is String if value.contains('$') -> references[key] = value + else -> throw IllegalArgumentException("Invalid '$key' value: $value ${exception()}") + } + "max" -> when (val value = value()) { + is Int -> max = value + is String if value.contains('$') -> references[key] = value + else -> throw IllegalArgumentException("Invalid '$key' value: $value ${exception()}") } - "min" -> min = int() - "max" -> max = int() "inventory_space" -> { type = key - min = int() + when (val value = value()) { + is Int -> min = value + is String if value.contains('$') -> references[key] = value + else -> throw IllegalArgumentException("Invalid '$key' value: $value ${exception()}") + } + } + "x" -> { + type = "tile" + when (val value = value()) { + is Int -> x = value + is String if value.contains('$') -> references[key] = value + else -> throw IllegalArgumentException("Invalid '$key' value: $value ${exception()}") + } + } + "y" -> { + type = "tile" + when (val value = value()) { + is Int -> y = value + is String if value.contains('$') -> references[key] = value + else -> throw IllegalArgumentException("Invalid '$key' value: $value ${exception()}") + } + } + "level" -> { + type = "tile" + when (val value = value()) { + is Int -> level = value + is String if value.contains('$') -> references[key] = value + else -> throw IllegalArgumentException("Invalid '$key' value: $value ${exception()}") + } + } + "radius" -> { + type = "tile" + when (val value = value()) { + is Int -> min = value + is String if value.contains('$') -> references[key] = value + else -> throw IllegalArgumentException("Invalid '$key' value: $value ${exception()}") + } } "value" -> value = value() } } - val requirement = when (type) { + var requirement = when (type) { "skill" -> RequiresSkill(id, min, max) "carries" -> RequiresCarriedItem(id, min) "owns" -> RequiresOwnedItem(id, min) @@ -122,9 +218,13 @@ private fun ConfigReader.requirements(): List { "clone" -> CloneRequirement(id) "inventory_space" -> RequiresInvSpace(min) "location" -> RequiresLocation(id) + "tile" -> RequiresTile(x, y, level, min) "holds" -> throw IllegalArgumentException("Unknown requirement type 'holds'; did you mean 'carries' or 'equips'? ${exception()}.") else -> throw IllegalArgumentException("Unknown requirement type: $type ${exception()}") } + if (references.isNotEmpty()) { + requirement = RequiresReference(requirement, references) + } list.add(requirement) } return list @@ -140,32 +240,67 @@ private fun ConfigReader.actions(): List { var retryMax = 0 var timeout = 0 var ticks = 0 - var radius = 0 + var radius = 10 + val references = mutableMapOf() while (nextEntry()) { when (val key = key()) { "go_to", "wait_for", "interface", "npc", "object", "clone" -> { type = key id = string() + if (id.contains('$')) { + references[key] = id + } + } + "target", "id" -> { + id = string() + if (id.contains('$')) { + references[key] = id + } + } + "option" -> { + option = string() + if (option.contains('$')) { + references[key] = option + } } "wait" -> { type = key - ticks = int() - } - "radius" -> radius = int() - "target", "id" -> id = string() - "retry_ticks" -> retryTicks = int() - "retry_max" -> retryMax = int() - "option" -> option = string() - "timeout" -> timeout = int() + when (val value = value()) { + is Int -> ticks = value + is String if value.contains('$') -> references[key] = value + else -> throw IllegalArgumentException("Invalid '$key' value: $value ${exception()}") + } + } + "radius" -> when (val value = value()) { + is Int -> radius = value + is String if value.contains('$') -> references[key] = value + else -> throw IllegalArgumentException("Invalid '$key' value: $value ${exception()}") + } + "retry_ticks" -> when (val value = value()) { + is Int -> retryTicks = value + is String if value.contains('$') -> references[key] = value + else -> throw IllegalArgumentException("Invalid '$key' value: $value ${exception()}") + } + "retry_max" -> when (val value = value()) { + is Int -> retryMax = value + is String if value.contains('$') -> references[key] = value + else -> throw IllegalArgumentException("Invalid '$key' value: $value ${exception()}") + + } + "timeout" -> when (val value = value()) { + is Int -> timeout = value + is String if value.contains('$') -> references[key] = value + else -> throw IllegalArgumentException("Invalid '$key' value: $value ${exception()}") + } else -> throw IllegalArgumentException("Unknown action key: $key ${exception()}") } } - val action = when (type) { + var action = when (type) { "go_to" -> BotAction.GoTo(id) "wait" -> BotAction.Wait(ticks) - "npc" -> BotAction.InteractNpc(id, option, retryTicks, retryMax, radius) - "object" -> BotAction.InteractObject(id, option, retryTicks, retryMax, radius) - "interface" -> BotAction.InterfaceOption(option, id) + "npc" -> BotAction.InteractNpc(id = id, option = option, retryTicks = retryTicks, retryMax = retryMax, radius = radius) + "object" -> BotAction.InteractObject(id = id, option = option, retryTicks = retryTicks, retryMax = retryMax, radius = radius) + "interface" -> BotAction.InterfaceOption(id = id, option = option) "clone" -> BotAction.Clone(id) "wait_for" -> when (id) { "full_inventory" -> BotAction.WaitFullInventory(timeout) @@ -173,6 +308,9 @@ private fun ConfigReader.actions(): List { } else -> throw IllegalArgumentException("Unknown action type: $type ${exception()}") } + if (references.isNotEmpty()) { + action = BotAction.Reference(action, references) + } list.add(action) } return list diff --git a/game/src/main/kotlin/content/bot/req/Requirement.kt b/game/src/main/kotlin/content/bot/req/Requirement.kt index 22e35e3a78..adb38deb1f 100644 --- a/game/src/main/kotlin/content/bot/req/Requirement.kt +++ b/game/src/main/kotlin/content/bot/req/Requirement.kt @@ -6,6 +6,11 @@ sealed interface Requirement { fun satisfied(bot: Bot): Boolean = false } -data class CloneRequirement( +internal data class CloneRequirement( val id: String, +) : Requirement + +internal data class RequiresReference( + var requirement: Requirement, + val references: Map, ) : Requirement \ No newline at end of file diff --git a/game/src/main/kotlin/content/bot/req/ResolvableRequirement.kt b/game/src/main/kotlin/content/bot/req/ResolvableRequirement.kt index c75ebcd8b1..a9352b8ea7 100644 --- a/game/src/main/kotlin/content/bot/req/ResolvableRequirement.kt +++ b/game/src/main/kotlin/content/bot/req/ResolvableRequirement.kt @@ -34,4 +34,11 @@ data class RequiresInvSpace( data class RequiresLocation( val id: String, +) : ResolvableRequirement + +data class RequiresTile( + val x: Int, + val y: Int, + val level: Int, + val radius: Int, ) : ResolvableRequirement \ No newline at end of file diff --git a/game/src/test/kotlin/WorldTest.kt b/game/src/test/kotlin/WorldTest.kt index 92ac3d659c..7ffd426c49 100644 --- a/game/src/test/kotlin/WorldTest.kt +++ b/game/src/test/kotlin/WorldTest.kt @@ -263,7 +263,7 @@ abstract class WorldTest : KoinTest { properties["storage.grand.exchange.offers.claim.path"] = "../temp/data/test-grand_exchange/claimable_offers.toml" properties["storage.grand.exchange.offers.path"] = "../temp/data/test-grand_exchange/offers.toml" properties["storage.grand.exchange.history.path"] = "../temp/data/test-grand_exchange/price_history/" - properties["storage.caching.path"] = "../data/.temp" + properties["storage.caching.path"] = "../data/.temp/" properties["quests.requirements.skipMissing"] = false properties["grandExchange.priceLimit"] = true properties["world.npcs.randomWalk"] = false diff --git a/game/src/test/kotlin/content/bot/action/BehaviourFragmentTest.kt b/game/src/test/kotlin/content/bot/action/BehaviourFragmentTest.kt new file mode 100644 index 0000000000..f4e2093800 --- /dev/null +++ b/game/src/test/kotlin/content/bot/action/BehaviourFragmentTest.kt @@ -0,0 +1,367 @@ +package content.bot.action + +import content.bot.req.CloneRequirement +import content.bot.req.Requirement +import content.bot.req.RequiresCarriedItem +import content.bot.req.RequiresEquippedItem +import content.bot.req.RequiresInvSpace +import content.bot.req.RequiresLocation +import content.bot.req.RequiresOwnedItem +import content.bot.req.RequiresReference +import content.bot.req.RequiresSkill +import content.bot.req.RequiresTile +import content.bot.req.RequiresVariable +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.DynamicTest.dynamicTest +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestFactory +import org.junit.jupiter.api.assertThrows + +class BehaviourFragmentTest { + + private fun fragment(fields: Map = emptyMap()) = + BehaviourFragment( + id = "test", + capacity = 1, + template = "tpl", + fields = fields + ) + + /* + Actions + */ + + @Test + fun `Missing action field reference throws`() { + val fragment = fragment() + val template = BotActivity( + id = "a", + capacity = 1, + plan = listOf( + BotAction.Reference( + BotAction.GoTo("x"), + references = mapOf("go_to" to "missing") + ) + ) + ) + + assertThrows { + fragment.resolveActions(template, mutableListOf()) + } + } + + @Test + fun `Nested clone action throws`() { + val fragment = fragment() + val template = BotActivity( + id = "a", + capacity = 1, + plan = listOf(BotAction.Clone("x")) + ) + assertThrows { + fragment.resolveActions(template, mutableListOf()) + } + } + + @Test + fun `Nested reference action throws`() { + val fragment = fragment() + val template = BotActivity( + id = "a", + capacity = 1, + plan = listOf( + BotAction.Reference( + BotAction.Reference( + BotAction.GoTo("x"), + emptyMap() + ), + emptyMap() + ) + ) + ) + assertThrows { + fragment.resolveActions(template, mutableListOf()) + } + } + + @Test + fun `Wrong type in action field throws`() { + val fragment = fragment(mapOf("dest" to 123)) + val template = BotActivity( + id = "a", + capacity = 1, + plan = listOf( + BotAction.Reference( + BotAction.GoTo("x"), + references = mapOf("go_to" to "dest") + ) + ) + ) + + assertThrows { + fragment.resolveActions(template, mutableListOf()) + } + } + + @Test + fun `No references uses default value`() { + val fragment = fragment() + val template = BotActivity( + id = "a", + capacity = 1, + plan = listOf( + BotAction.Reference( + BotAction.Wait(10), + references = emptyMap() + ) + ) + ) + + val actions = mutableListOf() + fragment.resolveActions(template, actions) + + assertEquals(BotAction.Wait(10), actions.single()) + } + + @Test + fun `Non-reference action is added`() { + val fragment = fragment() + val template = BotActivity( + id = "a", + capacity = 1, + plan = listOf( + BotAction.Wait(10) + ) + ) + + val actions = mutableListOf() + fragment.resolveActions(template, actions) + + assertEquals(BotAction.Wait(10), actions.single()) + } + + @TestFactory + fun `Resolve action references`() = listOf( + Triple(BotAction.GoTo("default"), mapOf("go_to" to "lumbridge"), BotAction.GoTo("lumbridge")), + Triple(BotAction.InterfaceOption(option = "click", id = "something"), mapOf("option" to "Open", "interface" to "bank"), BotAction.InterfaceOption(option = "Open", id = "bank")), + Triple( + BotAction.InteractNpc( + option = "talk", + id = "default", + retryTicks = 1, + retryMax = 2, + radius = 10 + ), + mapOf("option" to "Talk-to", "npc" to "bob", "retry_ticks" to 2, "retry_max" to 5, "radius" to 5), + BotAction.InteractNpc( + option = "Talk-to", + id = "bob", + retryTicks = 2, + retryMax = 5, + radius = 5 + ) + ), + Triple( + BotAction.InteractObject( + option = "interact", + id = "default", + retryTicks = 1, + retryMax = 2, + radius = 10 + ), + mapOf("option" to "Open", "object" to "door", "retry_ticks" to 2, "retry_max" to 5, "radius" to 5), + BotAction.InteractObject( + option = "Open", + id = "door", + retryTicks = 2, + retryMax = 5, + radius = 5 + ) + ), + Triple(BotAction.Wait(4), mapOf("wait" to 5), BotAction.Wait(5)), + Triple(BotAction.WaitFullInventory(4), mapOf("timeout" to 5), BotAction.WaitFullInventory(5)), + ).map { (default, values, expected) -> + dynamicTest("Resolve ${default::class.simpleName} references") { + val fields = values.mapKeys { "\$ref_${it.key}" } + val fragment = fragment(fields) + val references = values.map { it.key to "ref_${it.key}" }.toMap() + val template = BotActivity( + id = "a", + capacity = 1, + plan = listOf( + BotAction.Reference( + default, + references = references + ) + ) + ) + val actions = mutableListOf() + fragment.resolveActions(template, actions) + assertEquals(expected, actions.single()) + } + } + + @Test + fun `Resolve partial references`() { + val fragment = fragment(mapOf("type" to "fun")) + val template = BotActivity( + id = "a", + capacity = 1, + requirements = listOf( + RequiresReference( + RequiresLocation("default"), + references = mapOf( + "location" to $$"some_${type}_area" + ) + ) + ) + ) + val actions = mutableListOf() + fragment.resolveRequirements(template, actions) + assertEquals(RequiresLocation("some_fun_area"), actions.single()) + } + + @Test + fun `Resolve ending reference`() { + val fragment = fragment(mapOf("area_type" to "fun")) + val template = BotActivity( + id = "a", + capacity = 1, + requirements = listOf( + RequiresReference( + RequiresLocation("default"), + references = mapOf( + "location" to $$"some_$area_type" + ) + ) + ) + ) + val actions = mutableListOf() + fragment.resolveRequirements(template, actions) + assertEquals(RequiresLocation("some_fun"), actions.single()) + } + + /* + Requirements + */ + + @TestFactory + fun `Resolve requirement references`() = listOf( + Triple(RequiresSkill("default", 1, 120), mapOf("skill" to "attack", "min" to 5, "max" to 99), RequiresSkill("attack", 5, 99)), + Triple(RequiresVariable("default", 1), mapOf("variable" to "test", "value" to true), RequiresVariable("test", true)), + Triple(RequiresEquippedItem("default", 1), mapOf("equips" to "item", "amount" to 10), RequiresEquippedItem("item", 10)), + Triple(RequiresCarriedItem("default", 1), mapOf("carries" to "item", "amount" to 10), RequiresCarriedItem("item", 10)), + Triple(RequiresOwnedItem("default", 1), mapOf("owns" to "item", "amount" to 10), RequiresOwnedItem("item", 10)), + Triple(RequiresInvSpace(1), mapOf("inventory_space" to 10), RequiresInvSpace(10)), + Triple(RequiresLocation("default"), mapOf("location" to "area"), RequiresLocation("area")), + Triple(RequiresTile(0, 0, 0, 0), mapOf("x" to 4, "y" to 3, "level" to 2, "radius" to 1), RequiresTile(4, 3, 2, 1)), + ).map { (default, values, expected) -> + dynamicTest("Resolve ${default::class.simpleName} references") { + val fields = values.mapKeys { "ref_${it.key}" } + val fragment = fragment(fields) + val references = values.map { it.key to "\$ref_${it.key}" }.toMap() + val template = BotActivity( + id = "a", + capacity = 1, + requirements = listOf( + RequiresReference( + default, + references = references + ) + ) + ) + val actions = mutableListOf() + fragment.resolveRequirements(template, actions) + assertEquals(expected, actions.single()) + } + } + + @Test + fun `Missing requirement field throws`() { + val fragment = fragment() + val template = BotActivity( + id = "a", + capacity = 1, + requirements = listOf( + RequiresReference( + RequiresSkill("x"), + references = mapOf("skill" to "missing") + ) + ) + ) + assertThrows { + fragment.resolveRequirements(template, mutableListOf()) + } + } + + @Test + fun `No requirement reference uses default value`() { + val fragment = fragment() + val template = BotActivity( + id = "a", + capacity = 1, + requirements = listOf( + RequiresReference( + RequiresSkill("x"), + emptyMap() + ) + ) + ) + + val actions = mutableListOf() + fragment.resolveRequirements(template, actions) + + assertEquals(RequiresSkill("x"), actions.single()) + } + + @Test + fun `Non-reference requirement is added`() { + val fragment = fragment() + val template = BotActivity( + id = "a", + capacity = 1, + requirements = listOf( + RequiresSkill("x") + ) + ) + + val actions = mutableListOf() + fragment.resolveRequirements(template, actions) + + assertEquals(RequiresSkill("x"), actions.single()) + } + + @Test + fun `Any clone requirement throws`() { + val fragment = fragment() + val template = BotActivity( + id = "a", + capacity = 1, + requirements = listOf(CloneRequirement("x")) + ) + assertThrows { + fragment.resolveRequirements(template, mutableListOf()) + } + } + + @Test + fun `Invalid nested requirement reference throws`() { + val fragment = fragment() + val template = BotActivity( + id = "a", + capacity = 1, + requirements = listOf( + RequiresReference( + RequiresReference( + RequiresSkill("x"), + emptyMap() + ), + emptyMap() + ) + ) + ) + assertThrows { + fragment.resolveRequirements(template, mutableListOf()) + } + } +} \ No newline at end of file From bfe49ea8ce0515dbc45b89be1f7d9c8af80ba026 Mon Sep 17 00:00:00 2001 From: GregHib Date: Tue, 27 Jan 2026 15:00:33 +0000 Subject: [PATCH 009/101] Add requirement ordering --- .../kotlin/content/bot/action/BotActivity.kt | 3 +++ .../content/bot/req/MandatoryRequirement.kt | 6 +++--- .../kotlin/content/bot/req/Requirement.kt | 10 +++++++--- .../content/bot/req/ResolvableRequirement.kt | 20 +++++++++---------- 4 files changed, 23 insertions(+), 16 deletions(-) diff --git a/game/src/main/kotlin/content/bot/action/BotActivity.kt b/game/src/main/kotlin/content/bot/action/BotActivity.kt index acd2129f4f..efdca2dd1f 100644 --- a/game/src/main/kotlin/content/bot/action/BotActivity.kt +++ b/game/src/main/kotlin/content/bot/action/BotActivity.kt @@ -80,6 +80,7 @@ fun loadActivities(paths: List): Map { val requirements = activity.requirements as MutableList requirements.removeIf { it is CloneRequirement && it.id == cloneId } requirements.addAll(clone.requirements) + requirements.sortBy { it.priority } } } // Fragments are partially filled behaviours with template + fields @@ -92,6 +93,7 @@ fun loadActivities(paths: List): Map { val requirements = mutableListOf() requirements.addAll(fragment.requirements) fragment.resolveRequirements(template, requirements) + requirements.sortBy { it.priority } val actions = mutableListOf() actions.addAll(fragment.plan) @@ -227,6 +229,7 @@ private fun ConfigReader.requirements(): List { } list.add(requirement) } + list.sortBy { it.priority } return list } diff --git a/game/src/main/kotlin/content/bot/req/MandatoryRequirement.kt b/game/src/main/kotlin/content/bot/req/MandatoryRequirement.kt index aee71d6983..8a39e0e721 100644 --- a/game/src/main/kotlin/content/bot/req/MandatoryRequirement.kt +++ b/game/src/main/kotlin/content/bot/req/MandatoryRequirement.kt @@ -1,15 +1,15 @@ package content.bot.req -sealed interface MandatoryRequirement : Requirement +sealed class MandatoryRequirement(priority: Int) : Requirement(priority) data class RequiresSkill( val id: String, val min: Int = 1, val max: Int = 120 -) : MandatoryRequirement +) : MandatoryRequirement(0) data class RequiresVariable( val id: String, val value: Any? = null -) : MandatoryRequirement +) : MandatoryRequirement(0) diff --git a/game/src/main/kotlin/content/bot/req/Requirement.kt b/game/src/main/kotlin/content/bot/req/Requirement.kt index adb38deb1f..a9da15d1fe 100644 --- a/game/src/main/kotlin/content/bot/req/Requirement.kt +++ b/game/src/main/kotlin/content/bot/req/Requirement.kt @@ -2,15 +2,19 @@ package content.bot.req import content.bot.Bot -sealed interface Requirement { +/** + * A requirement that must be satisfied for a bot to perform a behaviour. + * @param priority Ensure bots aren't walking to locations before getting items etc... Lower values are prioritised first. + */ +sealed class Requirement(val priority: Int) { fun satisfied(bot: Bot): Boolean = false } internal data class CloneRequirement( val id: String, -) : Requirement +) : Requirement(-1) internal data class RequiresReference( var requirement: Requirement, val references: Map, -) : Requirement \ No newline at end of file +) : Requirement(-1) \ No newline at end of file diff --git a/game/src/main/kotlin/content/bot/req/ResolvableRequirement.kt b/game/src/main/kotlin/content/bot/req/ResolvableRequirement.kt index a9352b8ea7..42642094f2 100644 --- a/game/src/main/kotlin/content/bot/req/ResolvableRequirement.kt +++ b/game/src/main/kotlin/content/bot/req/ResolvableRequirement.kt @@ -2,17 +2,17 @@ package content.bot.req import content.bot.Bot -sealed interface ResolvableRequirement : Requirement +sealed class ResolvableRequirement(priority: Int) : Requirement(priority) -sealed interface ItemRequirement : ResolvableRequirement { - val id: String - val amount: Int +sealed class ItemRequirement : ResolvableRequirement(100) { + abstract val id: String + abstract val amount: Int } data class RequiresEquippedItem( override val id: String, override val amount: Int = 1, -) : ItemRequirement { +) : ItemRequirement() { fun check(bot: Bot) { // bot.inventory.has(id, amount) } @@ -21,24 +21,24 @@ data class RequiresEquippedItem( data class RequiresCarriedItem( override val id: String, override val amount: Int = 1, -) : ItemRequirement +) : ItemRequirement() data class RequiresOwnedItem( override val id: String, override val amount: Int = 1, -) : ItemRequirement +) : ItemRequirement() data class RequiresInvSpace( val amount: Int, -) : ResolvableRequirement +) : ResolvableRequirement(10) data class RequiresLocation( val id: String, -) : ResolvableRequirement +) : ResolvableRequirement(1000) data class RequiresTile( val x: Int, val y: Int, val level: Int, val radius: Int, -) : ResolvableRequirement \ No newline at end of file +) : ResolvableRequirement(1100) \ No newline at end of file From cfd96b33d5b693504f60f902586691b377d246da Mon Sep 17 00:00:00 2001 From: GregHib Date: Tue, 27 Jan 2026 15:30:54 +0000 Subject: [PATCH 010/101] Rename Requirement to Fact --- .../src/main/kotlin/content/bot/BotManager.kt | 4 +- .../kotlin/content/bot/action/Behaviour.kt | 4 +- .../content/bot/action/BehaviourFragment.kt | 48 +++++------ .../kotlin/content/bot/action/BotActivity.kt | 62 +++++++------- .../content/bot/action/RequirementResolver.kt | 13 +++ game/src/main/kotlin/content/bot/fact/Fact.kt | 20 +++++ .../kotlin/content/bot/fact/MandatoryFact.kt | 15 ++++ .../kotlin/content/bot/fact/ResolvableFact.kt | 44 ++++++++++ .../content/bot/req/MandatoryRequirement.kt | 15 ---- .../kotlin/content/bot/req/Requirement.kt | 20 ----- .../content/bot/req/ResolvableRequirement.kt | 44 ---------- .../test/kotlin/content/bot/BotManagerTest.kt | 8 +- .../bot/action/BehaviourFragmentTest.kt | 84 +++++++++---------- 13 files changed, 200 insertions(+), 181 deletions(-) create mode 100644 game/src/main/kotlin/content/bot/action/RequirementResolver.kt create mode 100644 game/src/main/kotlin/content/bot/fact/Fact.kt create mode 100644 game/src/main/kotlin/content/bot/fact/MandatoryFact.kt create mode 100644 game/src/main/kotlin/content/bot/fact/ResolvableFact.kt delete mode 100644 game/src/main/kotlin/content/bot/req/MandatoryRequirement.kt delete mode 100644 game/src/main/kotlin/content/bot/req/Requirement.kt delete mode 100644 game/src/main/kotlin/content/bot/req/ResolvableRequirement.kt diff --git a/game/src/main/kotlin/content/bot/BotManager.kt b/game/src/main/kotlin/content/bot/BotManager.kt index 8a4d57cb6c..be6541098b 100644 --- a/game/src/main/kotlin/content/bot/BotManager.kt +++ b/game/src/main/kotlin/content/bot/BotManager.kt @@ -2,6 +2,8 @@ package content.bot import com.github.michaelbull.logging.InlineLogger import content.bot.action.* +import content.bot.fact.MandatoryFact +import content.bot.fact.ResolvableFact import world.gregs.voidps.engine.data.ConfigFiles import world.gregs.voidps.engine.data.Settings import world.gregs.voidps.type.random @@ -33,7 +35,7 @@ class BotManager( } private fun hasRequirements(bot: Bot, activity: BotActivity): Boolean { - return slots.hasFree(activity) && !bot.blocked.contains(activity.id) && activity.requirements.all { it.satisfied(bot) } + return slots.hasFree(activity) && !bot.blocked.contains(activity.id) && activity.requirements.all { it is MandatoryFact && it.satisfied(bot) } } private fun assignActivity(bot: Bot) { diff --git a/game/src/main/kotlin/content/bot/action/Behaviour.kt b/game/src/main/kotlin/content/bot/action/Behaviour.kt index b7fca7f6f1..6ca1c67bce 100644 --- a/game/src/main/kotlin/content/bot/action/Behaviour.kt +++ b/game/src/main/kotlin/content/bot/action/Behaviour.kt @@ -1,9 +1,9 @@ package content.bot.action -import content.bot.req.Requirement +import content.bot.fact.Fact interface Behaviour { val id: String - val requirements: List + val requirements: List val plan: List } \ No newline at end of file diff --git a/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt b/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt index 453c9df454..3b031efeaf 100644 --- a/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt +++ b/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt @@ -1,22 +1,22 @@ package content.bot.action -import content.bot.req.CloneRequirement -import content.bot.req.Requirement -import content.bot.req.RequiresCarriedItem -import content.bot.req.RequiresEquippedItem -import content.bot.req.RequiresInvSpace -import content.bot.req.RequiresLocation -import content.bot.req.RequiresOwnedItem -import content.bot.req.RequiresReference -import content.bot.req.RequiresSkill -import content.bot.req.RequiresTile -import content.bot.req.RequiresVariable +import content.bot.fact.FactClone +import content.bot.fact.Fact +import content.bot.fact.CarriesItem +import content.bot.fact.EquipsItem +import content.bot.fact.HasInventorySpace +import content.bot.fact.AtLocation +import content.bot.fact.OwnsItem +import content.bot.fact.FactReference +import content.bot.fact.HasSkillLevel +import content.bot.fact.AtTile +import content.bot.fact.HasVariable data class BehaviourFragment( override val id: String, val capacity: Int, var template: String, - override val requirements: List = emptyList(), + override val requirements: List = emptyList(), override val plan: List = emptyList(), val fields: Map = emptyMap(), ) : Behaviour { @@ -54,46 +54,46 @@ data class BehaviourFragment( } } - fun resolveRequirements(template: BotActivity, requirements: MutableList) { + fun resolveRequirements(template: BotActivity, requirements: MutableList) { for (req in template.requirements) { val resolved = when (req) { - is RequiresReference -> when (val requirement = req.requirement) { - is RequiresSkill -> RequiresSkill( + is FactReference -> when (val requirement = req.fact) { + is HasSkillLevel -> HasSkillLevel( id = resolve(req.references["skill"], requirement.id), min = resolve(req.references["min"], requirement.min), max = resolve(req.references["max"], requirement.max), ) - is RequiresVariable -> RequiresVariable( + is HasVariable -> HasVariable( id = resolve(req.references["variable"], requirement.id), value = resolve(req.references["value"], requirement.value), ) - is RequiresCarriedItem -> RequiresCarriedItem( + is CarriesItem -> CarriesItem( id = resolve(req.references["carries"], requirement.id), amount = resolve(req.references["amount"], requirement.amount), ) - is RequiresEquippedItem -> RequiresEquippedItem( + is EquipsItem -> EquipsItem( id = resolve(req.references["equips"], requirement.id), amount = resolve(req.references["amount"], requirement.amount), ) - is RequiresOwnedItem -> RequiresOwnedItem( + is OwnsItem -> OwnsItem( id = resolve(req.references["owns"], requirement.id), amount = resolve(req.references["amount"], requirement.amount), ) - is RequiresInvSpace -> RequiresInvSpace( + is HasInventorySpace -> HasInventorySpace( amount = resolve(req.references["inventory_space"], requirement.amount), ) - is RequiresLocation -> RequiresLocation( + is AtLocation -> AtLocation( id = resolve(req.references["location"], requirement.id), ) - is RequiresTile -> RequiresTile( + is AtTile -> AtTile( x = resolve(req.references["x"], requirement.x), y = resolve(req.references["y"], requirement.y), level = resolve(req.references["level"], requirement.level), radius = resolve(req.references["radius"], requirement.radius), ) - is CloneRequirement, is RequiresReference -> throw IllegalArgumentException("Invalid requirement type: ${req.requirement::class.simpleName}.") + is FactClone, is FactReference -> throw IllegalArgumentException("Invalid requirement type: ${req.fact::class.simpleName}.") } - is CloneRequirement -> throw IllegalArgumentException("Unresolved clone requirement in template ${id}.") + is FactClone -> throw IllegalArgumentException("Unresolved clone requirement in template ${id}.") else -> req } requirements.add(resolved) diff --git a/game/src/main/kotlin/content/bot/action/BotActivity.kt b/game/src/main/kotlin/content/bot/action/BotActivity.kt index efdca2dd1f..4a89116da0 100644 --- a/game/src/main/kotlin/content/bot/action/BotActivity.kt +++ b/game/src/main/kotlin/content/bot/action/BotActivity.kt @@ -1,24 +1,28 @@ package content.bot.action -import content.bot.req.CloneRequirement -import content.bot.req.RequiresReference -import content.bot.req.RequiresInvSpace -import content.bot.req.RequiresCarriedItem -import content.bot.req.Requirement -import content.bot.req.RequiresEquippedItem -import content.bot.req.RequiresLocation -import content.bot.req.RequiresOwnedItem -import content.bot.req.RequiresSkill -import content.bot.req.RequiresTile -import content.bot.req.RequiresVariable +import content.bot.fact.FactClone +import content.bot.fact.FactReference +import content.bot.fact.HasInventorySpace +import content.bot.fact.CarriesItem +import content.bot.fact.Fact +import content.bot.fact.EquipsItem +import content.bot.fact.AtLocation +import content.bot.fact.OwnsItem +import content.bot.fact.HasSkillLevel +import content.bot.fact.AtTile +import content.bot.fact.HasVariable import world.gregs.config.Config import world.gregs.config.ConfigReader import world.gregs.voidps.engine.timedLoad +/** + * An activity with a limited number of slots that bots can perform + * E.g. cutting oak trees in varrock, mining copper ore in lumbridge + */ data class BotActivity( override val id: String, val capacity: Int, - override val requirements: List = emptyList(), + override val requirements: List = emptyList(), override val plan: List = emptyList(), ) : Behaviour @@ -36,7 +40,7 @@ fun loadActivities(paths: List): Map { var type = "activity" var weight = 0 var actions: List = emptyList() - var requirements: List = emptyList() + var requirements: List = emptyList() var fields: Map = emptyMap() while (nextPair()) { when (val key = key()) { @@ -51,7 +55,7 @@ fun loadActivities(paths: List): Map { else -> throw IllegalArgumentException("Unexpected key: '$key' ${exception()}") } } - val clone = requirements.filterIsInstance().firstOrNull() + val clone = requirements.filterIsInstance().firstOrNull() if (clone != null) { clones[id] = clone.id } @@ -77,8 +81,8 @@ fun loadActivities(paths: List): Map { for ((id, cloneId) in clones) { val activity = activities[id] ?: continue val clone = activities[cloneId] ?: continue - val requirements = activity.requirements as MutableList - requirements.removeIf { it is CloneRequirement && it.id == cloneId } + val requirements = activity.requirements as MutableList + requirements.removeIf { it is FactClone && it.id == cloneId } requirements.addAll(clone.requirements) requirements.sortBy { it.priority } } @@ -90,7 +94,7 @@ fun loadActivities(paths: List): Map { val template = activities[fragment.template] ?: throw IllegalArgumentException("Unable to find template '${fragment.template}' for activity '$id'.") templates.add(fragment.template) - val requirements = mutableListOf() + val requirements = mutableListOf() requirements.addAll(fragment.requirements) fragment.resolveRequirements(template, requirements) requirements.sortBy { it.priority } @@ -129,8 +133,8 @@ private fun ConfigReader.fields(): Map { return map } -private fun ConfigReader.requirements(): List { - val list = mutableListOf() +private fun ConfigReader.requirements(): List { + val list = mutableListOf() while (nextElement()) { var type = "" var id = "" @@ -212,20 +216,20 @@ private fun ConfigReader.requirements(): List { } } var requirement = when (type) { - "skill" -> RequiresSkill(id, min, max) - "carries" -> RequiresCarriedItem(id, min) - "owns" -> RequiresOwnedItem(id, min) - "equips" -> RequiresEquippedItem(id, min) - "variable" -> RequiresVariable(id, value) - "clone" -> CloneRequirement(id) - "inventory_space" -> RequiresInvSpace(min) - "location" -> RequiresLocation(id) - "tile" -> RequiresTile(x, y, level, min) + "skill" -> HasSkillLevel(id, min, max) + "carries" -> CarriesItem(id, min) + "owns" -> OwnsItem(id, min) + "equips" -> EquipsItem(id, min) + "variable" -> HasVariable(id, value) + "clone" -> FactClone(id) + "inventory_space" -> HasInventorySpace(min) + "location" -> AtLocation(id) + "tile" -> AtTile(x, y, level, min) "holds" -> throw IllegalArgumentException("Unknown requirement type 'holds'; did you mean 'carries' or 'equips'? ${exception()}.") else -> throw IllegalArgumentException("Unknown requirement type: $type ${exception()}") } if (references.isNotEmpty()) { - requirement = RequiresReference(requirement, references) + requirement = FactReference(requirement, references) } list.add(requirement) } diff --git a/game/src/main/kotlin/content/bot/action/RequirementResolver.kt b/game/src/main/kotlin/content/bot/action/RequirementResolver.kt new file mode 100644 index 0000000000..9e486daa19 --- /dev/null +++ b/game/src/main/kotlin/content/bot/action/RequirementResolver.kt @@ -0,0 +1,13 @@ +package content.bot.action + +import content.bot.fact.Fact + +/** + * An activity that can be performed to resolve a requirement + * E.g. buying a pickaxe from a shop, getting items out of the bank, picking up an item off of the floor + */ +data class RequirementResolver( + override val id: String, + override val requirements: List = emptyList(), + override val plan: List = emptyList(), +) : Behaviour \ No newline at end of file diff --git a/game/src/main/kotlin/content/bot/fact/Fact.kt b/game/src/main/kotlin/content/bot/fact/Fact.kt new file mode 100644 index 0000000000..2850d4e935 --- /dev/null +++ b/game/src/main/kotlin/content/bot/fact/Fact.kt @@ -0,0 +1,20 @@ +package content.bot.fact + +import content.bot.Bot + +/** + * A bots state which can be required for, or a product of performing a [content.bot.action.Behaviour] + * @param priority Ensure bots aren't walking to locations before getting items etc... lower values are prioritised first. + */ +sealed class Fact(val priority: Int) { + fun satisfied(bot: Bot): Boolean = false +} + +internal data class FactClone( + val id: String, +) : Fact(-1) + +internal data class FactReference( + var fact: Fact, + val references: Map, +) : Fact(-1) \ No newline at end of file diff --git a/game/src/main/kotlin/content/bot/fact/MandatoryFact.kt b/game/src/main/kotlin/content/bot/fact/MandatoryFact.kt new file mode 100644 index 0000000000..7507619fed --- /dev/null +++ b/game/src/main/kotlin/content/bot/fact/MandatoryFact.kt @@ -0,0 +1,15 @@ +package content.bot.fact + +sealed class MandatoryFact(priority: Int = 0) : Fact(priority) + +data class HasSkillLevel( + val id: String, + val min: Int = 1, + val max: Int = 120 +) : MandatoryFact() + + +data class HasVariable( + val id: String, + val value: Any? = null +) : MandatoryFact() diff --git a/game/src/main/kotlin/content/bot/fact/ResolvableFact.kt b/game/src/main/kotlin/content/bot/fact/ResolvableFact.kt new file mode 100644 index 0000000000..b748469270 --- /dev/null +++ b/game/src/main/kotlin/content/bot/fact/ResolvableFact.kt @@ -0,0 +1,44 @@ +package content.bot.fact + +import content.bot.Bot + +sealed class ResolvableFact(priority: Int) : Fact(priority) + +sealed class ItemFact : ResolvableFact(100) { + abstract val id: String + abstract val amount: Int +} + +data class EquipsItem( + override val id: String, + override val amount: Int = 1, +) : ItemFact() { + fun check(bot: Bot) { + // bot.inventory.has(id, amount) + } +} + +data class CarriesItem( + override val id: String, + override val amount: Int = 1, +) : ItemFact() + +data class OwnsItem( + override val id: String, + override val amount: Int = 1, +) : ItemFact() + +data class HasInventorySpace( + val amount: Int, +) : ResolvableFact(10) + +data class AtLocation( + val id: String, +) : ResolvableFact(1000) + +data class AtTile( + val x: Int, + val y: Int, + val level: Int, + val radius: Int, +) : ResolvableFact(1100) \ No newline at end of file diff --git a/game/src/main/kotlin/content/bot/req/MandatoryRequirement.kt b/game/src/main/kotlin/content/bot/req/MandatoryRequirement.kt deleted file mode 100644 index 8a39e0e721..0000000000 --- a/game/src/main/kotlin/content/bot/req/MandatoryRequirement.kt +++ /dev/null @@ -1,15 +0,0 @@ -package content.bot.req - -sealed class MandatoryRequirement(priority: Int) : Requirement(priority) - -data class RequiresSkill( - val id: String, - val min: Int = 1, - val max: Int = 120 -) : MandatoryRequirement(0) - - -data class RequiresVariable( - val id: String, - val value: Any? = null -) : MandatoryRequirement(0) diff --git a/game/src/main/kotlin/content/bot/req/Requirement.kt b/game/src/main/kotlin/content/bot/req/Requirement.kt deleted file mode 100644 index a9da15d1fe..0000000000 --- a/game/src/main/kotlin/content/bot/req/Requirement.kt +++ /dev/null @@ -1,20 +0,0 @@ -package content.bot.req - -import content.bot.Bot - -/** - * A requirement that must be satisfied for a bot to perform a behaviour. - * @param priority Ensure bots aren't walking to locations before getting items etc... Lower values are prioritised first. - */ -sealed class Requirement(val priority: Int) { - fun satisfied(bot: Bot): Boolean = false -} - -internal data class CloneRequirement( - val id: String, -) : Requirement(-1) - -internal data class RequiresReference( - var requirement: Requirement, - val references: Map, -) : Requirement(-1) \ No newline at end of file diff --git a/game/src/main/kotlin/content/bot/req/ResolvableRequirement.kt b/game/src/main/kotlin/content/bot/req/ResolvableRequirement.kt deleted file mode 100644 index 42642094f2..0000000000 --- a/game/src/main/kotlin/content/bot/req/ResolvableRequirement.kt +++ /dev/null @@ -1,44 +0,0 @@ -package content.bot.req - -import content.bot.Bot - -sealed class ResolvableRequirement(priority: Int) : Requirement(priority) - -sealed class ItemRequirement : ResolvableRequirement(100) { - abstract val id: String - abstract val amount: Int -} - -data class RequiresEquippedItem( - override val id: String, - override val amount: Int = 1, -) : ItemRequirement() { - fun check(bot: Bot) { - // bot.inventory.has(id, amount) - } -} - -data class RequiresCarriedItem( - override val id: String, - override val amount: Int = 1, -) : ItemRequirement() - -data class RequiresOwnedItem( - override val id: String, - override val amount: Int = 1, -) : ItemRequirement() - -data class RequiresInvSpace( - val amount: Int, -) : ResolvableRequirement(10) - -data class RequiresLocation( - val id: String, -) : ResolvableRequirement(1000) - -data class RequiresTile( - val x: Int, - val y: Int, - val level: Int, - val radius: Int, -) : ResolvableRequirement(1100) \ No newline at end of file diff --git a/game/src/test/kotlin/content/bot/BotManagerTest.kt b/game/src/test/kotlin/content/bot/BotManagerTest.kt index 464df46420..98ce07f056 100644 --- a/game/src/test/kotlin/content/bot/BotManagerTest.kt +++ b/game/src/test/kotlin/content/bot/BotManagerTest.kt @@ -5,8 +5,8 @@ import content.bot.action.BehaviourState import content.bot.action.BotAction import content.bot.action.BotActivity import content.bot.action.Reason -import content.bot.req.MandatoryRequirement -import content.bot.req.RequiresSkill +import content.bot.fact.MandatoryFact +import content.bot.fact.HasSkillLevel import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.Test import world.gregs.voidps.engine.entity.character.player.Player @@ -208,7 +208,7 @@ class BotManagerTest { val activity = testActivity( id = "test", requirements = listOf( - RequiresSkill("attack", 99, 99) + HasSkillLevel("attack", 99, 99) ), plan = listOf(BotAction.Wait(4)) ) @@ -227,7 +227,7 @@ class BotManagerTest { fun testActivity( id: String, - requirements: List = emptyList(), + requirements: List = emptyList(), plan: List ) = BotActivity(id, 1, requirements, plan) } \ No newline at end of file diff --git a/game/src/test/kotlin/content/bot/action/BehaviourFragmentTest.kt b/game/src/test/kotlin/content/bot/action/BehaviourFragmentTest.kt index f4e2093800..f92763388d 100644 --- a/game/src/test/kotlin/content/bot/action/BehaviourFragmentTest.kt +++ b/game/src/test/kotlin/content/bot/action/BehaviourFragmentTest.kt @@ -1,16 +1,16 @@ package content.bot.action -import content.bot.req.CloneRequirement -import content.bot.req.Requirement -import content.bot.req.RequiresCarriedItem -import content.bot.req.RequiresEquippedItem -import content.bot.req.RequiresInvSpace -import content.bot.req.RequiresLocation -import content.bot.req.RequiresOwnedItem -import content.bot.req.RequiresReference -import content.bot.req.RequiresSkill -import content.bot.req.RequiresTile -import content.bot.req.RequiresVariable +import content.bot.fact.FactClone +import content.bot.fact.Fact +import content.bot.fact.CarriesItem +import content.bot.fact.EquipsItem +import content.bot.fact.HasInventorySpace +import content.bot.fact.AtLocation +import content.bot.fact.OwnsItem +import content.bot.fact.FactReference +import content.bot.fact.HasSkillLevel +import content.bot.fact.AtTile +import content.bot.fact.HasVariable import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.DynamicTest.dynamicTest import org.junit.jupiter.api.Test @@ -208,17 +208,17 @@ class BehaviourFragmentTest { id = "a", capacity = 1, requirements = listOf( - RequiresReference( - RequiresLocation("default"), + FactReference( + AtLocation("default"), references = mapOf( "location" to $$"some_${type}_area" ) ) ) ) - val actions = mutableListOf() + val actions = mutableListOf() fragment.resolveRequirements(template, actions) - assertEquals(RequiresLocation("some_fun_area"), actions.single()) + assertEquals(AtLocation("some_fun_area"), actions.single()) } @Test @@ -228,17 +228,17 @@ class BehaviourFragmentTest { id = "a", capacity = 1, requirements = listOf( - RequiresReference( - RequiresLocation("default"), + FactReference( + AtLocation("default"), references = mapOf( "location" to $$"some_$area_type" ) ) ) ) - val actions = mutableListOf() + val actions = mutableListOf() fragment.resolveRequirements(template, actions) - assertEquals(RequiresLocation("some_fun"), actions.single()) + assertEquals(AtLocation("some_fun"), actions.single()) } /* @@ -247,14 +247,14 @@ class BehaviourFragmentTest { @TestFactory fun `Resolve requirement references`() = listOf( - Triple(RequiresSkill("default", 1, 120), mapOf("skill" to "attack", "min" to 5, "max" to 99), RequiresSkill("attack", 5, 99)), - Triple(RequiresVariable("default", 1), mapOf("variable" to "test", "value" to true), RequiresVariable("test", true)), - Triple(RequiresEquippedItem("default", 1), mapOf("equips" to "item", "amount" to 10), RequiresEquippedItem("item", 10)), - Triple(RequiresCarriedItem("default", 1), mapOf("carries" to "item", "amount" to 10), RequiresCarriedItem("item", 10)), - Triple(RequiresOwnedItem("default", 1), mapOf("owns" to "item", "amount" to 10), RequiresOwnedItem("item", 10)), - Triple(RequiresInvSpace(1), mapOf("inventory_space" to 10), RequiresInvSpace(10)), - Triple(RequiresLocation("default"), mapOf("location" to "area"), RequiresLocation("area")), - Triple(RequiresTile(0, 0, 0, 0), mapOf("x" to 4, "y" to 3, "level" to 2, "radius" to 1), RequiresTile(4, 3, 2, 1)), + Triple(HasSkillLevel("default", 1, 120), mapOf("skill" to "attack", "min" to 5, "max" to 99), HasSkillLevel("attack", 5, 99)), + Triple(HasVariable("default", 1), mapOf("variable" to "test", "value" to true), HasVariable("test", true)), + Triple(EquipsItem("default", 1), mapOf("equips" to "item", "amount" to 10), EquipsItem("item", 10)), + Triple(CarriesItem("default", 1), mapOf("carries" to "item", "amount" to 10), CarriesItem("item", 10)), + Triple(OwnsItem("default", 1), mapOf("owns" to "item", "amount" to 10), OwnsItem("item", 10)), + Triple(HasInventorySpace(1), mapOf("inventory_space" to 10), HasInventorySpace(10)), + Triple(AtLocation("default"), mapOf("location" to "area"), AtLocation("area")), + Triple(AtTile(0, 0, 0, 0), mapOf("x" to 4, "y" to 3, "level" to 2, "radius" to 1), AtTile(4, 3, 2, 1)), ).map { (default, values, expected) -> dynamicTest("Resolve ${default::class.simpleName} references") { val fields = values.mapKeys { "ref_${it.key}" } @@ -264,13 +264,13 @@ class BehaviourFragmentTest { id = "a", capacity = 1, requirements = listOf( - RequiresReference( + FactReference( default, references = references ) ) ) - val actions = mutableListOf() + val actions = mutableListOf() fragment.resolveRequirements(template, actions) assertEquals(expected, actions.single()) } @@ -283,8 +283,8 @@ class BehaviourFragmentTest { id = "a", capacity = 1, requirements = listOf( - RequiresReference( - RequiresSkill("x"), + FactReference( + HasSkillLevel("x"), references = mapOf("skill" to "missing") ) ) @@ -301,17 +301,17 @@ class BehaviourFragmentTest { id = "a", capacity = 1, requirements = listOf( - RequiresReference( - RequiresSkill("x"), + FactReference( + HasSkillLevel("x"), emptyMap() ) ) ) - val actions = mutableListOf() + val actions = mutableListOf() fragment.resolveRequirements(template, actions) - assertEquals(RequiresSkill("x"), actions.single()) + assertEquals(HasSkillLevel("x"), actions.single()) } @Test @@ -321,14 +321,14 @@ class BehaviourFragmentTest { id = "a", capacity = 1, requirements = listOf( - RequiresSkill("x") + HasSkillLevel("x") ) ) - val actions = mutableListOf() + val actions = mutableListOf() fragment.resolveRequirements(template, actions) - assertEquals(RequiresSkill("x"), actions.single()) + assertEquals(HasSkillLevel("x"), actions.single()) } @Test @@ -337,7 +337,7 @@ class BehaviourFragmentTest { val template = BotActivity( id = "a", capacity = 1, - requirements = listOf(CloneRequirement("x")) + requirements = listOf(FactClone("x")) ) assertThrows { fragment.resolveRequirements(template, mutableListOf()) @@ -351,9 +351,9 @@ class BehaviourFragmentTest { id = "a", capacity = 1, requirements = listOf( - RequiresReference( - RequiresReference( - RequiresSkill("x"), + FactReference( + FactReference( + HasSkillLevel("x"), emptyMap() ), emptyMap() From 4aff6dbaf3f1366aab506802a2db861703f71b7a Mon Sep 17 00:00:00 2001 From: GregHib Date: Tue, 27 Jan 2026 15:47:03 +0000 Subject: [PATCH 011/101] Rename requirements to requires --- game/src/main/kotlin/content/bot/BotManager.kt | 18 ++++++++++++++---- .../kotlin/content/bot/action/Behaviour.kt | 2 +- .../content/bot/action/BehaviourFragment.kt | 4 ++-- .../kotlin/content/bot/action/BotActivity.kt | 8 ++++---- .../{RequirementResolver.kt => Resolver.kt} | 4 ++-- .../bot/action/BehaviourFragmentTest.kt | 16 ++++++++-------- 6 files changed, 31 insertions(+), 21 deletions(-) rename game/src/main/kotlin/content/bot/action/{RequirementResolver.kt => Resolver.kt} (78%) diff --git a/game/src/main/kotlin/content/bot/BotManager.kt b/game/src/main/kotlin/content/bot/BotManager.kt index be6541098b..a6ac05520c 100644 --- a/game/src/main/kotlin/content/bot/BotManager.kt +++ b/game/src/main/kotlin/content/bot/BotManager.kt @@ -35,7 +35,7 @@ class BotManager( } private fun hasRequirements(bot: Bot, activity: BotActivity): Boolean { - return slots.hasFree(activity) && !bot.blocked.contains(activity.id) && activity.requirements.all { it is MandatoryFact && it.satisfied(bot) } + return slots.hasFree(activity) && !bot.blocked.contains(activity.id) && activity.requires.all { it is MandatoryFact && it.satisfied(bot) } } private fun assignActivity(bot: Bot) { @@ -53,9 +53,19 @@ class BotManager( } private fun start(bot: Bot, behaviour: Behaviour, frame: BehaviourFrame) { - if (behaviour.requirements.any { !it.satisfied(bot) }) { - frame.fail(Reason.Requirements) - return + for (requirement in behaviour.requires) { + if (!requirement.satisfied(bot)) { + if (requirement is MandatoryFact) { + frame.fail(Reason.Requirements) + return + } + if (requirement is ResolvableFact) { +// frame.blocked.add(requirement) + // TODO + + return + } + } } frame.start(bot) } diff --git a/game/src/main/kotlin/content/bot/action/Behaviour.kt b/game/src/main/kotlin/content/bot/action/Behaviour.kt index 6ca1c67bce..c5ebb67bb8 100644 --- a/game/src/main/kotlin/content/bot/action/Behaviour.kt +++ b/game/src/main/kotlin/content/bot/action/Behaviour.kt @@ -4,6 +4,6 @@ import content.bot.fact.Fact interface Behaviour { val id: String - val requirements: List + val requires: List val plan: List } \ No newline at end of file diff --git a/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt b/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt index 3b031efeaf..dec919e481 100644 --- a/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt +++ b/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt @@ -16,7 +16,7 @@ data class BehaviourFragment( override val id: String, val capacity: Int, var template: String, - override val requirements: List = emptyList(), + override val requires: List = emptyList(), override val plan: List = emptyList(), val fields: Map = emptyMap(), ) : Behaviour { @@ -55,7 +55,7 @@ data class BehaviourFragment( } fun resolveRequirements(template: BotActivity, requirements: MutableList) { - for (req in template.requirements) { + for (req in template.requires) { val resolved = when (req) { is FactReference -> when (val requirement = req.fact) { is HasSkillLevel -> HasSkillLevel( diff --git a/game/src/main/kotlin/content/bot/action/BotActivity.kt b/game/src/main/kotlin/content/bot/action/BotActivity.kt index 4a89116da0..d5eb07aeda 100644 --- a/game/src/main/kotlin/content/bot/action/BotActivity.kt +++ b/game/src/main/kotlin/content/bot/action/BotActivity.kt @@ -22,7 +22,7 @@ import world.gregs.voidps.engine.timedLoad data class BotActivity( override val id: String, val capacity: Int, - override val requirements: List = emptyList(), + override val requires: List = emptyList(), override val plan: List = emptyList(), ) : Behaviour @@ -81,9 +81,9 @@ fun loadActivities(paths: List): Map { for ((id, cloneId) in clones) { val activity = activities[id] ?: continue val clone = activities[cloneId] ?: continue - val requirements = activity.requirements as MutableList + val requirements = activity.requires as MutableList requirements.removeIf { it is FactClone && it.id == cloneId } - requirements.addAll(clone.requirements) + requirements.addAll(clone.requires) requirements.sortBy { it.priority } } } @@ -95,7 +95,7 @@ fun loadActivities(paths: List): Map { templates.add(fragment.template) val requirements = mutableListOf() - requirements.addAll(fragment.requirements) + requirements.addAll(fragment.requires) fragment.resolveRequirements(template, requirements) requirements.sortBy { it.priority } diff --git a/game/src/main/kotlin/content/bot/action/RequirementResolver.kt b/game/src/main/kotlin/content/bot/action/Resolver.kt similarity index 78% rename from game/src/main/kotlin/content/bot/action/RequirementResolver.kt rename to game/src/main/kotlin/content/bot/action/Resolver.kt index 9e486daa19..ed9ae12bc1 100644 --- a/game/src/main/kotlin/content/bot/action/RequirementResolver.kt +++ b/game/src/main/kotlin/content/bot/action/Resolver.kt @@ -6,8 +6,8 @@ import content.bot.fact.Fact * An activity that can be performed to resolve a requirement * E.g. buying a pickaxe from a shop, getting items out of the bank, picking up an item off of the floor */ -data class RequirementResolver( +data class Resolver( override val id: String, - override val requirements: List = emptyList(), + override val requires: List = emptyList(), override val plan: List = emptyList(), ) : Behaviour \ No newline at end of file diff --git a/game/src/test/kotlin/content/bot/action/BehaviourFragmentTest.kt b/game/src/test/kotlin/content/bot/action/BehaviourFragmentTest.kt index f92763388d..b96498ba3b 100644 --- a/game/src/test/kotlin/content/bot/action/BehaviourFragmentTest.kt +++ b/game/src/test/kotlin/content/bot/action/BehaviourFragmentTest.kt @@ -207,7 +207,7 @@ class BehaviourFragmentTest { val template = BotActivity( id = "a", capacity = 1, - requirements = listOf( + requires = listOf( FactReference( AtLocation("default"), references = mapOf( @@ -227,7 +227,7 @@ class BehaviourFragmentTest { val template = BotActivity( id = "a", capacity = 1, - requirements = listOf( + requires = listOf( FactReference( AtLocation("default"), references = mapOf( @@ -263,7 +263,7 @@ class BehaviourFragmentTest { val template = BotActivity( id = "a", capacity = 1, - requirements = listOf( + requires = listOf( FactReference( default, references = references @@ -282,7 +282,7 @@ class BehaviourFragmentTest { val template = BotActivity( id = "a", capacity = 1, - requirements = listOf( + requires = listOf( FactReference( HasSkillLevel("x"), references = mapOf("skill" to "missing") @@ -300,7 +300,7 @@ class BehaviourFragmentTest { val template = BotActivity( id = "a", capacity = 1, - requirements = listOf( + requires = listOf( FactReference( HasSkillLevel("x"), emptyMap() @@ -320,7 +320,7 @@ class BehaviourFragmentTest { val template = BotActivity( id = "a", capacity = 1, - requirements = listOf( + requires = listOf( HasSkillLevel("x") ) ) @@ -337,7 +337,7 @@ class BehaviourFragmentTest { val template = BotActivity( id = "a", capacity = 1, - requirements = listOf(FactClone("x")) + requires = listOf(FactClone("x")) ) assertThrows { fragment.resolveRequirements(template, mutableListOf()) @@ -350,7 +350,7 @@ class BehaviourFragmentTest { val template = BotActivity( id = "a", capacity = 1, - requirements = listOf( + requires = listOf( FactReference( FactReference( HasSkillLevel("x"), From 2525fd43abc9b4790ca2459674ed0ebb15397854 Mon Sep 17 00:00:00 2001 From: GregHib Date: Wed, 28 Jan 2026 09:38:30 +0000 Subject: [PATCH 012/101] Started on resolvers --- data/bot/woodcutting.bots.toml | 6 +-- .../src/main/kotlin/content/bot/BotManager.kt | 41 +++++++++++++++---- .../kotlin/content/bot/action/Behaviour.kt | 1 + .../content/bot/action/BehaviourFragment.kt | 2 + .../kotlin/content/bot/action/BotActivity.kt | 20 +++++++-- .../kotlin/content/bot/action/Resolver.kt | 1 + 6 files changed, 56 insertions(+), 15 deletions(-) diff --git a/data/bot/woodcutting.bots.toml b/data/bot/woodcutting.bots.toml index 22ba6ed31b..9f72f9d7e7 100644 --- a/data/bot/woodcutting.bots.toml +++ b/data/bot/woodcutting.bots.toml @@ -16,7 +16,7 @@ requires = [ { skill = "woodcutting", min = 1, max = 15 }, ] produces = [ - { item = "logs" } + { carries = "logs" } ] [oak_trees] @@ -27,7 +27,7 @@ requires = [ { skill = "woodcutting", min = 15, max = 30 }, ] produces = [ - { item = "oak_logs" } + { carries = "oak_logs" } ] @@ -42,7 +42,7 @@ plan = [ { option = "Buy-1", interface = "shop:inventory:$item" }, ] produces = [ - { item = "$item" } + { carries = "$item" } ] [buy_iron_hatchet] diff --git a/game/src/main/kotlin/content/bot/BotManager.kt b/game/src/main/kotlin/content/bot/BotManager.kt index a6ac05520c..663d200101 100644 --- a/game/src/main/kotlin/content/bot/BotManager.kt +++ b/game/src/main/kotlin/content/bot/BotManager.kt @@ -2,6 +2,7 @@ package content.bot import com.github.michaelbull.logging.InlineLogger import content.bot.action.* +import content.bot.fact.Fact import content.bot.fact.MandatoryFact import content.bot.fact.ResolvableFact import world.gregs.voidps.engine.data.ConfigFiles @@ -10,6 +11,7 @@ import world.gregs.voidps.type.random class BotManager( private var activities: Map = emptyMap(), + private var resolvers: Map> = emptyMap(), ) : Runnable { val slots = ActivitySlots() val bots = mutableListOf() @@ -54,22 +56,43 @@ class BotManager( private fun start(bot: Bot, behaviour: Behaviour, frame: BehaviourFrame) { for (requirement in behaviour.requires) { - if (!requirement.satisfied(bot)) { - if (requirement is MandatoryFact) { - frame.fail(Reason.Requirements) - return - } - if (requirement is ResolvableFact) { -// frame.blocked.add(requirement) - // TODO - + if (requirement.satisfied(bot)) { + continue + } + if (requirement is MandatoryFact) { + frame.fail(Reason.Requirements) + return + } else if (requirement is ResolvableFact) { + val resolver = pickResolver(bot, requirement, frame) + if (resolver == null) { + frame.fail(Reason.Requirements) // No way to resolve return } + // Attempt resolution + frame.blocked.add(resolver.id) + bot.queue(BehaviourFrame(resolver)) + return } } frame.start(bot) } + // TODO read resolvers + // Handle resolver execution and fallback + private fun pickResolver(bot: Bot, fact: ResolvableFact, frame: BehaviourFrame): Behaviour? { + val options = mutableListOf() + for (resolver in resolvers[fact] ?: return null) { + if (frame.blocked.contains(resolver.id)) { + continue + } + if (resolver.requires.any { it is MandatoryFact && !it.satisfied(bot) }) { + continue + } + options.add(resolver) + } + return options.randomOrNull(random) + } + private fun execute(bot: Bot) { val frame = bot.frame() val behaviour = frame.behaviour diff --git a/game/src/main/kotlin/content/bot/action/Behaviour.kt b/game/src/main/kotlin/content/bot/action/Behaviour.kt index c5ebb67bb8..e7d35834d9 100644 --- a/game/src/main/kotlin/content/bot/action/Behaviour.kt +++ b/game/src/main/kotlin/content/bot/action/Behaviour.kt @@ -6,4 +6,5 @@ interface Behaviour { val id: String val requires: List val plan: List + val produces: Set } \ No newline at end of file diff --git a/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt b/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt index dec919e481..7a6741ba3e 100644 --- a/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt +++ b/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt @@ -14,10 +14,12 @@ import content.bot.fact.HasVariable data class BehaviourFragment( override val id: String, + val type: String, val capacity: Int, var template: String, override val requires: List = emptyList(), override val plan: List = emptyList(), + override val produces: Set = emptySet(), val fields: Map = emptyMap(), ) : Behaviour { fun resolveActions(template: BotActivity, actions: MutableList) { diff --git a/game/src/main/kotlin/content/bot/action/BotActivity.kt b/game/src/main/kotlin/content/bot/action/BotActivity.kt index d5eb07aeda..dc2407f2a9 100644 --- a/game/src/main/kotlin/content/bot/action/BotActivity.kt +++ b/game/src/main/kotlin/content/bot/action/BotActivity.kt @@ -24,10 +24,12 @@ data class BotActivity( val capacity: Int, override val requires: List = emptyList(), override val plan: List = emptyList(), + override val produces: Set = emptySet(), ) : Behaviour fun loadActivities(paths: List): Map { val activities = mutableMapOf() + val resolvers = mutableMapOf() val fragments = mutableMapOf() timedLoad("bot activity") { val clones = mutableMapOf() @@ -41,12 +43,13 @@ fun loadActivities(paths: List): Map { var weight = 0 var actions: List = emptyList() var requirements: List = emptyList() + var produces: List = emptyList() var fields: Map = emptyMap() while (nextPair()) { when (val key = key()) { "requires" -> requirements = requirements() "plan" -> actions = actions() - "produces" -> produces() + "produces" -> produces = requirements() "capacity" -> capacity = int() "type" -> type = string() "template" -> template = string() @@ -59,8 +62,13 @@ fun loadActivities(paths: List): Map { if (clone != null) { clones[id] = clone.id } + if (template != null) { - fragments[id] = BehaviourFragment(id, capacity, template, requirements, plan = actions, fields = fields) + fragments[id] = BehaviourFragment(id, type, capacity, template, requirements, plan = actions, fields = fields) + } else if (type == "resolver") { + for (fact in produces) { + resolvers[fact] = Resolver(id, requirements, plan = actions) + } } else { activities[id] = BotActivity(id, capacity, requirements, plan = actions) } @@ -102,7 +110,13 @@ fun loadActivities(paths: List): Map { val actions = mutableListOf() actions.addAll(fragment.plan) fragment.resolveActions(template, actions) - activities[id] = BotActivity(id, fragment.capacity, requirements, actions) + if (fragment.type == "resolver") { + for (fact in fragment.produces) { + resolvers[fact] = Resolver(id, requirements, actions) + } + } else { + activities[id] = BotActivity(id, fragment.capacity, requirements, actions) + } } // Templates aren't selectable activities for (template in templates) { diff --git a/game/src/main/kotlin/content/bot/action/Resolver.kt b/game/src/main/kotlin/content/bot/action/Resolver.kt index ed9ae12bc1..6d019a478d 100644 --- a/game/src/main/kotlin/content/bot/action/Resolver.kt +++ b/game/src/main/kotlin/content/bot/action/Resolver.kt @@ -10,4 +10,5 @@ data class Resolver( override val id: String, override val requires: List = emptyList(), override val plan: List = emptyList(), + override val produces: Set = emptySet(), ) : Behaviour \ No newline at end of file From 52cd7321b5227278790ae9cf0d5930a4a52e0841 Mon Sep 17 00:00:00 2001 From: GregHib Date: Wed, 28 Jan 2026 11:40:34 +0000 Subject: [PATCH 013/101] Add fact checking --- data/bot/woodcutting.bots.toml | 2 +- .../src/main/kotlin/content/bot/BotManager.kt | 6 ++-- .../content/bot/action/BehaviourFragment.kt | 9 ++---- .../kotlin/content/bot/action/BotActivity.kt | 7 +++-- game/src/main/kotlin/content/bot/fact/Fact.kt | 2 +- .../kotlin/content/bot/fact/MandatoryFact.kt | 14 ++++++--- .../kotlin/content/bot/fact/ResolvableFact.kt | 29 +++++++++++-------- .../test/kotlin/content/bot/BotManagerTest.kt | 3 +- .../bot/action/BehaviourFragmentTest.kt | 22 +++++++------- 9 files changed, 52 insertions(+), 42 deletions(-) diff --git a/data/bot/woodcutting.bots.toml b/data/bot/woodcutting.bots.toml index 9f72f9d7e7..a94cccab1f 100644 --- a/data/bot/woodcutting.bots.toml +++ b/data/bot/woodcutting.bots.toml @@ -11,7 +11,7 @@ plan = [ [normal_trees] template = "woodcutting_template" capacity = 4 -fields = { hatchet = "bronze_hatchet,iron_hatchet", location = "normal_trees", tree = "tree*", wait = 10 } +fields = { hatchet = "iron_hatchet", location = "normal_trees", tree = "tree*", wait = 10 } requires = [ { skill = "woodcutting", min = 1, max = 15 }, ] diff --git a/game/src/main/kotlin/content/bot/BotManager.kt b/game/src/main/kotlin/content/bot/BotManager.kt index 663d200101..e861ba530e 100644 --- a/game/src/main/kotlin/content/bot/BotManager.kt +++ b/game/src/main/kotlin/content/bot/BotManager.kt @@ -37,7 +37,7 @@ class BotManager( } private fun hasRequirements(bot: Bot, activity: BotActivity): Boolean { - return slots.hasFree(activity) && !bot.blocked.contains(activity.id) && activity.requires.all { it is MandatoryFact && it.satisfied(bot) } + return slots.hasFree(activity) && !bot.blocked.contains(activity.id) && activity.requires.all { it is MandatoryFact && it.check(bot) } } private fun assignActivity(bot: Bot) { @@ -56,7 +56,7 @@ class BotManager( private fun start(bot: Bot, behaviour: Behaviour, frame: BehaviourFrame) { for (requirement in behaviour.requires) { - if (requirement.satisfied(bot)) { + if (requirement.check(bot)) { continue } if (requirement is MandatoryFact) { @@ -85,7 +85,7 @@ class BotManager( if (frame.blocked.contains(resolver.id)) { continue } - if (resolver.requires.any { it is MandatoryFact && !it.satisfied(bot) }) { + if (resolver.requires.any { it is MandatoryFact && !it.check(bot) }) { continue } options.add(resolver) diff --git a/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt b/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt index 7a6741ba3e..a03d1329f8 100644 --- a/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt +++ b/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt @@ -6,11 +6,12 @@ import content.bot.fact.CarriesItem import content.bot.fact.EquipsItem import content.bot.fact.HasInventorySpace import content.bot.fact.AtLocation -import content.bot.fact.OwnsItem import content.bot.fact.FactReference import content.bot.fact.HasSkillLevel import content.bot.fact.AtTile import content.bot.fact.HasVariable +import net.pearx.kasechange.toPascalCase +import world.gregs.voidps.engine.entity.character.player.skill.Skill data class BehaviourFragment( override val id: String, @@ -61,7 +62,7 @@ data class BehaviourFragment( val resolved = when (req) { is FactReference -> when (val requirement = req.fact) { is HasSkillLevel -> HasSkillLevel( - id = resolve(req.references["skill"], requirement.id), + skill = Skill.of(resolve(req.references["skill"], requirement.skill.name).toPascalCase())!!, min = resolve(req.references["min"], requirement.min), max = resolve(req.references["max"], requirement.max), ) @@ -77,10 +78,6 @@ data class BehaviourFragment( id = resolve(req.references["equips"], requirement.id), amount = resolve(req.references["amount"], requirement.amount), ) - is OwnsItem -> OwnsItem( - id = resolve(req.references["owns"], requirement.id), - amount = resolve(req.references["amount"], requirement.amount), - ) is HasInventorySpace -> HasInventorySpace( amount = resolve(req.references["inventory_space"], requirement.amount), ) diff --git a/game/src/main/kotlin/content/bot/action/BotActivity.kt b/game/src/main/kotlin/content/bot/action/BotActivity.kt index dc2407f2a9..44ecf285bf 100644 --- a/game/src/main/kotlin/content/bot/action/BotActivity.kt +++ b/game/src/main/kotlin/content/bot/action/BotActivity.kt @@ -7,12 +7,14 @@ import content.bot.fact.CarriesItem import content.bot.fact.Fact import content.bot.fact.EquipsItem import content.bot.fact.AtLocation -import content.bot.fact.OwnsItem import content.bot.fact.HasSkillLevel import content.bot.fact.AtTile import content.bot.fact.HasVariable +import net.pearx.kasechange.toPascalCase +import net.pearx.kasechange.toTitleCase import world.gregs.config.Config import world.gregs.config.ConfigReader +import world.gregs.voidps.engine.entity.character.player.skill.Skill import world.gregs.voidps.engine.timedLoad /** @@ -230,9 +232,8 @@ private fun ConfigReader.requirements(): List { } } var requirement = when (type) { - "skill" -> HasSkillLevel(id, min, max) + "skill" -> HasSkillLevel(Skill.of(id.toPascalCase())!!, min, max) "carries" -> CarriesItem(id, min) - "owns" -> OwnsItem(id, min) "equips" -> EquipsItem(id, min) "variable" -> HasVariable(id, value) "clone" -> FactClone(id) diff --git a/game/src/main/kotlin/content/bot/fact/Fact.kt b/game/src/main/kotlin/content/bot/fact/Fact.kt index 2850d4e935..f89341daf3 100644 --- a/game/src/main/kotlin/content/bot/fact/Fact.kt +++ b/game/src/main/kotlin/content/bot/fact/Fact.kt @@ -7,7 +7,7 @@ import content.bot.Bot * @param priority Ensure bots aren't walking to locations before getting items etc... lower values are prioritised first. */ sealed class Fact(val priority: Int) { - fun satisfied(bot: Bot): Boolean = false + open fun check(bot: Bot): Boolean = false } internal data class FactClone( diff --git a/game/src/main/kotlin/content/bot/fact/MandatoryFact.kt b/game/src/main/kotlin/content/bot/fact/MandatoryFact.kt index 7507619fed..e7b0f79f24 100644 --- a/game/src/main/kotlin/content/bot/fact/MandatoryFact.kt +++ b/game/src/main/kotlin/content/bot/fact/MandatoryFact.kt @@ -1,15 +1,21 @@ package content.bot.fact +import content.bot.Bot +import world.gregs.voidps.engine.entity.character.player.skill.Skill + sealed class MandatoryFact(priority: Int = 0) : Fact(priority) data class HasSkillLevel( - val id: String, + val skill: Skill, val min: Int = 1, val max: Int = 120 -) : MandatoryFact() - +) : MandatoryFact() { + override fun check(bot: Bot) = bot.player.levels.get(skill) in min..max +} data class HasVariable( val id: String, val value: Any? = null -) : MandatoryFact() +) : MandatoryFact() { + override fun check(bot: Bot) = bot.player.variables.get(id) == value +} diff --git a/game/src/main/kotlin/content/bot/fact/ResolvableFact.kt b/game/src/main/kotlin/content/bot/fact/ResolvableFact.kt index b748469270..8c6668bc3d 100644 --- a/game/src/main/kotlin/content/bot/fact/ResolvableFact.kt +++ b/game/src/main/kotlin/content/bot/fact/ResolvableFact.kt @@ -1,6 +1,10 @@ package content.bot.fact import content.bot.Bot +import world.gregs.voidps.engine.data.definition.Areas +import world.gregs.voidps.engine.inv.carriesItem +import world.gregs.voidps.engine.inv.equips +import world.gregs.voidps.engine.inv.inventory sealed class ResolvableFact(priority: Int) : Fact(priority) @@ -13,32 +17,33 @@ data class EquipsItem( override val id: String, override val amount: Int = 1, ) : ItemFact() { - fun check(bot: Bot) { - // bot.inventory.has(id, amount) - } + override fun check(bot: Bot) = bot.player.equips(id, amount) } data class CarriesItem( override val id: String, override val amount: Int = 1, -) : ItemFact() - -data class OwnsItem( - override val id: String, - override val amount: Int = 1, -) : ItemFact() +) : ItemFact() { + override fun check(bot: Bot) = bot.player.carriesItem(id, amount) +} data class HasInventorySpace( val amount: Int, -) : ResolvableFact(10) +) : ResolvableFact(10) { + override fun check(bot: Bot) = bot.player.inventory.spaces >= amount +} data class AtLocation( val id: String, -) : ResolvableFact(1000) +) : ResolvableFact(1000) { + override fun check(bot: Bot) = bot.player.tile in Areas[id] +} data class AtTile( val x: Int, val y: Int, val level: Int, val radius: Int, -) : ResolvableFact(1100) \ No newline at end of file +) : ResolvableFact(1100) { + override fun check(bot: Bot) = bot.player.tile.within(x, y, radius, level) +} \ No newline at end of file diff --git a/game/src/test/kotlin/content/bot/BotManagerTest.kt b/game/src/test/kotlin/content/bot/BotManagerTest.kt index 98ce07f056..af74b0a627 100644 --- a/game/src/test/kotlin/content/bot/BotManagerTest.kt +++ b/game/src/test/kotlin/content/bot/BotManagerTest.kt @@ -10,6 +10,7 @@ import content.bot.fact.HasSkillLevel import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.Test import world.gregs.voidps.engine.entity.character.player.Player +import world.gregs.voidps.engine.entity.character.player.skill.Skill class BotManagerTest { @@ -208,7 +209,7 @@ class BotManagerTest { val activity = testActivity( id = "test", requirements = listOf( - HasSkillLevel("attack", 99, 99) + HasSkillLevel(Skill.Attack, 99, 99) ), plan = listOf(BotAction.Wait(4)) ) diff --git a/game/src/test/kotlin/content/bot/action/BehaviourFragmentTest.kt b/game/src/test/kotlin/content/bot/action/BehaviourFragmentTest.kt index b96498ba3b..ac7c79ccd7 100644 --- a/game/src/test/kotlin/content/bot/action/BehaviourFragmentTest.kt +++ b/game/src/test/kotlin/content/bot/action/BehaviourFragmentTest.kt @@ -6,7 +6,6 @@ import content.bot.fact.CarriesItem import content.bot.fact.EquipsItem import content.bot.fact.HasInventorySpace import content.bot.fact.AtLocation -import content.bot.fact.OwnsItem import content.bot.fact.FactReference import content.bot.fact.HasSkillLevel import content.bot.fact.AtTile @@ -16,6 +15,7 @@ import org.junit.jupiter.api.DynamicTest.dynamicTest import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestFactory import org.junit.jupiter.api.assertThrows +import world.gregs.voidps.engine.entity.character.player.skill.Skill class BehaviourFragmentTest { @@ -24,6 +24,7 @@ class BehaviourFragmentTest { id = "test", capacity = 1, template = "tpl", + type = "activity", fields = fields ) @@ -182,9 +183,9 @@ class BehaviourFragmentTest { Triple(BotAction.WaitFullInventory(4), mapOf("timeout" to 5), BotAction.WaitFullInventory(5)), ).map { (default, values, expected) -> dynamicTest("Resolve ${default::class.simpleName} references") { - val fields = values.mapKeys { "\$ref_${it.key}" } + val fields = values.mapKeys { "ref_${it.key}" } val fragment = fragment(fields) - val references = values.map { it.key to "ref_${it.key}" }.toMap() + val references = values.map { it.key to "\$ref_${it.key}" }.toMap() val template = BotActivity( id = "a", capacity = 1, @@ -247,11 +248,10 @@ class BehaviourFragmentTest { @TestFactory fun `Resolve requirement references`() = listOf( - Triple(HasSkillLevel("default", 1, 120), mapOf("skill" to "attack", "min" to 5, "max" to 99), HasSkillLevel("attack", 5, 99)), + Triple(HasSkillLevel(Skill.Defence, 1, 120), mapOf("skill" to "attack", "min" to 5, "max" to 99), HasSkillLevel(Skill.Attack, 5, 99)), Triple(HasVariable("default", 1), mapOf("variable" to "test", "value" to true), HasVariable("test", true)), Triple(EquipsItem("default", 1), mapOf("equips" to "item", "amount" to 10), EquipsItem("item", 10)), Triple(CarriesItem("default", 1), mapOf("carries" to "item", "amount" to 10), CarriesItem("item", 10)), - Triple(OwnsItem("default", 1), mapOf("owns" to "item", "amount" to 10), OwnsItem("item", 10)), Triple(HasInventorySpace(1), mapOf("inventory_space" to 10), HasInventorySpace(10)), Triple(AtLocation("default"), mapOf("location" to "area"), AtLocation("area")), Triple(AtTile(0, 0, 0, 0), mapOf("x" to 4, "y" to 3, "level" to 2, "radius" to 1), AtTile(4, 3, 2, 1)), @@ -284,7 +284,7 @@ class BehaviourFragmentTest { capacity = 1, requires = listOf( FactReference( - HasSkillLevel("x"), + HasSkillLevel(Skill.Attack), references = mapOf("skill" to "missing") ) ) @@ -302,7 +302,7 @@ class BehaviourFragmentTest { capacity = 1, requires = listOf( FactReference( - HasSkillLevel("x"), + HasSkillLevel(Skill.Attack), emptyMap() ) ) @@ -311,7 +311,7 @@ class BehaviourFragmentTest { val actions = mutableListOf() fragment.resolveRequirements(template, actions) - assertEquals(HasSkillLevel("x"), actions.single()) + assertEquals(HasSkillLevel(Skill.Attack), actions.single()) } @Test @@ -321,14 +321,14 @@ class BehaviourFragmentTest { id = "a", capacity = 1, requires = listOf( - HasSkillLevel("x") + HasSkillLevel(Skill.Attack) ) ) val actions = mutableListOf() fragment.resolveRequirements(template, actions) - assertEquals(HasSkillLevel("x"), actions.single()) + assertEquals(HasSkillLevel(Skill.Attack), actions.single()) } @Test @@ -353,7 +353,7 @@ class BehaviourFragmentTest { requires = listOf( FactReference( FactReference( - HasSkillLevel("x"), + HasSkillLevel(Skill.Attack), emptyMap() ), emptyMap() From e1413e945d4bbd3fa4a5f9511eefbc0bdcf042cb Mon Sep 17 00:00:00 2001 From: GregHib Date: Wed, 28 Jan 2026 11:52:10 +0000 Subject: [PATCH 014/101] Add fact checks and EquipsOne/CarriesOne of a few items given wildcards --- data/bot/woodcutting.bots.toml | 5 ++- .../gregs/voidps/engine/event/Wildcards.kt | 6 ++++ .../content/bot/action/BehaviourFragment.kt | 28 +++++++++++++++++ .../kotlin/content/bot/action/BotActivity.kt | 17 ++++++++-- .../kotlin/content/bot/fact/ResolvableFact.kt | 31 ++++++++++++------- 5 files changed, 70 insertions(+), 17 deletions(-) diff --git a/data/bot/woodcutting.bots.toml b/data/bot/woodcutting.bots.toml index a94cccab1f..c2031fb3bc 100644 --- a/data/bot/woodcutting.bots.toml +++ b/data/bot/woodcutting.bots.toml @@ -30,7 +30,6 @@ produces = [ { carries = "oak_logs" } ] - [buy_from_shop] requires = [ { carries = "coins", amount = "$cost" }, @@ -65,7 +64,7 @@ requires = [ type = "teleport" weight = 10 requires = [ - { equips = "ring_of_dueling_*" } + { equips = "ring_of_duelling_*" } ] plan = [ { option = "Castle Wars", interface = "equipment:inventory" }, @@ -79,7 +78,7 @@ type = "teleport" weight = 8 requires = [ { variable = "spellbook", value = "normal" }, - { carries = "ring_of_dueling_*" } + { carries = "ring_of_duelling_*" } ] plan = [ { option = "Rub", interface = "inventory:inventory" }, diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/event/Wildcards.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/event/Wildcards.kt index 92fb9a7627..b8a0da5b6d 100644 --- a/engine/src/main/kotlin/world/gregs/voidps/engine/event/Wildcards.kt +++ b/engine/src/main/kotlin/world/gregs/voidps/engine/event/Wildcards.kt @@ -79,6 +79,12 @@ object Wildcards { } } + fun get(key: String, type: Wildcard): Set { + val set = mutableSetOf() + find(key, type) { set.add(it) } + return set + } + fun find(key: String, type: Wildcard, block: (String) -> Unit) { if (key == "*") { block(key) diff --git a/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt b/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt index a03d1329f8..47c891a506 100644 --- a/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt +++ b/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt @@ -9,9 +9,13 @@ import content.bot.fact.AtLocation import content.bot.fact.FactReference import content.bot.fact.HasSkillLevel import content.bot.fact.AtTile +import content.bot.fact.CarriesOne +import content.bot.fact.EquipsOne import content.bot.fact.HasVariable import net.pearx.kasechange.toPascalCase import world.gregs.voidps.engine.entity.character.player.skill.Skill +import world.gregs.voidps.engine.event.Wildcard +import world.gregs.voidps.engine.event.Wildcards data class BehaviourFragment( override val id: String, @@ -74,10 +78,34 @@ data class BehaviourFragment( id = resolve(req.references["carries"], requirement.id), amount = resolve(req.references["amount"], requirement.amount), ) + is CarriesOne -> { + val resolve = resolve(req.references["equips"], "") + val ids = if (resolve.isBlank()) { + requirement.ids + } else { + Wildcards.get(resolve, Wildcard.Item) + } + CarriesOne( + ids = ids, + amount = resolve(req.references["amount"], requirement.amount), + ) + } is EquipsItem -> EquipsItem( id = resolve(req.references["equips"], requirement.id), amount = resolve(req.references["amount"], requirement.amount), ) + is EquipsOne -> { + val resolve = resolve(req.references["equips"], "") + val ids = if (resolve.isBlank()) { + requirement.ids + } else { + Wildcards.get(resolve, Wildcard.Item) + } + EquipsOne( + ids = ids, + amount = resolve(req.references["amount"], requirement.amount), + ) + } is HasInventorySpace -> HasInventorySpace( amount = resolve(req.references["inventory_space"], requirement.amount), ) diff --git a/game/src/main/kotlin/content/bot/action/BotActivity.kt b/game/src/main/kotlin/content/bot/action/BotActivity.kt index 44ecf285bf..9de58f98f1 100644 --- a/game/src/main/kotlin/content/bot/action/BotActivity.kt +++ b/game/src/main/kotlin/content/bot/action/BotActivity.kt @@ -9,12 +9,15 @@ import content.bot.fact.EquipsItem import content.bot.fact.AtLocation import content.bot.fact.HasSkillLevel import content.bot.fact.AtTile +import content.bot.fact.CarriesOne +import content.bot.fact.EquipsOne import content.bot.fact.HasVariable import net.pearx.kasechange.toPascalCase -import net.pearx.kasechange.toTitleCase import world.gregs.config.Config import world.gregs.config.ConfigReader import world.gregs.voidps.engine.entity.character.player.skill.Skill +import world.gregs.voidps.engine.event.Wildcard +import world.gregs.voidps.engine.event.Wildcards import world.gregs.voidps.engine.timedLoad /** @@ -233,8 +236,16 @@ private fun ConfigReader.requirements(): List { } var requirement = when (type) { "skill" -> HasSkillLevel(Skill.of(id.toPascalCase())!!, min, max) - "carries" -> CarriesItem(id, min) - "equips" -> EquipsItem(id, min) + "carries" -> if (id.any { it == '*' || it == '#' }) { + CarriesOne(Wildcards.get(id, Wildcard.Item), min) + } else { + CarriesItem(id, min) + } + "equips" -> if (id.any { it == '*' || it == '#' }) { + EquipsOne(Wildcards.get(id, Wildcard.Item), min) + } else { + EquipsItem(id, min) + } "variable" -> HasVariable(id, value) "clone" -> FactClone(id) "inventory_space" -> HasInventorySpace(min) diff --git a/game/src/main/kotlin/content/bot/fact/ResolvableFact.kt b/game/src/main/kotlin/content/bot/fact/ResolvableFact.kt index 8c6668bc3d..ee590de05e 100644 --- a/game/src/main/kotlin/content/bot/fact/ResolvableFact.kt +++ b/game/src/main/kotlin/content/bot/fact/ResolvableFact.kt @@ -8,25 +8,34 @@ import world.gregs.voidps.engine.inv.inventory sealed class ResolvableFact(priority: Int) : Fact(priority) -sealed class ItemFact : ResolvableFact(100) { - abstract val id: String - abstract val amount: Int -} - data class EquipsItem( - override val id: String, - override val amount: Int = 1, -) : ItemFact() { + val id: String, + val amount: Int = 1, +) : ResolvableFact(100) { override fun check(bot: Bot) = bot.player.equips(id, amount) } data class CarriesItem( - override val id: String, - override val amount: Int = 1, -) : ItemFact() { + val id: String, + val amount: Int = 1, +) : ResolvableFact(100) { override fun check(bot: Bot) = bot.player.carriesItem(id, amount) } +data class EquipsOne( + val ids: Set, + val amount: Int = 1, +) : ResolvableFact(100) { + override fun check(bot: Bot) = ids.any { id -> bot.player.equips(id, amount) } +} + +data class CarriesOne( + val ids: Set, + val amount: Int = 1, +) : ResolvableFact(100) { + override fun check(bot: Bot) = ids.any { id -> bot.player.carriesItem(id, amount) } +} + data class HasInventorySpace( val amount: Int, ) : ResolvableFact(10) { From e173a5f9ce883ffa15a6efd18bfb7acf38c9a279 Mon Sep 17 00:00:00 2001 From: GregHib Date: Wed, 28 Jan 2026 12:35:02 +0000 Subject: [PATCH 015/101] Add manager debugging --- data/bot/woodcutting.bots.toml | 7 +- game/src/main/kotlin/content/bot/Bot.kt | 4 + .../src/main/kotlin/content/bot/BotManager.kt | 85 +++++++++++++++---- game/src/main/kotlin/content/bot/BotSpawns.kt | 38 +++++---- .../content/bot/action/BehaviourFragment.kt | 1 + .../kotlin/content/bot/action/BotActivity.kt | 11 +-- .../main/kotlin/content/bot/action/Reason.kt | 4 +- .../kotlin/content/bot/action/Resolver.kt | 2 + .../test/kotlin/content/bot/BotManagerTest.kt | 22 ++--- .../bot/action/BehaviourFragmentTest.kt | 3 +- 10 files changed, 125 insertions(+), 52 deletions(-) diff --git a/data/bot/woodcutting.bots.toml b/data/bot/woodcutting.bots.toml index c2031fb3bc..886334b982 100644 --- a/data/bot/woodcutting.bots.toml +++ b/data/bot/woodcutting.bots.toml @@ -1,3 +1,9 @@ +[idle] +capacity = 2048 +plan = [ + { wait = 50 } # 30s +] + [woodcutting_template] requires = [ { carries = "$hatchet" }, @@ -77,7 +83,6 @@ produces = [ type = "teleport" weight = 8 requires = [ - { variable = "spellbook", value = "normal" }, { carries = "ring_of_duelling_*" } ] plan = [ diff --git a/game/src/main/kotlin/content/bot/Bot.kt b/game/src/main/kotlin/content/bot/Bot.kt index dab530d771..9484120fc8 100644 --- a/game/src/main/kotlin/content/bot/Bot.kt +++ b/game/src/main/kotlin/content/bot/Bot.kt @@ -37,4 +37,8 @@ data class Bot(val player: Player) : Character by player { } frame().state = BehaviourState.Failed(Reason.Cancelled) } + + override fun toString(): String { + return "BOT ${player.accountName}" + } } diff --git a/game/src/main/kotlin/content/bot/BotManager.kt b/game/src/main/kotlin/content/bot/BotManager.kt index e861ba530e..6ce9585308 100644 --- a/game/src/main/kotlin/content/bot/BotManager.kt +++ b/game/src/main/kotlin/content/bot/BotManager.kt @@ -7,18 +7,37 @@ import content.bot.fact.MandatoryFact import content.bot.fact.ResolvableFact import world.gregs.voidps.engine.data.ConfigFiles import world.gregs.voidps.engine.data.Settings +import world.gregs.voidps.engine.event.AuditLog import world.gregs.voidps.type.random +/** + * Each tick checks + * 1. Assigns [activities] if a bot has none. + * 2. Moves bots onto their next action if they have completed their current. + * 3. Queues [resolvers] when a bot has an activity with unresolved requirements. + */ class BotManager( - private var activities: Map = emptyMap(), - private var resolvers: Map> = emptyMap(), + private val activities: MutableMap = mutableMapOf(), + private val resolvers: MutableMap> = mutableMapOf(), ) : Runnable { val slots = ActivitySlots() val bots = mutableListOf() - val logger = InlineLogger("BotManager") + private val logger = InlineLogger("BotManager") + + fun add(bot: Bot) { + bots.add(bot) + } + + fun remove(bot: Bot): Boolean { + if (bots.remove(bot)) { + stop(bot) + return true + } + return false + } fun load(files: ConfigFiles): BotManager { - activities = loadActivities(files.list(Settings["bots.definitions"])) + loadActivities(files.list(Settings["bots.definitions"]), activities, resolvers) return this } @@ -30,7 +49,7 @@ class BotManager( fun tick(bot: Bot) { if (bot.noTask()) { - assignActivity(bot) + assignRandom(bot) return } execute(bot) @@ -40,15 +59,34 @@ class BotManager( return slots.hasFree(activity) && !bot.blocked.contains(activity.id) && activity.requires.all { it is MandatoryFact && it.check(bot) } } - private fun assignActivity(bot: Bot) { + fun assign(bot: Bot, id: String): Boolean { + val activity = activities[id] ?: return false + assign(bot, activity) + return true + } + + private fun assignRandom(bot: Bot) { val activity = if (bot.previous != null && hasRequirements(bot, bot.previous!!)) { - bot.previous!! + bot.previous } else { activities.values .filter { hasRequirements(bot, it) } - .randomOrNull(random) ?: return + .randomOrNull(random) + } + if (activity == null) { + if (bot.player["debug", false]) { + logger.info { "No activities with requirements met for bot: ${bot.player.accountName}." } + } + return + } + assign(bot, activity) + } + + private fun assign(bot: Bot, activity: BotActivity) { + AuditLog.event(bot, "assigned", activity.id) + if (bot.player["debug", false]) { + logger.info { "Assigned bot: ${bot.player.accountName} task ${activity.id}." } } - logger.info { "Assigned bot: ${bot.player.accountName} task ${activity.id}." } slots.occupy(activity) bot.previous = activity bot.queue(BehaviourFrame(activity)) @@ -60,32 +98,38 @@ class BotManager( continue } if (requirement is MandatoryFact) { - frame.fail(Reason.Requirements) + frame.fail(Reason.Requirement(requirement)) return } else if (requirement is ResolvableFact) { val resolver = pickResolver(bot, requirement, frame) if (resolver == null) { - frame.fail(Reason.Requirements) // No way to resolve + frame.fail(Reason.Requirement(requirement)) // No way to resolve return } // Attempt resolution + AuditLog.event(bot, "start_resolver", resolver.id, behaviour.id) + if (bot.player["debug", false]) { + logger.info { "Starting resolution: ${resolver.id} for ${behaviour.id} req ${requirement}." } + } frame.blocked.add(resolver.id) bot.queue(BehaviourFrame(resolver)) return } } + AuditLog.event(bot, "start_activity", behaviour.id) + if (bot.player["debug", false]) { + logger.info { "Starting activity: ${behaviour.id}." } + } frame.start(bot) } - // TODO read resolvers - // Handle resolver execution and fallback private fun pickResolver(bot: Bot, fact: ResolvableFact, frame: BehaviourFrame): Behaviour? { val options = mutableListOf() for (resolver in resolvers[fact] ?: return null) { if (frame.blocked.contains(resolver.id)) { continue } - if (resolver.requires.any { it is MandatoryFact && !it.check(bot) }) { + if (resolver.requires.any { fact -> fact is MandatoryFact && !fact.check(bot) }) { continue } options.add(resolver) @@ -100,6 +144,7 @@ class BotManager( BehaviourState.Running -> return BehaviourState.Pending -> start(bot, behaviour, frame) BehaviourState.Success -> if (!frame.next()) { + AuditLog.event(bot, "completed", frame.behaviour.id) bot.frames.pop() if (behaviour is BotActivity) { slots.release(behaviour) @@ -110,10 +155,20 @@ class BotManager( if (action is BotAction.RetryableAction && action.retryMax > 0) { frame.state = BehaviourState.Wait(action.retryTicks) if (frame.retries++ < action.retryMax) { + AuditLog.event(bot, "retry", frame.behaviour.id, frame.index, frame.retries, action::class.simpleName) return } } - bot.frames.pop() + + if (bot.player["debug", false]) { + logger.info { "Failed action: ${action::class.simpleName} for ${behaviour.id}, reason: ${state.reason}." } + } + AuditLog.event(bot, "failed", behaviour.id, state.reason, frame.index, frame.retries, action::class.simpleName) + if (state.reason is HardReason) { + stop(bot) + } else { + bot.frames.pop() + } if (behaviour is BotActivity) { bot.blocked.add(behaviour.id) slots.release(behaviour) diff --git a/game/src/main/kotlin/content/bot/BotSpawns.kt b/game/src/main/kotlin/content/bot/BotSpawns.kt index 65840563d0..182fdccf6d 100644 --- a/game/src/main/kotlin/content/bot/BotSpawns.kt +++ b/game/src/main/kotlin/content/bot/BotSpawns.kt @@ -37,6 +37,8 @@ class BotSpawns( val structs: StructDefinitions, val tasks: TaskManager, val loader: PlayerAccountLoader, + val manager: BotManager, + val accounts: AccountManager, ) : Script { val bots = mutableListOf() @@ -96,11 +98,11 @@ class BotSpawns( fun clear(player: Player, args: List) { val count = args[0].toIntOrNull() ?: MAX_PLAYERS - World.queue("bot_$counter") { - val manager = get() + World.queue("bot_clear") { runBlocking { - for (bot in bots.take(count)) { - manager.logout(bot, false) + for (bot in manager.bots.take(count)) { + manager.remove(bot) + accounts.logout(bot.player, false) } } } @@ -108,12 +110,14 @@ class BotSpawns( fun toggle(player: Player, args: List) { if (player.isBot) { + manager.remove(player.bot) player.clear("bot") player.message("Bot disabled.") } else { - player.initBot() - if (args[0].isNotBlank()) { - player["task_bot"] = args[0] + val bot = player.initBot() + manager.add(bot) + if (args.getOrNull(0)?.isNotBlank() == true) { + manager.assign(bot, args[0]) } Bots.start(player) player.message("Bot enabled.") @@ -137,18 +141,18 @@ class BotSpawns( } "${prefix}${selected}" } - val bot = Player(tile = Areas["lumbridge_teleport"].random(), accountName = name) - bot.initBot() - loader.connect(bot, if (Settings["development.bots.live", false]) DummyClient() else null) - setAppearance(bot) - if (bot.inventory.isEmpty()) { - bot.inventory.add("coins", 10000) + val player = Player(tile = Areas["lumbridge_teleport"].random(), accountName = name) + val bot = player.initBot() + loader.connect(player, if (Settings["development.bots.live", false]) DummyClient() else null) + setAppearance(player) + if (player.inventory.isEmpty()) { + player.inventory.add("coins", 10000) } - Bots.start(bot) - bot.viewport?.loaded = true + Bots.start(player) + player.viewport?.loaded = true delay(3) - bots.add(bot) - bot.running = true + manager.add(bot) + player.running = true } } diff --git a/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt b/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt index 47c891a506..b1a967220e 100644 --- a/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt +++ b/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt @@ -21,6 +21,7 @@ data class BehaviourFragment( override val id: String, val type: String, val capacity: Int, + val weight: Int, var template: String, override val requires: List = emptyList(), override val plan: List = emptyList(), diff --git a/game/src/main/kotlin/content/bot/action/BotActivity.kt b/game/src/main/kotlin/content/bot/action/BotActivity.kt index 9de58f98f1..be282b7e51 100644 --- a/game/src/main/kotlin/content/bot/action/BotActivity.kt +++ b/game/src/main/kotlin/content/bot/action/BotActivity.kt @@ -32,9 +32,7 @@ data class BotActivity( override val produces: Set = emptySet(), ) : Behaviour -fun loadActivities(paths: List): Map { - val activities = mutableMapOf() - val resolvers = mutableMapOf() +fun loadActivities(paths: List, activities: MutableMap, resolvers: MutableMap>) { val fragments = mutableMapOf() timedLoad("bot activity") { val clones = mutableMapOf() @@ -69,10 +67,10 @@ fun loadActivities(paths: List): Map { } if (template != null) { - fragments[id] = BehaviourFragment(id, type, capacity, template, requirements, plan = actions, fields = fields) + fragments[id] = BehaviourFragment(id, type, capacity, weight, template, requirements, plan = actions, fields = fields) } else if (type == "resolver") { for (fact in produces) { - resolvers[fact] = Resolver(id, requirements, plan = actions) + resolvers.getOrPut(fact) { mutableListOf() }.add(Resolver(id, weight, requirements, plan = actions)) } } else { activities[id] = BotActivity(id, capacity, requirements, plan = actions) @@ -117,7 +115,7 @@ fun loadActivities(paths: List): Map { fragment.resolveActions(template, actions) if (fragment.type == "resolver") { for (fact in fragment.produces) { - resolvers[fact] = Resolver(id, requirements, actions) + resolvers.getOrPut(fact) { mutableListOf() }.add(Resolver(id, fragment.weight, requirements, actions)) } } else { activities[id] = BotActivity(id, fragment.capacity, requirements, actions) @@ -129,7 +127,6 @@ fun loadActivities(paths: List): Map { } activities.size } - return activities } private fun ConfigReader.produces() { diff --git a/game/src/main/kotlin/content/bot/action/Reason.kt b/game/src/main/kotlin/content/bot/action/Reason.kt index fdbb4a6e09..6a43fc3059 100644 --- a/game/src/main/kotlin/content/bot/action/Reason.kt +++ b/game/src/main/kotlin/content/bot/action/Reason.kt @@ -1,8 +1,10 @@ package content.bot.action +import content.bot.fact.Fact + interface Reason { object Cancelled : HardReason - object Requirements : HardReason + data class Requirement(val fact: Fact) : HardReason } interface SoftReason : Reason interface HardReason : Reason diff --git a/game/src/main/kotlin/content/bot/action/Resolver.kt b/game/src/main/kotlin/content/bot/action/Resolver.kt index 6d019a478d..54a469e61e 100644 --- a/game/src/main/kotlin/content/bot/action/Resolver.kt +++ b/game/src/main/kotlin/content/bot/action/Resolver.kt @@ -5,9 +5,11 @@ import content.bot.fact.Fact /** * An activity that can be performed to resolve a requirement * E.g. buying a pickaxe from a shop, getting items out of the bank, picking up an item off of the floor + * [weight] specifies resolver preference; lower is more likely to be chosen. */ data class Resolver( override val id: String, + val weight: Int, override val requires: List = emptyList(), override val plan: List = emptyList(), override val produces: Set = emptySet(), diff --git a/game/src/test/kotlin/content/bot/BotManagerTest.kt b/game/src/test/kotlin/content/bot/BotManagerTest.kt index af74b0a627..96bcf53536 100644 --- a/game/src/test/kotlin/content/bot/BotManagerTest.kt +++ b/game/src/test/kotlin/content/bot/BotManagerTest.kt @@ -5,6 +5,8 @@ import content.bot.action.BehaviourState import content.bot.action.BotAction import content.bot.action.BotActivity import content.bot.action.Reason +import content.bot.fact.Fact +import content.bot.fact.FactClone import content.bot.fact.MandatoryFact import content.bot.fact.HasSkillLevel import org.junit.jupiter.api.Assertions.* @@ -22,7 +24,7 @@ class BotManagerTest { id = "woodcutting", plan = listOf(BotAction.Wait(1)) ) - val manager = BotManager(mapOf(activity.id to activity)) + val manager = BotManager(mutableMapOf(activity.id to activity)) val bot = testBot() manager.tick(bot) @@ -37,7 +39,7 @@ class BotManagerTest { id = "mine", plan = listOf(BotAction.Wait(1)) ) - val manager = BotManager(mapOf(activity.id to activity)) + val manager = BotManager(mutableMapOf(activity.id to activity)) val bot1 = testBot("bot1") val bot2 = testBot("bot2") @@ -55,7 +57,7 @@ class BotManagerTest { id = "walk", plan = listOf(BotAction.Wait(1)) ) - val manager = BotManager(mapOf(activity.id to activity)) + val manager = BotManager(mutableMapOf(activity.id to activity)) val bot = testBot() manager.tick(bot) @@ -100,7 +102,7 @@ class BotManagerTest { plan = listOf(action) ) - val manager = BotManager(mapOf(activity.id to activity)) + val manager = BotManager(mutableMapOf(activity.id to activity)) val bot = testBot() manager.tick(bot) @@ -108,7 +110,7 @@ class BotManagerTest { val frame = bot.frame() repeat(3) { - frame.fail(Reason.Requirements) + frame.fail(Reason.Requirement(FactClone(""))) manager.tick(bot) assertTrue(frame.state is BehaviourState.Wait) manager.tick(bot) // Tick 1 @@ -128,7 +130,7 @@ class BotManagerTest { plan = listOf(BotAction.Wait(1)) ) - val manager = BotManager(mapOf(activity.id to activity)) + val manager = BotManager(mutableMapOf(activity.id to activity)) val bot = testBot() manager.tick(bot) @@ -172,7 +174,7 @@ class BotManagerTest { plan = listOf(BotAction.Wait(1)) ) - val manager = BotManager(mapOf(activity.id to activity)) + val manager = BotManager(mutableMapOf(activity.id to activity)) val bot = testBot() manager.tick(bot) @@ -191,12 +193,12 @@ class BotManagerTest { plan = listOf(BotAction.Wait(1)) ) - val manager = BotManager(mapOf(activity.id to activity)) + val manager = BotManager(mutableMapOf(activity.id to activity)) val bot = testBot() manager.tick(bot) manager.tick(bot) - bot.frame().fail(Reason.Requirements) + bot.frame().fail(Reason.Requirement(FactClone(""))) manager.tick(bot) manager.tick(bot) @@ -214,7 +216,7 @@ class BotManagerTest { plan = listOf(BotAction.Wait(4)) ) - val manager = BotManager(mapOf(activity.id to activity)) + val manager = BotManager(mutableMapOf(activity.id to activity)) val bot = testBot() bot.frames.add(BehaviourFrame(activity)) diff --git a/game/src/test/kotlin/content/bot/action/BehaviourFragmentTest.kt b/game/src/test/kotlin/content/bot/action/BehaviourFragmentTest.kt index ac7c79ccd7..20abfd23c9 100644 --- a/game/src/test/kotlin/content/bot/action/BehaviourFragmentTest.kt +++ b/game/src/test/kotlin/content/bot/action/BehaviourFragmentTest.kt @@ -25,7 +25,8 @@ class BehaviourFragmentTest { capacity = 1, template = "tpl", type = "activity", - fields = fields + fields = fields, + weight = 1, ) /* From d76ac5713a3ca6685a812f228f10ec2a92f1c229 Mon Sep 17 00:00:00 2001 From: GregHib Date: Wed, 28 Jan 2026 16:16:51 +0000 Subject: [PATCH 016/101] Tweak mechanics and add tests for resolver behaviour --- game/src/main/kotlin/content/bot/Bot.kt | 1 + .../src/main/kotlin/content/bot/BotManager.kt | 6 +- .../test/kotlin/content/bot/BotManagerTest.kt | 203 +++++++++++++++++- 3 files changed, 205 insertions(+), 5 deletions(-) diff --git a/game/src/main/kotlin/content/bot/Bot.kt b/game/src/main/kotlin/content/bot/Bot.kt index 9484120fc8..13415becb4 100644 --- a/game/src/main/kotlin/content/bot/Bot.kt +++ b/game/src/main/kotlin/content/bot/Bot.kt @@ -25,6 +25,7 @@ data class Bot(val player: Player) : Character by player { internal fun reset() { frames.clear() + blocked.clear() } internal fun queue(frame: BehaviourFrame) { diff --git a/game/src/main/kotlin/content/bot/BotManager.kt b/game/src/main/kotlin/content/bot/BotManager.kt index 6ce9585308..320cfe7679 100644 --- a/game/src/main/kotlin/content/bot/BotManager.kt +++ b/game/src/main/kotlin/content/bot/BotManager.kt @@ -56,7 +56,7 @@ class BotManager( } private fun hasRequirements(bot: Bot, activity: BotActivity): Boolean { - return slots.hasFree(activity) && !bot.blocked.contains(activity.id) && activity.requires.all { it is MandatoryFact && it.check(bot) } + return slots.hasFree(activity) && !bot.blocked.contains(activity.id) && activity.requires.all { it !is MandatoryFact || it.check(bot) } } fun assign(bot: Bot, id: String): Boolean { @@ -85,7 +85,7 @@ class BotManager( private fun assign(bot: Bot, activity: BotActivity) { AuditLog.event(bot, "assigned", activity.id) if (bot.player["debug", false]) { - logger.info { "Assigned bot: ${bot.player.accountName} task ${activity.id}." } + logger.info { "Assigned bot: '${bot.player.accountName}' task '${activity.id}'." } } slots.occupy(activity) bot.previous = activity @@ -134,7 +134,7 @@ class BotManager( } options.add(resolver) } - return options.randomOrNull(random) + return options.minByOrNull { it.weight } } private fun execute(bot: Bot) { diff --git a/game/src/test/kotlin/content/bot/BotManagerTest.kt b/game/src/test/kotlin/content/bot/BotManagerTest.kt index 96bcf53536..b9f9cac943 100644 --- a/game/src/test/kotlin/content/bot/BotManagerTest.kt +++ b/game/src/test/kotlin/content/bot/BotManagerTest.kt @@ -5,21 +5,33 @@ import content.bot.action.BehaviourState import content.bot.action.BotAction import content.bot.action.BotActivity import content.bot.action.Reason +import content.bot.action.Resolver +import content.bot.action.SoftReason +import content.bot.fact.AtTile +import content.bot.fact.CarriesItem +import content.bot.fact.EquipsItem import content.bot.fact.Fact import content.bot.fact.FactClone import content.bot.fact.MandatoryFact import content.bot.fact.HasSkillLevel +import content.bot.fact.HasVariable import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.Test +import world.gregs.voidps.cache.config.data.InventoryDefinition +import world.gregs.voidps.engine.data.definition.InventoryDefinitions import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.entity.character.player.skill.Skill +import world.gregs.voidps.engine.inv.add +import world.gregs.voidps.engine.inv.inventory +import world.gregs.voidps.engine.inv.restrict.ValidItemRestriction +import world.gregs.voidps.engine.inv.stack.ItemDependentStack class BotManagerTest { fun testBot(name: String = "bot") = Bot(Player(accountName = name)) @Test - fun `Idle bot gets assigned an activity`() { + fun `Taskless bot gets assigned an activity`() { val activity = testActivity( id = "woodcutting", plan = listOf(BotAction.Wait(1)) @@ -228,9 +240,196 @@ class BotManagerTest { assertTrue("test" in bot.blocked) } + @Test + fun `Resolvable requirement queues resolver before activity starts`() { + val fact = AtTile(100, 100, 2, 1) + val resolver = Resolver( + id = "go_to_area", + weight = 1, + plan = listOf(BotAction.Wait(1)), + produces = setOf(fact) + ) + val activity = testActivity( + id = "woodcut", + requirements = listOf(fact), + plan = listOf(BotAction.Wait(1)) + ) + val manager = BotManager( + mutableMapOf(activity.id to activity), + mutableMapOf(fact to mutableListOf(resolver)) + ) + + val bot = testBot() + manager.tick(bot) + manager.tick(bot) + + assertEquals(2, bot.frames.size) + assertEquals(resolver, bot.frames.peek().behaviour) + } + + @Test + fun `Lowest weight resolver is selected`() { + val fact = AtTile(100, 200, 300, 4) + + val bad = Resolver("bad", weight = 10) + val good = Resolver("good", weight = 1) + + val activity = testActivity( + id = "mine", + requirements = listOf(fact), + plan = listOf(BotAction.Wait(1)) + ) + + val manager = BotManager( + mutableMapOf(activity.id to activity), + mutableMapOf(fact to mutableListOf(bad, good)) + ) + + val bot = testBot() + manager.tick(bot) + manager.tick(bot) + + assertEquals("good", bot.frames.peek().behaviour.id) + } + + @Test + fun `Blocked resolver is not reselected`() { + val fact = AtTile(100, 200, 3, 4) + val resolver = Resolver(id = "get_key", weight = 1) + val activity = testActivity( + id = "open_door", + requirements = listOf(fact), + plan = listOf(BotAction.Wait(1)) + ) + val manager = BotManager( + mutableMapOf(activity.id to activity), + mutableMapOf(fact to mutableListOf(resolver)) + ) + + val bot = testBot() + manager.tick(bot) + assertEquals(1, bot.frames.size) + val frame = bot.frames.last() + frame.blocked.add("get_key") + manager.tick(bot) + manager.tick(bot) + + assertTrue(bot.frames.isEmpty()) + assertTrue("open_door" in bot.blocked) + } + + @Test + fun `Hard failure in resolver stops bot`() { + val fact = AtTile(100, 200, 3, 4) + val resolver = Resolver( + id = "walk", + weight = 1, + plan = listOf(BotAction.Wait(1)) + ) + val activity = testActivity( + id = "enter_zone", + requirements = listOf(fact), + plan = listOf(BotAction.Wait(1)) + ) + val manager = BotManager( + mutableMapOf(activity.id to activity), + mutableMapOf(fact to mutableListOf(resolver)) + ) + + val bot = testBot() + manager.tick(bot) + manager.tick(bot) + assertEquals(2, bot.frames.size) + bot.frame().fail(Reason.Cancelled) + manager.tick(bot) + + assertTrue(bot.frames.isEmpty()) + } + + @Test + fun `Soft failure in resolver only pops resolver`() { + val fact = AtTile(100, 200, 3, 4) + val resolver = Resolver( + id = "test", + weight = 1, + plan = listOf(BotAction.Wait(1)) + ) + val activity = testActivity( + id = "smelt", + requirements = listOf(fact), + plan = listOf(BotAction.Wait(1)) + ) + val manager = BotManager( + mutableMapOf(activity.id to activity), + mutableMapOf(fact to mutableListOf(resolver)) + ) + + val bot = testBot() + bot.player["debug"] = true + manager.tick(bot) + manager.tick(bot) + assertEquals(2, bot.frames.size) + bot.frame().fail(object : SoftReason {}) + manager.tick(bot) + + assertEquals(activity, bot.frame().behaviour) + } + + @Test + fun `Resolver with unmet mandatory requirements is skipped`() { + val fact = AtTile(100, 200, 3, 4) + val resolver = Resolver( + id = "mine_gem", + weight = 1, + requires = listOf(HasSkillLevel(Skill.Mining, 99, 99)) + ) + val activity = testActivity( + id = "craft", + requirements = listOf(fact), + plan = listOf(BotAction.Wait(1)) + ) + val manager = BotManager( + mutableMapOf(activity.id to activity), + mutableMapOf(fact to mutableListOf(resolver)) + ) + + val bot = testBot() + manager.tick(bot) + manager.tick(bot) + manager.tick(bot) + + assertTrue(bot.frames.isEmpty()) + assertTrue("craft" in bot.blocked) + } + + @Test + fun `Activity are occupied while resolver is running`() { + val fact = AtTile(100, 200, 3, 4) + val resolver = Resolver( + id = "get_tool", + weight = 1, + plan = listOf(BotAction.Wait(1)) + ) + val activity = testActivity( + id = "work", + requirements = listOf(fact), + plan = listOf(BotAction.Wait(1)) + ) + val manager = BotManager( + mutableMapOf(activity.id to activity), + mutableMapOf(fact to mutableListOf(resolver)) + ) + + val bot = testBot() + manager.tick(bot) + manager.tick(bot) + + assertFalse(manager.slots.hasFree(activity)) + } + fun testActivity( id: String, - requirements: List = emptyList(), + requirements: List = emptyList(), plan: List ) = BotActivity(id, 1, requirements, plan) } \ No newline at end of file From bc8f7d44fd86ccd1efaa85573cf1207729be133d Mon Sep 17 00:00:00 2001 From: GregHib Date: Wed, 28 Jan 2026 17:47:03 +0000 Subject: [PATCH 017/101] Hack together action implementations --- data/bot/walking.bots.toml | 24 ++++ data/bot/woodcutting.bots.toml | 7 +- game/src/main/kotlin/content/bot/Bot.kt | 2 - .../src/main/kotlin/content/bot/BotManager.kt | 12 +- .../content/bot/action/BehaviourFrame.kt | 3 +- .../kotlin/content/bot/action/BotAction.kt | 114 +++++++++++++++++- .../main/kotlin/content/bot/action/Reason.kt | 2 + .../kotlin/content/bot/fact/MandatoryFact.kt | 7 -- .../kotlin/content/bot/fact/ResolvableFact.kt | 7 ++ .../content/bot/interact/navigation/GoTo.kt | 10 +- 10 files changed, 162 insertions(+), 26 deletions(-) create mode 100644 data/bot/walking.bots.toml diff --git a/data/bot/walking.bots.toml b/data/bot/walking.bots.toml new file mode 100644 index 0000000000..ec194acfbe --- /dev/null +++ b/data/bot/walking.bots.toml @@ -0,0 +1,24 @@ +[run_to_varrock] +type = "activity" +capacity = 1 +requires = [ + { variable = "movement", value = "run" } +] +plan = [ + { go_to = "varrock_teleport" } +] +produces = [ + { location = "varrock_teleport" } +] + +[toggle_run] +type = "resolver" +requires = [ + { variable = "movement", value = "walk" } +] +plan = [ + { option = "Turn Run mode on", interface = "energy_orb:run_background" } +] +produces = [ + { variable = "movement", value = "run" } +] diff --git a/data/bot/woodcutting.bots.toml b/data/bot/woodcutting.bots.toml index 886334b982..16c1debc67 100644 --- a/data/bot/woodcutting.bots.toml +++ b/data/bot/woodcutting.bots.toml @@ -1,10 +1,5 @@ -[idle] -capacity = 2048 -plan = [ - { wait = 50 } # 30s -] - [woodcutting_template] +type = "activity" requires = [ { carries = "$hatchet" }, { location = "$location" }, diff --git a/game/src/main/kotlin/content/bot/Bot.kt b/game/src/main/kotlin/content/bot/Bot.kt index 13415becb4..b625219ede 100644 --- a/game/src/main/kotlin/content/bot/Bot.kt +++ b/game/src/main/kotlin/content/bot/Bot.kt @@ -12,7 +12,6 @@ import java.util.Stack data class Bot(val player: Player) : Character by player { var step: Instruction? = null - val blocked: MutableSet = mutableSetOf() var previous: BotActivity? = null val frames = Stack() @@ -25,7 +24,6 @@ data class Bot(val player: Player) : Character by player { internal fun reset() { frames.clear() - blocked.clear() } internal fun queue(frame: BehaviourFrame) { diff --git a/game/src/main/kotlin/content/bot/BotManager.kt b/game/src/main/kotlin/content/bot/BotManager.kt index 320cfe7679..c9bfc7c691 100644 --- a/game/src/main/kotlin/content/bot/BotManager.kt +++ b/game/src/main/kotlin/content/bot/BotManager.kt @@ -5,6 +5,7 @@ import content.bot.action.* import content.bot.fact.Fact import content.bot.fact.MandatoryFact import content.bot.fact.ResolvableFact +import world.gregs.voidps.engine.client.variable.start import world.gregs.voidps.engine.data.ConfigFiles import world.gregs.voidps.engine.data.Settings import world.gregs.voidps.engine.event.AuditLog @@ -69,9 +70,16 @@ class BotManager( val activity = if (bot.previous != null && hasRequirements(bot, bot.previous!!)) { bot.previous } else { + // TODO could be a command +// if (bot.player["debug", false]) { +// for (activity in activities.values) { +// logger.info { "Activity ${activity.id}: ${hasRequirements(bot, activity)} ${activity.requires.map { "[${it}=${it !is MandatoryFact}+${it.check(bot)}]" }}." } +// } +// } activities.values .filter { hasRequirements(bot, it) } .randomOrNull(random) + ?: BotActivity("idle", 2048, plan = listOf(BotAction.Wait(50))) // 30s } if (activity == null) { if (bot.player["debug", false]) { @@ -112,7 +120,9 @@ class BotManager( logger.info { "Starting resolution: ${resolver.id} for ${behaviour.id} req ${requirement}." } } frame.blocked.add(resolver.id) - bot.queue(BehaviourFrame(resolver)) + val resolverFrame = BehaviourFrame(resolver) + bot.queue(resolverFrame) + resolverFrame.start(bot) return } } diff --git a/game/src/main/kotlin/content/bot/action/BehaviourFrame.kt b/game/src/main/kotlin/content/bot/action/BehaviourFrame.kt index 118f1b9ff9..7142623722 100644 --- a/game/src/main/kotlin/content/bot/action/BehaviourFrame.kt +++ b/game/src/main/kotlin/content/bot/action/BehaviourFrame.kt @@ -16,8 +16,7 @@ data class BehaviourFrame( fun start(bot: Bot) { val action = action() - action.start(bot) - state = BehaviourState.Running + state = action.start(bot) } fun next(): Boolean { diff --git a/game/src/main/kotlin/content/bot/action/BotAction.kt b/game/src/main/kotlin/content/bot/action/BotAction.kt index bcbc8c1889..db5789dc21 100644 --- a/game/src/main/kotlin/content/bot/action/BotAction.kt +++ b/game/src/main/kotlin/content/bot/action/BotAction.kt @@ -1,10 +1,32 @@ package content.bot.action import content.bot.Bot +import content.bot.bot +import content.bot.interact.navigation.navigate +import content.bot.interact.navigation.updateGraph +import content.bot.interact.path.AreaStrategy +import content.bot.interact.path.Dijkstra +import content.bot.interact.path.EdgeTraversal +import content.entity.combat.attackers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import world.gregs.voidps.engine.data.definition.Areas +import world.gregs.voidps.engine.data.definition.InterfaceDefinitions +import world.gregs.voidps.engine.entity.character.npc.NPC +import world.gregs.voidps.engine.entity.character.npc.NPCs +import world.gregs.voidps.engine.entity.obj.GameObject +import world.gregs.voidps.engine.entity.obj.GameObjects +import world.gregs.voidps.engine.event.wildcardEquals +import world.gregs.voidps.engine.get +import world.gregs.voidps.engine.map.Spiral +import world.gregs.voidps.network.client.instruction.InteractInterface +import world.gregs.voidps.network.client.instruction.InteractNPC +import world.gregs.voidps.type.random sealed interface BotAction { - open fun start(bot: Bot) { + open fun start(bot: Bot): BehaviourState { // TODO here's where code goes, either handle this way or put in an event handler for each type + return BehaviourState.Running } sealed class RetryableAction : BotAction { @@ -12,11 +34,35 @@ sealed interface BotAction { abstract val retryMax: Int } - data class GoTo(val target: String) : BotAction data class Clone(val id: String) : BotAction data class Reference(val action: BotAction, val references: Map) : BotAction - data class Wait(val ticks: Int) : BotAction + data class Wait(val ticks: Int) : BotAction { + override fun start(bot: Bot): BehaviourState { + bot.frame().state = BehaviourState.Wait(ticks) + return BehaviourState.Running + } + } + + data class GoTo(val target: String) : BotAction { + override fun start(bot: Bot): BehaviourState { + val def = Areas.getOrNull(target) ?: return BehaviourState.Failed(Reason.NoRoute) + if (bot.tile in def.area) { + return BehaviourState.Success + } + updateGraph(bot) + val strategy = AreaStrategy(def.area) + val result = get().find(bot.player, strategy, EdgeTraversal()) + bot["navigating"] = result == null + if (result != null) { + bot["area"] = def + GlobalScope.launch { bot.navigate() } + return BehaviourState.Running + } else { + return BehaviourState.Failed(Reason.NoRoute) + } + } + } data class InteractNpc( val option: String, @@ -24,7 +70,26 @@ sealed interface BotAction { override val retryTicks: Int = 0, override val retryMax: Int = 0, val radius: Int = 10, - ) : RetryableAction() + ) : RetryableAction() { + override fun start(bot: Bot) : BehaviourState { + val npcs = mutableListOf() + for (tile in Spiral.spiral(bot.player.tile, radius)) { + for (npc in NPCs.at(tile)) { + if (!wildcardEquals(id, npc.id)) { + continue + } + if (option == "Attack" && npc.attackers.isNotEmpty() && !npc.attackers.contains(bot.player)) { + continue + } + npcs.add(npc) + } + } + val npc = npcs.randomOrNull(random) ?: return BehaviourState.Failed(Reason.NoTarget) + val index = npc.def(bot.player).options.indexOf(option) + bot.player.instructions.trySend(InteractNPC(npc.index, index)) + return BehaviourState.Running + } + } data class InteractObject( val option: String, @@ -32,8 +97,45 @@ sealed interface BotAction { override val retryTicks: Int = 0, override val retryMax: Int = 0, val radius: Int = 10, - ) : RetryableAction() + ) : RetryableAction() { + override fun start(bot: Bot): BehaviourState { + val objects = mutableListOf() + for (tile in Spiral.spiral(bot.player.tile, radius)) { + for (obj in GameObjects.at(tile)) { + if (wildcardEquals(id, obj.id)) { + objects.add(obj) + } + } + } + val obj = objects.randomOrNull(random) ?: return BehaviourState.Failed(Reason.NoTarget) + val index = obj.def(bot.player).options?.indexOf(option) ?: return BehaviourState.Failed(Reason.NoTarget) + bot.player.instructions.trySend(world.gregs.voidps.network.client.instruction.InteractObject(obj.intId, obj.x, obj.y, index)) + return BehaviourState.Running + } + } + + data class InterfaceOption(val id: String, val option: String) : BotAction { + override fun start(bot: Bot): BehaviourState { + val definitions = get() + val split = id.split(":") + val (id, component) = split + val item = split.getOrNull(2) + val def = definitions.getOrNull(id) ?: return BehaviourState.Failed(Reason.NoTarget) + val componentId = definitions.getComponentId(id, component) ?: return BehaviourState.Failed(Reason.NoTarget) + val componentDef = definitions.getComponent(id, component) ?: return BehaviourState.Failed(Reason.NoTarget) + val index = componentDef.options?.indexOf(option) ?: return BehaviourState.Failed(Reason.NoTarget) + bot.player.instructions.trySend( + InteractInterface( + interfaceId = def.id, + componentId = componentId, + itemId = -1, + itemSlot = -1, + option = index + ) + ) // TODO could await actual response, or something to get actual feedback + return BehaviourState.Success + } + } - data class InterfaceOption(val id: String, val option: String) : BotAction data class WaitFullInventory(val timeout: Int) : BotAction } \ No newline at end of file diff --git a/game/src/main/kotlin/content/bot/action/Reason.kt b/game/src/main/kotlin/content/bot/action/Reason.kt index 6a43fc3059..c56b827b26 100644 --- a/game/src/main/kotlin/content/bot/action/Reason.kt +++ b/game/src/main/kotlin/content/bot/action/Reason.kt @@ -4,6 +4,8 @@ import content.bot.fact.Fact interface Reason { object Cancelled : HardReason + object NoRoute : HardReason + object NoTarget : SoftReason data class Requirement(val fact: Fact) : HardReason } interface SoftReason : Reason diff --git a/game/src/main/kotlin/content/bot/fact/MandatoryFact.kt b/game/src/main/kotlin/content/bot/fact/MandatoryFact.kt index e7b0f79f24..df0d00185d 100644 --- a/game/src/main/kotlin/content/bot/fact/MandatoryFact.kt +++ b/game/src/main/kotlin/content/bot/fact/MandatoryFact.kt @@ -12,10 +12,3 @@ data class HasSkillLevel( ) : MandatoryFact() { override fun check(bot: Bot) = bot.player.levels.get(skill) in min..max } - -data class HasVariable( - val id: String, - val value: Any? = null -) : MandatoryFact() { - override fun check(bot: Bot) = bot.player.variables.get(id) == value -} diff --git a/game/src/main/kotlin/content/bot/fact/ResolvableFact.kt b/game/src/main/kotlin/content/bot/fact/ResolvableFact.kt index ee590de05e..1ad8461de1 100644 --- a/game/src/main/kotlin/content/bot/fact/ResolvableFact.kt +++ b/game/src/main/kotlin/content/bot/fact/ResolvableFact.kt @@ -15,6 +15,13 @@ data class EquipsItem( override fun check(bot: Bot) = bot.player.equips(id, amount) } +data class HasVariable( + val id: String, + val value: Any? = null, +) : ResolvableFact(1) { + override fun check(bot: Bot) = bot.player.variables.get(id) == value +} + data class CarriesItem( val id: String, val amount: Int = 1, diff --git a/game/src/main/kotlin/content/bot/interact/navigation/GoTo.kt b/game/src/main/kotlin/content/bot/interact/navigation/GoTo.kt index ea319f52d3..9d97285c7c 100644 --- a/game/src/main/kotlin/content/bot/interact/navigation/GoTo.kt +++ b/game/src/main/kotlin/content/bot/interact/navigation/GoTo.kt @@ -2,12 +2,17 @@ package content.bot.interact.navigation import com.github.michaelbull.logging.InlineLogger import content.bot.Bot +import content.bot.bot import content.bot.interact.navigation.graph.Edge import content.bot.interact.navigation.graph.NavigationGraph import content.bot.interact.navigation.graph.waypoints import content.bot.interact.path.* +import content.bot.isBot import content.entity.player.effect.energy.energyPercent +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeoutOrNull +import world.gregs.voidps.engine.Script import world.gregs.voidps.engine.client.update.view.Viewport.Companion.VIEW_RADIUS import world.gregs.voidps.engine.data.definition.AreaDefinition import world.gregs.voidps.engine.entity.character.move.running @@ -71,7 +76,7 @@ private suspend fun Bot.goTo(strategy: NodeTargetStrategy): Tile? { return result } -private fun updateGraph(bot: Bot) { +fun updateGraph(bot: Bot) { val graph: NavigationGraph = get() val edges = graph.get(bot.player) edges.clear() @@ -101,7 +106,7 @@ private suspend fun Bot.run() { player.instructions.send(InteractInterface(interfaceId = 750, componentId = 1, itemId = -1, itemSlot = -1, option = 0)) } -private suspend fun Bot.navigate() { +suspend fun Bot.navigate() { val waypoints = player.waypoints.toMutableList().iterator() while (waypoints.hasNext()) { val waypoint = waypoints.next() @@ -127,4 +132,5 @@ private suspend fun Bot.navigate() { waypoints.remove() } player["navigating"] = false + frame().completed() } From 8c9409256656adb6f038ac9c24982b99986cf733 Mon Sep 17 00:00:00 2001 From: GregHib Date: Thu, 29 Jan 2026 10:29:15 +0000 Subject: [PATCH 018/101] Fixes --- .../kotlin/content/bot/action/BotAction.kt | 27 +++++++++++++++---- .../content/bot/interact/navigation/GoTo.kt | 2 +- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/game/src/main/kotlin/content/bot/action/BotAction.kt b/game/src/main/kotlin/content/bot/action/BotAction.kt index db5789dc21..052021ebc4 100644 --- a/game/src/main/kotlin/content/bot/action/BotAction.kt +++ b/game/src/main/kotlin/content/bot/action/BotAction.kt @@ -24,10 +24,8 @@ import world.gregs.voidps.network.client.instruction.InteractNPC import world.gregs.voidps.type.random sealed interface BotAction { - open fun start(bot: Bot): BehaviourState { - // TODO here's where code goes, either handle this way or put in an event handler for each type - return BehaviourState.Running - } + fun start(bot: Bot): BehaviourState = BehaviourState.Failed(Reason.Cancelled) + fun update(): BehaviourState = BehaviourState.Running sealed class RetryableAction : BotAction { abstract val retryTicks: Int @@ -71,7 +69,7 @@ sealed interface BotAction { override val retryMax: Int = 0, val radius: Int = 10, ) : RetryableAction() { - override fun start(bot: Bot) : BehaviourState { + override fun start(bot: Bot): BehaviourState { val npcs = mutableListOf() for (tile in Spiral.spiral(bot.player.tile, radius)) { for (npc in NPCs.at(tile)) { @@ -138,4 +136,23 @@ sealed interface BotAction { } data class WaitFullInventory(val timeout: Int) : BotAction + + /** + * TODO how to handle repeat actions e.g. repeat Chop-down trees until inv is full + * more resolvers like bank all, drop cheap items + * how to handle combat, one task or multiple? + * frames should have tick(): State methods + * Combat should be an action which has a state machine for eating, retargeting, looting etc.. + * GatheringActivity + * TravelActivity + * how to handle navigation in a non-hacky way + * navigation behaviours + * make nav-graph points only? + * combine nav-graph requirements with facts + * Goal generators + * Rather than check all req for all activties do it reactively + * Received an item recently? Add relevant activties to that item to the list of posibilities + * Been too long since you picked up an item, now remove that goal from the list + * No posibilities? Now expand search wider + */ } \ No newline at end of file diff --git a/game/src/main/kotlin/content/bot/interact/navigation/GoTo.kt b/game/src/main/kotlin/content/bot/interact/navigation/GoTo.kt index 9d97285c7c..4cfcea44d0 100644 --- a/game/src/main/kotlin/content/bot/interact/navigation/GoTo.kt +++ b/game/src/main/kotlin/content/bot/interact/navigation/GoTo.kt @@ -132,5 +132,5 @@ suspend fun Bot.navigate() { waypoints.remove() } player["navigating"] = false - frame().completed() + frame().success() } From 29b526c47a63cb6ae7bd009cf09fdf3687f0b6f6 Mon Sep 17 00:00:00 2001 From: GregHib Date: Thu, 29 Jan 2026 13:57:04 +0000 Subject: [PATCH 019/101] Split mandatory and resolvable facts into two lists --- .../src/main/kotlin/content/bot/BotManager.kt | 47 +++++++++--------- .../kotlin/content/bot/action/Behaviour.kt | 1 + .../content/bot/action/BehaviourFragment.kt | 1 + .../kotlin/content/bot/action/BotAction.kt | 40 ++++++++++++++- .../kotlin/content/bot/action/BotActivity.kt | 31 ++++++++++-- .../kotlin/content/bot/action/Resolver.kt | 1 + .../kotlin/content/bot/fact/MandatoryFact.kt | 4 +- .../kotlin/content/bot/fact/ResolvableFact.kt | 18 +++---- .../test/kotlin/content/bot/BotManagerTest.kt | 49 +++++++------------ 9 files changed, 116 insertions(+), 76 deletions(-) diff --git a/game/src/main/kotlin/content/bot/BotManager.kt b/game/src/main/kotlin/content/bot/BotManager.kt index c9bfc7c691..65d8566fdc 100644 --- a/game/src/main/kotlin/content/bot/BotManager.kt +++ b/game/src/main/kotlin/content/bot/BotManager.kt @@ -3,9 +3,6 @@ package content.bot import com.github.michaelbull.logging.InlineLogger import content.bot.action.* import content.bot.fact.Fact -import content.bot.fact.MandatoryFact -import content.bot.fact.ResolvableFact -import world.gregs.voidps.engine.client.variable.start import world.gregs.voidps.engine.data.ConfigFiles import world.gregs.voidps.engine.data.Settings import world.gregs.voidps.engine.event.AuditLog @@ -57,7 +54,7 @@ class BotManager( } private fun hasRequirements(bot: Bot, activity: BotActivity): Boolean { - return slots.hasFree(activity) && !bot.blocked.contains(activity.id) && activity.requires.all { it !is MandatoryFact || it.check(bot) } + return slots.hasFree(activity) && !bot.blocked.contains(activity.id) && activity.requires.all { it.check(bot) } } fun assign(bot: Bot, id: String): Boolean { @@ -105,26 +102,28 @@ class BotManager( if (requirement.check(bot)) { continue } - if (requirement is MandatoryFact) { - frame.fail(Reason.Requirement(requirement)) - return - } else if (requirement is ResolvableFact) { - val resolver = pickResolver(bot, requirement, frame) - if (resolver == null) { - frame.fail(Reason.Requirement(requirement)) // No way to resolve - return - } - // Attempt resolution - AuditLog.event(bot, "start_resolver", resolver.id, behaviour.id) - if (bot.player["debug", false]) { - logger.info { "Starting resolution: ${resolver.id} for ${behaviour.id} req ${requirement}." } - } - frame.blocked.add(resolver.id) - val resolverFrame = BehaviourFrame(resolver) - bot.queue(resolverFrame) - resolverFrame.start(bot) + frame.fail(Reason.Requirement(requirement)) + return + } + for (requirement in behaviour.resolve) { + if (requirement.check(bot)) { + continue + } + val resolver = pickResolver(bot, requirement, frame) + if (resolver == null) { + frame.fail(Reason.Requirement(requirement)) // No way to resolve return } + // Attempt resolution + AuditLog.event(bot, "start_resolver", resolver.id, behaviour.id) + if (bot.player["debug", false]) { + logger.info { "Starting resolution: ${resolver.id} for ${behaviour.id} req ${requirement}." } + } + frame.blocked.add(resolver.id) + val resolverFrame = BehaviourFrame(resolver) + bot.queue(resolverFrame) + resolverFrame.start(bot) + return } AuditLog.event(bot, "start_activity", behaviour.id) if (bot.player["debug", false]) { @@ -133,13 +132,13 @@ class BotManager( frame.start(bot) } - private fun pickResolver(bot: Bot, fact: ResolvableFact, frame: BehaviourFrame): Behaviour? { + private fun pickResolver(bot: Bot, fact: Fact, frame: BehaviourFrame): Behaviour? { val options = mutableListOf() for (resolver in resolvers[fact] ?: return null) { if (frame.blocked.contains(resolver.id)) { continue } - if (resolver.requires.any { fact -> fact is MandatoryFact && !fact.check(bot) }) { + if (resolver.requires.any { fact -> !fact.check(bot) }) { continue } options.add(resolver) diff --git a/game/src/main/kotlin/content/bot/action/Behaviour.kt b/game/src/main/kotlin/content/bot/action/Behaviour.kt index e7d35834d9..933c04c724 100644 --- a/game/src/main/kotlin/content/bot/action/Behaviour.kt +++ b/game/src/main/kotlin/content/bot/action/Behaviour.kt @@ -5,6 +5,7 @@ import content.bot.fact.Fact interface Behaviour { val id: String val requires: List + val resolve: List val plan: List val produces: Set } \ No newline at end of file diff --git a/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt b/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt index b1a967220e..7121fafa2b 100644 --- a/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt +++ b/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt @@ -24,6 +24,7 @@ data class BehaviourFragment( val weight: Int, var template: String, override val requires: List = emptyList(), + override val resolve: List = emptyList(), override val plan: List = emptyList(), override val produces: Set = emptySet(), val fields: Map = emptyMap(), diff --git a/game/src/main/kotlin/content/bot/action/BotAction.kt b/game/src/main/kotlin/content/bot/action/BotAction.kt index 052021ebc4..f783e02da3 100644 --- a/game/src/main/kotlin/content/bot/action/BotAction.kt +++ b/game/src/main/kotlin/content/bot/action/BotAction.kt @@ -138,9 +138,9 @@ sealed interface BotAction { data class WaitFullInventory(val timeout: Int) : BotAction /** - * TODO how to handle repeat actions e.g. repeat Chop-down trees until inv is full + * TODO how to handle repeat actions e.g. repeat Chop-down trees until inv is full - These are actions Gathering, Skilling etc.. * more resolvers like bank all, drop cheap items - * how to handle combat, one task or multiple? + * how to handle combat, one task or multiple? - One Fight action * frames should have tick(): State methods * Combat should be an action which has a state machine for eating, retargeting, looting etc.. * GatheringActivity @@ -154,5 +154,41 @@ sealed interface BotAction { * Received an item recently? Add relevant activties to that item to the list of posibilities * Been too long since you picked up an item, now remove that goal from the list * No posibilities? Now expand search wider + * + * Open questions: + * - Complex activities like minigames, quests + * Minigames: + * They are closed mechanical systems so they are actions. + * JoinMinigameLobby - Success, Timeout, Kicked etc.. + * PlayMinigame - Roll selection, objectives, movement, combat, scoring. Fails on game end or leaving/disconnect + * Trading with players: + * Outcomes are non-deterministic, waiting on player timing + * SellingAction + * TradeAction + * inits trade + * trade rules + * max wait + * accepted items + * price bounds + * reacts to + * offer chances + * cancellation +* terminates with success/failure + * Quests: + * Some complex quest mechanics might need custom actions + * Activities: + * TalkToCook + * GetBucket + * GetMilk + * GetEgg + * ReturnToCook + * - navigation + actions + * - targetting + * - activity generators - reactive loading + * Separate mandatory and resolvable requirements + * mandatory requirements become gates for if activities are in the current pool + * Listeners wait for state changes on specific mandatory requirements (level up, skill changes) and revaluate adding to activity pool + * These listeners also check current activity requirements and fail it if no longer gated + * */ } \ No newline at end of file diff --git a/game/src/main/kotlin/content/bot/action/BotActivity.kt b/game/src/main/kotlin/content/bot/action/BotActivity.kt index be282b7e51..682b63948d 100644 --- a/game/src/main/kotlin/content/bot/action/BotActivity.kt +++ b/game/src/main/kotlin/content/bot/action/BotActivity.kt @@ -28,6 +28,7 @@ data class BotActivity( override val id: String, val capacity: Int, override val requires: List = emptyList(), + override val resolve: List = emptyList(), override val plan: List = emptyList(), override val produces: Set = emptySet(), ) : Behaviour @@ -35,7 +36,8 @@ data class BotActivity( fun loadActivities(paths: List, activities: MutableMap, resolvers: MutableMap>) { val fragments = mutableMapOf() timedLoad("bot activity") { - val clones = mutableMapOf() + val reqClones = mutableMapOf() + val resClones = mutableMapOf() for (path in paths) { Config.fileReader(path) { while (nextSection()) { @@ -46,11 +48,13 @@ fun loadActivities(paths: List, activities: MutableMap = emptyList() var requirements: List = emptyList() + var resolvables: List = emptyList() var produces: List = emptyList() var fields: Map = emptyMap() while (nextPair()) { when (val key = key()) { "requires" -> requirements = requirements() + "resolve" -> resolvables = requirements() "plan" -> actions = actions() "produces" -> produces = requirements() "capacity" -> capacity = int() @@ -63,7 +67,11 @@ fun loadActivities(paths: List, activities: MutableMap().firstOrNull() if (clone != null) { - clones[id] = clone.id + reqClones[id] = clone.id + } + val resolveClone = resolvables.filterIsInstance().firstOrNull() + if (resolveClone != null) { + resClones[id] = resolveClone.id } if (template != null) { @@ -89,7 +97,7 @@ fun loadActivities(paths: List, activities: MutableMap @@ -97,6 +105,14 @@ fun loadActivities(paths: List, activities: MutableMap + resolvables.removeIf { it is FactClone && it.id == cloneId } + resolvables.addAll(clone.resolve) + resolvables.sortBy { it.priority } + } } // Fragments are partially filled behaviours with template + fields // This code resolves those fields into actual values taken from the template. @@ -110,15 +126,20 @@ fun loadActivities(paths: List, activities: MutableMap() + resolvables.addAll(fragment.requires) + fragment.resolveRequirements(template, resolvables) + resolvables.sortBy { it.priority } + val actions = mutableListOf() actions.addAll(fragment.plan) fragment.resolveActions(template, actions) if (fragment.type == "resolver") { for (fact in fragment.produces) { - resolvers.getOrPut(fact) { mutableListOf() }.add(Resolver(id, fragment.weight, requirements, actions)) + resolvers.getOrPut(fact) { mutableListOf() }.add(Resolver(id, fragment.weight, requirements, resolvables, actions)) } } else { - activities[id] = BotActivity(id, fragment.capacity, requirements, actions) + activities[id] = BotActivity(id, fragment.capacity, requirements, resolvables, actions) } } // Templates aren't selectable activities diff --git a/game/src/main/kotlin/content/bot/action/Resolver.kt b/game/src/main/kotlin/content/bot/action/Resolver.kt index 54a469e61e..46d4eeb92d 100644 --- a/game/src/main/kotlin/content/bot/action/Resolver.kt +++ b/game/src/main/kotlin/content/bot/action/Resolver.kt @@ -11,6 +11,7 @@ data class Resolver( override val id: String, val weight: Int, override val requires: List = emptyList(), + override val resolve: List = emptyList(), override val plan: List = emptyList(), override val produces: Set = emptySet(), ) : Behaviour \ No newline at end of file diff --git a/game/src/main/kotlin/content/bot/fact/MandatoryFact.kt b/game/src/main/kotlin/content/bot/fact/MandatoryFact.kt index df0d00185d..bd2bb32523 100644 --- a/game/src/main/kotlin/content/bot/fact/MandatoryFact.kt +++ b/game/src/main/kotlin/content/bot/fact/MandatoryFact.kt @@ -3,12 +3,10 @@ package content.bot.fact import content.bot.Bot import world.gregs.voidps.engine.entity.character.player.skill.Skill -sealed class MandatoryFact(priority: Int = 0) : Fact(priority) - data class HasSkillLevel( val skill: Skill, val min: Int = 1, val max: Int = 120 -) : MandatoryFact() { +) : Fact(0) { override fun check(bot: Bot) = bot.player.levels.get(skill) in min..max } diff --git a/game/src/main/kotlin/content/bot/fact/ResolvableFact.kt b/game/src/main/kotlin/content/bot/fact/ResolvableFact.kt index 1ad8461de1..d85783faa0 100644 --- a/game/src/main/kotlin/content/bot/fact/ResolvableFact.kt +++ b/game/src/main/kotlin/content/bot/fact/ResolvableFact.kt @@ -6,52 +6,50 @@ import world.gregs.voidps.engine.inv.carriesItem import world.gregs.voidps.engine.inv.equips import world.gregs.voidps.engine.inv.inventory -sealed class ResolvableFact(priority: Int) : Fact(priority) - data class EquipsItem( val id: String, val amount: Int = 1, -) : ResolvableFact(100) { +) : Fact(100) { override fun check(bot: Bot) = bot.player.equips(id, amount) } data class HasVariable( val id: String, val value: Any? = null, -) : ResolvableFact(1) { +) : Fact(1) { override fun check(bot: Bot) = bot.player.variables.get(id) == value } data class CarriesItem( val id: String, val amount: Int = 1, -) : ResolvableFact(100) { +) : Fact(100) { override fun check(bot: Bot) = bot.player.carriesItem(id, amount) } data class EquipsOne( val ids: Set, val amount: Int = 1, -) : ResolvableFact(100) { +) : Fact(100) { override fun check(bot: Bot) = ids.any { id -> bot.player.equips(id, amount) } } data class CarriesOne( val ids: Set, val amount: Int = 1, -) : ResolvableFact(100) { +) : Fact(100) { override fun check(bot: Bot) = ids.any { id -> bot.player.carriesItem(id, amount) } } data class HasInventorySpace( val amount: Int, -) : ResolvableFact(10) { +) : Fact(10) { override fun check(bot: Bot) = bot.player.inventory.spaces >= amount } data class AtLocation( val id: String, -) : ResolvableFact(1000) { +) : Fact(1000) { override fun check(bot: Bot) = bot.player.tile in Areas[id] } @@ -60,6 +58,6 @@ data class AtTile( val y: Int, val level: Int, val radius: Int, -) : ResolvableFact(1100) { +) : Fact(1100) { override fun check(bot: Bot) = bot.player.tile.within(x, y, radius, level) } \ No newline at end of file diff --git a/game/src/test/kotlin/content/bot/BotManagerTest.kt b/game/src/test/kotlin/content/bot/BotManagerTest.kt index b9f9cac943..a54ce4f0f5 100644 --- a/game/src/test/kotlin/content/bot/BotManagerTest.kt +++ b/game/src/test/kotlin/content/bot/BotManagerTest.kt @@ -1,30 +1,14 @@ package content.bot -import content.bot.action.BehaviourFrame -import content.bot.action.BehaviourState -import content.bot.action.BotAction -import content.bot.action.BotActivity -import content.bot.action.Reason -import content.bot.action.Resolver -import content.bot.action.SoftReason +import content.bot.action.* import content.bot.fact.AtTile -import content.bot.fact.CarriesItem -import content.bot.fact.EquipsItem import content.bot.fact.Fact import content.bot.fact.FactClone -import content.bot.fact.MandatoryFact import content.bot.fact.HasSkillLevel -import content.bot.fact.HasVariable import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.Test -import world.gregs.voidps.cache.config.data.InventoryDefinition -import world.gregs.voidps.engine.data.definition.InventoryDefinitions import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.entity.character.player.skill.Skill -import world.gregs.voidps.engine.inv.add -import world.gregs.voidps.engine.inv.inventory -import world.gregs.voidps.engine.inv.restrict.ValidItemRestriction -import world.gregs.voidps.engine.inv.stack.ItemDependentStack class BotManagerTest { @@ -49,7 +33,7 @@ class BotManagerTest { fun `Activity capacity is respected`() { val activity = testActivity( id = "mine", - plan = listOf(BotAction.Wait(1)) + plan = listOf(BotAction.Clone("")) ) val manager = BotManager(mutableMapOf(activity.id to activity)) @@ -83,8 +67,8 @@ class BotManagerTest { val activity = testActivity( id = "task", plan = listOf( - BotAction.Wait(1), - BotAction.Wait(1) + BotAction.Clone("1"), + BotAction.Clone("1") ) ) val frame = BehaviourFrame(activity) @@ -202,7 +186,7 @@ class BotManagerTest { fun `Failed activity is blocked`() { val activity = testActivity( id = "fish", - plan = listOf(BotAction.Wait(1)) + plan = listOf(BotAction.Clone("")) ) val manager = BotManager(mutableMapOf(activity.id to activity)) @@ -222,7 +206,7 @@ class BotManagerTest { fun `Behaviour without requirements isn't started`() { val activity = testActivity( id = "test", - requirements = listOf( + requires = listOf( HasSkillLevel(Skill.Attack, 99, 99) ), plan = listOf(BotAction.Wait(4)) @@ -251,7 +235,7 @@ class BotManagerTest { ) val activity = testActivity( id = "woodcut", - requirements = listOf(fact), + resolves = listOf(fact), plan = listOf(BotAction.Wait(1)) ) val manager = BotManager( @@ -276,8 +260,8 @@ class BotManagerTest { val activity = testActivity( id = "mine", - requirements = listOf(fact), - plan = listOf(BotAction.Wait(1)) + resolves = listOf(fact), + plan = listOf(BotAction.Clone("")) ) val manager = BotManager( @@ -298,7 +282,7 @@ class BotManagerTest { val resolver = Resolver(id = "get_key", weight = 1) val activity = testActivity( id = "open_door", - requirements = listOf(fact), + resolves = listOf(fact), plan = listOf(BotAction.Wait(1)) ) val manager = BotManager( @@ -328,7 +312,7 @@ class BotManagerTest { ) val activity = testActivity( id = "enter_zone", - requirements = listOf(fact), + resolves = listOf(fact), plan = listOf(BotAction.Wait(1)) ) val manager = BotManager( @@ -356,7 +340,7 @@ class BotManagerTest { ) val activity = testActivity( id = "smelt", - requirements = listOf(fact), + resolves = listOf(fact), plan = listOf(BotAction.Wait(1)) ) val manager = BotManager( @@ -385,7 +369,7 @@ class BotManagerTest { ) val activity = testActivity( id = "craft", - requirements = listOf(fact), + resolves = listOf(fact), plan = listOf(BotAction.Wait(1)) ) val manager = BotManager( @@ -412,7 +396,7 @@ class BotManagerTest { ) val activity = testActivity( id = "work", - requirements = listOf(fact), + resolves = listOf(fact), plan = listOf(BotAction.Wait(1)) ) val manager = BotManager( @@ -429,7 +413,8 @@ class BotManagerTest { fun testActivity( id: String, - requirements: List = emptyList(), + requires: List = emptyList(), + resolves: List = emptyList(), plan: List - ) = BotActivity(id, 1, requirements, plan) + ) = BotActivity(id, 1, requires, resolves, plan) } \ No newline at end of file From bbcca16115fbb561e4a8b95358343d357c98acf8 Mon Sep 17 00:00:00 2001 From: GregHib Date: Thu, 29 Jan 2026 14:27:51 +0000 Subject: [PATCH 020/101] Add activity visibility and grouping based on facts --- data/bot/walking.bots.toml | 2 +- data/bot/woodcutting.bots.toml | 6 +- game/src/main/kotlin/content/bot/Bot.kt | 1 + .../src/main/kotlin/content/bot/BotManager.kt | 32 +++++--- game/src/main/kotlin/content/bot/BotSpawns.kt | 10 +++ .../kotlin/content/bot/action/BotActivity.kt | 10 ++- game/src/main/kotlin/content/bot/fact/Fact.kt | 82 ++++++++++++++++++- .../kotlin/content/bot/fact/MandatoryFact.kt | 12 --- .../kotlin/content/bot/fact/ResolvableFact.kt | 63 -------------- 9 files changed, 127 insertions(+), 91 deletions(-) delete mode 100644 game/src/main/kotlin/content/bot/fact/MandatoryFact.kt delete mode 100644 game/src/main/kotlin/content/bot/fact/ResolvableFact.kt diff --git a/data/bot/walking.bots.toml b/data/bot/walking.bots.toml index ec194acfbe..9e2b64d7c8 100644 --- a/data/bot/walking.bots.toml +++ b/data/bot/walking.bots.toml @@ -1,7 +1,7 @@ [run_to_varrock] type = "activity" capacity = 1 -requires = [ +resolve = [ { variable = "movement", value = "run" } ] plan = [ diff --git a/data/bot/woodcutting.bots.toml b/data/bot/woodcutting.bots.toml index 16c1debc67..d1ea75d14a 100644 --- a/data/bot/woodcutting.bots.toml +++ b/data/bot/woodcutting.bots.toml @@ -1,6 +1,6 @@ [woodcutting_template] type = "activity" -requires = [ +resolve = [ { carries = "$hatchet" }, { location = "$location" }, { inventory_space = 20 }, @@ -32,7 +32,7 @@ produces = [ ] [buy_from_shop] -requires = [ +resolve = [ { carries = "coins", amount = "$cost" }, { location = "$shop_location" }, { inventory_space = 1 }, @@ -93,6 +93,8 @@ type = "teleport" weight = 10 requires = [ { variable = "spellbook", value = "normal" }, +] +resolve = [ { carries = "fire_rune", amount = 1 }, { carries = "air_rune", amount = 3 }, { carries = "law_rune", amount = 1 }, diff --git a/game/src/main/kotlin/content/bot/Bot.kt b/game/src/main/kotlin/content/bot/Bot.kt index b625219ede..b152cfbce1 100644 --- a/game/src/main/kotlin/content/bot/Bot.kt +++ b/game/src/main/kotlin/content/bot/Bot.kt @@ -15,6 +15,7 @@ data class Bot(val player: Player) : Character by player { val blocked: MutableSet = mutableSetOf() var previous: BotActivity? = null val frames = Stack() + val available = mutableSetOf() fun noTask() = frames.isEmpty() diff --git a/game/src/main/kotlin/content/bot/BotManager.kt b/game/src/main/kotlin/content/bot/BotManager.kt index 65d8566fdc..0e4ca589bc 100644 --- a/game/src/main/kotlin/content/bot/BotManager.kt +++ b/game/src/main/kotlin/content/bot/BotManager.kt @@ -17,6 +17,7 @@ import world.gregs.voidps.type.random class BotManager( private val activities: MutableMap = mutableMapOf(), private val resolvers: MutableMap> = mutableMapOf(), + private val groups: MutableMap> = mutableMapOf(), ) : Runnable { val slots = ActivitySlots() val bots = mutableListOf() @@ -24,6 +25,16 @@ class BotManager( fun add(bot: Bot) { bots.add(bot) + for (activity in activities.values) { + if (activity.requires.any { !it.check(bot) }) { + continue + } + bot.available.add(activity.id) + } + } + + fun update(bot: Bot, event: String) { + } fun remove(bot: Bot): Boolean { @@ -35,7 +46,7 @@ class BotManager( } fun load(files: ConfigFiles): BotManager { - loadActivities(files.list(Settings["bots.definitions"]), activities, resolvers) + loadActivities(files.list(Settings["bots.definitions"]), activities, groups, resolvers) return this } @@ -67,16 +78,15 @@ class BotManager( val activity = if (bot.previous != null && hasRequirements(bot, bot.previous!!)) { bot.previous } else { - // TODO could be a command -// if (bot.player["debug", false]) { -// for (activity in activities.values) { -// logger.info { "Activity ${activity.id}: ${hasRequirements(bot, activity)} ${activity.requires.map { "[${it}=${it !is MandatoryFact}+${it.check(bot)}]" }}." } -// } -// } - activities.values - .filter { hasRequirements(bot, it) } - .randomOrNull(random) - ?: BotActivity("idle", 2048, plan = listOf(BotAction.Wait(50))) // 30s + val id = bot.available.filter { + val activity = activities[it] + activity != null && hasRequirements(bot, activity) + }.randomOrNull(random) + if (id == null) { + BotActivity("idle", 2048, plan = listOf(BotAction.Wait(50))) // 30s + } else { + activities[id] + } } if (activity == null) { if (bot.player["debug", false]) { diff --git a/game/src/main/kotlin/content/bot/BotSpawns.kt b/game/src/main/kotlin/content/bot/BotSpawns.kt index 182fdccf6d..45cb3df657 100644 --- a/game/src/main/kotlin/content/bot/BotSpawns.kt +++ b/game/src/main/kotlin/content/bot/BotSpawns.kt @@ -18,6 +18,7 @@ import world.gregs.voidps.engine.entity.World import world.gregs.voidps.engine.entity.character.move.running import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.entity.character.player.appearance +import world.gregs.voidps.engine.entity.character.player.chat.ChatType import world.gregs.voidps.engine.entity.character.player.sex import world.gregs.voidps.engine.get import world.gregs.voidps.engine.inv.add @@ -68,6 +69,7 @@ class BotSpawns( adminCommand("bots", intArg("count", optional = true), desc = "Spawn (count) number of bots", handler = ::spawn) adminCommand("clear_bots", intArg("count", optional = true), desc = "Clear all or some amount of bots", handler = ::clear) adminCommand("bot", stringArg("task", optional = true, autofill = tasks.names), desc = "Toggle yourself on/off as a bot player", handler = ::toggle) + adminCommand("bot_info", desc = "Print bot info", handler = ::info) } private fun loadSettings() { @@ -108,6 +110,14 @@ class BotSpawns( } } + fun info(player: Player, args: List) { + val bot = player.bot + player.message("Available activities:", ChatType.Console) + for (activity in bot.available) { + player.message(" $activity", ChatType.Console) + } + } + fun toggle(player: Player, args: List) { if (player.isBot) { manager.remove(player.bot) diff --git a/game/src/main/kotlin/content/bot/action/BotActivity.kt b/game/src/main/kotlin/content/bot/action/BotActivity.kt index 682b63948d..2eb5280b6d 100644 --- a/game/src/main/kotlin/content/bot/action/BotActivity.kt +++ b/game/src/main/kotlin/content/bot/action/BotActivity.kt @@ -33,7 +33,7 @@ data class BotActivity( override val produces: Set = emptySet(), ) : Behaviour -fun loadActivities(paths: List, activities: MutableMap, resolvers: MutableMap>) { +fun loadActivities(paths: List, activities: MutableMap, groups: MutableMap>, resolvers: MutableMap>) { val fragments = mutableMapOf() timedLoad("bot activity") { val reqClones = mutableMapOf() @@ -146,6 +146,14 @@ fun loadActivities(paths: List, activities: MutableMap = emptySet() } internal data class FactClone( @@ -17,4 +23,78 @@ internal data class FactClone( internal data class FactReference( var fact: Fact, val references: Map, -) : Fact(-1) \ No newline at end of file +) : Fact(-1) + +data class HasSkillLevel( + val skill: Skill, + val min: Int = 1, + val max: Int = 120 +) : Fact(0) { + override fun check(bot: Bot) = bot.player.levels.get(skill) in min..max + override fun keys() = setOf(skill.name) +} + + +data class EquipsItem( + val id: String, + val amount: Int = 1, +) : Fact(100) { + override fun check(bot: Bot) = bot.player.equips(id, amount) + override fun keys() = setOf("equipment") +} + +data class HasVariable( + val id: String, + val value: Any? = null, +) : Fact(1) { + override fun check(bot: Bot) = bot.player.variables.get(id) == value + override fun keys() = setOf("var:${id}") +} + +data class CarriesItem( + val id: String, + val amount: Int = 1, +) : Fact(100) { + override fun check(bot: Bot) = bot.player.carriesItem(id, amount) + override fun keys() = setOf("inventory") +} + +data class EquipsOne( + val ids: Set, + val amount: Int = 1, +) : Fact(100) { + override fun check(bot: Bot) = ids.any { id -> bot.player.equips(id, amount) } + override fun keys() = setOf("equipment") +} + +data class CarriesOne( + val ids: Set, + val amount: Int = 1, +) : Fact(100) { + override fun check(bot: Bot) = ids.any { id -> bot.player.carriesItem(id, amount) } + override fun keys() = setOf("inventory") +} + +data class HasInventorySpace( + val amount: Int, +) : Fact(10) { + override fun check(bot: Bot) = bot.player.inventory.spaces >= amount + override fun keys() = setOf("inventory") +} + +data class AtLocation( + val id: String, +) : Fact(1000) { + override fun check(bot: Bot) = bot.player.tile in Areas[id] + override fun keys() = setOf("enter:$id") +} + +data class AtTile( + val x: Int, + val y: Int, + val level: Int, + val radius: Int, +) : Fact(1100) { + override fun check(bot: Bot) = bot.player.tile.within(x, y, radius, level) + override fun keys() = setOf("tile") +} diff --git a/game/src/main/kotlin/content/bot/fact/MandatoryFact.kt b/game/src/main/kotlin/content/bot/fact/MandatoryFact.kt deleted file mode 100644 index bd2bb32523..0000000000 --- a/game/src/main/kotlin/content/bot/fact/MandatoryFact.kt +++ /dev/null @@ -1,12 +0,0 @@ -package content.bot.fact - -import content.bot.Bot -import world.gregs.voidps.engine.entity.character.player.skill.Skill - -data class HasSkillLevel( - val skill: Skill, - val min: Int = 1, - val max: Int = 120 -) : Fact(0) { - override fun check(bot: Bot) = bot.player.levels.get(skill) in min..max -} diff --git a/game/src/main/kotlin/content/bot/fact/ResolvableFact.kt b/game/src/main/kotlin/content/bot/fact/ResolvableFact.kt deleted file mode 100644 index d85783faa0..0000000000 --- a/game/src/main/kotlin/content/bot/fact/ResolvableFact.kt +++ /dev/null @@ -1,63 +0,0 @@ -package content.bot.fact - -import content.bot.Bot -import world.gregs.voidps.engine.data.definition.Areas -import world.gregs.voidps.engine.inv.carriesItem -import world.gregs.voidps.engine.inv.equips -import world.gregs.voidps.engine.inv.inventory - -data class EquipsItem( - val id: String, - val amount: Int = 1, -) : Fact(100) { - override fun check(bot: Bot) = bot.player.equips(id, amount) -} - -data class HasVariable( - val id: String, - val value: Any? = null, -) : Fact(1) { - override fun check(bot: Bot) = bot.player.variables.get(id) == value -} - -data class CarriesItem( - val id: String, - val amount: Int = 1, -) : Fact(100) { - override fun check(bot: Bot) = bot.player.carriesItem(id, amount) -} - -data class EquipsOne( - val ids: Set, - val amount: Int = 1, -) : Fact(100) { - override fun check(bot: Bot) = ids.any { id -> bot.player.equips(id, amount) } -} - -data class CarriesOne( - val ids: Set, - val amount: Int = 1, -) : Fact(100) { - override fun check(bot: Bot) = ids.any { id -> bot.player.carriesItem(id, amount) } -} - -data class HasInventorySpace( - val amount: Int, -) : Fact(10) { - override fun check(bot: Bot) = bot.player.inventory.spaces >= amount -} - -data class AtLocation( - val id: String, -) : Fact(1000) { - override fun check(bot: Bot) = bot.player.tile in Areas[id] -} - -data class AtTile( - val x: Int, - val y: Int, - val level: Int, - val radius: Int, -) : Fact(1100) { - override fun check(bot: Bot) = bot.player.tile.within(x, y, radius, level) -} \ No newline at end of file From 6ee5fe940fc2d93a5ceb869255078e60f8402506 Mon Sep 17 00:00:00 2001 From: GregHib Date: Thu, 29 Jan 2026 14:40:57 +0000 Subject: [PATCH 021/101] Update enter/exit handler to pass AreaDefinition instead of Area --- .../gregs/voidps/engine/data/AccountManager.kt | 4 ++-- .../engine/entity/character/mode/move/AreaQueue.kt | 4 ++-- .../engine/entity/character/mode/move/Moved.kt | 13 +++++++------ .../engine/entity/character/mode/move/Movement.kt | 4 ++-- .../engine/entity/character/mode/move/MovedTest.kt | 11 +++++++---- .../kotlin/content/area/misthalin/BorderGuard.kt | 9 +++++---- 6 files changed, 25 insertions(+), 20 deletions(-) diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/data/AccountManager.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/data/AccountManager.kt index 5a7f07cec1..bd54863a37 100644 --- a/engine/src/main/kotlin/world/gregs/voidps/engine/data/AccountManager.kt +++ b/engine/src/main/kotlin/world/gregs/voidps/engine/data/AccountManager.kt @@ -95,7 +95,7 @@ class AccountManager( val original = player.tile.minus(offset) for (def in Areas.get(original.zone)) { if (original in def.area) { - Moved.enter(player, def.name, def.area) + Moved.enter(player, def.name, def) } } } @@ -127,7 +127,7 @@ class AccountManager( val original = player.tile.minus(offset) for (def in Areas.get(original.zone)) { if (original in def.area) { - Moved.exit(player, def.name, def.area) + Moved.exit(player, def.name, def) } } Despawn.player(player) diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/mode/move/AreaQueue.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/mode/move/AreaQueue.kt index e968d3a6fd..449b3e2346 100644 --- a/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/mode/move/AreaQueue.kt +++ b/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/mode/move/AreaQueue.kt @@ -24,12 +24,12 @@ class AreaQueue( val to = player.tile for (def in Areas.get(from.zone)) { if (from in def.area && to !in def.area) { - Moved.exit(player, def.name, def.area) + Moved.exit(player, def.name, def) } } for (def in Areas.get(to.zone)) { if (to in def.area && from !in def.area) { - Moved.enter(player, def.name, def.area) + Moved.enter(player, def.name, def) } } } diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/mode/move/Moved.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/mode/move/Moved.kt index dee0e52c8a..143e555340 100644 --- a/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/mode/move/Moved.kt +++ b/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/mode/move/Moved.kt @@ -2,6 +2,7 @@ package world.gregs.voidps.engine.entity.character.mode.move import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap import it.unimi.dsi.fastutil.objects.ObjectArrayList +import world.gregs.voidps.engine.data.definition.AreaDefinition import world.gregs.voidps.engine.entity.character.npc.NPC import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.event.Wildcard @@ -23,21 +24,21 @@ interface Moved { } } - fun entered(area: String, handler: Player.(area: Area) -> Unit) { + fun entered(area: String, handler: Player.(area: AreaDefinition) -> Unit) { entered.getOrPut(area) { mutableListOf() }.add(handler) } - fun exited(area: String, handler: Player.(area: Area) -> Unit) { + fun exited(area: String, handler: Player.(area: AreaDefinition) -> Unit) { exited.getOrPut(area) { mutableListOf() }.add(handler) } companion object : AutoCloseable { - private val entered = Object2ObjectOpenHashMap Unit>>(25) - private val exited = Object2ObjectOpenHashMap Unit>>(25) + private val entered = Object2ObjectOpenHashMap Unit>>(25) + private val exited = Object2ObjectOpenHashMap Unit>>(25) val playerMoved = ObjectArrayList<(Player, Tile) -> Unit>(15) private val npcMoved = Object2ObjectOpenHashMap Unit>>(10) - fun enter(player: Player, id: String, area: Area) { + fun enter(player: Player, id: String, area: AreaDefinition) { for (handler in entered[id] ?: emptyList()) { handler(player, area) } @@ -46,7 +47,7 @@ interface Moved { } } - fun exit(player: Player, id: String, area: Area) { + fun exit(player: Player, id: String, area: AreaDefinition) { for (handler in exited[id] ?: emptyList()) { handler(player, area) } diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/mode/move/Movement.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/mode/move/Movement.kt index 5fc879afc8..6f271ba7f2 100644 --- a/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/mode/move/Movement.kt +++ b/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/mode/move/Movement.kt @@ -222,12 +222,12 @@ open class Movement( val fromOriginal = from.minus(offset) for (def in Areas.get(fromOriginal.zone)) { if (fromOriginal in def.area && toOriginal !in def.area) { - Moved.exit(character, def.name, def.area) + Moved.exit(character, def.name, def) } } for (def in Areas.get(toOriginal.zone)) { if (toOriginal in def.area && fromOriginal !in def.area) { - Moved.enter(character, def.name, def.area) + Moved.enter(character, def.name, def) } } } else if (character is NPC) { diff --git a/engine/src/test/kotlin/world/gregs/voidps/engine/entity/character/mode/move/MovedTest.kt b/engine/src/test/kotlin/world/gregs/voidps/engine/entity/character/mode/move/MovedTest.kt index 734f421d98..867e9959d6 100644 --- a/engine/src/test/kotlin/world/gregs/voidps/engine/entity/character/mode/move/MovedTest.kt +++ b/engine/src/test/kotlin/world/gregs/voidps/engine/entity/character/mode/move/MovedTest.kt @@ -4,6 +4,7 @@ import org.junit.jupiter.api.Nested import world.gregs.voidps.engine.Caller import world.gregs.voidps.engine.Script import world.gregs.voidps.engine.ScriptTest +import world.gregs.voidps.engine.data.definition.AreaDefinition import world.gregs.voidps.engine.entity.character.npc.NPC import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.type.Tile @@ -67,12 +68,13 @@ class MovedTest { override fun Script.register(args: List, caller: Caller) { entered(args[0]) { area -> caller.call() - assertEquals(Rectangle(Tile(1, 2), 1, 2), area) + assertEquals(Rectangle(Tile(1, 2), 1, 2), area.area) } } override fun invoke(args: List) { - Moved.enter(Player(), args[0], Rectangle(Tile(1, 2), 1, 2)) + val def = AreaDefinition("", area = Rectangle(Tile(1, 2), 1, 2), tags = emptySet()) + Moved.enter(Player(), args[0], def) } override val apis = listOf(Moved) @@ -89,12 +91,13 @@ class MovedTest { override fun Script.register(args: List, caller: Caller) { exited(args[0]) { area -> caller.call() - assertEquals(Rectangle(Tile(1, 2), 1, 2), area) + assertEquals(Rectangle(Tile(1, 2), 1, 2), area.area) } } override fun invoke(args: List) { - Moved.exit(Player(), args[0], Rectangle(Tile(1, 2), 1, 2)) + val def = AreaDefinition("", area = Rectangle(Tile(1, 2), 1, 2), tags = emptySet()) + Moved.exit(Player(), args[0], def) } override val apis = listOf(Moved) diff --git a/game/src/main/kotlin/content/area/misthalin/BorderGuard.kt b/game/src/main/kotlin/content/area/misthalin/BorderGuard.kt index cce97942b6..06db37df70 100644 --- a/game/src/main/kotlin/content/area/misthalin/BorderGuard.kt +++ b/game/src/main/kotlin/content/area/misthalin/BorderGuard.kt @@ -1,6 +1,7 @@ package content.area.misthalin import world.gregs.voidps.engine.Script +import world.gregs.voidps.engine.data.definition.AreaDefinition import world.gregs.voidps.engine.data.definition.Areas import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.entity.obj.GameObject @@ -47,8 +48,8 @@ class BorderGuard : Script { exited("border_guard_draynor_falador", ::exit) } - fun enter(player: Player, area: Area) { - val border = area as Rectangle + fun enter(player: Player, def: AreaDefinition) { + val border = def.area as Rectangle if (player.steps.destination in border || player.steps.isEmpty()) { val tile = border.nearestTo(player.tile) val endSide = Border.getOppositeSide(border, tile) @@ -60,8 +61,8 @@ class BorderGuard : Script { changeGuardState(guards, true) } - fun exit(player: Player, area: Area) { - val border = area as Rectangle + fun exit(player: Player, def: AreaDefinition) { + val border = def.area as Rectangle val guards = guards[border] ?: return player.steps.update(noCollision = false, noRun = false) changeGuardState(guards, false) From 167c7f26bc873ff199a2dca4b52eb39ed370b5be Mon Sep 17 00:00:00 2001 From: GregHib Date: Thu, 29 Jan 2026 14:42:06 +0000 Subject: [PATCH 022/101] Add recalculating activities reactively --- .../src/main/kotlin/content/bot/BotManager.kt | 19 +++++++++- .../src/main/kotlin/content/bot/BotUpdates.kt | 37 +++++++++++++++++++ game/src/main/kotlin/content/bot/fact/Fact.kt | 10 ++--- 3 files changed, 59 insertions(+), 7 deletions(-) create mode 100644 game/src/main/kotlin/content/bot/BotUpdates.kt diff --git a/game/src/main/kotlin/content/bot/BotManager.kt b/game/src/main/kotlin/content/bot/BotManager.kt index 0e4ca589bc..fbecfe131f 100644 --- a/game/src/main/kotlin/content/bot/BotManager.kt +++ b/game/src/main/kotlin/content/bot/BotManager.kt @@ -33,8 +33,23 @@ class BotManager( } } - fun update(bot: Bot, event: String) { - + fun update(bot: Bot, group: String) { + val iterator = bot.available.iterator() + while (iterator.hasNext()) { + val id = iterator.next() + val activity = activities[id] ?: continue + // TODO could filter by keys + if (activity.requires.any { !it.check(bot) }) { + iterator.remove() + } + } + for (id in groups[group] ?: return) { + val activity = activities[id] ?: continue + if (activity.requires.any { !it.check(bot) }) { + continue + } + bot.available.add(activity.id) + } } fun remove(bot: Bot): Boolean { diff --git a/game/src/main/kotlin/content/bot/BotUpdates.kt b/game/src/main/kotlin/content/bot/BotUpdates.kt new file mode 100644 index 0000000000..3788d333b6 --- /dev/null +++ b/game/src/main/kotlin/content/bot/BotUpdates.kt @@ -0,0 +1,37 @@ +package content.bot + +import world.gregs.voidps.engine.Script + +class BotUpdates(val manager: BotManager) : Script { + init { + levelChanged { skill, _, _ -> + if (isBot) { + manager.update(bot, skill.name) + } + } + + moved { from -> + if (isBot && tile != from) { + manager.update(bot, "tile") + } + } + + variableSet { key, from, to -> + if (isBot && from != to) { + manager.update(bot, "var:$key") + } + } + + inventoryUpdated { inventory, _ -> + if (isBot) { + manager.update(bot, "inv:$inventory") + } + } + + entered("*") { + if (isBot) { + manager.update(bot, "enter:${it.name}") + } + } + } +} \ No newline at end of file diff --git a/game/src/main/kotlin/content/bot/fact/Fact.kt b/game/src/main/kotlin/content/bot/fact/Fact.kt index 47e4de070f..db0a043dc0 100644 --- a/game/src/main/kotlin/content/bot/fact/Fact.kt +++ b/game/src/main/kotlin/content/bot/fact/Fact.kt @@ -40,7 +40,7 @@ data class EquipsItem( val amount: Int = 1, ) : Fact(100) { override fun check(bot: Bot) = bot.player.equips(id, amount) - override fun keys() = setOf("equipment") + override fun keys() = setOf("inv:equipment") } data class HasVariable( @@ -56,7 +56,7 @@ data class CarriesItem( val amount: Int = 1, ) : Fact(100) { override fun check(bot: Bot) = bot.player.carriesItem(id, amount) - override fun keys() = setOf("inventory") + override fun keys() = setOf("inv:inventory") } data class EquipsOne( @@ -64,7 +64,7 @@ data class EquipsOne( val amount: Int = 1, ) : Fact(100) { override fun check(bot: Bot) = ids.any { id -> bot.player.equips(id, amount) } - override fun keys() = setOf("equipment") + override fun keys() = setOf("inv:equipment") } data class CarriesOne( @@ -72,14 +72,14 @@ data class CarriesOne( val amount: Int = 1, ) : Fact(100) { override fun check(bot: Bot) = ids.any { id -> bot.player.carriesItem(id, amount) } - override fun keys() = setOf("inventory") + override fun keys() = setOf("inv:inventory") } data class HasInventorySpace( val amount: Int, ) : Fact(10) { override fun check(bot: Bot) = bot.player.inventory.spaces >= amount - override fun keys() = setOf("inventory") + override fun keys() = setOf("inv:inventory") } data class AtLocation( From 01a412d2d9fca438256ec8c266a8c29be274beb2 Mon Sep 17 00:00:00 2001 From: GregHib Date: Fri, 30 Jan 2026 21:31:40 +0000 Subject: [PATCH 023/101] Facts and conditions --- data/bot/axe_shop.bots.toml | 37 ++++ data/bot/teleport.bots.toml | 59 ++++++ data/bot/walking.bots.toml | 24 +-- data/bot/woodcutting.bots.toml | 114 +++-------- .../src/main/kotlin/content/bot/BotManager.kt | 24 +-- .../src/main/kotlin/content/bot/BotUpdates.kt | 10 +- .../kotlin/content/bot/action/Behaviour.kt | 8 +- .../content/bot/action/BehaviourFragment.kt | 137 +++++++------- .../content/bot/action/BehaviourFrame.kt | 5 + .../kotlin/content/bot/action/BotAction.kt | 132 +++++++------ .../kotlin/content/bot/action/BotActivity.kt | 178 ++++++++---------- .../main/kotlin/content/bot/action/Reason.kt | 4 +- .../kotlin/content/bot/action/Resolver.kt | 8 +- .../main/kotlin/content/bot/fact/Condition.kt | 100 ++++++++++ game/src/main/kotlin/content/bot/fact/Fact.kt | 175 +++++++++-------- .../test/kotlin/content/bot/BotManagerTest.kt | 117 ++++++------ .../bot/action/BehaviourFragmentTest.kt | 110 ++++------- 17 files changed, 676 insertions(+), 566 deletions(-) create mode 100644 data/bot/axe_shop.bots.toml create mode 100644 data/bot/teleport.bots.toml create mode 100644 game/src/main/kotlin/content/bot/fact/Condition.kt diff --git a/data/bot/axe_shop.bots.toml b/data/bot/axe_shop.bots.toml new file mode 100644 index 0000000000..118f345c0b --- /dev/null +++ b/data/bot/axe_shop.bots.toml @@ -0,0 +1,37 @@ +[buy_from_shop] +resolve = [ + { carries = "coins", amount = "$cost" }, + { location = "$shop_location" }, + { inventory_space = 1 }, +] +plan = [ + { option = "Trade", npc = "$shopkeeper" }, + { option = "Buy 1", interface = "shop:inventory:$item" }, +] +produces = [ + { carries = "$item" } +] + +[buy_bronze_hatchet] +type = "resolver" +template = "buy_from_shop" +fields = { cost = 16, shop_location = "bobs_axe_shop", shopkeeper = "bob", item = "bronze_hatchet" } +requires = [ + { skill = "woodcutting", min = 1 }, +] + +[buy_iron_hatchet] +type = "resolver" +template = "buy_from_shop" +fields = { cost = 56, shop_location = "bobs_axe_shop", shopkeeper = "bob", item = "iron_hatchet" } +requires = [ + { skill = "woodcutting", min = 1 }, +] + +[buy_steel_hatchet] +type = "resolver" +template = "buy_from_shop" +fields = { cost = 200, shop_location = "bobs_axe_shop", shopkeeper = "bob", item = "steel_hatchet" } +requires = [ + { skill = "woodcutting", min = 6 }, +] diff --git a/data/bot/teleport.bots.toml b/data/bot/teleport.bots.toml new file mode 100644 index 0000000000..48868e8181 --- /dev/null +++ b/data/bot/teleport.bots.toml @@ -0,0 +1,59 @@ + +[ring_of_duelling_equipped] +type = "teleport" +weight = 10 +requires = [ + { equips = "ring_of_duelling_*" } +] +plan = [ + { option = "Castle Wars", interface = "equipment:inventory" }, +] +produces = [ + { location = "castle_wars_teleport" } +] + +[ring_of_duelling] +type = "teleport" +weight = 8 +requires = [ + { carries = "ring_of_duelling_*" } +] +plan = [ + { option = "Rub", interface = "inventory:inventory" }, + { option = "Select", interface = "choice:line1" }, +] +produces = [ + { location = "castle_wars_teleport" } +] + +[teleport_varrock] +type = "teleport" +weight = 10 +requires = [ + { variable = "spellbook", value = "normal" }, +] +resolve = [ + { carries = "fire_rune", amount = 1 }, + { carries = "air_rune", amount = 3 }, + { carries = "law_rune", amount = 1 }, +] +plan = [ + { option = "Cast", interface = "normal_spellbook:varrock_teleport" }, +] +produces = [ + { location = "varrock_teleport" } +] + +[teleport_lumbridge] +type = "teleport" +weight = 5 +requires = [ + { variable = "spellbook", value = "normal" }, + { variable = "lumbridge_cooldown", value = "normal" }, +] +plan = [ + { option = "Cast", interface = "normal_spellbook:home_teleport" }, +] +produces = [ + { location = "lumbridge_teleport" } +] diff --git a/data/bot/walking.bots.toml b/data/bot/walking.bots.toml index 9e2b64d7c8..413964ac55 100644 --- a/data/bot/walking.bots.toml +++ b/data/bot/walking.bots.toml @@ -1,15 +1,15 @@ -[run_to_varrock] -type = "activity" -capacity = 1 -resolve = [ - { variable = "movement", value = "run" } -] -plan = [ - { go_to = "varrock_teleport" } -] -produces = [ - { location = "varrock_teleport" } -] +#[run_to_varrock] +#type = "activity" +#capacity = 1 +#resolve = [ +# { variable = "movement", value = "run" } +#] +#plan = [ +# { go_to = "varrock_teleport" } +#] +#produces = [ +# { location = "varrock_teleport" } +#] [toggle_run] type = "resolver" diff --git a/data/bot/woodcutting.bots.toml b/data/bot/woodcutting.bots.toml index d1ea75d14a..644440e017 100644 --- a/data/bot/woodcutting.bots.toml +++ b/data/bot/woodcutting.bots.toml @@ -6,116 +6,64 @@ resolve = [ { inventory_space = 20 }, ] plan = [ - { option = "Chop-down", object = "$tree", retry_ticks = "$wait", retry_max = 10 }, -] - -[normal_trees] -template = "woodcutting_template" -capacity = 4 -fields = { hatchet = "iron_hatchet", location = "normal_trees", tree = "tree*", wait = 10 } -requires = [ - { skill = "woodcutting", min = 1, max = 15 }, + { option = "Chop-down", object = "$tree" }, ] produces = [ - { carries = "logs" } + { skill = "woodcutting" } ] -[oak_trees] -template = "woodcutting_template" -capacity = 4 -fields = { hatchet = "steel_hatchet", location = "oak_trees", tree = "oak_tree", wait = 10 } -requires = [ - { skill = "woodcutting", min = 15, max = 30 }, -] -produces = [ - { carries = "oak_logs" } -] - -[buy_from_shop] +[go_to_lumbridge_north_trees] +type = "resolver" resolve = [ - { carries = "coins", amount = "$cost" }, - { location = "$shop_location" }, - { inventory_space = 1 }, + { variable = "movement", value = "run" } ] plan = [ - { option = "Trade", npc = "$shopkeeper" }, - { option = "Buy-1", interface = "shop:inventory:$item" }, + { go_to = "lumbridge_north_trees" } ] produces = [ - { carries = "$item" } -] - -[buy_iron_hatchet] -type = "resolver" -template = "buy_from_shop" -fields = { cost = 25, shop_location = "axe_shop", shopkeeper = "bob", item = "iron_hatchet" } -requires = [ - { skill = "woodcutting", min = 1 }, -] - -[buy_steel_hatchet] -type = "resolver" -template = "buy_from_shop" -fields = { cost = 50, shop_location = "axe_shop", shopkeeper = "bob", item = "steel_hatchet" } -requires = [ - { skill = "woodcutting", min = 10 }, + { location = "lumbridge_north_trees" } ] -[ring_of_duelling_equipped] -type = "teleport" -weight = 10 +[lumbridge_trees] +template = "woodcutting_template" +capacity = 4 +fields = { hatchet = "iron_hatchet,steel_hatchet", location = "lumbridge_north_trees", tree = "tree" } requires = [ - { equips = "ring_of_duelling_*" } -] -plan = [ - { option = "Castle Wars", interface = "equipment:inventory" }, + { skill = "woodcutting", min = 1, max = 15 }, ] produces = [ - { location = "castle_wars_teleport" } + { carries = "logs" } ] -[ring_of_duelling] -type = "teleport" -weight = 8 +[lumbridge_oak_trees] +template = "woodcutting_template" +capacity = 2 +fields = { hatchet = "steel_hatchet", location = "lumbridge_north_trees", tree = "oak" } requires = [ - { carries = "ring_of_duelling_*" } -] -plan = [ - { option = "Rub", interface = "inventory:inventory" }, - { option = "Select", interface = "choice:line1" }, + { skill = "woodcutting", min = 15, max = 30 }, ] produces = [ - { location = "castle_wars_teleport" } + { carries = "oak_logs" } ] -[teleport_varrock] -type = "teleport" -weight = 10 +[draynor_oak_trees] +template = "woodcutting_template" +capacity = 2 +fields = { hatchet = "steel_hatchet", location = "draynor_oak_trees", tree = "oak" } requires = [ - { variable = "spellbook", value = "normal" }, -] -resolve = [ - { carries = "fire_rune", amount = 1 }, - { carries = "air_rune", amount = 3 }, - { carries = "law_rune", amount = 1 }, -] -plan = [ - { option = "Cast", interface = "normal_spellbook:varrock_teleport" }, + { skill = "woodcutting", min = 15, max = 30 }, ] produces = [ - { location = "varrock_teleport" } + { carries = "oak_logs" } ] -[teleport_lumbridge] -type = "teleport" -weight = 5 +[draynor_willow_trees] +template = "woodcutting_template" +capacity = 6 +fields = { hatchet = "steel_hatchet,mithril_hatchet", location = "draynor_willow_trees", tree = "willow*" } requires = [ - { variable = "spellbook", value = "normal" }, - { variable = "lumbridge_cooldown", value = "normal" }, -] -plan = [ - { option = "Cast", interface = "normal_spellbook:home_teleport" }, + { skill = "woodcutting", min = 30, max = 45 }, ] produces = [ - { location = "lumbridge_teleport" } + { carries = "willow_logs" } ] diff --git a/game/src/main/kotlin/content/bot/BotManager.kt b/game/src/main/kotlin/content/bot/BotManager.kt index fbecfe131f..c1ee637ba8 100644 --- a/game/src/main/kotlin/content/bot/BotManager.kt +++ b/game/src/main/kotlin/content/bot/BotManager.kt @@ -2,7 +2,7 @@ package content.bot import com.github.michaelbull.logging.InlineLogger import content.bot.action.* -import content.bot.fact.Fact +import content.bot.fact.Condition import world.gregs.voidps.engine.data.ConfigFiles import world.gregs.voidps.engine.data.Settings import world.gregs.voidps.engine.event.AuditLog @@ -16,7 +16,7 @@ import world.gregs.voidps.type.random */ class BotManager( private val activities: MutableMap = mutableMapOf(), - private val resolvers: MutableMap> = mutableMapOf(), + private val resolvers: MutableMap> = mutableMapOf(), private val groups: MutableMap> = mutableMapOf(), ) : Runnable { val slots = ActivitySlots() @@ -157,16 +157,18 @@ class BotManager( frame.start(bot) } - private fun pickResolver(bot: Bot, fact: Fact, frame: BehaviourFrame): Behaviour? { + private fun pickResolver(bot: Bot, condition: Condition, frame: BehaviourFrame): Behaviour? { val options = mutableListOf() - for (resolver in resolvers[fact] ?: return null) { - if (frame.blocked.contains(resolver.id)) { - continue - } - if (resolver.requires.any { fact -> !fact.check(bot) }) { - continue + for (key in condition.keys()) { + for (resolver in resolvers[key] ?: return null) { + if (frame.blocked.contains(resolver.id)) { + continue + } + if (resolver.requires.any { fact -> !fact.check(bot) }) { + continue + } + options.add(resolver) } - options.add(resolver) } return options.minByOrNull { it.weight } } @@ -175,7 +177,7 @@ class BotManager( val frame = bot.frame() val behaviour = frame.behaviour when (val state = frame.state) { - BehaviourState.Running -> return + BehaviourState.Running -> frame.update(bot) BehaviourState.Pending -> start(bot, behaviour, frame) BehaviourState.Success -> if (!frame.next()) { AuditLog.event(bot, "completed", frame.behaviour.id) diff --git a/game/src/main/kotlin/content/bot/BotUpdates.kt b/game/src/main/kotlin/content/bot/BotUpdates.kt index 3788d333b6..65cb1bc460 100644 --- a/game/src/main/kotlin/content/bot/BotUpdates.kt +++ b/game/src/main/kotlin/content/bot/BotUpdates.kt @@ -10,11 +10,11 @@ class BotUpdates(val manager: BotManager) : Script { } } - moved { from -> - if (isBot && tile != from) { - manager.update(bot, "tile") - } - } +// moved { from -> +// if (isBot && tile != from) { +// manager.update(bot, "tile") // FIXME expensive +// } +// } variableSet { key, from, to -> if (isBot && from != to) { diff --git a/game/src/main/kotlin/content/bot/action/Behaviour.kt b/game/src/main/kotlin/content/bot/action/Behaviour.kt index 933c04c724..314917920a 100644 --- a/game/src/main/kotlin/content/bot/action/Behaviour.kt +++ b/game/src/main/kotlin/content/bot/action/Behaviour.kt @@ -1,11 +1,11 @@ package content.bot.action -import content.bot.fact.Fact +import content.bot.fact.Condition interface Behaviour { val id: String - val requires: List - val resolve: List + val requires: List + val resolve: List val plan: List - val produces: Set + val produces: Set } \ No newline at end of file diff --git a/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt b/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt index 7121fafa2b..e94e64661c 100644 --- a/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt +++ b/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt @@ -1,19 +1,6 @@ package content.bot.action -import content.bot.fact.FactClone -import content.bot.fact.Fact -import content.bot.fact.CarriesItem -import content.bot.fact.EquipsItem -import content.bot.fact.HasInventorySpace -import content.bot.fact.AtLocation -import content.bot.fact.FactReference -import content.bot.fact.HasSkillLevel -import content.bot.fact.AtTile -import content.bot.fact.CarriesOne -import content.bot.fact.EquipsOne -import content.bot.fact.HasVariable -import net.pearx.kasechange.toPascalCase -import world.gregs.voidps.engine.entity.character.player.skill.Skill +import content.bot.fact.* import world.gregs.voidps.engine.event.Wildcard import world.gregs.voidps.engine.event.Wildcards @@ -23,10 +10,10 @@ data class BehaviourFragment( val capacity: Int, val weight: Int, var template: String, - override val requires: List = emptyList(), - override val resolve: List = emptyList(), + override val requires: List = emptyList(), + override val resolve: List = emptyList(), override val plan: List = emptyList(), - override val produces: Set = emptySet(), + override val produces: Set = emptySet(), val fields: Map = emptyMap(), ) : Behaviour { fun resolveActions(template: BotActivity, actions: MutableList) { @@ -63,68 +50,64 @@ data class BehaviourFragment( } } - fun resolveRequirements(template: BotActivity, requirements: MutableList) { - for (req in template.requires) { + fun resolveRequirements(requirements: MutableList, facts: List) { + for (req in facts) { val resolved = when (req) { - is FactReference -> when (val requirement = req.fact) { - is HasSkillLevel -> HasSkillLevel( - skill = Skill.of(resolve(req.references["skill"], requirement.skill.name).toPascalCase())!!, - min = resolve(req.references["min"], requirement.min), - max = resolve(req.references["max"], requirement.max), - ) - is HasVariable -> HasVariable( - id = resolve(req.references["variable"], requirement.id), - value = resolve(req.references["value"], requirement.value), - ) - is CarriesItem -> CarriesItem( - id = resolve(req.references["carries"], requirement.id), - amount = resolve(req.references["amount"], requirement.amount), - ) - is CarriesOne -> { - val resolve = resolve(req.references["equips"], "") - val ids = if (resolve.isBlank()) { - requirement.ids - } else { - Wildcards.get(resolve, Wildcard.Item) + is Condition.Reference -> { + val references = req.references + val min = resolve(references["min"], req.min) + val max = resolve(references["max"], req.max) + when (req.type) { + "skill" -> { + val id = resolve(references[req.type], req.id) + Condition.range(Fact.SkillLevel.of(id), min, max) } - CarriesOne( - ids = ids, - amount = resolve(req.references["amount"], requirement.amount), - ) - } - is EquipsItem -> EquipsItem( - id = resolve(req.references["equips"], requirement.id), - amount = resolve(req.references["amount"], requirement.amount), - ) - is EquipsOne -> { - val resolve = resolve(req.references["equips"], "") - val ids = if (resolve.isBlank()) { - requirement.ids - } else { - Wildcards.get(resolve, Wildcard.Item) + "carries" -> { + val id = resolve(references[req.type], req.id) + val min = resolve(references["amount"], req.min) + if (id.contains(",")) { + Condition.Any(id.split(",").map { Condition.range(Fact.InventoryCount(it), min, max) }) + } else if (id.any { it == '*' || it == '#' }) { + Condition.Any(Wildcards.get(id, Wildcard.Item).map { Condition.range(Fact.InventoryCount(it), min, max) }) + } else { + Condition.range(Fact.InventoryCount(id), min, max) + } } - EquipsOne( - ids = ids, - amount = resolve(req.references["amount"], requirement.amount), - ) + "equips" -> { + val id = resolve(references[req.type], req.id) + val min = resolve(references["amount"], req.min) + if (id.contains(",")) { + Condition.Any(id.split(",").map { Condition.range(Fact.EquipCount(id), min, max) }) + } else if (id.any { it == '*' || it == '#' }) { + Condition.Any(Wildcards.get(id, Wildcard.Item).map { Condition.range(Fact.EquipCount(id), min, max) }) + } else { + Condition.range(Fact.EquipCount(id), min, max) + } + } + "variable" -> { + val id = resolve(references[req.type], req.id) + when (val value = resolve(references["value"], req.value)) { + is Int -> Condition.Equals(Fact.IntVariable(id), value) + is String -> Condition.Equals(Fact.StringVariable(id), value) + is Double -> Condition.Equals(Fact.DoubleVariable(id), value) + is Boolean -> Condition.Equals(Fact.BoolVariable(id), value) + else -> null + } + } + "inventory_space" -> { + val min = resolve(references["inventory_space"], req.min) + Condition.range(Fact.InventorySpace, min, null) + } + "location" -> { + val id = resolve(references["location"], req.id) + Condition.Area(Fact.PlayerTile, id) + } + else -> null } - is HasInventorySpace -> HasInventorySpace( - amount = resolve(req.references["inventory_space"], requirement.amount), - ) - is AtLocation -> AtLocation( - id = resolve(req.references["location"], requirement.id), - ) - is AtTile -> AtTile( - x = resolve(req.references["x"], requirement.x), - y = resolve(req.references["y"], requirement.y), - level = resolve(req.references["level"], requirement.level), - radius = resolve(req.references["radius"], requirement.radius), - ) - is FactClone, is FactReference -> throw IllegalArgumentException("Invalid requirement type: ${req.fact::class.simpleName}.") } - is FactClone -> throw IllegalArgumentException("Unresolved clone requirement in template ${id}.") + is Condition.Clone -> throw IllegalArgumentException("Unresolved clone requirement in template ${id}.") else -> req - } + } ?: continue requirements.add(resolved) } } @@ -161,6 +144,14 @@ data class BehaviourFragment( } } + private fun resolve(reference: String?, default: Int?): Int? { + return if (reference != null) { + fields[reference.key()] as? Int ?: throw IllegalArgumentException("Unable to find field '$reference' in ${id}.") + } else { + default + } + } + private fun resolve(reference: String?, default: String): String { return if (reference != null) { val key = reference.key() diff --git a/game/src/main/kotlin/content/bot/action/BehaviourFrame.kt b/game/src/main/kotlin/content/bot/action/BehaviourFrame.kt index 7142623722..315377cc46 100644 --- a/game/src/main/kotlin/content/bot/action/BehaviourFrame.kt +++ b/game/src/main/kotlin/content/bot/action/BehaviourFrame.kt @@ -19,6 +19,11 @@ data class BehaviourFrame( state = action.start(bot) } + fun update(bot: Bot) { + val action = action() + state = action.update(bot) + } + fun next(): Boolean { index++ state = BehaviourState.Pending diff --git a/game/src/main/kotlin/content/bot/action/BotAction.kt b/game/src/main/kotlin/content/bot/action/BotAction.kt index f783e02da3..5e7096cab4 100644 --- a/game/src/main/kotlin/content/bot/action/BotAction.kt +++ b/game/src/main/kotlin/content/bot/action/BotAction.kt @@ -7,11 +7,14 @@ import content.bot.interact.navigation.updateGraph import content.bot.interact.path.AreaStrategy import content.bot.interact.path.Dijkstra import content.bot.interact.path.EdgeTraversal +import content.entity.Movement import content.entity.combat.attackers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import world.gregs.voidps.engine.data.definition.Areas import world.gregs.voidps.engine.data.definition.InterfaceDefinitions +import world.gregs.voidps.engine.entity.character.mode.interact.Interact +import world.gregs.voidps.engine.entity.character.mode.interact.PlayerOnObjectInteract import world.gregs.voidps.engine.entity.character.npc.NPC import world.gregs.voidps.engine.entity.character.npc.NPCs import world.gregs.voidps.engine.entity.obj.GameObject @@ -24,16 +27,22 @@ import world.gregs.voidps.network.client.instruction.InteractNPC import world.gregs.voidps.type.random sealed interface BotAction { - fun start(bot: Bot): BehaviourState = BehaviourState.Failed(Reason.Cancelled) - fun update(): BehaviourState = BehaviourState.Running + fun start(bot: Bot): BehaviourState = BehaviourState.Running + fun update(bot: Bot): BehaviourState = BehaviourState.Running sealed class RetryableAction : BotAction { abstract val retryTicks: Int abstract val retryMax: Int } - data class Clone(val id: String) : BotAction - data class Reference(val action: BotAction, val references: Map) : BotAction + data class Clone(val id: String) : BotAction { + override fun start(bot: Bot) = BehaviourState.Failed(Reason.Cancelled) + override fun update(bot: Bot) = BehaviourState.Failed(Reason.Cancelled) + } + data class Reference(val action: BotAction, val references: Map) : BotAction { + override fun start(bot: Bot) = BehaviourState.Failed(Reason.Cancelled) + override fun update(bot: Bot) = BehaviourState.Failed(Reason.Cancelled) + } data class Wait(val ticks: Int) : BotAction { override fun start(bot: Bot): BehaviourState { @@ -96,7 +105,10 @@ sealed interface BotAction { override val retryMax: Int = 0, val radius: Int = 10, ) : RetryableAction() { - override fun start(bot: Bot): BehaviourState { + override fun update(bot: Bot): BehaviourState { + if (bot.mode is PlayerOnObjectInteract) { + return BehaviourState.Running + } val objects = mutableListOf() for (tile in Spiral.spiral(bot.player.tile, radius)) { for (obj in GameObjects.at(tile)) { @@ -138,57 +150,63 @@ sealed interface BotAction { data class WaitFullInventory(val timeout: Int) : BotAction /** - * TODO how to handle repeat actions e.g. repeat Chop-down trees until inv is full - These are actions Gathering, Skilling etc.. - * more resolvers like bank all, drop cheap items - * how to handle combat, one task or multiple? - One Fight action - * frames should have tick(): State methods - * Combat should be an action which has a state machine for eating, retargeting, looting etc.. - * GatheringActivity - * TravelActivity - * how to handle navigation in a non-hacky way - * navigation behaviours - * make nav-graph points only? - * combine nav-graph requirements with facts - * Goal generators - * Rather than check all req for all activties do it reactively - * Received an item recently? Add relevant activties to that item to the list of posibilities - * Been too long since you picked up an item, now remove that goal from the list - * No posibilities? Now expand search wider - * - * Open questions: - * - Complex activities like minigames, quests - * Minigames: - * They are closed mechanical systems so they are actions. - * JoinMinigameLobby - Success, Timeout, Kicked etc.. - * PlayMinigame - Roll selection, objectives, movement, combat, scoring. Fails on game end or leaving/disconnect - * Trading with players: - * Outcomes are non-deterministic, waiting on player timing - * SellingAction - * TradeAction - * inits trade - * trade rules - * max wait - * accepted items - * price bounds - * reacts to - * offer chances - * cancellation -* terminates with success/failure - * Quests: - * Some complex quest mechanics might need custom actions - * Activities: - * TalkToCook - * GetBucket - * GetMilk - * GetEgg - * ReturnToCook - * - navigation + actions - * - targetting - * - activity generators - reactive loading - * Separate mandatory and resolvable requirements - * mandatory requirements become gates for if activities are in the current pool - * Listeners wait for state changes on specific mandatory requirements (level up, skill changes) and revaluate adding to activity pool - * These listeners also check current activity requirements and fail it if no longer gated - * +TODO how to handle repeat actions e.g. repeat Chop-down trees until inv is full - These are actions Gathering, Skilling etc.. + more resolvers like bank all, drop cheap items + how to handle combat, one task or multiple? - One Fight action + frames should have tick(): State methods + Combat should be an action which has a state machine for eating, retargeting, looting etc.. + GatheringActivity + TravelActivity + how to handle navigation in a non-hacky way + navigation behaviours + make nav-graph points only? + combine nav-graph requirements with facts + Goal generators + Rather than check all req for all activities do it reactively + Received an item recently? Add relevant activities to that item to the list of posibilities + Been too long since you picked up an item, now remove that goal from the list + No possibilities? Now expand search wider + + Open questions: + - Complex activities like minigames, quests + Minigames: + They are closed mechanical systems so they are actions. + JoinMinigameLobby - Success, Timeout, Kicked etc.. + PlayMinigame - Roll selection, objectives, movement, combat, scoring. Fails on game end or leaving/disconnect + Trading with players: + Outcomes are non-deterministic, waiting on player timing + SellingAction + TradeAction + inits trade + trade rules + max wait + accepted items + price bounds + reacts to + offer chances + cancellation + terminates with success/failure + Quests: + Some complex quest mechanics might need custom actions + Activities: + TalkToCook + GetBucket + GetMilk + GetEgg + ReturnToCook + - navigation + actions + Virtual nodes + Create a temp node + link the current tile as a weight = 0 + link any applicable teleports + run traversal from temp source node + - targeting + policy + - activity generators - reactive loading + Separate mandatory and resolvable requirements + mandatory requirements become gates for if activities are in the current pool + Listeners wait for state changes on specific mandatory requirements (level up, skill changes) and revaluate adding to activity pool + These listeners also check current activity requirements and fail it if no longer gated + */ } \ No newline at end of file diff --git a/game/src/main/kotlin/content/bot/action/BotActivity.kt b/game/src/main/kotlin/content/bot/action/BotActivity.kt index 2eb5280b6d..d2f1c01f78 100644 --- a/game/src/main/kotlin/content/bot/action/BotActivity.kt +++ b/game/src/main/kotlin/content/bot/action/BotActivity.kt @@ -1,21 +1,9 @@ package content.bot.action -import content.bot.fact.FactClone -import content.bot.fact.FactReference -import content.bot.fact.HasInventorySpace -import content.bot.fact.CarriesItem +import content.bot.fact.Condition import content.bot.fact.Fact -import content.bot.fact.EquipsItem -import content.bot.fact.AtLocation -import content.bot.fact.HasSkillLevel -import content.bot.fact.AtTile -import content.bot.fact.CarriesOne -import content.bot.fact.EquipsOne -import content.bot.fact.HasVariable -import net.pearx.kasechange.toPascalCase import world.gregs.config.Config import world.gregs.config.ConfigReader -import world.gregs.voidps.engine.entity.character.player.skill.Skill import world.gregs.voidps.engine.event.Wildcard import world.gregs.voidps.engine.event.Wildcards import world.gregs.voidps.engine.timedLoad @@ -27,13 +15,13 @@ import world.gregs.voidps.engine.timedLoad data class BotActivity( override val id: String, val capacity: Int, - override val requires: List = emptyList(), - override val resolve: List = emptyList(), + override val requires: List = emptyList(), + override val resolve: List = emptyList(), override val plan: List = emptyList(), - override val produces: Set = emptySet(), + override val produces: Set = emptySet(), ) : Behaviour -fun loadActivities(paths: List, activities: MutableMap, groups: MutableMap>, resolvers: MutableMap>) { +fun loadActivities(paths: List, activities: MutableMap, groups: MutableMap>, resolvers: MutableMap>) { val fragments = mutableMapOf() timedLoad("bot activity") { val reqClones = mutableMapOf() @@ -47,16 +35,16 @@ fun loadActivities(paths: List, activities: MutableMap = emptyList() - var requirements: List = emptyList() - var resolvables: List = emptyList() - var produces: List = emptyList() + val requirements: MutableList = mutableListOf() + val resolvables: MutableList = mutableListOf() + val produces: MutableList = mutableListOf() var fields: Map = emptyMap() while (nextPair()) { when (val key = key()) { - "requires" -> requirements = requirements() - "resolve" -> resolvables = requirements() + "requires" -> requirements(requirements) // TODO convert to pass mutable list + clone list + "resolve" -> requirements(resolvables) "plan" -> actions = actions() - "produces" -> produces = requirements() + "produces" -> requirements(produces) "capacity" -> capacity = int() "type" -> type = string() "template" -> template = string() @@ -65,23 +53,25 @@ fun loadActivities(paths: List, activities: MutableMap throw IllegalArgumentException("Unexpected key: '$key' ${exception()}") } } - val clone = requirements.filterIsInstance().firstOrNull() + val clone = requirements.filterIsInstance().firstOrNull() if (clone != null) { reqClones[id] = clone.id } - val resolveClone = resolvables.filterIsInstance().firstOrNull() + val resolveClone = resolvables.filterIsInstance().firstOrNull() if (resolveClone != null) { resClones[id] = resolveClone.id } if (template != null) { - fragments[id] = BehaviourFragment(id, type, capacity, weight, template, requirements, plan = actions, fields = fields) + fragments[id] = BehaviourFragment(id, type, capacity, weight, template, requirements, resolvables, plan = actions, fields = fields) } else if (type == "resolver") { for (fact in produces) { - resolvers.getOrPut(fact) { mutableListOf() }.add(Resolver(id, weight, requirements, plan = actions)) + for (key in fact.keys()) { + resolvers.getOrPut(key) { mutableListOf() }.add(Resolver(id, weight, requirements, resolvables, plan = actions)) + } } } else { - activities[id] = BotActivity(id, capacity, requirements, plan = actions) + activities[id] = BotActivity(id, capacity, requirements, resolvables, plan = actions) } } } @@ -100,18 +90,18 @@ fun loadActivities(paths: List, activities: MutableMap - requirements.removeIf { it is FactClone && it.id == cloneId } + val requirements = activity.requires as MutableList + requirements.removeIf { it is Condition.Clone && it.id == cloneId } requirements.addAll(clone.requires) - requirements.sortBy { it.priority } + requirements.sortBy { it.priority() } } for ((id, cloneId) in resClones) { val activity = activities[id] ?: continue val clone = activities[cloneId] ?: continue - val resolvables = activity.resolve as MutableList - resolvables.removeIf { it is FactClone && it.id == cloneId } + val resolvables = activity.resolve as MutableList + resolvables.removeIf { it is Condition.Clone && it.id == cloneId } resolvables.addAll(clone.resolve) - resolvables.sortBy { it.priority } + resolvables.sortBy { it.priority() } } } // Fragments are partially filled behaviours with template + fields @@ -121,22 +111,24 @@ fun loadActivities(paths: List, activities: MutableMap() + val requirements = mutableListOf() requirements.addAll(fragment.requires) - fragment.resolveRequirements(template, requirements) - requirements.sortBy { it.priority } + fragment.resolveRequirements(requirements, template.requires) + requirements.sortBy { it.priority() } - val resolvables = mutableListOf() - resolvables.addAll(fragment.requires) - fragment.resolveRequirements(template, resolvables) - resolvables.sortBy { it.priority } + val resolvables = mutableListOf() + resolvables.addAll(fragment.resolve) + fragment.resolveRequirements(resolvables, template.resolve) + resolvables.sortBy { it.priority() } val actions = mutableListOf() actions.addAll(fragment.plan) fragment.resolveActions(template, actions) if (fragment.type == "resolver") { for (fact in fragment.produces) { - resolvers.getOrPut(fact) { mutableListOf() }.add(Resolver(id, fragment.weight, requirements, resolvables, actions)) + for (key in fact.keys()) { + resolvers.getOrPut(key) { mutableListOf() }.add(Resolver(id, fragment.weight, requirements, resolvables, actions)) + } } } else { activities[id] = BotActivity(id, fragment.capacity, requirements, resolvables, actions) @@ -153,21 +145,12 @@ fun loadActivities(paths: List, activities: MutableMap { val map = mutableMapOf() while (nextEntry()) { @@ -178,17 +161,13 @@ private fun ConfigReader.fields(): Map { return map } -private fun ConfigReader.requirements(): List { - val list = mutableListOf() +private fun ConfigReader.requirements(list: MutableList) { while (nextElement()) { var type = "" var id = "" var value: Any? = null - var min = 1 - var max = 1 - var x = 0 - var y = 0 - var level = 0 + var min: Int? = null + var max: Int? = null val references = mutableMapOf() while (nextEntry()) { when (val key = key()) { @@ -225,30 +204,6 @@ private fun ConfigReader.requirements(): List { else -> throw IllegalArgumentException("Invalid '$key' value: $value ${exception()}") } } - "x" -> { - type = "tile" - when (val value = value()) { - is Int -> x = value - is String if value.contains('$') -> references[key] = value - else -> throw IllegalArgumentException("Invalid '$key' value: $value ${exception()}") - } - } - "y" -> { - type = "tile" - when (val value = value()) { - is Int -> y = value - is String if value.contains('$') -> references[key] = value - else -> throw IllegalArgumentException("Invalid '$key' value: $value ${exception()}") - } - } - "level" -> { - type = "tile" - when (val value = value()) { - is Int -> level = value - is String if value.contains('$') -> references[key] = value - else -> throw IllegalArgumentException("Invalid '$key' value: $value ${exception()}") - } - } "radius" -> { type = "tile" when (val value = value()) { @@ -260,33 +215,48 @@ private fun ConfigReader.requirements(): List { "value" -> value = value() } } - var requirement = when (type) { - "skill" -> HasSkillLevel(Skill.of(id.toPascalCase())!!, min, max) - "carries" -> if (id.any { it == '*' || it == '#' }) { - CarriesOne(Wildcards.get(id, Wildcard.Item), min) - } else { - CarriesItem(id, min) + var requirement = getRequirement(type, id, min, max, value) + if (requirement == null) { + if (type == "holds") { + throw IllegalArgumentException("Unknown requirement type 'holds'; did you mean 'carries' or 'equips'? ${exception()}.") } - "equips" -> if (id.any { it == '*' || it == '#' }) { - EquipsOne(Wildcards.get(id, Wildcard.Item), min) - } else { - EquipsItem(id, min) - } - "variable" -> HasVariable(id, value) - "clone" -> FactClone(id) - "inventory_space" -> HasInventorySpace(min) - "location" -> AtLocation(id) - "tile" -> AtTile(x, y, level, min) - "holds" -> throw IllegalArgumentException("Unknown requirement type 'holds'; did you mean 'carries' or 'equips'? ${exception()}.") - else -> throw IllegalArgumentException("Unknown requirement type: $type ${exception()}") + throw IllegalArgumentException("Unknown requirement type: $type ${exception()}") } if (references.isNotEmpty()) { - requirement = FactReference(requirement, references) + requirement = Condition.Reference(type, id, value, min, max, references) } list.add(requirement) } - list.sortBy { it.priority } - return list + list.sortBy { it.priority() } +} + +private fun getRequirement(type: String, id: String, min: Int?, max: Int?, value: Any?): Condition? = when (type) { + "skill" -> Condition.range(Fact.SkillLevel.of(id), min, max) + "carries" -> if (id.contains(",")) { + Condition.Any(id.split(",").map { Condition.range(Fact.InventoryCount(it), min, max) }) + } else if (id.any { it == '*' || it == '#' }) { + Condition.Any(Wildcards.get(id, Wildcard.Item).map { Condition.range(Fact.InventoryCount(it), min, max) }) + } else { + Condition.range(Fact.InventoryCount(id), min, max) + } + "equips" -> if (id.contains(",")) { + Condition.Any(id.split(",").map { Condition.range(Fact.EquipCount(it), min, max) }) + } else if (id.any { it == '*' || it == '#' }) { + Condition.Any(Wildcards.get(id, Wildcard.Item).map { Condition.range(Fact.EquipCount(it), min, max) }) + } else { + Condition.range(Fact.EquipCount(id), min, max) + } + "variable" -> when(value) { + is Int -> Condition.Equals(Fact.IntVariable(id), value) + is String -> Condition.Equals(Fact.StringVariable(id), value) + is Double -> Condition.Equals(Fact.DoubleVariable(id), value) + is Boolean -> Condition.Equals(Fact.BoolVariable(id), value) + else -> null + } + "clone" -> Condition.Clone(id) + "inventory_space" -> Condition.range(Fact.InventorySpace, min, max) + "location" -> Condition.Area(Fact.PlayerTile, id) + else -> null } private fun ConfigReader.actions(): List { diff --git a/game/src/main/kotlin/content/bot/action/Reason.kt b/game/src/main/kotlin/content/bot/action/Reason.kt index c56b827b26..00d2b38b97 100644 --- a/game/src/main/kotlin/content/bot/action/Reason.kt +++ b/game/src/main/kotlin/content/bot/action/Reason.kt @@ -1,12 +1,12 @@ package content.bot.action -import content.bot.fact.Fact +import content.bot.fact.Condition interface Reason { object Cancelled : HardReason object NoRoute : HardReason object NoTarget : SoftReason - data class Requirement(val fact: Fact) : HardReason + data class Requirement(val fact: Condition) : HardReason } interface SoftReason : Reason interface HardReason : Reason diff --git a/game/src/main/kotlin/content/bot/action/Resolver.kt b/game/src/main/kotlin/content/bot/action/Resolver.kt index 46d4eeb92d..ffc3481e89 100644 --- a/game/src/main/kotlin/content/bot/action/Resolver.kt +++ b/game/src/main/kotlin/content/bot/action/Resolver.kt @@ -1,6 +1,6 @@ package content.bot.action -import content.bot.fact.Fact +import content.bot.fact.Condition /** * An activity that can be performed to resolve a requirement @@ -10,8 +10,8 @@ import content.bot.fact.Fact data class Resolver( override val id: String, val weight: Int, - override val requires: List = emptyList(), - override val resolve: List = emptyList(), + override val requires: List = emptyList(), + override val resolve: List = emptyList(), override val plan: List = emptyList(), - override val produces: Set = emptySet(), + override val produces: Set = emptySet(), ) : Behaviour \ No newline at end of file diff --git a/game/src/main/kotlin/content/bot/fact/Condition.kt b/game/src/main/kotlin/content/bot/fact/Condition.kt new file mode 100644 index 0000000000..60402fafee --- /dev/null +++ b/game/src/main/kotlin/content/bot/fact/Condition.kt @@ -0,0 +1,100 @@ +package content.bot.fact + +import content.bot.Bot +import world.gregs.voidps.engine.data.definition.Areas +import world.gregs.voidps.type.Tile + +sealed interface Condition { + fun check(bot: Bot): Boolean + fun keys(): Set + fun priority(): Int + + data class Equals(val fact: Fact, val value: T) : Condition { + override fun check(bot: Bot) = fact.getValue(bot) == value + override fun priority() = fact.priority + override fun keys() = fact.keys() + } + + data class AtLeast(val fact: Fact, val min: Int) : Condition { + override fun check(bot: Bot) = fact.getValue(bot) >= min + override fun priority() = fact.priority + override fun keys() = fact.keys() + } + + data class AtMost(val fact: Fact, val max: Int) : Condition { + override fun check(bot: Bot) = fact.getValue(bot) <= max + override fun priority() = fact.priority + override fun keys() = fact.keys() + } + + data class Range(val fact: Fact, val min: Int, val max: Int) : Condition { + override fun check(bot: Bot) = fact.getValue(bot) in min..max + override fun priority() = fact.priority + override fun keys() = fact.keys() + } + + data class Within(val fact: Fact, val tile: Tile, val radius: Int) : Condition { + override fun check(bot: Bot) = fact.getValue(bot).within(tile, radius) + override fun priority() = fact.priority + override fun keys() = fact.keys() + } + + data class Area(val fact: Fact, val area: String) : Condition { + override fun check(bot: Bot) = fact.getValue(bot) in Areas[area] + override fun priority() = fact.priority + override fun keys() = fact.keys() + } + + data class OneOf(val fact: Fact, val values: Set) : Condition { + override fun check(bot: Bot) = fact.getValue(bot) in values + override fun priority() = fact.priority + override fun keys() = fact.keys() + } + + data class Not(val inner: Condition) : Condition { + override fun check(bot: Bot) = !inner.check(bot) + override fun priority() = inner.priority() + override fun keys() = inner.keys() + } + + data class All(val conditions: List) : Condition { + override fun check(bot: Bot) = conditions.all { it.check(bot) } + override fun priority() = conditions.first().priority() + override fun keys() = conditions.flatMap { it.keys() }.toSet() + } + + data class Any(val conditions: List) : Condition { + override fun check(bot: Bot) = conditions.any { it.check(bot) } + override fun priority() = conditions.first().priority() + override fun keys() = conditions.flatMap { it.keys() }.toSet() + } + + data class Reference( + val type: String = "", + val id: String = "", + val value: kotlin.Any? = null, + val min: Int? = null, + val max: Int? = null, + val references: Map = emptyMap(), + ) : Condition { + override fun check(bot: Bot) = false + override fun priority() = -1 + override fun keys() = emptySet() + } + + class Clone(val id: String) : Condition { + override fun check(bot: Bot) = false + override fun priority() = -1 + override fun keys() = emptySet() + } + + companion object { + fun range(fact: Fact, min: Int?, max: Int?) = when { + min != null && max != null -> Range(fact, min, max) + min != null -> AtLeast(fact, min) + max != null -> AtMost(fact, max) + else -> Equals(fact, 1) + } + } + +} \ No newline at end of file diff --git a/game/src/main/kotlin/content/bot/fact/Fact.kt b/game/src/main/kotlin/content/bot/fact/Fact.kt index db0a043dc0..7f64f4c2ae 100644 --- a/game/src/main/kotlin/content/bot/fact/Fact.kt +++ b/game/src/main/kotlin/content/bot/fact/Fact.kt @@ -1,100 +1,121 @@ package content.bot.fact import content.bot.Bot -import world.gregs.voidps.engine.data.definition.Areas import world.gregs.voidps.engine.entity.character.player.skill.Skill -import world.gregs.voidps.engine.inv.carriesItem -import world.gregs.voidps.engine.inv.equips +import world.gregs.voidps.engine.inv.equipment import world.gregs.voidps.engine.inv.inventory +import world.gregs.voidps.type.Tile /** - * A bots state which can be required for, or a product of performing a [content.bot.action.Behaviour] + * A bots state which can be a [Condition] for, or a product of performing a [content.bot.action.Behaviour] * @param priority Ensure bots aren't walking to locations before getting items etc... lower values are prioritised first. */ -sealed class Fact(val priority: Int) { - open fun check(bot: Bot): Boolean = false +sealed class Fact(val priority: Int) { + abstract fun getValue(bot: Bot): T open fun keys(): Set = emptySet() -} -internal data class FactClone( - val id: String, -) : Fact(-1) + data class InventoryCount(val id: String) : Fact(100) { + override fun keys() = setOf("inv:inventory") + override fun getValue(bot: Bot) = bot.player.inventory.count(id) + } -internal data class FactReference( - var fact: Fact, - val references: Map, -) : Fact(-1) + object InventorySpace : Fact(10) { + override fun keys() = setOf("inv:inventory") + override fun getValue(bot: Bot) = bot.player.inventory.spaces + } -data class HasSkillLevel( - val skill: Skill, - val min: Int = 1, - val max: Int = 120 -) : Fact(0) { - override fun check(bot: Bot) = bot.player.levels.get(skill) in min..max - override fun keys() = setOf(skill.name) -} + data class EquipCount(val id: String) : Fact(100) { + override fun keys() = setOf("inv:equipment") + override fun getValue(bot: Bot) = bot.player.equipment.count(id) + } + data class IntVariable(val id: String) : Fact(1) { + override fun keys() = setOf("var:${id}") + override fun getValue(bot: Bot) = bot.player.variables.get(id) + } -data class EquipsItem( - val id: String, - val amount: Int = 1, -) : Fact(100) { - override fun check(bot: Bot) = bot.player.equips(id, amount) - override fun keys() = setOf("inv:equipment") -} + data class BoolVariable(val id: String) : Fact(1) { + override fun keys() = setOf("var:${id}") + override fun getValue(bot: Bot) = bot.player.variables.get(id) + } -data class HasVariable( - val id: String, - val value: Any? = null, -) : Fact(1) { - override fun check(bot: Bot) = bot.player.variables.get(id) == value - override fun keys() = setOf("var:${id}") -} + data class StringVariable(val id: String) : Fact(1) { + override fun keys() = setOf("var:${id}") + override fun getValue(bot: Bot) = bot.player.variables.get(id) + } -data class CarriesItem( - val id: String, - val amount: Int = 1, -) : Fact(100) { - override fun check(bot: Bot) = bot.player.carriesItem(id, amount) - override fun keys() = setOf("inv:inventory") -} + data class DoubleVariable(val id: String) : Fact(1) { + override fun keys() = setOf("var:${id}") + override fun getValue(bot: Bot) = bot.player.variables.get(id) + } -data class EquipsOne( - val ids: Set, - val amount: Int = 1, -) : Fact(100) { - override fun check(bot: Bot) = ids.any { id -> bot.player.equips(id, amount) } - override fun keys() = setOf("inv:equipment") -} + object PlayerTile : Fact(1000) { + override fun keys() = setOf("tile") + override fun getValue(bot: Bot) = bot.player.tile + } -data class CarriesOne( - val ids: Set, - val amount: Int = 1, -) : Fact(100) { - override fun check(bot: Bot) = ids.any { id -> bot.player.carriesItem(id, amount) } - override fun keys() = setOf("inv:inventory") -} + object AttackLevel : SkillLevel(Skill.Attack) + object DefenceLevel : SkillLevel(Skill.Defence) + object StrengthLevel : SkillLevel(Skill.Strength) + object ConstitutionLevel : SkillLevel(Skill.Constitution) + object RangedLevel : SkillLevel(Skill.Ranged) + object PrayerLevel : SkillLevel(Skill.Prayer) + object MagicLevel : SkillLevel(Skill.Magic) + object CookingLevel : SkillLevel(Skill.Cooking) + object WoodcuttingLevel : SkillLevel(Skill.Woodcutting) + object FletchingLevel : SkillLevel(Skill.Fletching) + object FishingLevel : SkillLevel(Skill.Fishing) + object FiremakingLevel : SkillLevel(Skill.Firemaking) + object CraftingLevel : SkillLevel(Skill.Crafting) + object SmithingLevel : SkillLevel(Skill.Smithing) + object MiningLevel : SkillLevel(Skill.Mining) + object HerbloreLevel : SkillLevel(Skill.Herblore) + object AgilityLevel : SkillLevel(Skill.Agility) + object ThievingLevel : SkillLevel(Skill.Thieving) + object SlayerLevel : SkillLevel(Skill.Slayer) + object FarmingLevel : SkillLevel(Skill.Farming) + object RunecraftingLevel : SkillLevel(Skill.Runecrafting) + object HunterLevel : SkillLevel(Skill.Hunter) + object ConstructionLevel : SkillLevel(Skill.Construction) + object SummoningLevel : SkillLevel(Skill.Summoning) + object DungeoneeringLevel : SkillLevel(Skill.Dungeoneering) -data class HasInventorySpace( - val amount: Int, -) : Fact(10) { - override fun check(bot: Bot) = bot.player.inventory.spaces >= amount - override fun keys() = setOf("inv:inventory") -} + abstract class SkillLevel( + val skill: Skill, + ) : Fact(0) { + override fun keys() = setOf("skill:${skill.name.lowercase()}") + override fun getValue(bot: Bot) = bot.player.levels.get(skill) -data class AtLocation( - val id: String, -) : Fact(1000) { - override fun check(bot: Bot) = bot.player.tile in Areas[id] - override fun keys() = setOf("enter:$id") -} + companion object { + fun of(skill: String): SkillLevel = when (skill.lowercase()) { + "attack" -> AttackLevel + "defence" -> DefenceLevel + "strength" -> StrengthLevel + "constitution" -> ConstitutionLevel + "ranged" -> RangedLevel + "prayer" -> PrayerLevel + "magic" -> MagicLevel + "cooking" -> CookingLevel + "woodcutting" -> WoodcuttingLevel + "fletching" -> FletchingLevel + "fishing" -> FishingLevel + "firemaking" -> FiremakingLevel + "crafting" -> CraftingLevel + "smithing" -> SmithingLevel + "mining" -> MiningLevel + "herblore" -> HerbloreLevel + "agility" -> AgilityLevel + "thieving" -> ThievingLevel + "slayer" -> SlayerLevel + "farming" -> FarmingLevel + "runecrafting" -> RunecraftingLevel + "hunter" -> HunterLevel + "construction" -> ConstructionLevel + "summoning" -> SummoningLevel + "dungeoneering" -> DungeoneeringLevel + else -> throw IllegalArgumentException("Unknown skill: $skill") + } + } + } -data class AtTile( - val x: Int, - val y: Int, - val level: Int, - val radius: Int, -) : Fact(1100) { - override fun check(bot: Bot) = bot.player.tile.within(x, y, radius, level) - override fun keys() = setOf("tile") } diff --git a/game/src/test/kotlin/content/bot/BotManagerTest.kt b/game/src/test/kotlin/content/bot/BotManagerTest.kt index a54ce4f0f5..6b0a2a0299 100644 --- a/game/src/test/kotlin/content/bot/BotManagerTest.kt +++ b/game/src/test/kotlin/content/bot/BotManagerTest.kt @@ -1,18 +1,16 @@ package content.bot import content.bot.action.* -import content.bot.fact.AtTile +import content.bot.fact.Condition import content.bot.fact.Fact -import content.bot.fact.FactClone -import content.bot.fact.HasSkillLevel import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.Test import world.gregs.voidps.engine.entity.character.player.Player -import world.gregs.voidps.engine.entity.character.player.skill.Skill +import world.gregs.voidps.type.Tile class BotManagerTest { - fun testBot(name: String = "bot") = Bot(Player(accountName = name)) + fun testBot(vararg activities: BotActivity, name: String = "bot") = Bot(Player(accountName = name)).also { it.available.addAll(activities.map { a -> a.id }) } @Test fun `Taskless bot gets assigned an activity`() { @@ -21,7 +19,7 @@ class BotManagerTest { plan = listOf(BotAction.Wait(1)) ) val manager = BotManager(mutableMapOf(activity.id to activity)) - val bot = testBot() + val bot = testBot(activity) manager.tick(bot) @@ -33,18 +31,18 @@ class BotManagerTest { fun `Activity capacity is respected`() { val activity = testActivity( id = "mine", - plan = listOf(BotAction.Clone("")) + plan = listOf(BotAction.Wait(1)) ) val manager = BotManager(mutableMapOf(activity.id to activity)) - val bot1 = testBot("bot1") - val bot2 = testBot("bot2") + val bot1 = testBot(activity, name = "bot1") + val bot2 = testBot(activity, name = "bot2") manager.tick(bot1) manager.tick(bot2) assertEquals(1, bot1.frames.size) - assertTrue(bot2.frames.isEmpty()) + assertEquals("idle", bot2.frames.first().behaviour.id) } @Test @@ -54,7 +52,7 @@ class BotManagerTest { plan = listOf(BotAction.Wait(1)) ) val manager = BotManager(mutableMapOf(activity.id to activity)) - val bot = testBot() + val bot = testBot(activity) manager.tick(bot) manager.tick(bot) @@ -72,7 +70,7 @@ class BotManagerTest { ) ) val frame = BehaviourFrame(activity) - frame.start(testBot()) + frame.start(testBot(activity)) frame.success() @@ -99,14 +97,14 @@ class BotManagerTest { ) val manager = BotManager(mutableMapOf(activity.id to activity)) - val bot = testBot() + val bot = testBot(activity) manager.tick(bot) manager.tick(bot) val frame = bot.frame() repeat(3) { - frame.fail(Reason.Requirement(FactClone(""))) + frame.fail(Reason.Requirement(Condition.Clone(""))) manager.tick(bot) assertTrue(frame.state is BehaviourState.Wait) manager.tick(bot) // Tick 1 @@ -115,7 +113,7 @@ class BotManagerTest { } // after retries exhausted → popped - assertTrue(bot.frames.isEmpty()) + assertEquals("idle", bot.frames.first().behaviour.id) assertTrue("talk" in bot.blocked) } @@ -127,7 +125,7 @@ class BotManagerTest { ) val manager = BotManager(mutableMapOf(activity.id to activity)) - val bot = testBot() + val bot = testBot(activity) manager.tick(bot) manager.tick(bot) @@ -151,7 +149,7 @@ class BotManagerTest { val activities = mutableMapOf(activity.id to activity, test.id to test) val manager = BotManager(activities) - val bot = testBot() + val bot = testBot(activity, test) bot.previous = activity @@ -171,7 +169,7 @@ class BotManagerTest { ) val manager = BotManager(mutableMapOf(activity.id to activity)) - val bot = testBot() + val bot = testBot(activity) manager.tick(bot) manager.tick(bot) @@ -190,15 +188,15 @@ class BotManagerTest { ) val manager = BotManager(mutableMapOf(activity.id to activity)) - val bot = testBot() + val bot = testBot(activity) manager.tick(bot) manager.tick(bot) - bot.frame().fail(Reason.Requirement(FactClone(""))) + bot.frame().fail(Reason.Requirement(Condition.Clone(""))) manager.tick(bot) manager.tick(bot) - assertTrue(bot.frames.isEmpty()) + assertEquals("idle", bot.frames.first().behaviour.id) assertTrue("fish" in bot.blocked) } @@ -207,13 +205,13 @@ class BotManagerTest { val activity = testActivity( id = "test", requires = listOf( - HasSkillLevel(Skill.Attack, 99, 99) + Condition.Range(Fact.AttackLevel, 99, 99) ), plan = listOf(BotAction.Wait(4)) ) val manager = BotManager(mutableMapOf(activity.id to activity)) - val bot = testBot() + val bot = testBot(activity) bot.frames.add(BehaviourFrame(activity)) manager.tick(bot) @@ -226,24 +224,24 @@ class BotManagerTest { @Test fun `Resolvable requirement queues resolver before activity starts`() { - val fact = AtTile(100, 100, 2, 1) + val condition = Condition.Within(Fact.PlayerTile, Tile(100, 100, 2), 2) val resolver = Resolver( id = "go_to_area", weight = 1, plan = listOf(BotAction.Wait(1)), - produces = setOf(fact) + produces = setOf(condition) ) val activity = testActivity( id = "woodcut", - resolves = listOf(fact), + resolves = listOf(condition), plan = listOf(BotAction.Wait(1)) ) val manager = BotManager( mutableMapOf(activity.id to activity), - mutableMapOf(fact to mutableListOf(resolver)) + mutableMapOf(condition.keys().first() to mutableListOf(resolver)) ) - val bot = testBot() + val bot = testBot(activity) manager.tick(bot) manager.tick(bot) @@ -253,23 +251,23 @@ class BotManagerTest { @Test fun `Lowest weight resolver is selected`() { - val fact = AtTile(100, 200, 300, 4) + val condition = Condition.Within(Fact.PlayerTile, Tile(100, 100, 2), 2) - val bad = Resolver("bad", weight = 10) - val good = Resolver("good", weight = 1) + val bad = Resolver("bad", weight = 10, plan = listOf(BotAction.Clone(""))) + val good = Resolver("good", weight = 1, plan = listOf(BotAction.Clone(""))) val activity = testActivity( id = "mine", - resolves = listOf(fact), + resolves = listOf(condition), plan = listOf(BotAction.Clone("")) ) val manager = BotManager( mutableMapOf(activity.id to activity), - mutableMapOf(fact to mutableListOf(bad, good)) + mutableMapOf(condition.keys().first() to mutableListOf(bad, good)) ) - val bot = testBot() + val bot = testBot(activity) manager.tick(bot) manager.tick(bot) @@ -278,19 +276,19 @@ class BotManagerTest { @Test fun `Blocked resolver is not reselected`() { - val fact = AtTile(100, 200, 3, 4) - val resolver = Resolver(id = "get_key", weight = 1) + val condition = Condition.Within(Fact.PlayerTile, Tile(100, 100, 2), 2) + val resolver = Resolver(id = "get_key", weight = 1, plan = listOf(BotAction.Clone(""))) val activity = testActivity( id = "open_door", - resolves = listOf(fact), + resolves = listOf(condition), plan = listOf(BotAction.Wait(1)) ) val manager = BotManager( mutableMapOf(activity.id to activity), - mutableMapOf(fact to mutableListOf(resolver)) + mutableMapOf(condition.keys().first() to mutableListOf(resolver)) ) - val bot = testBot() + val bot = testBot(activity) manager.tick(bot) assertEquals(1, bot.frames.size) val frame = bot.frames.last() @@ -304,7 +302,7 @@ class BotManagerTest { @Test fun `Hard failure in resolver stops bot`() { - val fact = AtTile(100, 200, 3, 4) + val condition = Condition.Within(Fact.PlayerTile, Tile(100, 100, 2), 2) val resolver = Resolver( id = "walk", weight = 1, @@ -312,15 +310,15 @@ class BotManagerTest { ) val activity = testActivity( id = "enter_zone", - resolves = listOf(fact), + resolves = listOf(condition), plan = listOf(BotAction.Wait(1)) ) val manager = BotManager( mutableMapOf(activity.id to activity), - mutableMapOf(fact to mutableListOf(resolver)) + mutableMapOf(condition.keys().first() to mutableListOf(resolver)) ) - val bot = testBot() + val bot = testBot(activity) manager.tick(bot) manager.tick(bot) assertEquals(2, bot.frames.size) @@ -332,7 +330,7 @@ class BotManagerTest { @Test fun `Soft failure in resolver only pops resolver`() { - val fact = AtTile(100, 200, 3, 4) + val condition = Condition.Within(Fact.PlayerTile, Tile(100, 100, 2), 2) val resolver = Resolver( id = "test", weight = 1, @@ -340,15 +338,15 @@ class BotManagerTest { ) val activity = testActivity( id = "smelt", - resolves = listOf(fact), + resolves = listOf(condition), plan = listOf(BotAction.Wait(1)) ) val manager = BotManager( mutableMapOf(activity.id to activity), - mutableMapOf(fact to mutableListOf(resolver)) + mutableMapOf(condition.keys().first() to mutableListOf(resolver)) ) - val bot = testBot() + val bot = testBot(activity) bot.player["debug"] = true manager.tick(bot) manager.tick(bot) @@ -361,23 +359,24 @@ class BotManagerTest { @Test fun `Resolver with unmet mandatory requirements is skipped`() { - val fact = AtTile(100, 200, 3, 4) + val condition = Condition.Within(Fact.PlayerTile, Tile(100, 100, 2), 2) val resolver = Resolver( id = "mine_gem", weight = 1, - requires = listOf(HasSkillLevel(Skill.Mining, 99, 99)) + plan = listOf(BotAction.Clone("")), + requires = listOf(Condition.Range(Fact.MiningLevel, 99, 99)) ) val activity = testActivity( id = "craft", - resolves = listOf(fact), + resolves = listOf(condition), plan = listOf(BotAction.Wait(1)) ) val manager = BotManager( mutableMapOf(activity.id to activity), - mutableMapOf(fact to mutableListOf(resolver)) + mutableMapOf(condition.keys().first() to mutableListOf(resolver)) ) - val bot = testBot() + val bot = testBot(activity) manager.tick(bot) manager.tick(bot) manager.tick(bot) @@ -388,7 +387,7 @@ class BotManagerTest { @Test fun `Activity are occupied while resolver is running`() { - val fact = AtTile(100, 200, 3, 4) + val condition = Condition.Within(Fact.PlayerTile, Tile(100, 100, 2), 2) val resolver = Resolver( id = "get_tool", weight = 1, @@ -396,15 +395,15 @@ class BotManagerTest { ) val activity = testActivity( id = "work", - resolves = listOf(fact), + resolves = listOf(condition), plan = listOf(BotAction.Wait(1)) ) val manager = BotManager( mutableMapOf(activity.id to activity), - mutableMapOf(fact to mutableListOf(resolver)) + mutableMapOf(condition.keys().first() to mutableListOf(resolver)) ) - val bot = testBot() + val bot = testBot(activity) manager.tick(bot) manager.tick(bot) @@ -413,8 +412,8 @@ class BotManagerTest { fun testActivity( id: String, - requires: List = emptyList(), - resolves: List = emptyList(), - plan: List + requires: List = emptyList(), + resolves: List = emptyList(), + plan: List, ) = BotActivity(id, 1, requires, resolves, plan) } \ No newline at end of file diff --git a/game/src/test/kotlin/content/bot/action/BehaviourFragmentTest.kt b/game/src/test/kotlin/content/bot/action/BehaviourFragmentTest.kt index 20abfd23c9..75ac77bfcf 100644 --- a/game/src/test/kotlin/content/bot/action/BehaviourFragmentTest.kt +++ b/game/src/test/kotlin/content/bot/action/BehaviourFragmentTest.kt @@ -1,21 +1,11 @@ package content.bot.action -import content.bot.fact.FactClone -import content.bot.fact.Fact -import content.bot.fact.CarriesItem -import content.bot.fact.EquipsItem -import content.bot.fact.HasInventorySpace -import content.bot.fact.AtLocation -import content.bot.fact.FactReference -import content.bot.fact.HasSkillLevel -import content.bot.fact.AtTile -import content.bot.fact.HasVariable -import org.junit.jupiter.api.Assertions.* +import content.bot.fact.* +import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.DynamicTest.dynamicTest import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestFactory import org.junit.jupiter.api.assertThrows -import world.gregs.voidps.engine.entity.character.player.skill.Skill class BehaviourFragmentTest { @@ -210,17 +200,16 @@ class BehaviourFragmentTest { id = "a", capacity = 1, requires = listOf( - FactReference( - AtLocation("default"), - references = mapOf( + Condition.Reference( + "location", "default", references = mapOf( "location" to $$"some_${type}_area" ) ) ) ) - val actions = mutableListOf() - fragment.resolveRequirements(template, actions) - assertEquals(AtLocation("some_fun_area"), actions.single()) + val actions = mutableListOf() + fragment.resolveRequirements(actions, template.requires) + assertEquals(Condition.Area(Fact.PlayerTile, "some_fun_area"), actions.single()) } @Test @@ -230,17 +219,16 @@ class BehaviourFragmentTest { id = "a", capacity = 1, requires = listOf( - FactReference( - AtLocation("default"), + Condition.Reference("location", "default", references = mapOf( "location" to $$"some_$area_type" ) ) ) ) - val actions = mutableListOf() - fragment.resolveRequirements(template, actions) - assertEquals(AtLocation("some_fun"), actions.single()) + val actions = mutableListOf() + fragment.resolveRequirements(actions, template.requires) + assertEquals(Condition.Area(Fact.PlayerTile, "some_fun"), actions.single()) } /* @@ -249,15 +237,14 @@ class BehaviourFragmentTest { @TestFactory fun `Resolve requirement references`() = listOf( - Triple(HasSkillLevel(Skill.Defence, 1, 120), mapOf("skill" to "attack", "min" to 5, "max" to 99), HasSkillLevel(Skill.Attack, 5, 99)), - Triple(HasVariable("default", 1), mapOf("variable" to "test", "value" to true), HasVariable("test", true)), - Triple(EquipsItem("default", 1), mapOf("equips" to "item", "amount" to 10), EquipsItem("item", 10)), - Triple(CarriesItem("default", 1), mapOf("carries" to "item", "amount" to 10), CarriesItem("item", 10)), - Triple(HasInventorySpace(1), mapOf("inventory_space" to 10), HasInventorySpace(10)), - Triple(AtLocation("default"), mapOf("location" to "area"), AtLocation("area")), - Triple(AtTile(0, 0, 0, 0), mapOf("x" to 4, "y" to 3, "level" to 2, "radius" to 1), AtTile(4, 3, 2, 1)), - ).map { (default, values, expected) -> - dynamicTest("Resolve ${default::class.simpleName} references") { + Triple(Condition.Reference("skill", "defence", min = 1, max = 120), mapOf("skill" to "attack", "min" to 5, "max" to 99), Condition.Range(Fact.AttackLevel, 5, 99)), + Triple(Condition.Reference("variable", "default", value = 1), mapOf("variable" to "test", "value" to true), Condition.Equals(Fact.BoolVariable("test"), true)), + Triple(Condition.Reference("equips", "default", min = 1), mapOf("equips" to "item", "amount" to 10), Condition.AtLeast(Fact.EquipCount("item"), 10)), + Triple(Condition.Reference("carries", "default", min = 1), mapOf("carries" to "item", "amount" to 10), Condition.AtLeast(Fact.InventoryCount("item"), 10)), + Triple(Condition.Reference("inventory_space", min = 1), mapOf("inventory_space" to 10), Condition.AtLeast(Fact.InventorySpace, 10)), + Triple(Condition.Reference("location", "default"), mapOf("location" to "area"), Condition.Area(Fact.PlayerTile, "area")), + ).map { (reference, values, expected) -> + dynamicTest("Resolve ${reference::class.simpleName} references") { val fields = values.mapKeys { "ref_${it.key}" } val fragment = fragment(fields) val references = values.map { it.key to "\$ref_${it.key}" }.toMap() @@ -265,14 +252,11 @@ class BehaviourFragmentTest { id = "a", capacity = 1, requires = listOf( - FactReference( - default, - references = references - ) + reference.copy(references = references) ) ) - val actions = mutableListOf() - fragment.resolveRequirements(template, actions) + val actions = mutableListOf() + fragment.resolveRequirements(actions, template.requires) assertEquals(expected, actions.single()) } } @@ -284,14 +268,11 @@ class BehaviourFragmentTest { id = "a", capacity = 1, requires = listOf( - FactReference( - HasSkillLevel(Skill.Attack), - references = mapOf("skill" to "missing") - ) + Condition.Reference("skill", "attack", references = mapOf("skill" to "missing")) ) ) assertThrows { - fragment.resolveRequirements(template, mutableListOf()) + fragment.resolveRequirements(mutableListOf(), template.requires) } } @@ -302,17 +283,14 @@ class BehaviourFragmentTest { id = "a", capacity = 1, requires = listOf( - FactReference( - HasSkillLevel(Skill.Attack), - emptyMap() - ) + Condition.Reference("skill", "attack", references = emptyMap()) ) ) - val actions = mutableListOf() - fragment.resolveRequirements(template, actions) + val actions = mutableListOf() + fragment.resolveRequirements(actions, template.requires) - assertEquals(HasSkillLevel(Skill.Attack), actions.single()) + assertEquals(Condition.Equals(Fact.AttackLevel, 1), actions.single()) } @Test @@ -322,47 +300,29 @@ class BehaviourFragmentTest { id = "a", capacity = 1, requires = listOf( - HasSkillLevel(Skill.Attack) + Condition.Reference("skill", "attack") ) ) - val actions = mutableListOf() - fragment.resolveRequirements(template, actions) + val actions = mutableListOf() + fragment.resolveRequirements(actions, template.requires) - assertEquals(HasSkillLevel(Skill.Attack), actions.single()) + assertEquals(Condition.Equals(Fact.AttackLevel, 1), actions.single()) } @Test fun `Any clone requirement throws`() { - val fragment = fragment() - val template = BotActivity( - id = "a", - capacity = 1, - requires = listOf(FactClone("x")) - ) - assertThrows { - fragment.resolveRequirements(template, mutableListOf()) - } - } - - @Test - fun `Invalid nested requirement reference throws`() { val fragment = fragment() val template = BotActivity( id = "a", capacity = 1, requires = listOf( - FactReference( - FactReference( - HasSkillLevel(Skill.Attack), - emptyMap() - ), - emptyMap() - ) + Condition.Clone("x") ) ) assertThrows { - fragment.resolveRequirements(template, mutableListOf()) + fragment.resolveRequirements(mutableListOf(), template.requires) } } + } \ No newline at end of file From 92f5c12b5a2b8ca7ee541b5dd3a5ca57f4070672 Mon Sep 17 00:00:00 2001 From: GregHib Date: Sat, 31 Jan 2026 23:42:51 +0000 Subject: [PATCH 024/101] Add automatic location/area resolver --- data/bot/woodcutting.bots.toml | 16 ++-------------- game/src/main/kotlin/content/bot/BotManager.kt | 4 ++++ .../main/kotlin/content/bot/action/BotAction.kt | 2 +- .../main/kotlin/content/bot/fact/Condition.kt | 2 +- 4 files changed, 8 insertions(+), 16 deletions(-) diff --git a/data/bot/woodcutting.bots.toml b/data/bot/woodcutting.bots.toml index 644440e017..192a5bc9bd 100644 --- a/data/bot/woodcutting.bots.toml +++ b/data/bot/woodcutting.bots.toml @@ -1,29 +1,17 @@ [woodcutting_template] type = "activity" -resolve = [ +resolve = [ # setup? { carries = "$hatchet" }, { location = "$location" }, { inventory_space = 20 }, ] plan = [ - { option = "Chop-down", object = "$tree" }, + { option = "Chop down", object = "$tree" }, ] produces = [ { skill = "woodcutting" } ] -[go_to_lumbridge_north_trees] -type = "resolver" -resolve = [ - { variable = "movement", value = "run" } -] -plan = [ - { go_to = "lumbridge_north_trees" } -] -produces = [ - { location = "lumbridge_north_trees" } -] - [lumbridge_trees] template = "woodcutting_template" capacity = 4 diff --git a/game/src/main/kotlin/content/bot/BotManager.kt b/game/src/main/kotlin/content/bot/BotManager.kt index c1ee637ba8..3135c723b9 100644 --- a/game/src/main/kotlin/content/bot/BotManager.kt +++ b/game/src/main/kotlin/content/bot/BotManager.kt @@ -158,6 +158,10 @@ class BotManager( } private fun pickResolver(bot: Bot, condition: Condition, frame: BehaviourFrame): Behaviour? { + if (condition is Condition.Area) { + return Resolver("go_to_${condition.area}", -1, plan = listOf(BotAction.GoTo(condition.area)), produces = setOf(condition)) + } + // TODO actions should have retry policies? val options = mutableListOf() for (key in condition.keys()) { for (resolver in resolvers[key] ?: return null) { diff --git a/game/src/main/kotlin/content/bot/action/BotAction.kt b/game/src/main/kotlin/content/bot/action/BotAction.kt index 5e7096cab4..2e0d038c5f 100644 --- a/game/src/main/kotlin/content/bot/action/BotAction.kt +++ b/game/src/main/kotlin/content/bot/action/BotAction.kt @@ -119,7 +119,7 @@ sealed interface BotAction { } val obj = objects.randomOrNull(random) ?: return BehaviourState.Failed(Reason.NoTarget) val index = obj.def(bot.player).options?.indexOf(option) ?: return BehaviourState.Failed(Reason.NoTarget) - bot.player.instructions.trySend(world.gregs.voidps.network.client.instruction.InteractObject(obj.intId, obj.x, obj.y, index)) + bot.player.instructions.trySend(world.gregs.voidps.network.client.instruction.InteractObject(obj.intId, obj.x, obj.y, index + 1)) return BehaviourState.Running } } diff --git a/game/src/main/kotlin/content/bot/fact/Condition.kt b/game/src/main/kotlin/content/bot/fact/Condition.kt index 60402fafee..d3bfe36009 100644 --- a/game/src/main/kotlin/content/bot/fact/Condition.kt +++ b/game/src/main/kotlin/content/bot/fact/Condition.kt @@ -42,7 +42,7 @@ sealed interface Condition { data class Area(val fact: Fact, val area: String) : Condition { override fun check(bot: Bot) = fact.getValue(bot) in Areas[area] override fun priority() = fact.priority - override fun keys() = fact.keys() + override fun keys() = setOf("enter:$area") } data class OneOf(val fact: Fact, val values: Set) : Condition { From 2df602334e724554cad6655c125ada300fc06e5e Mon Sep 17 00:00:00 2001 From: GregHib Date: Sun, 1 Feb 2026 14:10:58 +0000 Subject: [PATCH 025/101] Convert Fact's and Conditions to check Players not Bots --- .../src/main/kotlin/content/bot/BotManager.kt | 14 +++++----- .../kotlin/content/bot/action/BotAction.kt | 1 + .../main/kotlin/content/bot/fact/Condition.kt | 28 +++++++++---------- game/src/main/kotlin/content/bot/fact/Fact.kt | 22 +++++++-------- 4 files changed, 33 insertions(+), 32 deletions(-) diff --git a/game/src/main/kotlin/content/bot/BotManager.kt b/game/src/main/kotlin/content/bot/BotManager.kt index 3135c723b9..4fd57a6035 100644 --- a/game/src/main/kotlin/content/bot/BotManager.kt +++ b/game/src/main/kotlin/content/bot/BotManager.kt @@ -26,7 +26,7 @@ class BotManager( fun add(bot: Bot) { bots.add(bot) for (activity in activities.values) { - if (activity.requires.any { !it.check(bot) }) { + if (activity.requires.any { !it.check(bot.player) }) { continue } bot.available.add(activity.id) @@ -39,13 +39,13 @@ class BotManager( val id = iterator.next() val activity = activities[id] ?: continue // TODO could filter by keys - if (activity.requires.any { !it.check(bot) }) { + if (activity.requires.any { !it.check(bot.player) }) { iterator.remove() } } for (id in groups[group] ?: return) { val activity = activities[id] ?: continue - if (activity.requires.any { !it.check(bot) }) { + if (activity.requires.any { !it.check(bot.player) }) { continue } bot.available.add(activity.id) @@ -80,7 +80,7 @@ class BotManager( } private fun hasRequirements(bot: Bot, activity: BotActivity): Boolean { - return slots.hasFree(activity) && !bot.blocked.contains(activity.id) && activity.requires.all { it.check(bot) } + return slots.hasFree(activity) && !bot.blocked.contains(activity.id) && activity.requires.all { it.check(bot.player) } } fun assign(bot: Bot, id: String): Boolean { @@ -124,14 +124,14 @@ class BotManager( private fun start(bot: Bot, behaviour: Behaviour, frame: BehaviourFrame) { for (requirement in behaviour.requires) { - if (requirement.check(bot)) { + if (requirement.check(bot.player)) { continue } frame.fail(Reason.Requirement(requirement)) return } for (requirement in behaviour.resolve) { - if (requirement.check(bot)) { + if (requirement.check(bot.player)) { continue } val resolver = pickResolver(bot, requirement, frame) @@ -168,7 +168,7 @@ class BotManager( if (frame.blocked.contains(resolver.id)) { continue } - if (resolver.requires.any { fact -> !fact.check(bot) }) { + if (resolver.requires.any { fact -> !fact.check(bot.player) }) { continue } options.add(resolver) diff --git a/game/src/main/kotlin/content/bot/action/BotAction.kt b/game/src/main/kotlin/content/bot/action/BotAction.kt index 2e0d038c5f..0601c68e60 100644 --- a/game/src/main/kotlin/content/bot/action/BotAction.kt +++ b/game/src/main/kotlin/content/bot/action/BotAction.kt @@ -58,6 +58,7 @@ sealed interface BotAction { return BehaviourState.Success } updateGraph(bot) + // TODO new format for nav graph val strategy = AreaStrategy(def.area) val result = get().find(bot.player, strategy, EdgeTraversal()) bot["navigating"] = result == null diff --git a/game/src/main/kotlin/content/bot/fact/Condition.kt b/game/src/main/kotlin/content/bot/fact/Condition.kt index d3bfe36009..031307b429 100644 --- a/game/src/main/kotlin/content/bot/fact/Condition.kt +++ b/game/src/main/kotlin/content/bot/fact/Condition.kt @@ -1,70 +1,70 @@ package content.bot.fact -import content.bot.Bot import world.gregs.voidps.engine.data.definition.Areas +import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.type.Tile sealed interface Condition { - fun check(bot: Bot): Boolean + fun check(player: Player): Boolean fun keys(): Set fun priority(): Int data class Equals(val fact: Fact, val value: T) : Condition { - override fun check(bot: Bot) = fact.getValue(bot) == value + override fun check(player: Player) = fact.getValue(player) == value override fun priority() = fact.priority override fun keys() = fact.keys() } data class AtLeast(val fact: Fact, val min: Int) : Condition { - override fun check(bot: Bot) = fact.getValue(bot) >= min + override fun check(player: Player) = fact.getValue(player) >= min override fun priority() = fact.priority override fun keys() = fact.keys() } data class AtMost(val fact: Fact, val max: Int) : Condition { - override fun check(bot: Bot) = fact.getValue(bot) <= max + override fun check(player: Player) = fact.getValue(player) <= max override fun priority() = fact.priority override fun keys() = fact.keys() } data class Range(val fact: Fact, val min: Int, val max: Int) : Condition { - override fun check(bot: Bot) = fact.getValue(bot) in min..max + override fun check(player: Player) = fact.getValue(player) in min..max override fun priority() = fact.priority override fun keys() = fact.keys() } data class Within(val fact: Fact, val tile: Tile, val radius: Int) : Condition { - override fun check(bot: Bot) = fact.getValue(bot).within(tile, radius) + override fun check(player: Player) = fact.getValue(player).within(tile, radius) override fun priority() = fact.priority override fun keys() = fact.keys() } data class Area(val fact: Fact, val area: String) : Condition { - override fun check(bot: Bot) = fact.getValue(bot) in Areas[area] + override fun check(player: Player) = fact.getValue(player) in Areas[area] override fun priority() = fact.priority override fun keys() = setOf("enter:$area") } data class OneOf(val fact: Fact, val values: Set) : Condition { - override fun check(bot: Bot) = fact.getValue(bot) in values + override fun check(player: Player) = fact.getValue(player) in values override fun priority() = fact.priority override fun keys() = fact.keys() } data class Not(val inner: Condition) : Condition { - override fun check(bot: Bot) = !inner.check(bot) + override fun check(player: Player) = !inner.check(player) override fun priority() = inner.priority() override fun keys() = inner.keys() } data class All(val conditions: List) : Condition { - override fun check(bot: Bot) = conditions.all { it.check(bot) } + override fun check(player: Player) = conditions.all { it.check(player) } override fun priority() = conditions.first().priority() override fun keys() = conditions.flatMap { it.keys() }.toSet() } data class Any(val conditions: List) : Condition { - override fun check(bot: Bot) = conditions.any { it.check(bot) } + override fun check(player: Player) = conditions.any { it.check(player) } override fun priority() = conditions.first().priority() override fun keys() = conditions.flatMap { it.keys() }.toSet() } @@ -77,13 +77,13 @@ sealed interface Condition { val max: Int? = null, val references: Map = emptyMap(), ) : Condition { - override fun check(bot: Bot) = false + override fun check(player: Player) = false override fun priority() = -1 override fun keys() = emptySet() } class Clone(val id: String) : Condition { - override fun check(bot: Bot) = false + override fun check(player: Player) = false override fun priority() = -1 override fun keys() = emptySet() } diff --git a/game/src/main/kotlin/content/bot/fact/Fact.kt b/game/src/main/kotlin/content/bot/fact/Fact.kt index 7f64f4c2ae..48c4c8ca1d 100644 --- a/game/src/main/kotlin/content/bot/fact/Fact.kt +++ b/game/src/main/kotlin/content/bot/fact/Fact.kt @@ -1,6 +1,6 @@ package content.bot.fact -import content.bot.Bot +import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.entity.character.player.skill.Skill import world.gregs.voidps.engine.inv.equipment import world.gregs.voidps.engine.inv.inventory @@ -11,47 +11,47 @@ import world.gregs.voidps.type.Tile * @param priority Ensure bots aren't walking to locations before getting items etc... lower values are prioritised first. */ sealed class Fact(val priority: Int) { - abstract fun getValue(bot: Bot): T + abstract fun getValue(player: Player): T open fun keys(): Set = emptySet() data class InventoryCount(val id: String) : Fact(100) { override fun keys() = setOf("inv:inventory") - override fun getValue(bot: Bot) = bot.player.inventory.count(id) + override fun getValue(player: Player) = player.inventory.count(id) } object InventorySpace : Fact(10) { override fun keys() = setOf("inv:inventory") - override fun getValue(bot: Bot) = bot.player.inventory.spaces + override fun getValue(player: Player) = player.inventory.spaces } data class EquipCount(val id: String) : Fact(100) { override fun keys() = setOf("inv:equipment") - override fun getValue(bot: Bot) = bot.player.equipment.count(id) + override fun getValue(player: Player) = player.equipment.count(id) } data class IntVariable(val id: String) : Fact(1) { override fun keys() = setOf("var:${id}") - override fun getValue(bot: Bot) = bot.player.variables.get(id) + override fun getValue(player: Player) = player.variables.get(id) } data class BoolVariable(val id: String) : Fact(1) { override fun keys() = setOf("var:${id}") - override fun getValue(bot: Bot) = bot.player.variables.get(id) + override fun getValue(player: Player) = player.variables.get(id) } data class StringVariable(val id: String) : Fact(1) { override fun keys() = setOf("var:${id}") - override fun getValue(bot: Bot) = bot.player.variables.get(id) + override fun getValue(player: Player) = player.variables.get(id) } data class DoubleVariable(val id: String) : Fact(1) { override fun keys() = setOf("var:${id}") - override fun getValue(bot: Bot) = bot.player.variables.get(id) + override fun getValue(player: Player) = player.variables.get(id) } object PlayerTile : Fact(1000) { override fun keys() = setOf("tile") - override fun getValue(bot: Bot) = bot.player.tile + override fun getValue(player: Player) = player.tile } object AttackLevel : SkillLevel(Skill.Attack) @@ -84,7 +84,7 @@ sealed class Fact(val priority: Int) { val skill: Skill, ) : Fact(0) { override fun keys() = setOf("skill:${skill.name.lowercase()}") - override fun getValue(bot: Bot) = bot.player.levels.get(skill) + override fun getValue(player: Player) = player.levels.get(skill) companion object { fun of(skill: String): SkillLevel = when (skill.lowercase()) { From a6d5bd721d4cdd03db1b2d25ce6fd3a54ed00288 Mon Sep 17 00:00:00 2001 From: GregHib Date: Sun, 1 Feb 2026 18:36:35 +0000 Subject: [PATCH 026/101] Rename plan to actions, resolve to setup --- data/bot/axe_shop.bots.toml | 4 +- data/bot/teleport.bots.toml | 10 ++--- data/bot/walking.bots.toml | 6 +-- data/bot/woodcutting.bots.toml | 4 +- .../src/main/kotlin/content/bot/BotManager.kt | 4 +- .../kotlin/content/bot/action/Behaviour.kt | 2 +- .../content/bot/action/BehaviourFragment.kt | 8 +++- .../content/bot/action/BehaviourFrame.kt | 6 +-- .../kotlin/content/bot/action/BotAction.kt | 8 ++++ .../kotlin/content/bot/action/BotActivity.kt | 41 ++++++++++++++----- .../kotlin/content/bot/action/Resolver.kt | 2 +- .../test/kotlin/content/bot/BotManagerTest.kt | 16 ++++---- .../bot/action/BehaviourFragmentTest.kt | 14 +++---- 13 files changed, 79 insertions(+), 46 deletions(-) diff --git a/data/bot/axe_shop.bots.toml b/data/bot/axe_shop.bots.toml index 118f345c0b..c6f6cf3054 100644 --- a/data/bot/axe_shop.bots.toml +++ b/data/bot/axe_shop.bots.toml @@ -1,10 +1,10 @@ [buy_from_shop] -resolve = [ +setup = [ { carries = "coins", amount = "$cost" }, { location = "$shop_location" }, { inventory_space = 1 }, ] -plan = [ +actions = [ { option = "Trade", npc = "$shopkeeper" }, { option = "Buy 1", interface = "shop:inventory:$item" }, ] diff --git a/data/bot/teleport.bots.toml b/data/bot/teleport.bots.toml index 48868e8181..4137c2b46a 100644 --- a/data/bot/teleport.bots.toml +++ b/data/bot/teleport.bots.toml @@ -5,7 +5,7 @@ weight = 10 requires = [ { equips = "ring_of_duelling_*" } ] -plan = [ +actions = [ { option = "Castle Wars", interface = "equipment:inventory" }, ] produces = [ @@ -18,7 +18,7 @@ weight = 8 requires = [ { carries = "ring_of_duelling_*" } ] -plan = [ +actions = [ { option = "Rub", interface = "inventory:inventory" }, { option = "Select", interface = "choice:line1" }, ] @@ -32,12 +32,12 @@ weight = 10 requires = [ { variable = "spellbook", value = "normal" }, ] -resolve = [ +setup = [ { carries = "fire_rune", amount = 1 }, { carries = "air_rune", amount = 3 }, { carries = "law_rune", amount = 1 }, ] -plan = [ +actions = [ { option = "Cast", interface = "normal_spellbook:varrock_teleport" }, ] produces = [ @@ -51,7 +51,7 @@ requires = [ { variable = "spellbook", value = "normal" }, { variable = "lumbridge_cooldown", value = "normal" }, ] -plan = [ +actions = [ { option = "Cast", interface = "normal_spellbook:home_teleport" }, ] produces = [ diff --git a/data/bot/walking.bots.toml b/data/bot/walking.bots.toml index 413964ac55..a2d067ca66 100644 --- a/data/bot/walking.bots.toml +++ b/data/bot/walking.bots.toml @@ -1,10 +1,10 @@ #[run_to_varrock] #type = "activity" #capacity = 1 -#resolve = [ +#setup = [ # { variable = "movement", value = "run" } #] -#plan = [ +#actions = [ # { go_to = "varrock_teleport" } #] #produces = [ @@ -16,7 +16,7 @@ type = "resolver" requires = [ { variable = "movement", value = "walk" } ] -plan = [ +actions = [ { option = "Turn Run mode on", interface = "energy_orb:run_background" } ] produces = [ diff --git a/data/bot/woodcutting.bots.toml b/data/bot/woodcutting.bots.toml index 192a5bc9bd..23285ec352 100644 --- a/data/bot/woodcutting.bots.toml +++ b/data/bot/woodcutting.bots.toml @@ -1,11 +1,11 @@ [woodcutting_template] type = "activity" -resolve = [ # setup? +setup = [ { carries = "$hatchet" }, { location = "$location" }, { inventory_space = 20 }, ] -plan = [ +actions = [ { option = "Chop down", object = "$tree" }, ] produces = [ diff --git a/game/src/main/kotlin/content/bot/BotManager.kt b/game/src/main/kotlin/content/bot/BotManager.kt index 4fd57a6035..deb20a319d 100644 --- a/game/src/main/kotlin/content/bot/BotManager.kt +++ b/game/src/main/kotlin/content/bot/BotManager.kt @@ -98,7 +98,7 @@ class BotManager( activity != null && hasRequirements(bot, activity) }.randomOrNull(random) if (id == null) { - BotActivity("idle", 2048, plan = listOf(BotAction.Wait(50))) // 30s + BotActivity("idle", 2048, actions = listOf(BotAction.Wait(50))) // 30s } else { activities[id] } @@ -159,7 +159,7 @@ class BotManager( private fun pickResolver(bot: Bot, condition: Condition, frame: BehaviourFrame): Behaviour? { if (condition is Condition.Area) { - return Resolver("go_to_${condition.area}", -1, plan = listOf(BotAction.GoTo(condition.area)), produces = setOf(condition)) + return Resolver("go_to_${condition.area}", -1, actions = listOf(BotAction.GoTo(condition.area)), produces = setOf(condition)) } // TODO actions should have retry policies? val options = mutableListOf() diff --git a/game/src/main/kotlin/content/bot/action/Behaviour.kt b/game/src/main/kotlin/content/bot/action/Behaviour.kt index 314917920a..941c2fa40d 100644 --- a/game/src/main/kotlin/content/bot/action/Behaviour.kt +++ b/game/src/main/kotlin/content/bot/action/Behaviour.kt @@ -6,6 +6,6 @@ interface Behaviour { val id: String val requires: List val resolve: List - val plan: List + val actions: List val produces: Set } \ No newline at end of file diff --git a/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt b/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt index e94e64661c..f6eb34a326 100644 --- a/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt +++ b/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt @@ -12,12 +12,12 @@ data class BehaviourFragment( var template: String, override val requires: List = emptyList(), override val resolve: List = emptyList(), - override val plan: List = emptyList(), + override val actions: List = emptyList(), override val produces: Set = emptySet(), val fields: Map = emptyMap(), ) : Behaviour { fun resolveActions(template: BotActivity, actions: MutableList) { - for (action in template.plan) { + for (action in template.actions) { val resolved = when (action) { is BotAction.Reference -> when (val copy = action.action) { is BotAction.GoTo -> BotAction.GoTo(resolve(action.references["go_to"], copy.target)) @@ -39,6 +39,10 @@ data class BehaviourFragment( retryMax = resolve(action.references["retry_max"], copy.retryMax), radius = resolve(action.references["radius"], copy.radius), ) + is BotAction.WalkTo -> BotAction.WalkTo( + x = resolve(action.references["x"], copy.x), + y = resolve(action.references["y"], copy.y), + ) is BotAction.Wait -> BotAction.Wait(resolve(action.references["wait"], copy.ticks)) is BotAction.WaitFullInventory -> BotAction.WaitFullInventory(resolve(action.references["timeout"], copy.timeout)) is BotAction.Clone, is BotAction.Reference -> throw IllegalArgumentException("Invalid reference action type: ${action.action::class.simpleName}.") diff --git a/game/src/main/kotlin/content/bot/action/BehaviourFrame.kt b/game/src/main/kotlin/content/bot/action/BehaviourFrame.kt index 315377cc46..1dc67811e1 100644 --- a/game/src/main/kotlin/content/bot/action/BehaviourFrame.kt +++ b/game/src/main/kotlin/content/bot/action/BehaviourFrame.kt @@ -10,9 +10,9 @@ data class BehaviourFrame( var blocked: MutableSet = mutableSetOf(), ) { - fun action(): BotAction = behaviour.plan[index] + fun action(): BotAction = behaviour.actions[index] - fun completed() = index >= behaviour.plan.size + fun completed() = index >= behaviour.actions.size fun start(bot: Bot) { val action = action() @@ -27,7 +27,7 @@ data class BehaviourFrame( fun next(): Boolean { index++ state = BehaviourState.Pending - return index < behaviour.plan.size + return index < behaviour.actions.size } fun fail(reason: Reason) { diff --git a/game/src/main/kotlin/content/bot/action/BotAction.kt b/game/src/main/kotlin/content/bot/action/BotAction.kt index 0601c68e60..e30577819d 100644 --- a/game/src/main/kotlin/content/bot/action/BotAction.kt +++ b/game/src/main/kotlin/content/bot/action/BotAction.kt @@ -24,6 +24,7 @@ import world.gregs.voidps.engine.get import world.gregs.voidps.engine.map.Spiral import world.gregs.voidps.network.client.instruction.InteractInterface import world.gregs.voidps.network.client.instruction.InteractNPC +import world.gregs.voidps.network.client.instruction.Walk import world.gregs.voidps.type.random sealed interface BotAction { @@ -150,6 +151,13 @@ sealed interface BotAction { data class WaitFullInventory(val timeout: Int) : BotAction + data class WalkTo(val x: Int, val y: Int): BotAction { + override fun start(bot: Bot): BehaviourState { + bot.player.instructions.trySend(Walk(x, y)) + return BehaviourState.Success + } + } + /** TODO how to handle repeat actions e.g. repeat Chop-down trees until inv is full - These are actions Gathering, Skilling etc.. more resolvers like bank all, drop cheap items diff --git a/game/src/main/kotlin/content/bot/action/BotActivity.kt b/game/src/main/kotlin/content/bot/action/BotActivity.kt index d2f1c01f78..c0f4a6802c 100644 --- a/game/src/main/kotlin/content/bot/action/BotActivity.kt +++ b/game/src/main/kotlin/content/bot/action/BotActivity.kt @@ -17,7 +17,7 @@ data class BotActivity( val capacity: Int, override val requires: List = emptyList(), override val resolve: List = emptyList(), - override val plan: List = emptyList(), + override val actions: List = emptyList(), override val produces: Set = emptySet(), ) : Behaviour @@ -43,7 +43,7 @@ fun loadActivities(paths: List, activities: MutableMap requirements(requirements) // TODO convert to pass mutable list + clone list "resolve" -> requirements(resolvables) - "plan" -> actions = actions() + "actions" -> actions = actions() "produces" -> requirements(produces) "capacity" -> capacity = int() "type" -> type = string() @@ -63,26 +63,26 @@ fun loadActivities(paths: List, activities: MutableMap + val list = activities[action.id]?.actions ?: throw IllegalArgumentException("Unable to find activity to clone '${action.id}'.") + val actions = activity.actions as MutableList actions.removeAt(index) actions.addAll(index, list) } @@ -122,7 +122,7 @@ fun loadActivities(paths: List, activities: MutableMap() - actions.addAll(fragment.plan) + actions.addAll(fragment.actions) fragment.resolveActions(template, actions) if (fragment.type == "resolver") { for (fact in fragment.produces) { @@ -270,6 +270,8 @@ private fun ConfigReader.actions(): List { var timeout = 0 var ticks = 0 var radius = 10 + var x = 0 + var y = 0 val references = mutableMapOf() while (nextEntry()) { when (val key = key()) { @@ -280,6 +282,24 @@ private fun ConfigReader.actions(): List { references[key] = id } } + "x" -> { + type = "tile" + val value = string() + if (value.contains('$')) { + references[key] = value + } else { + x = value.toInt() + } + } + "y" -> { + type = "tile" + val value = string() + if (value.contains('$')) { + references[key] = value + } else { + y = value.toInt() + } + } "target", "id" -> { id = string() if (id.contains('$')) { @@ -328,6 +348,7 @@ private fun ConfigReader.actions(): List { "go_to" -> BotAction.GoTo(id) "wait" -> BotAction.Wait(ticks) "npc" -> BotAction.InteractNpc(id = id, option = option, retryTicks = retryTicks, retryMax = retryMax, radius = radius) + "tile" -> BotAction.WalkTo(x = x, y = y) "object" -> BotAction.InteractObject(id = id, option = option, retryTicks = retryTicks, retryMax = retryMax, radius = radius) "interface" -> BotAction.InterfaceOption(id = id, option = option) "clone" -> BotAction.Clone(id) diff --git a/game/src/main/kotlin/content/bot/action/Resolver.kt b/game/src/main/kotlin/content/bot/action/Resolver.kt index ffc3481e89..251b883aac 100644 --- a/game/src/main/kotlin/content/bot/action/Resolver.kt +++ b/game/src/main/kotlin/content/bot/action/Resolver.kt @@ -12,6 +12,6 @@ data class Resolver( val weight: Int, override val requires: List = emptyList(), override val resolve: List = emptyList(), - override val plan: List = emptyList(), + override val actions: List = emptyList(), override val produces: Set = emptySet(), ) : Behaviour \ No newline at end of file diff --git a/game/src/test/kotlin/content/bot/BotManagerTest.kt b/game/src/test/kotlin/content/bot/BotManagerTest.kt index 6b0a2a0299..3192b174fc 100644 --- a/game/src/test/kotlin/content/bot/BotManagerTest.kt +++ b/game/src/test/kotlin/content/bot/BotManagerTest.kt @@ -228,7 +228,7 @@ class BotManagerTest { val resolver = Resolver( id = "go_to_area", weight = 1, - plan = listOf(BotAction.Wait(1)), + actions = listOf(BotAction.Wait(1)), produces = setOf(condition) ) val activity = testActivity( @@ -253,8 +253,8 @@ class BotManagerTest { fun `Lowest weight resolver is selected`() { val condition = Condition.Within(Fact.PlayerTile, Tile(100, 100, 2), 2) - val bad = Resolver("bad", weight = 10, plan = listOf(BotAction.Clone(""))) - val good = Resolver("good", weight = 1, plan = listOf(BotAction.Clone(""))) + val bad = Resolver("bad", weight = 10, actions = listOf(BotAction.Clone(""))) + val good = Resolver("good", weight = 1, actions = listOf(BotAction.Clone(""))) val activity = testActivity( id = "mine", @@ -277,7 +277,7 @@ class BotManagerTest { @Test fun `Blocked resolver is not reselected`() { val condition = Condition.Within(Fact.PlayerTile, Tile(100, 100, 2), 2) - val resolver = Resolver(id = "get_key", weight = 1, plan = listOf(BotAction.Clone(""))) + val resolver = Resolver(id = "get_key", weight = 1, actions = listOf(BotAction.Clone(""))) val activity = testActivity( id = "open_door", resolves = listOf(condition), @@ -306,7 +306,7 @@ class BotManagerTest { val resolver = Resolver( id = "walk", weight = 1, - plan = listOf(BotAction.Wait(1)) + actions = listOf(BotAction.Wait(1)) ) val activity = testActivity( id = "enter_zone", @@ -334,7 +334,7 @@ class BotManagerTest { val resolver = Resolver( id = "test", weight = 1, - plan = listOf(BotAction.Wait(1)) + actions = listOf(BotAction.Wait(1)) ) val activity = testActivity( id = "smelt", @@ -363,7 +363,7 @@ class BotManagerTest { val resolver = Resolver( id = "mine_gem", weight = 1, - plan = listOf(BotAction.Clone("")), + actions = listOf(BotAction.Clone("")), requires = listOf(Condition.Range(Fact.MiningLevel, 99, 99)) ) val activity = testActivity( @@ -391,7 +391,7 @@ class BotManagerTest { val resolver = Resolver( id = "get_tool", weight = 1, - plan = listOf(BotAction.Wait(1)) + actions = listOf(BotAction.Wait(1)) ) val activity = testActivity( id = "work", diff --git a/game/src/test/kotlin/content/bot/action/BehaviourFragmentTest.kt b/game/src/test/kotlin/content/bot/action/BehaviourFragmentTest.kt index 75ac77bfcf..abeff6bc3e 100644 --- a/game/src/test/kotlin/content/bot/action/BehaviourFragmentTest.kt +++ b/game/src/test/kotlin/content/bot/action/BehaviourFragmentTest.kt @@ -29,7 +29,7 @@ class BehaviourFragmentTest { val template = BotActivity( id = "a", capacity = 1, - plan = listOf( + actions = listOf( BotAction.Reference( BotAction.GoTo("x"), references = mapOf("go_to" to "missing") @@ -48,7 +48,7 @@ class BehaviourFragmentTest { val template = BotActivity( id = "a", capacity = 1, - plan = listOf(BotAction.Clone("x")) + actions = listOf(BotAction.Clone("x")) ) assertThrows { fragment.resolveActions(template, mutableListOf()) @@ -61,7 +61,7 @@ class BehaviourFragmentTest { val template = BotActivity( id = "a", capacity = 1, - plan = listOf( + actions = listOf( BotAction.Reference( BotAction.Reference( BotAction.GoTo("x"), @@ -82,7 +82,7 @@ class BehaviourFragmentTest { val template = BotActivity( id = "a", capacity = 1, - plan = listOf( + actions = listOf( BotAction.Reference( BotAction.GoTo("x"), references = mapOf("go_to" to "dest") @@ -101,7 +101,7 @@ class BehaviourFragmentTest { val template = BotActivity( id = "a", capacity = 1, - plan = listOf( + actions = listOf( BotAction.Reference( BotAction.Wait(10), references = emptyMap() @@ -121,7 +121,7 @@ class BehaviourFragmentTest { val template = BotActivity( id = "a", capacity = 1, - plan = listOf( + actions = listOf( BotAction.Wait(10) ) ) @@ -180,7 +180,7 @@ class BehaviourFragmentTest { val template = BotActivity( id = "a", capacity = 1, - plan = listOf( + actions = listOf( BotAction.Reference( default, references = references From ac947a427fc7f1f5cc09cc5ad73abfb97d21336b Mon Sep 17 00:00:00 2001 From: GregHib Date: Sun, 1 Feb 2026 23:02:28 +0000 Subject: [PATCH 027/101] Add variable defaults and walk to tile action --- .../content/bot/action/BehaviourFragment.kt | 9 +++-- .../kotlin/content/bot/action/BotActivity.kt | 40 +++++++++---------- .../main/kotlin/content/bot/fact/Condition.kt | 3 +- game/src/main/kotlin/content/bot/fact/Fact.kt | 16 ++++---- 4 files changed, 35 insertions(+), 33 deletions(-) diff --git a/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt b/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt index f6eb34a326..2688b390ff 100644 --- a/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt +++ b/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt @@ -90,11 +90,12 @@ data class BehaviourFragment( } "variable" -> { val id = resolve(references[req.type], req.id) + val default = resolve(references["default"], req.default) when (val value = resolve(references["value"], req.value)) { - is Int -> Condition.Equals(Fact.IntVariable(id), value) - is String -> Condition.Equals(Fact.StringVariable(id), value) - is Double -> Condition.Equals(Fact.DoubleVariable(id), value) - is Boolean -> Condition.Equals(Fact.BoolVariable(id), value) + is Int -> Condition.Equals(Fact.IntVariable(id, default as? Int), value) + is String -> Condition.Equals(Fact.StringVariable(id, default as? String), value) + is Double -> Condition.Equals(Fact.DoubleVariable(id, default as? Double), value) + is Boolean -> Condition.Equals(Fact.BoolVariable(id, default as? Boolean), value) else -> null } } diff --git a/game/src/main/kotlin/content/bot/action/BotActivity.kt b/game/src/main/kotlin/content/bot/action/BotActivity.kt index c0f4a6802c..00daf7276f 100644 --- a/game/src/main/kotlin/content/bot/action/BotActivity.kt +++ b/game/src/main/kotlin/content/bot/action/BotActivity.kt @@ -34,16 +34,16 @@ fun loadActivities(paths: List, activities: MutableMap = emptyList() + val actions: MutableList = mutableListOf() val requirements: MutableList = mutableListOf() val resolvables: MutableList = mutableListOf() val produces: MutableList = mutableListOf() var fields: Map = emptyMap() while (nextPair()) { when (val key = key()) { - "requires" -> requirements(requirements) // TODO convert to pass mutable list + clone list - "resolve" -> requirements(resolvables) - "actions" -> actions = actions() + "requires" -> requirements(requirements) + "setup" -> requirements(resolvables) + "actions" -> actions(actions) "produces" -> requirements(produces) "capacity" -> capacity = int() "type" -> type = string() @@ -166,6 +166,7 @@ private fun ConfigReader.requirements(list: MutableList) { var type = "" var id = "" var value: Any? = null + var default: Any? = null var min: Int? = null var max: Int? = null val references = mutableMapOf() @@ -213,9 +214,10 @@ private fun ConfigReader.requirements(list: MutableList) { } } "value" -> value = value() + "default" -> default = value() } } - var requirement = getRequirement(type, id, min, max, value) + var requirement = getRequirement(type, id, min, max, value, default) if (requirement == null) { if (type == "holds") { throw IllegalArgumentException("Unknown requirement type 'holds'; did you mean 'carries' or 'equips'? ${exception()}.") @@ -223,7 +225,7 @@ private fun ConfigReader.requirements(list: MutableList) { throw IllegalArgumentException("Unknown requirement type: $type ${exception()}") } if (references.isNotEmpty()) { - requirement = Condition.Reference(type, id, value, min, max, references) + requirement = Condition.Reference(type, id, value, default, min, max, references) } list.add(requirement) } @@ -246,11 +248,11 @@ private fun getRequirement(type: String, id: String, min: Int?, max: Int?, value } else { Condition.range(Fact.EquipCount(id), min, max) } - "variable" -> when(value) { - is Int -> Condition.Equals(Fact.IntVariable(id), value) - is String -> Condition.Equals(Fact.StringVariable(id), value) - is Double -> Condition.Equals(Fact.DoubleVariable(id), value) - is Boolean -> Condition.Equals(Fact.BoolVariable(id), value) + "variable" -> when (value) { + is Int -> Condition.Equals(Fact.IntVariable(id, default as? Int), value) + is String -> Condition.Equals(Fact.StringVariable(id, default as? String), value) + is Double -> Condition.Equals(Fact.DoubleVariable(id, default as? Double), value) + is Boolean -> Condition.Equals(Fact.BoolVariable(id, default as? Boolean), value) else -> null } "clone" -> Condition.Clone(id) @@ -259,8 +261,7 @@ private fun getRequirement(type: String, id: String, min: Int?, max: Int?, value else -> null } -private fun ConfigReader.actions(): List { - val list = mutableListOf() +fun ConfigReader.actions(list: MutableList) { while (nextElement()) { var type = "" var id = "" @@ -284,20 +285,20 @@ private fun ConfigReader.actions(): List { } "x" -> { type = "tile" - val value = string() - if (value.contains('$')) { + val value = value() + if (value is String && value.contains('$')) { references[key] = value } else { - x = value.toInt() + x = value as Int } } "y" -> { type = "tile" - val value = string() - if (value.contains('$')) { + val value = value() + if (value is String && value.contains('$')) { references[key] = value } else { - y = value.toInt() + y = value as Int } } "target", "id" -> { @@ -363,5 +364,4 @@ private fun ConfigReader.actions(): List { } list.add(action) } - return list } \ No newline at end of file diff --git a/game/src/main/kotlin/content/bot/fact/Condition.kt b/game/src/main/kotlin/content/bot/fact/Condition.kt index 031307b429..f3890d5e6c 100644 --- a/game/src/main/kotlin/content/bot/fact/Condition.kt +++ b/game/src/main/kotlin/content/bot/fact/Condition.kt @@ -39,7 +39,7 @@ sealed interface Condition { override fun keys() = fact.keys() } - data class Area(val fact: Fact, val area: String) : Condition { + data class Area(val fact: Fact, val area: String) : Condition { // TODO make fact always PlayerTile? override fun check(player: Player) = fact.getValue(player) in Areas[area] override fun priority() = fact.priority override fun keys() = setOf("enter:$area") @@ -73,6 +73,7 @@ sealed interface Condition { val type: String = "", val id: String = "", val value: kotlin.Any? = null, + val default: kotlin.Any? = null, val min: Int? = null, val max: Int? = null, val references: Map = emptyMap(), diff --git a/game/src/main/kotlin/content/bot/fact/Fact.kt b/game/src/main/kotlin/content/bot/fact/Fact.kt index 48c4c8ca1d..ba400b9d7f 100644 --- a/game/src/main/kotlin/content/bot/fact/Fact.kt +++ b/game/src/main/kotlin/content/bot/fact/Fact.kt @@ -29,24 +29,24 @@ sealed class Fact(val priority: Int) { override fun getValue(player: Player) = player.equipment.count(id) } - data class IntVariable(val id: String) : Fact(1) { + data class IntVariable(val id: String, val default: Int?) : Fact(1) { override fun keys() = setOf("var:${id}") - override fun getValue(player: Player) = player.variables.get(id) + override fun getValue(player: Player) = player.variables.get(id) ?: default } - data class BoolVariable(val id: String) : Fact(1) { + data class BoolVariable(val id: String, val default: Boolean?) : Fact(1) { override fun keys() = setOf("var:${id}") - override fun getValue(player: Player) = player.variables.get(id) + override fun getValue(player: Player) = player.variables.get(id) ?: default } - data class StringVariable(val id: String) : Fact(1) { + data class StringVariable(val id: String, val default: String?) : Fact(1) { override fun keys() = setOf("var:${id}") - override fun getValue(player: Player) = player.variables.get(id) + override fun getValue(player: Player) = player.variables.get(id) ?: default } - data class DoubleVariable(val id: String) : Fact(1) { + data class DoubleVariable(val id: String, val default: Double?) : Fact(1) { override fun keys() = setOf("var:${id}") - override fun getValue(player: Player) = player.variables.get(id) + override fun getValue(player: Player) = player.variables.get(id) ?: default } object PlayerTile : Fact(1000) { From f8781b37f5ccf6646c6aa95c5a4b1b91a9a025f2 Mon Sep 17 00:00:00 2001 From: GregHib Date: Sun, 1 Feb 2026 23:03:07 +0000 Subject: [PATCH 028/101] Add a new navigation graph with navigation shortcuts --- data/bot/lumbridge.nav-edges.toml | 213 ++++++++++++ data/bot/teleport.bots.toml | 67 ++-- .../voidps/engine/data/definition/Areas.kt | 4 +- .../src/main/kotlin/content/bot/BotManager.kt | 7 +- .../kotlin/content/bot/action/BotActivity.kt | 28 +- .../content/bot/action/NavigationShortcut.kt | 12 + .../kotlin/content/bot/interact/path/Graph.kt | 276 +++++++++++++++ .../player/command/PathFindingCommands.kt | 17 + game/src/main/resources/game.properties | 3 + .../content/bot/interact/path/GraphTest.kt | 320 ++++++++++++++++++ 10 files changed, 902 insertions(+), 45 deletions(-) create mode 100644 data/bot/lumbridge.nav-edges.toml create mode 100644 game/src/main/kotlin/content/bot/action/NavigationShortcut.kt create mode 100644 game/src/main/kotlin/content/bot/interact/path/Graph.kt create mode 100644 game/src/test/kotlin/content/bot/interact/path/GraphTest.kt diff --git a/data/bot/lumbridge.nav-edges.toml b/data/bot/lumbridge.nav-edges.toml new file mode 100644 index 0000000000..a1f1e5a626 --- /dev/null +++ b/data/bot/lumbridge.nav-edges.toml @@ -0,0 +1,213 @@ +edges = [ + { from_x = 3234, from_y = 3218, to_x = 3236, to_y = 3205 }, # lumbridge_gate_south_to_village + { from_x = 3234, from_y = 3218, to_x = 3243, to_y = 3209 }, # lumbridge_gate_south_to_church + { from_x = 3236, from_y = 3205, to_x = 3243, to_y = 3209 }, # lumbridge_south_village_to_church + { from_x = 3234, from_y = 3218, to_x = 3250, to_y = 3212 }, # lumbridge_gate_south_to_behind_church + { from_x = 3250, from_y = 3212, to_x = 3258, to_y = 3206 }, # lumbridge_behind_church_to_church_fishing_spot + { from_x = 3236, from_y = 3205, to_x = 3231, to_y = 3203, cost = 5, actions = [{ option = "Open", object = "door_720_closed", x = 3234, y = 3203 }, { x = 3231, y = 3203 }] }, # lumbridge_south_village_to_bobs_axes + { from_x = 3231, from_y = 3203, to_x = 3236, to_y = 3205, cost = 5, actions = [{ option = "Open", object = "door_720_closed", x = 3234, y = 3203 }, { x = 3236, y = 3205 }] }, # lumbridge_bobs_axes_to_south_village + { from_x = 3236, from_y = 3205, to_x = 3231, to_y = 3197, cost = 5, actions = [{ option = "Open", object = "door_720_closed", x = 3235, y = 3199 }, { x = 3231, y = 3197 }] }, # lumbridge_south_village_to_south_range_house + { from_x = 3231, from_y = 3197, to_x = 3236, to_y = 3205, cost = 5, actions = [{ option = "Open", object = "door_720_closed", x = 3235, y = 3199 }, { x = 3236, y = 3205 }] }, # lumbridge_south_range_house_to_south_village + { from_x = 3236, from_y = 3205, to_x = 3244, to_y = 3190 }, # lumbridge_south_village_to_graveyard_exit + { from_x = 3244, from_y = 3190, to_x = 3253, to_y = 3200 }, # lumbridge_graveyard_exit_to_behind_graveyard + { from_x = 3250, from_y = 3212, to_x = 3253, to_y = 3200 }, # lumbridge_behind_church_to_behind_graveyard + { from_x = 3258, from_y = 3206, to_x = 3253, to_y = 3200 }, # lumbridge_church_fishing_spot_to_behind_graveyard + { from_x = 3205, from_y = 3209, to_x = 3205, to_y = 3209, cost = 1, actions = [{ option = "Climb-down", object = "lumbridge_staircase_top", x = 3204, y = 3207 }] }, # lumbridge_castle_2nd_floor_south_stairs_to_1st_floor + { from_x = 3208, from_y = 3219, to_x = 3205, to_y = 3209 }, # lumbridge_castle_2nd_floor_bank_to_south_stairs + { from_x = 3205, from_y = 3209, to_x = 3205, to_y = 3209, cost = 1, actions = [{ option = "Climb-up", object = "lumbridge_staircase", x = 3204, y = 3207 }] }, # lumbridge_castle_ground_floor_south_stairs_to_1st_floor + { from_x = 3205, from_y = 3209, to_x = 3208, to_y = 3210 }, # lumbridge_castle_ground_floor_south_stairs_to_kitchen_corridor + { from_x = 3208, from_y = 3210, to_x = 3215, to_y = 3216 }, # lumbridge_castle_kitchen_corridor_to_castle_south_entrance + { from_x = 3208, from_y = 3210, to_x = 3211, to_y = 3214 }, # lumbridge_castle_kitchen_corridor_to_kitchen + { from_x = 3215, from_y = 3216, to_x = 3222, to_y = 3218 }, # lumbridge_castle_south_entrance_to_courtyard_south + { from_x = 3205, from_y = 3209, to_x = 3205, to_y = 3209, cost = 1, actions = [{ option = "Climb-down", object = "lumbridge_castle_staircase_south_middle", x = 3204, y = 3207 }] }, # lumbridge_castle_1st_floor_south_stairs_to_ground_floor + { from_x = 3205, from_y = 3209, to_x = 3205, to_y = 3209, cost = 1, actions = [{ option = "Climb-up", object = "lumbridge_castle_staircase_south_middle", x = 3204, y = 3207 }] }, # lumbridge_castle_1st_floor_south_stairs_to_2nd_floor + { from_x = 3209, from_y = 3205, to_x = 3199, to_y = 3218 }, # lumbridge_castle_grounds_south_to_tower_west + { from_x = 3199, from_y = 3218, to_x = 3184, to_y = 3225 }, # lumbridge_castle_tower_west_to_yew_trees + { from_x = 3199, from_y = 3218, to_x = 3193, to_y = 3236 }, # lumbridge_castle_tower_west_to_tree_patch + { from_x = 3184, from_y = 3225, to_x = 3168, to_y = 3221 }, # lumbridge_castle_yew_trees_to_yew_trees_west + { from_x = 3184, from_y = 3225, to_x = 3193, to_y = 3236 }, # lumbridge_castle_yew_trees_to_tree_patch + { from_x = 3226, from_y = 3214, to_x = 3227, to_y = 3214, cost = 3, actions = [{ option = "Open", object = "door_627_closed", x = 3226, y = 3214 }] }, # lumbridge_south_tower_to_ground_floor + { from_x = 3227, from_y = 3214, to_x = 3229, to_y = 3214, cost = 1, actions = [{ option = "Climb-up", object = "36768", x = 3229, y = 3213 }] }, # lumbridge_south_tower_ground_floor_to_1st_floor + { from_x = 3229, from_y = 3214, to_x = 3229, to_y = 3214, cost = 1, actions = [{ option = "Climb-up", object = "36769", x = 3229, y = 3213 }] }, # lumbridge_south_tower_1st_floor_to_2nd_floor + { from_x = 3229, from_y = 3214, to_x = 3229, to_y = 3214, cost = 1, actions = [{ option = "Climb-down", object = "36769", x = 3229, y = 3213 }] }, # lumbridge_south_tower_1st_floor_to_ground_floor + { from_x = 3229, from_y = 3214, to_x = 3229, to_y = 3214, cost = 1, actions = [{ option = "Climb-down", object = "36770", x = 3229, y = 3213 }] }, # lumbridge_south_tower_2nd_floor_to_1st_floor + { from_x = 3234, from_y = 3220, to_x = 3236, to_y = 3225 }, # lumbridge_gate_north_to_bridge_west + { from_x = 3236, from_y = 3225, to_x = 3230, to_y = 3232 }, # lumbridge_bridge_west_to_unstable_house + { from_x = 3236, from_y = 3225, to_x = 3253, to_y = 3225 }, # lumbridge_bridge_west_to_bridge_east + { from_x = 3253, from_y = 3225, to_x = 3263, to_y = 3222 }, # lumbridge_bridge_east_to_trees_east + { from_x = 3253, from_y = 3225, to_x = 3260, to_y = 3228 }, # lumbridge_bridge_east_to_east_crossroad + { from_x = 3260, from_y = 3228, to_x = 3263, to_y = 3222 }, # lumbridge_east_crossroad_to_trees_east + { from_x = 3235, from_y = 3261, to_x = 3250, to_y = 3266 }, # lumbridge_bridge_north_to_cow_entrance + { from_x = 3251, from_y = 3252, to_x = 3250, to_y = 3266 }, # lumbridge_goblin_path_north_to_cow_entrance + { from_x = 3250, from_y = 3266, to_x = 3240, to_y = 3280 }, # lumbridge_cow_entrance_to_cow_path + { from_x = 3240, from_y = 3280, to_x = 3238, to_y = 3295 }, # lumbridge_cow_path_to_chicken_entrance + { from_x = 3238, from_y = 3295, to_x = 3235, to_y = 3295, cost = 0, actions = [{ option = "Open", object = "gate_235_closed", x = 3237, y = 3295 }] }, # lumbridge_chicken_entrance_to_chicken_pen + { from_x = 3235, from_y = 3295, to_x = 3238, to_y = 3295, cost = 0, actions = [{ option = "Open", object = "gate_237_closed", x = 3237, y = 3296 }] }, # lumbridge_chicken_pen_to_chicken_entrance + { from_x = 3250, from_y = 3266, to_x = 3255, to_y = 3266, cost = 0, actions = [{ option = "Open", object = "gate_241_closed", x = 3252, y = 3266 }] }, # lumbridge_cow_entrance_to_cow_field + { from_x = 3255, from_y = 3266, to_x = 3250, to_y = 3266, cost = 0, actions = [{ option = "Open", object = "gate_239_closed", x = 3252, y = 3267 }] }, # lumbridge_cow_field_to_cow_entrance + { from_x = 3244, from_y = 3190, to_x = 3241, to_y = 3176 }, # lumbridge_graveyard_exit_to_swamp_path + { from_x = 3241, from_y = 3176, to_x = 3239, to_y = 3160 }, # lumbridge_swamp_path_to_swamp_cross_roads + { from_x = 3244, from_y = 3190, to_x = 3231, to_y = 3188 }, # lumbridge_graveyard_exit_to_rats_north_east + { from_x = 3244, from_y = 3190, to_x = 3231, to_y = 3181 }, # lumbridge_graveyard_exit_to_rats_south_east + { from_x = 3241, from_y = 3176, to_x = 3231, to_y = 3188 }, # lumbridge_swamp_path_to_rats_north_east + { from_x = 3241, from_y = 3176, to_x = 3231, to_y = 3181 }, # lumbridge_swamp_path_to_rats_south_east + { from_x = 3231, from_y = 3188, to_x = 3231, to_y = 3181 }, # lumbridge_swamp_rats_north_east_to_south_east + { from_x = 3239, from_y = 3160, to_x = 3228, to_y = 3148 }, # lumbridge_swamp_cross_roads_to_copper_mine + { from_x = 3228, from_y = 3148, to_x = 3225, to_y = 3147 }, # lumbridge_swamp_copper_mine_to_tin_mine + { from_x = 3228, from_y = 3148, to_x = 3221, to_y = 3156 }, # lumbridge_swamp_copper_mine_to_east_mine + { from_x = 3239, from_y = 3160, to_x = 3221, to_y = 3156 }, # lumbridge_swamp_cross_roads_to_east_mine + { from_x = 3239, from_y = 3160, to_x = 3243, to_y = 3155 }, # lumbridge_swamp_cross_roads_to_fishing_spot + { from_x = 3221, from_y = 3156, to_x = 3201, to_y = 3155 }, # lumbridge_swamp_east_mine_to_urhney_house + { from_x = 3201, from_y = 3155, to_x = 3181, to_y = 3153 }, # lumbridge_swamp_urhney_house_to_water_altar + { from_x = 3181, from_y = 3153, to_x = 3161, to_y = 3151 }, # lumbridge_swamp_water_altar_to_south + { from_x = 3161, from_y = 3151, to_x = 3148, to_y = 3149 }, # lumbridge_swamp_south_to_west_mine + { from_x = 3148, from_y = 3149, to_x = 3146, to_y = 3147 }, # lumbridge_swamp_west_mine_to_mithril_mine + { from_x = 3148, from_y = 3149, to_x = 3146, to_y = 3150 }, # lumbridge_swamp_west_mine_to_coal_mine + { from_x = 3148, from_y = 3149, to_x = 3146, to_y = 3167 }, # lumbridge_swamp_west_mine_to_west_1 + { from_x = 3146, from_y = 3167, to_x = 3143, to_y = 3186 }, # lumbridge_swamp_west_1_to_2 + { from_x = 3143, from_y = 3186, to_x = 3142, to_y = 3203 }, # lumbridge_swamp_west_2_to_west_camp + { from_x = 3142, from_y = 3203, to_x = 3136, to_y = 3219 }, # lumbridge_swamp_west_camp_to_west_wall + { from_x = 3142, from_y = 3203, to_x = 3153, to_y = 3216 }, # lumbridge_swamp_west_camp_to_north_wall + { from_x = 3136, from_y = 3219, to_x = 3153, to_y = 3216 }, # lumbridge_swamp_west_wall_to_north_wall + { from_x = 3136, from_y = 3219, to_x = 3138, to_y = 3227 }, # lumbridge_swamp_west_wall_to_draynor_east_path + { from_x = 3136, from_y = 3219, to_x = 3119, to_y = 3228 }, # lumbridge_swamp_west_wall_to_draynor_jail_path_south + { from_x = 3222, from_y = 3218, to_x = 3226, to_y = 3214 }, # lumbridge_courtyard_south_to_tower_door + { from_x = 3222, from_y = 3218, to_x = 3222, to_y = 3220 }, # lumbridge_courtyard_south_to_north + { from_x = 3222, from_y = 3218, to_x = 3234, to_y = 3218 }, # lumbridge_courtyard_south_to_gate_north + { from_x = 3222, from_y = 3220, to_x = 3234, to_y = 3220 }, # lumbridge_courtyard_north_to_gate_north + { from_x = 3234, from_y = 3218, to_x = 3234, to_y = 3220 }, # lumbridge_courtyard_gate_south_to_gate_north + { from_x = 3222, from_y = 3218, to_x = 3209, to_y = 3205 }, # lumbridge_courtyard_south_to_grounds_south + { from_x = 3230, from_y = 3232, to_x = 3222, to_y = 3241 }, # lumbridge_unstable_house_to_general_store_east + { from_x = 3222, from_y = 3241, to_x = 3217, to_y = 3241, cost = 6, actions = [{ option = "Open", object = "door_720_closed", x = 3219, y = 3241 }, { x = 3217, y = 3241 }] }, # lumbridge_general_store_east_to_general_store + { from_x = 3217, from_y = 3241, to_x = 3222, to_y = 3241, cost = 6, actions = [{ option = "Open", object = "door_720_closed", x = 3219, y = 3241 }, { x = 3222, y = 3241 }] }, # lumbridge_general_store_to_general_store_east + { from_x = 3222, from_y = 3241, to_x = 3226, to_y = 3245 }, # lumbridge_general_store_east_to_trees_west + { from_x = 3226, from_y = 3245, to_x = 3235, to_y = 3261 }, # lumbridge_west_village_trees_to_bridge_north + { from_x = 3222, from_y = 3241, to_x = 3204, to_y = 3247 }, # lumbridge_general_store_east_to_task_building + { from_x = 3217, from_y = 3241, to_x = 3204, to_y = 3247 }, # lumbridge_general_store_to_task_building + { from_x = 3204, from_y = 3247, to_x = 3194, to_y = 3247 }, # lumbridge_task_building_to_fishing_shop_entrance + { from_x = 3194, from_y = 3247, to_x = 3172, to_y = 3239 }, # lumbridge_fishing_shop_entrance_to_west_path + { from_x = 3194, from_y = 3247, to_x = 3193, to_y = 3236 }, # lumbridge_fishing_shop_entrance_to_tree_patch + { from_x = 3194, from_y = 3247, to_x = 3195, to_y = 3251 }, # lumbridge_fishing_shop_entrance_to_fishing_shop + { from_x = 3172, from_y = 3239, to_x = 3157, to_y = 3234 }, # lumbridge_west_path_to_ham_path + { from_x = 3172, from_y = 3239, to_x = 3184, to_y = 3225 }, # lumbridge_west_path_to_yew_trees + { from_x = 3172, from_y = 3239, to_x = 3168, to_y = 3221 }, # lumbridge_west_path_to_yew_trees_west + { from_x = 3168, from_y = 3221, to_x = 3153, to_y = 3216 }, # lumbridge_yew_trees_west_to_swamp_north_wall + { from_x = 3157, from_y = 3234, to_x = 3138, to_y = 3227 }, # lumbridge_ham_path_to_draynor_east_path + { from_x = 3222, from_y = 3241, to_x = 3219, to_y = 3247 }, # lumbridge_general_store_east_to_west_crossroad + { from_x = 3219, from_y = 3247, to_x = 3212, to_y = 3247 }, # lumbridge_west_crossroad_to_combat_hall_east_entrance + { from_x = 3212, from_y = 3247, to_x = 3204, to_y = 3247 }, # lumbridge_combat_hall_east_entrance_to_task_building + { from_x = 3212, from_y = 3247, to_x = 3212, to_y = 3250 }, # lumbridge_combat_hall_entrance_to_east + { from_x = 3204, from_y = 3247, to_x = 3204, to_y = 3250 }, # lumbridge_task_building_to_combat_hall_west + { from_x = 3226, from_y = 3245, to_x = 3225, to_y = 3252 }, # lumbridge_village_trees_west_to_smiths_south + { from_x = 3225, from_y = 3252, to_x = 3222, to_y = 3255 }, # lumbridge_smiths_south_to_west + { from_x = 3222, from_y = 3255, to_x = 3218, to_y = 3255 }, # lumbridge_smiths_west_to_smiths_crossroad + { from_x = 3218, from_y = 3255, to_x = 3219, to_y = 3247 }, # lumbridge_smiths_crossroad_to_west_crossroad + { from_x = 3225, from_y = 3252, to_x = 3219, to_y = 3247 }, # lumbridge_smiths_south_to_west_crossroad + { from_x = 3253, from_y = 3225, to_x = 3245, to_y = 3238 }, # lumbridge_bridge_east_to_goblins_river + { from_x = 3253, from_y = 3225, to_x = 3254, to_y = 3239 }, # lumbridge_bridge_east_to_goblins + { from_x = 3254, from_y = 3239, to_x = 3258, to_y = 3250 }, # lumbridge_goblins_to_trees_east + { from_x = 3254, from_y = 3239, to_x = 3245, to_y = 3238 }, # lumbridge_goblins_to_river + { from_x = 3254, from_y = 3239, to_x = 3251, to_y = 3252 }, # lumbridge_goblins_to_path_north + { from_x = 3251, from_y = 3252, to_x = 3235, to_y = 3261 }, # lumbridge_goblins_path_north_to_bridge_north + { from_x = 3235, from_y = 3261, to_x = 3240, to_y = 3249 }, # lumbridge_bridge_north_to_goblin_fishing_spot + { from_x = 3251, from_y = 3252, to_x = 3240, to_y = 3249 }, # lumbridge_goblin_path_north_to_fishing_spot + { from_x = 3240, from_y = 3249, to_x = 3245, to_y = 3238 }, # lumbridge_goblin_fishing_spot_to_river + { from_x = 3260, from_y = 3228, to_x = 3254, to_y = 3239 }, # lumbridge_east_crossroad_to_goblins + { from_x = 3260, from_y = 3228, to_x = 3267, to_y = 3227 }, # lumbridge_east_crossroad_to_tollgate + { from_x = 3260, from_y = 3228, to_x = 3267, to_y = 3228 }, # lumbridge_east_crossroad_to_tollgate_north + { from_x = 3260, from_y = 3228, to_x = 3263, to_y = 3222 }, # lumbridge_east_crossroad_to_trees_east + { from_x = 3213, from_y = 3428, to_x = 3223, to_y = 3429 }, # varrock_teleport_to_centre_east + { from_x = 3213, from_y = 3428, to_x = 3200, to_y = 3429 }, # varrock_teleport_to_centre_west + { from_x = 3213, from_y = 3428, to_x = 3211, to_y = 3420 }, # varrock_teleport_to_centre_south + { from_x = 3200, from_y = 3429, to_x = 3186, to_y = 3430 }, # varrock_centre_west_to_west_bank_south_entrance + { from_x = 3186, from_y = 3430, to_x = 3186, to_y = 3435 }, # varrock_west_bank_south_entrance_to_bank_south + { from_x = 3186, from_y = 3430, to_x = 3171, to_y = 3429 }, # varrock_west_bank_south_entrance_to_romeo_crossroad + { from_x = 3171, from_y = 3429, to_x = 3162, to_y = 3420 }, # varrock_west_romeo_crossroads_to_oak_tress + { from_x = 3162, from_y = 3420, to_x = 3150, to_y = 3416 }, # varrock_west_oak_trees_to_cat_house + { from_x = 3150, from_y = 3416, to_x = 3135, to_y = 3416 }, # varrock_west_cat_house_to_barbarian_path + { from_x = 3135, from_y = 3416, to_x = 3128, to_y = 3407 }, # varrock_west_barbarian_path_to_air_altar_ruins + { from_x = 3128, from_y = 3407, to_x = 2841, to_y = 4830, cost = 3, actions = [{ option = "null", object = "air_altar_ruins", x = 3126, y = 3404 }] }, # varrock_west_air_altar_ruins_to_air_altar + { from_x = 2841, from_y = 4830, to_x = 2841, to_y = 4829 }, # varrock_west_air_altar_to_exit + { from_x = 2841, from_y = 4829, to_x = 3128, to_y = 3407, cost = 3, actions = [{ option = "Enter", object = "air_altar_portal", x = 2841, y = 4828 }] }, # varrock_west_air_altar_exit_to_ruins + { from_x = 3304, from_y = 3474, to_x = 2655, to_y = 4831, cost = 3, actions = [{ option = "null", object = "earth_altar_ruins", x = 3305, y = 3473 }] }, # varrock_east_earth_altar_ruins_to_altar + { from_x = 2655, from_y = 4831, to_x = 2655, to_y = 4830 }, # varrock_east_earth_altar_to_exit + { from_x = 2655, from_y = 4830, to_x = 3304, to_y = 3474, cost = 3, actions = [{ option = "Enter", object = "earth_altar_portal", x = 2655, y = 4829 }] }, # varrock_east_earth_altar_exit_to_ruins + { from_x = 3254, from_y = 3422, to_x = 3265, to_y = 3428 }, # varrock_east_bank_to_dirt_crossroad + { from_x = 3265, from_y = 3428, to_x = 3280, to_y = 3428 }, # varrock_east_dirt_crossroad_to_east_crossroad + { from_x = 3280, from_y = 3428, to_x = 3287, to_y = 3414 }, # varrock_east_crossroad_to_east_path_south + { from_x = 3287, from_y = 3414, to_x = 3291, to_y = 3399 }, # varrock_east_path_to_south + { from_x = 3291, from_y = 3399, to_x = 3293, to_y = 3384 }, # varrock_east_path_south_to_east_border + { from_x = 3280, from_y = 3428, to_x = 3286, to_y = 3442 }, # varrock_east_crossroad_to_path_north + { from_x = 3286, from_y = 3442, to_x = 3287, to_y = 3457 }, # varrock_east_path_to_north + { from_x = 3287, from_y = 3457, to_x = 3296, to_y = 3462 }, # varrock_east_path_north_to_sawmill_crossroad + { from_x = 3296, from_y = 3462, to_x = 3304, to_y = 3474 }, # varrock_east_sawmill_crossroad_to_eath_altar_ruins + { from_x = 3265, from_y = 3428, to_x = 3246, to_y = 3429 }, # varrock_east_dirt_crossroad_to_bank_crossroad + { from_x = 3246, from_y = 3429, to_x = 3254, to_y = 3422 }, # varrock_east_bank_crossroad_to_east_bank + { from_x = 3230, from_y = 3430, to_x = 3246, to_y = 3429 }, # varrock_east_armour_shop_to_bank_crossroad + { from_x = 3230, from_y = 3430, to_x = 3223, to_y = 3429 }, # varrock_east_armour_shop_to_centre_east + { from_x = 3238, from_y = 3295, to_x = 3238, to_y = 3304 }, # lumbridge_chicken_entrance_to_varrock_south_split + { from_x = 3238, from_y = 3304, to_x = 3251, to_y = 3319 }, # varrock_south_split_to_al_kharid_path_west + { from_x = 3251, from_y = 3319, to_x = 3268, to_y = 3331 }, # varrock_al_kharid_path_west_to_crossroads + { from_x = 3283, from_y = 3331, to_x = 3295, to_y = 3334 }, # al_kharid_north_entrance_to_varrock_north_west + { from_x = 3295, from_y = 3334, to_x = 3304, to_y = 3335 }, # varrock_al_kharid_north_west_to_crossroad + { from_x = 3295, from_y = 3334, to_x = 3299, to_y = 3346 }, # varrock_al_kharid_north_west_to_south_east_path + { from_x = 3304, from_y = 3335, to_x = 3299, to_y = 3346 }, # varrock_al_kharid_north_west_crossroad_to_south_east_path + { from_x = 3299, from_y = 3346, to_x = 3298, to_y = 3359 }, # varrock_south_east_path_to_east_mine_path_south + { from_x = 3298, from_y = 3359, to_x = 3295, to_y = 3372 }, # varrock_south_east_mine_south_to_path + { from_x = 3295, from_y = 3372, to_x = 3286, to_y = 3371 }, # varrock_south_east_mine_path_to_east + { from_x = 3286, from_y = 3371, to_x = 3293, to_y = 3384 }, # varrock_south_east_mine_to_border + { from_x = 3295, from_y = 3372, to_x = 3293, to_y = 3384 }, # varrock_south_east_mine_path_to_border + { from_x = 3211, from_y = 3420, to_x = 3210, to_y = 3407 }, # varrock_centre_south_to_dirt_crossroads + { from_x = 3210, from_y = 3407, to_x = 3211, to_y = 3395 }, # varrock_south_dirt_crossroads_to_blue_moon_inn + { from_x = 3211, from_y = 3395, to_x = 3211, to_y = 3381 }, # varrock_blue_moon_inn_to_south_border + { from_x = 3211, from_y = 3381, to_x = 3214, to_y = 3367 }, # varrock_south_border_to_dark_mages + { from_x = 3214, from_y = 3367, to_x = 3226, to_y = 3352 }, # varrock_south_dark_mages_to_south + { from_x = 3226, from_y = 3352, to_x = 3228, to_y = 3337 }, # varrock_dark_mages_to_south_fields_path + { from_x = 3228, from_y = 3337, to_x = 3240, to_y = 3336 }, # varrock_south_fields_path_to_stile + { from_x = 3240, from_y = 3336, to_x = 3254, to_y = 3333 }, # varrock_south_stile_to_path + { from_x = 3254, from_y = 3333, to_x = 3268, to_y = 3331 }, # varrock_south_stile_to_al_kharid_crossroad + { from_x = 3138, from_y = 3227, to_x = 3119, to_y = 3228 }, # draynor_east_to_jail_path_south + { from_x = 3119, from_y = 3228, to_x = 3105, to_y = 3238 }, # draynor_path_south_to_west + { from_x = 3105, from_y = 3238, to_x = 3104, to_y = 3248 }, # draynor_path_west_to_bank_crossroad + { from_x = 3104, from_y = 3248, to_x = 3093, to_y = 3245 }, # draynor_bank_crossroad_to_bank + { from_x = 3093, from_y = 3245, to_x = 3079, to_y = 3249 }, # draynor_bank_to_stalls + { from_x = 3093, from_y = 3245, to_x = 3099, to_y = 3246 }, # draynor_bank_to_trees + { from_x = 3104, from_y = 3248, to_x = 3099, to_y = 3246 }, # draynor_bank_crossroad_to_trees + { from_x = 3079, from_y = 3249, to_x = 3071, to_y = 3266 }, # draynor_stalls_to_pigsty + { from_x = 3079, from_y = 3249, to_x = 3086, to_y = 3237 }, # draynor_stall_to_willow_trees + { from_x = 3079, from_y = 3249, to_x = 3072, to_y = 3250 }, # draynor_stalls_to_west_trees + { from_x = 3079, from_y = 3249, to_x = 3079, to_y = 3265 }, # draynor_stalls_to_north_trees + { from_x = 3093, from_y = 3245, to_x = 3086, to_y = 3237 }, # draynor_bank_to_willow_trees + { from_x = 3086, from_y = 3237, to_x = 3097, to_y = 3235 }, # draynor_willow_trees_to_south + { from_x = 3086, from_y = 3237, to_x = 3086, to_y = 3231 }, # draynor_willow_trees_to_fishing_spot + { from_x = 3086, from_y = 3231, to_x = 3097, to_y = 3235 }, # draynor_fishing_spot_to_south + { from_x = 3105, from_y = 3238, to_x = 3097, to_y = 3235 }, # draynor_jail_path_west_to_south + { from_x = 3138, from_y = 3227, to_x = 3153, to_y = 3216 }, # draynor_east_path_to_swamp_north_wall + { from_x = 3268, from_y = 3331, to_x = 3283, to_y = 3331 }, # varrock_al_kharid_crossroad_to_north_entrance + { from_x = 3283, from_y = 3331, to_x = 3284, to_y = 3313 }, # al_kharid_mine_north_entrance_to_west_path + { from_x = 3284, from_y = 3313, to_x = 3287, to_y = 3294 }, # al_kharid_mine_west_path_to_south + { from_x = 3287, from_y = 3294, to_x = 3298, to_y = 3280 }, # al_kharid_mine_west_path_to_entrance + { from_x = 3298, from_y = 3280, to_x = 3299, to_y = 3263 }, # al_kharid_mine_entrance_to_mine_south + { from_x = 3299, from_y = 3263, to_x = 3294, to_y = 3242 }, # al_kharid_mine_south_to_north_path + { from_x = 3294, from_y = 3242, to_x = 3278, to_y = 3228 }, # al_kharid_north_path_to_crossroad + { from_x = 3278, from_y = 3228, to_x = 3268, to_y = 3228 }, # al_kharid_crossroad_to_tollgate_north + { from_x = 3278, from_y = 3228, to_x = 3268, to_y = 3227 }, # al_kharid_crossroad_to_tollgate + { from_x = 3278, from_y = 3228, to_x = 3280, to_y = 3216 }, # al_kharid_crossroad_to_glider + { from_x = 3267, from_y = 3228, to_x = 3268, to_y = 3228, cost = 1, actions = [{ option = "Pay-toll(10gp)", object = "toll_gate_al_kharid_north", x = 3268, y = 3228 }], conditions = [{ carries = "coins", amount = 10 }] }, # lumbridge_tollgate_north_to_al_kharid_tollgate + { from_x = 3268, from_y = 3228, to_x = 3267, to_y = 3228, cost = 1, actions = [{ option = "Pay-toll(10gp)", object = "toll_gate_al_kharid_north", x = 3268, y = 3228 }], conditions = [{ carries = "coins", amount = 10 }] }, # al_kharid_tollgate_north_to_lumbridge_tollgate + { from_x = 3267, from_y = 3227, to_x = 3268, to_y = 3227, cost = 1, actions = [{ option = "Pay-toll(10gp)", object = "toll_gate_al_kharid_south", x = 3268, y = 3227 }], conditions = [{ carries = "coins", amount = 10 }] }, # lumbridge_tollgate_to_al_kharid + { from_x = 3268, from_y = 3227, to_x = 3267, to_y = 3227, cost = 1, actions = [{ option = "Pay-toll(10gp)", object = "toll_gate_al_kharid_south", x = 3268, y = 3227 }], conditions = [{ carries = "coins", amount = 10 }] }, # al_kharid_tollgate_to_lumbridge + { from_x = 3268, from_y = 3227, to_x = 3280, to_y = 3216 }, # al_kharid_tollgate_to_glider + { from_x = 3268, from_y = 3228, to_x = 3280, to_y = 3216 }, # al_kharid_tollgate_north_to_glider + { from_x = 3280, from_y = 3216, to_x = 3292, to_y = 3215 }, # al_kharid_glider_to_musician + { from_x = 3292, from_y = 3215, to_x = 3300, to_y = 3198 }, # al_kharid_musician_to_silk_path + { from_x = 3300, from_y = 3198, to_x = 3288, to_y = 3189 }, # al_kharid_silk_path_to_scimitar_shop + { from_x = 3280, from_y = 3216, to_x = 3280, to_y = 3200 }, # al_kharid_glider_to_west_shortcut + { from_x = 3280, from_y = 3200, to_x = 3288, to_y = 3189 }, # al_kharid_west_shortcut_to_scimitar_shop + { from_x = 3288, from_y = 3189, to_x = 3282, to_y = 3185 }, # al_kharid_scimitar_shop_to_furnace_entrance + { from_x = 3280, from_y = 3200, to_x = 3282, to_y = 3185 }, # al_kharid_west_shortcut_to_furnace_to_entrance + { from_x = 3282, from_y = 3185, to_x = 3278, to_y = 3177 }, # al_kharid_furnace_entrance_to_bank_crossroads + { from_x = 3278, from_y = 3177, to_x = 3276, to_y = 3168 }, # al_kharid_bank_crossroad_to_entrance + { from_x = 3278, from_y = 3177, to_x = 3274, to_y = 3180 }, # al_kharid_bank_crossroad_to_kebab_shop + { from_x = 3276, from_y = 3168, to_x = 3270, to_y = 3167 }, # al_kharid_bank_entrance_to_bank +] diff --git a/data/bot/teleport.bots.toml b/data/bot/teleport.bots.toml index 4137c2b46a..b95448ee10 100644 --- a/data/bot/teleport.bots.toml +++ b/data/bot/teleport.bots.toml @@ -1,36 +1,35 @@ - -[ring_of_duelling_equipped] -type = "teleport" -weight = 10 -requires = [ - { equips = "ring_of_duelling_*" } -] -actions = [ - { option = "Castle Wars", interface = "equipment:inventory" }, -] -produces = [ - { location = "castle_wars_teleport" } -] - -[ring_of_duelling] -type = "teleport" -weight = 8 -requires = [ - { carries = "ring_of_duelling_*" } -] -actions = [ - { option = "Rub", interface = "inventory:inventory" }, - { option = "Select", interface = "choice:line1" }, -] -produces = [ - { location = "castle_wars_teleport" } -] +#[ring_of_duelling_equipped] +#type = "shortcut" +#weight = 10 +#requires = [ +# { equips = "ring_of_duelling_*" } +#] +#actions = [ +# { option = "Castle Wars", interface = "equipment:inventory" }, +#] +#produces = [ +# { location = "castle_wars_teleport" } +#] +# +#[ring_of_duelling] +#type = "shortcut" +#weight = 8 +#requires = [ +# { carries = "ring_of_duelling_*" } +#] +#actions = [ +# { option = "Rub", interface = "inventory:inventory" }, +# { option = "Select", interface = "choice:line1" }, +#] +#produces = [ +# { location = "castle_wars_teleport" } +#] [teleport_varrock] -type = "teleport" +type = "shortcut" weight = 10 requires = [ - { variable = "spellbook", value = "normal" }, + { variable = "spellbook_config", value = 0, default = 0 }, ] setup = [ { carries = "fire_rune", amount = 1 }, @@ -38,21 +37,21 @@ setup = [ { carries = "law_rune", amount = 1 }, ] actions = [ - { option = "Cast", interface = "normal_spellbook:varrock_teleport" }, + { option = "Cast", interface = "modern_spellbook:varrock_teleport" }, ] produces = [ { location = "varrock_teleport" } ] [teleport_lumbridge] -type = "teleport" +type = "shortcut" weight = 5 requires = [ - { variable = "spellbook", value = "normal" }, - { variable = "lumbridge_cooldown", value = "normal" }, + { variable = "spellbook_config", value = 0, default = 0 }, + { variable = "lumbridge_cooldown", value = "normal" }, # clock/timer teleport_delay ] actions = [ - { option = "Cast", interface = "normal_spellbook:home_teleport" }, + { option = "Cast", interface = "modern_spellbook:lumbridge_home_teleport" }, ] produces = [ { location = "lumbridge_teleport" } diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/data/definition/Areas.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/data/definition/Areas.kt index 4d4360ecc0..6fcc9c2b99 100644 --- a/engine/src/main/kotlin/world/gregs/voidps/engine/data/definition/Areas.kt +++ b/engine/src/main/kotlin/world/gregs/voidps/engine/data/definition/Areas.kt @@ -5,6 +5,7 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap import it.unimi.dsi.fastutil.ints.IntArrayList import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet +import org.jetbrains.annotations.TestOnly import world.gregs.config.Config import world.gregs.voidps.engine.timedLoad import world.gregs.voidps.type.Area @@ -103,7 +104,8 @@ object Areas { return this } - internal fun set(named: Map, tagged: Map>, areas: Map>) { + @TestOnly + fun set(named: Map, tagged: Map>, areas: Map>) { this.named = named this.tagged = tagged this.areas = areas diff --git a/game/src/main/kotlin/content/bot/BotManager.kt b/game/src/main/kotlin/content/bot/BotManager.kt index deb20a319d..a25b2247c3 100644 --- a/game/src/main/kotlin/content/bot/BotManager.kt +++ b/game/src/main/kotlin/content/bot/BotManager.kt @@ -3,6 +3,8 @@ package content.bot import com.github.michaelbull.logging.InlineLogger import content.bot.action.* import content.bot.fact.Condition +import content.bot.interact.path.Graph +import content.bot.interact.path.Graph.Companion.loadGraph import world.gregs.voidps.engine.data.ConfigFiles import world.gregs.voidps.engine.data.Settings import world.gregs.voidps.engine.event.AuditLog @@ -19,6 +21,7 @@ class BotManager( private val resolvers: MutableMap> = mutableMapOf(), private val groups: MutableMap> = mutableMapOf(), ) : Runnable { + lateinit var graph: Graph val slots = ActivitySlots() val bots = mutableListOf() private val logger = InlineLogger("BotManager") @@ -61,7 +64,9 @@ class BotManager( } fun load(files: ConfigFiles): BotManager { - loadActivities(files.list(Settings["bots.definitions"]), activities, groups, resolvers) + val shortcuts = mutableListOf() + loadActivities(files.list(Settings["bots.definitions"]), activities, groups, resolvers, shortcuts) + graph = loadGraph(files.list(Settings["bots.nav.definitions"]), shortcuts) return this } diff --git a/game/src/main/kotlin/content/bot/action/BotActivity.kt b/game/src/main/kotlin/content/bot/action/BotActivity.kt index 00daf7276f..82198f622b 100644 --- a/game/src/main/kotlin/content/bot/action/BotActivity.kt +++ b/game/src/main/kotlin/content/bot/action/BotActivity.kt @@ -21,7 +21,13 @@ data class BotActivity( override val produces: Set = emptySet(), ) : Behaviour -fun loadActivities(paths: List, activities: MutableMap, groups: MutableMap>, resolvers: MutableMap>) { +fun loadActivities( + paths: List, + activities: MutableMap, + groups: MutableMap>, + resolvers: MutableMap>, + shortcuts: MutableList, +) { val fragments = mutableMapOf() timedLoad("bot activity") { val reqClones = mutableMapOf() @@ -70,6 +76,8 @@ fun loadActivities(paths: List, activities: MutableMap, activities: MutableMap() actions.addAll(fragment.actions) fragment.resolveActions(template, actions) - if (fragment.type == "resolver") { - for (fact in fragment.produces) { - for (key in fact.keys()) { - resolvers.getOrPut(key) { mutableListOf() }.add(Resolver(id, fragment.weight, requirements, resolvables, actions)) + when (fragment.type) { + "resolver" -> { + for (fact in fragment.produces) { + for (key in fact.keys()) { + resolvers.getOrPut(key) { mutableListOf() }.add(Resolver(id, fragment.weight, requirements, resolvables, actions)) + } } } - } else { - activities[id] = BotActivity(id, fragment.capacity, requirements, resolvables, actions) + "shortcut" -> shortcuts.add(NavigationShortcut(id, fragment.weight, requirements, resolvables, actions = actions)) + else -> activities[id] = BotActivity(id, fragment.capacity, requirements, resolvables, actions) } } // Templates aren't selectable activities @@ -161,7 +171,7 @@ private fun ConfigReader.fields(): Map { return map } -private fun ConfigReader.requirements(list: MutableList) { +fun ConfigReader.requirements(list: MutableList) { while (nextElement()) { var type = "" var id = "" @@ -232,7 +242,7 @@ private fun ConfigReader.requirements(list: MutableList) { list.sortBy { it.priority() } } -private fun getRequirement(type: String, id: String, min: Int?, max: Int?, value: Any?): Condition? = when (type) { +private fun getRequirement(type: String, id: String, min: Int?, max: Int?, value: Any?, default: Any?): Condition? = when (type) { "skill" -> Condition.range(Fact.SkillLevel.of(id), min, max) "carries" -> if (id.contains(",")) { Condition.Any(id.split(",").map { Condition.range(Fact.InventoryCount(it), min, max) }) diff --git a/game/src/main/kotlin/content/bot/action/NavigationShortcut.kt b/game/src/main/kotlin/content/bot/action/NavigationShortcut.kt new file mode 100644 index 0000000000..abad9917eb --- /dev/null +++ b/game/src/main/kotlin/content/bot/action/NavigationShortcut.kt @@ -0,0 +1,12 @@ +package content.bot.action + +import content.bot.fact.Condition + +data class NavigationShortcut( + override val id: String, + val weight: Int, + override val requires: List = emptyList(), + override val resolve: List = emptyList(), + override val actions: List = emptyList(), + override val produces: Set = emptySet(), +) : Behaviour \ No newline at end of file diff --git a/game/src/main/kotlin/content/bot/interact/path/Graph.kt b/game/src/main/kotlin/content/bot/interact/path/Graph.kt new file mode 100644 index 0000000000..3fb8616b55 --- /dev/null +++ b/game/src/main/kotlin/content/bot/interact/path/Graph.kt @@ -0,0 +1,276 @@ +package content.bot.interact.path + +import content.bot.action.BotAction +import content.bot.action.NavigationShortcut +import content.bot.action.actions +import content.bot.action.requirements +import content.bot.bot +import content.bot.fact.Condition +import content.bot.isBot +import world.gregs.config.Config +import world.gregs.voidps.engine.data.definition.Areas +import world.gregs.voidps.engine.entity.character.player.Player +import world.gregs.voidps.engine.timedLoad +import world.gregs.voidps.type.Distance +import world.gregs.voidps.type.Tile +import java.util.PriorityQueue + +class Graph( + val endNodes: IntArray = intArrayOf(), + val edgeWeights: IntArray = intArrayOf(), + val edgeConditions: Array?> = emptyArray(), + val actions: Array?> = emptyArray(), + val adjacentEdges: Array = emptyArray(), + val tiles: IntArray = intArrayOf(), + val shortcuts: Map = emptyMap(), + var nodeCount: Int = 0, +) { + + fun actions(edge: Int): List? = actions[edge] + + fun conditions(edge: Int): List? = edgeConditions[edge] + + fun tile(edge: Int): Tile { + val nodeIndex = endNodes[edge] + return Tile(tiles[nodeIndex]) + } + + fun findNearest(player: Player, tag: String, output: MutableList): Boolean { + val start = startingPoints(player) + return find(player, output, start, target = { + val tile = Tile(tiles[it]) + Areas.tagged(tag).any { a -> tile in a.area } + }) + } + + fun find(player: Player, output: MutableList, area: String): Boolean { + val start = startingPoints(player) + return find(player, output, start, target = { Tile(tiles[it]) in Areas[area] }) + } + + internal fun startingPoints(player: Player): Set = buildSet { + for (index in tiles.indices) { + val tile = tiles[index] + if (!player.tile.within(Tile(tile), 25)) { + continue + } + add(index) + } + val blocked = if (player.isBot) player.bot.blocked else emptySet() + for (shortcut in shortcuts.values) { + if (blocked.contains(shortcut.id)) { + continue + } + if (shortcut.requires.any { !it.check(player) }) { + continue + } + add(0) + break + } + } + + fun find(player: Player, output: MutableList, start: Int, target: Int) = find(player, output, setOf(start)) { it == target } + + fun find(player: Player, output: MutableList, start: Set, target: Int) = find(player, output, start) { it == target } + + fun find(player: Player, output: MutableList, start: Int, target: (Int) -> Boolean) = find(player, output, setOf(start), target) + + fun find(player: Player, output: MutableList, startingPoints: Set, target: (Int) -> Boolean): Boolean { + output.clear() + val queue = PriorityQueue() + val visited = BooleanArray(nodeCount) + val distance = IntArray(nodeCount) + distance.fill(Int.MAX_VALUE) + val previousNode = IntArray(nodeCount) + val previousEdge = IntArray(nodeCount) + + for (start in startingPoints) { + distance[start] = 0 + queue.add(Node(start, 0)) + } + while (queue.isNotEmpty()) { + val (node, cost) = queue.poll() + if (target(node)) { + // Reconstruct the path + var previous = node + while (distance[previous] != 0) { + output.add(0, previousEdge[previous]) + previous = previousNode[previous] + } + return true + } + if (visited[node]) { + continue + } + visited[node] = true + for (edge in adjacentEdges[node] ?: continue) { + val to = endNodes[edge] + if (visited[to]) { + continue + } + val weight = edgeWeights[edge] + if (cost + weight >= distance[to]) { + continue + } + val conditions = edgeConditions[edge] + if (conditions != null && conditions.any { !it.check(player) }) { + continue + } + distance[to] = cost + weight + previousNode[to] = node + previousEdge[to] = edge + queue.add(Node(to, cost + weight)) + } + } + return false + } + + private data class Node(val index: Int, val cost: Int) : Comparable { + override fun compareTo(other: Node) = cost.compareTo(other.cost) + } + + class Builder { + // Nodes + val tiles = LinkedHashSet() + val nodes = mutableSetOf() + + // Edges + val endNodes = mutableListOf() + val weights = mutableListOf() + val conditions = mutableListOf?>() + val actions = mutableListOf?>() + val edges = mutableMapOf>() + var edgeCount = 0 + + val shortcuts = mutableMapOf() + + init { + tiles.add(Tile.EMPTY) // Virtual + nodes.add(0) + } + + fun add(shortcut: NavigationShortcut): Int { + val first = shortcut.produces.filterIsInstance().firstOrNull() ?: throw IllegalArgumentException("Shortcut requires location product ${shortcut.id}") + val area = Areas[first.area] + val end = tiles.indexOfFirst { it in area } + if (end == -1) { + throw IllegalArgumentException("Unable to find nav graph tile in shortcut area '${first.area}'.") + } + val index = addEdge(0, end, shortcut.weight, shortcut.actions, shortcut.requires) + shortcuts[index] = shortcut + return index + } + + fun addBiEdge(from: Tile, to: Tile, weight: Int, actions: List) { + val start = add(from) + val end = add(to) + addEdge(start, end, weight, actions) + addEdge(end, start, weight, actions) + } + + fun addEdge(from: Tile, to: Tile, weight: Int, actions: List, conditions: List?) { + val start = add(from) + val end = add(to) + addEdge(start, end, weight, actions, conditions) + } + + fun add(tile: Tile): Int { + if (tiles.add(tile)) { + return tiles.size - 1 + } + return tiles.indexOf(tile) + } + + fun addBiEdge(start: Int, end: Int, weight: Int) { + addEdge(start, end, weight) + addEdge(end, start, weight) + } + + fun addEdge(start: Int, end: Int, weight: Int, actions: List? = null, conditions: List? = null): Int { + val edgeIndex = edgeCount++ + nodes.add(start) + nodes.add(end) + edges.getOrPut(start) { mutableListOf() }.add(edgeIndex) + weights.add(weight) + endNodes.add(end) + this.conditions.add(conditions) + this.actions.add(actions) + return edgeIndex + } + + fun build() = Graph( + endNodes = endNodes.toIntArray(), + edgeWeights = weights.toIntArray(), + edgeConditions = conditions.toTypedArray(), + actions = actions.toTypedArray(), + adjacentEdges = Array(nodes.size) { edges[it]?.toIntArray() }, + nodeCount = nodes.size, + tiles = tiles.map { it.id }.toIntArray(), + shortcuts = shortcuts, + ) + + fun print() { + for (start in edges.keys.sorted()) { + val adj = edges[start] ?: continue + for (edge in adj.sorted()) { + val end = endNodes[edge] + val weight = weights[edge] + println("Edge ${edge}: $start -> $end ($weight)") + } + } + println("Nodes: ${nodes.size} edges: $edgeCount") + } + } + + companion object { + fun loadGraph(paths: List, shortcuts: List): Graph { + val builder = Builder() + timedLoad("nav graph edge") { + for (path in paths) { + Config.fileReader(path) { + while (nextPair()) { + val list = key() + assert(list == "edges") { "Expected edges list, got: $list ${exception()}" } + while (nextElement()) { + var fromX = 0 + var fromY = 0 + var toX = 0 + var toY = 0 + var cost = 0 + val actions: MutableList = mutableListOf() + val requirements: MutableList = mutableListOf() + while (nextEntry()) { + when (val key = key()) { + "from_x" -> fromX = int() + "from_y" -> fromY = int() + "to_x" -> toX = int() + "to_y" -> toY = int() + "cost" -> cost = int() + "actions" -> actions(actions) + "conditions" -> requirements(requirements) + else -> throw IllegalArgumentException("Unexpected key: '$key' ${exception()}") + } + } + when { + actions.isEmpty() -> { + val cost = Distance.manhattan(fromX, fromY, toX, toY) + actions.add(BotAction.WalkTo(toX, toY)) + builder.addBiEdge(Tile(fromX, fromY), Tile(toX, toY), cost, actions) + } + requirements.isEmpty() -> builder.addEdge(Tile(fromX, fromY), Tile(toX, toY), cost, actions, null) + else -> builder.addEdge(Tile(fromX, fromY), Tile(toX, toY), cost, actions, requirements) + } + } + } + } + } + for (shortcut in shortcuts) { + builder.add(shortcut) + } + builder.edgeCount + } +// builder.print() + return builder.build() + } + } +} \ No newline at end of file diff --git a/game/src/main/kotlin/content/entity/player/command/PathFindingCommands.kt b/game/src/main/kotlin/content/entity/player/command/PathFindingCommands.kt index 81aa1392b1..a49fea9fdd 100644 --- a/game/src/main/kotlin/content/entity/player/command/PathFindingCommands.kt +++ b/game/src/main/kotlin/content/entity/player/command/PathFindingCommands.kt @@ -1,5 +1,8 @@ package content.entity.player.command +import content.bot.Bot +import content.bot.BotManager +import content.bot.bot import content.bot.interact.path.Dijkstra import content.bot.interact.path.EdgeTraversal import content.bot.interact.path.NodeTargetStrategy @@ -118,6 +121,20 @@ class PathFindingCommands(val patrols: PatrolDefinitions) : Script { // println(pf.findPath(3205, 3220, 3205, 3223, 2)) } + adminCommand("walk_test") { + val manager = get() + val list = mutableListOf() + set("bot", Bot(this)) + bot.blocked.add("teleport_varrock") + // TODO convert nav_graph.toml to n + println(manager.graph.find(this, list, "varrock_teleport")) + println(list) + for (edge in list) { + println(manager.graph.tile(edge)) + } + + } + adminCommand("walk_to_bank") { val east = Tile(3179, 3433).toCuboid(15, 14) val west = Tile(3250, 3417).toCuboid(7, 8) diff --git a/game/src/main/resources/game.properties b/game/src/main/resources/game.properties index d55737fe87..eb00c09d2d 100644 --- a/game/src/main/resources/game.properties +++ b/game/src/main/resources/game.properties @@ -256,6 +256,9 @@ bots.namePrefix="" # File ending for bot definitions bots.definitions=bots.toml +# File ending for bot navigation graph definitions +bots.nav.definitions=nav-edges.toml + #=================================== # Storage & File System #=================================== diff --git a/game/src/test/kotlin/content/bot/interact/path/GraphTest.kt b/game/src/test/kotlin/content/bot/interact/path/GraphTest.kt new file mode 100644 index 0000000000..03daf203fb --- /dev/null +++ b/game/src/test/kotlin/content/bot/interact/path/GraphTest.kt @@ -0,0 +1,320 @@ +package content.bot.interact.path + +import content.bot.action.NavigationShortcut +import content.bot.fact.Condition +import content.bot.fact.Fact +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Test +import world.gregs.voidps.engine.data.definition.AreaDefinition +import world.gregs.voidps.engine.data.definition.Areas +import world.gregs.voidps.engine.entity.character.player.Player +import world.gregs.voidps.type.Tile +import world.gregs.voidps.type.area.Rectangle +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class GraphTest { + + @Test + fun `Shortest path is found`() { + /* + + B + 10 / | + A | 1 + 1 \ | + E + + */ + val builder = Graph.Builder() + val a = 0 + val b = 1 + val c = 2 + builder.addEdge(a, b, 10) + builder.addEdge(a, c, 1) + builder.addEdge(c, b, 1) +// builder.print() + + val output = mutableListOf() + val success = builder.build().find(Player(), output, a, b) + assertTrue(success) + assertEquals(listOf(1, 2), output) + } + + @Test + fun `Branching path`() { + /* + 6 + B-----E + 2 / |5 / | \ 9 + A | /3 |1 C + 8 \ | / | / 3 + D-----F + 2 + */ + val builder = Graph.Builder() + val a = 0 + val b = 1 + val c = 2 + val d = 3 + val e = 4 + val f = 5 + val ab = builder.addEdge(a, b, 2) + builder.addEdge(a, d, 8) + builder.addEdge(b, e, 6) + val bd = builder.addEdge(b, d, 5) + builder.addEdge(d, e, 3) + val df = builder.addEdge(d, f, 2) + builder.addEdge(e, f, 1) + builder.addEdge(e, c, 9) + val fc = builder.addEdge(f, c, 3) +// builder.print() + + val output = mutableListOf() + val success = builder.build().find(Player(), output, a, c) + assertTrue(success) + assertEquals(listOf(ab, bd, df, fc), output) + } + + @Test + fun `Differing starting location`() { + /* + 1 + ------- + / 2 \ + G---D | + 2 | | 1 | + F B--/ + 3 | / + C / 1 + 4 |/ + A + */ + val builder = Graph.Builder() + val a = 0 + val b = 1 + val c = 2 + val d = 3 + val e = 4 + val f = 5 + val g = 6 + builder.addEdge(a, b, 1) + builder.addEdge(c, a, 4) + val bd = builder.addEdge(b, d, 1) + builder.addEdge(b, e, 3) + builder.addEdge(f, c, 5) + builder.addEdge(d, g, 2) + val gb = builder.addEdge(g, b, 1) + val fg = builder.addEdge(f, g, 2) +// builder.print() + + val output = mutableListOf() + val success = builder.build().find(Player(), output, f, d) + assertTrue(success) + assertEquals(listOf(fg, gb, bd), output) + } + + @Test + fun `Condition is avoided if failed`() { + /* + 3 + B-----C + 7 / |8 / | + A | /2 |6 + 1 \ | / | + E-----D + 7-X + */ + val builder = Graph.Builder() + val a = 0 + val b = 1 + val c = 2 + val d = 3 + val e = 4 + builder.addEdge(a, b, 7) + val ae = builder.addEdge(a, e, 1) + builder.addEdge(b, e, 8) + builder.addEdge(b, c, 3) + val cd = builder.addEdge(c, d, 6) + val ec = builder.addEdge(e, c, 2) + builder.addEdge(e, d, 7, conditions = listOf(Condition.Equals(Fact.PlayerTile, Tile(100)))) +// builder.print() + + val output = mutableListOf() + val success = builder.build().find(Player(), output, a, d) + assertTrue(success) + assertEquals(listOf(ae, ec, cd), output) + } + + @Test + fun `Can only traverse in one direction`() { + /* + B + 1 / + A + */ + val builder = Graph.Builder() + val a = 0 + val b = 1 + builder.addEdge(a, b, 10) +// builder.print() + + val output = mutableListOf() + val success = builder.build().find(Player(), output, b, a) + assertFalse(success) + } + + + @Test + fun `Multiple starting points`() { + /* + 1 + ------- + / 2 \ + G---D | + 2 | | 1 | + F B--/ + 3 | / + C / 1 + 4 |/ + A + */ + val builder = Graph.Builder() + val a = 0 + val b = 1 + val c = 2 + val d = 3 + val e = 4 + val f = 5 + val g = 6 + val ab = builder.addEdge(a, b, 1) + builder.addEdge(c, a, 4) + val bd = builder.addEdge(b, d, 1) + builder.addEdge(b, e, 3) + builder.addEdge(f, c, 5) + builder.addEdge(d, g, 2) + builder.addEdge(g, b, 1) + builder.addEdge(f, g, 2) +// builder.print() + + val output = mutableListOf() + val success = builder.build().find(Player(), output, setOf(f, a), d) + assertTrue(success) + assertEquals(listOf(ab, bd), output) + } + + @Test + fun `Find returns shortest path`() { + val builder = Graph.Builder() + + val a = Tile(0) + val b = Tile(1) + val c = Tile(2) + + builder.addBiEdge(a, b, weight = 1, actions = emptyList()) + builder.addBiEdge(b, c, weight = 1, actions = emptyList()) + builder.addBiEdge(a, c, weight = 10, actions = emptyList()) + + val graph = builder.build() + val player = Player() + player.tile = a + + val path = mutableListOf() + val found = graph.find(player, path, start = 0, target = 2) + + assertTrue(found) + assertEquals(2, path.size, "Shortest path should be two edges, not direct edge") + } + + @Test + fun `Find respects edge conditions`() { + val builder = Graph.Builder() + + val a = Tile(0) + val b = Tile(1) + + builder.addEdge( + from = a, + to = b, + weight = 1, + actions = emptyList(), + conditions = listOf(Condition.Equals(Fact.PlayerTile, Tile(100))) + ) + + val graph = builder.build() + val player = Player() + player.tile = a + + val path = mutableListOf() + val found = graph.find(player, path, start = 0, target = 1) + + assertFalse(found, "Edge condition blocks traversal") + assertTrue(path.isEmpty()) + } + + @Test + fun `Starting points include nearby tiles`() { + val builder = Graph.Builder() + + val a = Tile(0) + val b = Tile(20) + val c = Tile(100) + + builder.add(a) + builder.add(b) + builder.add(c) + + val graph = builder.build() + val player = Player() + player.tile = Tile(10) + + val starts = graph.startingPoints(player) + + assertTrue(starts.contains(0)) + assertTrue(starts.contains(1)) + assertFalse(starts.contains(2)) + } + + @Test + fun `Shortcut adds starting point when requirements met`() { + Areas.set(mapOf("town" to AreaDefinition("town", Rectangle(75, 75, 75, 75), setOf())), mapOf(), mapOf()) + + val shortcut = NavigationShortcut( + id = "teleport", + weight = 1, + requires = listOf(Condition.Equals(Fact.PlayerTile, Tile(50, 50))), + produces = setOf(Condition.Area(Fact.PlayerTile, "town")) + ) + + val builder = Graph.Builder() + val edge = builder.add(shortcut) + + val graph = builder.build() + val player = Player() + player.tile = Tile(50, 50) + + val starts = graph.startingPoints(player) + + assertTrue(starts.contains(edge)) + } + + @Test + fun `Path reconstruction produces correct edge order`() { + val builder = Graph.Builder() + + val a = Tile(0) + + val e1 = builder.addEdge(0, 1, weight = 1) + val e2 = builder.addEdge(1, 2, weight = 1) + + val graph = builder.build() + val player = Player() + player.tile = a + + val path = mutableListOf() + val found = graph.find(player, path, start = 0, target = 2) + + assertTrue(found) + assertEquals(listOf(e1, e2), path) + } +} \ No newline at end of file From dad1a8f3ae7a9cb9e4f84026f075dc69db885f4c Mon Sep 17 00:00:00 2001 From: GregHib Date: Mon, 2 Feb 2026 13:53:24 +0000 Subject: [PATCH 029/101] Replace nav graph in MapViewer and add reload button --- .../gregs/voidps/tools/map/view/MapViewer.kt | 5 +- .../voidps/tools/map/view/draw/GraphDrawer.kt | 83 +++++++++---------- .../voidps/tools/map/view/draw/MapView.kt | 13 ++- .../voidps/tools/map/view/ui/OptionsPane.kt | 12 +++ 4 files changed, 66 insertions(+), 47 deletions(-) diff --git a/tools/src/main/kotlin/world/gregs/voidps/tools/map/view/MapViewer.kt b/tools/src/main/kotlin/world/gregs/voidps/tools/map/view/MapViewer.kt index 06065fbe2b..6b46ab3443 100644 --- a/tools/src/main/kotlin/world/gregs/voidps/tools/map/view/MapViewer.kt +++ b/tools/src/main/kotlin/world/gregs/voidps/tools/map/view/MapViewer.kt @@ -3,6 +3,7 @@ package world.gregs.voidps.tools.map.view import com.github.weisj.darklaf.LafManager import com.github.weisj.darklaf.LafManager.getPreferredThemeStyle import content.bot.interact.navigation.graph.NavigationGraph +import content.bot.interact.path.Graph import world.gregs.voidps.cache.CacheDelegate import world.gregs.voidps.cache.definition.decoder.ObjectDecoder import world.gregs.voidps.engine.data.Settings @@ -30,13 +31,13 @@ class MapViewer { val files = configFiles() ObjectDefinitions.init(decoder).load(files.list(Settings["definitions.objects"])) Areas.load(files.list(Settings["map.areas"])) - val nav = NavigationGraph().load(files.find(Settings["map.navGraph"])) + val graph = Graph.loadGraph(files.list(Settings["bots.nav.definitions"]), emptyList()) if (DISPLAY_AREA_COLLISIONS || DISPLAY_ALL_COLLISIONS) { ObjectDefinitions.init(ObjectDecoder(member = true, lowDetail = false).load(cache)) .load(files.list(Settings["definitions.objects"])) MapDefinitions(CollisionDecoder(), cache).load(files) } - frame.add(MapView(nav, files.list(Settings["map.areas"]))) + frame.add(MapView(graph, files.list(Settings["map.areas"]))) frame.pack() frame.setLocationRelativeTo(null) frame.isVisible = true diff --git a/tools/src/main/kotlin/world/gregs/voidps/tools/map/view/draw/GraphDrawer.kt b/tools/src/main/kotlin/world/gregs/voidps/tools/map/view/draw/GraphDrawer.kt index ad0c97c054..d8ead0fb9e 100644 --- a/tools/src/main/kotlin/world/gregs/voidps/tools/map/view/draw/GraphDrawer.kt +++ b/tools/src/main/kotlin/world/gregs/voidps/tools/map/view/draw/GraphDrawer.kt @@ -1,6 +1,6 @@ package world.gregs.voidps.tools.map.view.draw -import content.bot.interact.navigation.graph.NavigationGraph +import content.bot.interact.path.Graph import org.rsmod.game.pathfinder.StepValidator import org.rsmod.game.pathfinder.collision.CollisionStrategies import org.rsmod.game.pathfinder.collision.CollisionStrategy @@ -11,14 +11,13 @@ import world.gregs.voidps.tools.map.view.MapViewer.Companion.FILTER_VIEWPORT import world.gregs.voidps.tools.map.view.graph.Area import world.gregs.voidps.tools.map.view.graph.AreaSet import world.gregs.voidps.tools.map.view.graph.Link -import world.gregs.voidps.type.Distance import world.gregs.voidps.type.Tile import java.awt.* import kotlin.math.sqrt class GraphDrawer( private val view: MapView, - private val nav: NavigationGraph?, + private var graph: Graph?, private val area: AreaSet, ) { @@ -29,11 +28,10 @@ class GraphDrawer( private val areaColour = Color(1.0f, 0.0f, 1.0f, 0.2f) private val walkableColour = Color(0.0f, 1.0f, 0.0f, 0.3f) private val collisionColour = Color(1.0f, 0.0f, 0.0f, 0.3f) - private val distances = nav?.nodes?.map { nav.get(it) }?.flatten()?.distinct()?.mapNotNull { edge -> - val start = edge.start as? Tile ?: return@mapNotNull null - val end = edge.end as? Tile ?: return@mapNotNull null - edge to Distance.chebyshev(start.x, start.y, end.x, end.y) - }?.toMap() + + fun reload(graph: Graph?) { + this.graph = graph + } fun repaint(link: Link) { repaint(link.start) @@ -49,40 +47,41 @@ class GraphDrawer( fun draw(g: Graphics) { g.color = linkColour - nav?.nodes?.filterIsInstance()?.forEach { node -> - if (node.level != view.level) { - return@forEach - } - val viewX = view.mapToViewX(node.x) - val viewY = view.mapToViewY(view.flipMapY(node.y)) - if (!view.contains(viewX, viewY)) { - return@forEach - } - val width = view.mapToImageX(1) - val height = view.mapToImageY(1) - g.fillOval(viewX, viewY, width, height) - - val edges = nav.getAdjacent(node) - edges.forEachIndexed { index, edge -> - val start = edge.start as? Tile ?: return@forEachIndexed - val end = edge.end as? Tile ?: return@forEachIndexed - if (start.level != view.level || end.level != view.level) { - return@forEachIndexed + if (graph != null) { + val graph = graph!! + for (i in 1 until graph.nodeCount) { + val tile = Tile(graph.tiles[i]) + if (tile.level != view.level) { + continue + } + val viewX = view.mapToViewX(tile.x) + val viewY = view.mapToViewY(view.flipMapY(tile.y)) + if (!view.contains(viewX, viewY)) { + continue } - val distance = distances?.get(edge) - val endX = view.mapToViewX(end.x) + width / 2 - val endY = view.mapToViewY(view.flipMapY(end.y)) + height / 2 - val offset = width / 4 - val startX = viewX + width / 2 - val startY = viewY + height / 2 - if (view.scale > 10) { - g.drawArrowHead(startX, startY, endX, endY, offset * 3, width / 2, index.toString()) - if (distance != null) { + val width = view.mapToImageX(1) + val height = view.mapToImageY(1) + g.fillOval(viewX, viewY, width, height) + + val edges = graph.adjacentEdges[i] + edges?.forEachIndexed { index, edge -> + val end = graph.tile(edge) + if (tile.level != view.level || end.level != view.level) { + return@forEachIndexed + } + val distance = graph.edgeWeights[edge] + val endX = view.mapToViewX(end.x) + width / 2 + val endY = view.mapToViewY(view.flipMapY(end.y)) + height / 2 + val offset = width / 4 + val startX = viewX + width / 2 + val startY = viewY + height / 2 + if (view.scale > 10) { + g.drawArrowHead(startX, startY, endX, endY, offset * 3, width / 2, index.toString()) g.drawString(distance.toString(), startX + (endX - startX) / 2, startY + (endY - startY) / 2) } - } - if (view.contains(startX, startY) || view.contains(endX, endY)) { - g.drawLine(startX, startY, endX, endY) + if (view.contains(startX, startY) || view.contains(endX, endY)) { + g.drawLine(startX, startY, endX, endY) + } } } } @@ -179,9 +178,9 @@ class GraphDrawer( } private fun canTravel(steps: StepValidator, x: Int, y: Int, level: Int, collision: CollisionStrategy) = steps.canTravel(x = x, z = y - 1, level = level, size = 1, offsetX = 0, offsetZ = 1, extraFlag = 0, collision = collision) || - steps.canTravel(x = x, z = y + 1, level = level, size = 1, offsetX = 0, offsetZ = -1, extraFlag = 0, collision = collision) || - steps.canTravel(x = x - 1, z = y, level = level, size = 1, offsetX = 1, offsetZ = 0, extraFlag = 0, collision = collision) || - steps.canTravel(x = x + 1, z = y, level = level, size = 1, offsetX = -1, offsetZ = 0, extraFlag = 0, collision = collision) + steps.canTravel(x = x, z = y + 1, level = level, size = 1, offsetX = 0, offsetZ = -1, extraFlag = 0, collision = collision) || + steps.canTravel(x = x - 1, z = y, level = level, size = 1, offsetX = 1, offsetZ = 0, extraFlag = 0, collision = collision) || + steps.canTravel(x = x + 1, z = y, level = level, size = 1, offsetX = -1, offsetZ = 0, extraFlag = 0, collision = collision) /** * Draws an arrow of [length] at [offset] along the line [x1], [y1] -> [x2], [y2] diff --git a/tools/src/main/kotlin/world/gregs/voidps/tools/map/view/draw/MapView.kt b/tools/src/main/kotlin/world/gregs/voidps/tools/map/view/draw/MapView.kt index 05766037f3..5e28f713ce 100644 --- a/tools/src/main/kotlin/world/gregs/voidps/tools/map/view/draw/MapView.kt +++ b/tools/src/main/kotlin/world/gregs/voidps/tools/map/view/draw/MapView.kt @@ -1,7 +1,10 @@ package world.gregs.voidps.tools.map.view.draw -import content.bot.interact.navigation.graph.NavigationGraph +import content.bot.interact.path.Graph import kotlinx.coroutines.* +import world.gregs.voidps.engine.data.ConfigFiles +import world.gregs.voidps.engine.data.Settings +import world.gregs.voidps.engine.data.configFiles import world.gregs.voidps.tools.map.view.draw.WorldMap.Companion.flipRegionY import world.gregs.voidps.tools.map.view.graph.AreaSet import world.gregs.voidps.tools.map.view.interact.MouseDrag @@ -16,7 +19,7 @@ import java.awt.Graphics import javax.swing.JPanel import javax.swing.SwingUtilities -class MapView(nav: NavigationGraph?, private val areaFiles: List) : JPanel() { +class MapView(graph: Graph, private val areaFiles: List) : JPanel() { private val options = OptionsPane(this) private val areaSet = AreaSet.load(areaFiles) @@ -28,7 +31,7 @@ class MapView(nav: NavigationGraph?, private val areaFiles: List) : JPan private val hover = MouseHover(highlight, area) private val map = WorldMap(this) private val resize = ResizeListener(map) - private val graph = GraphDrawer(this, nav, areaSet) + private val graph = GraphDrawer(this, graph, areaSet) // private val click = MouseClick(this, nav, graph, area, areaSet) private val apc = AreaPointConnector(this, areaSet) @@ -87,6 +90,10 @@ class MapView(nav: NavigationGraph?, private val areaFiles: List) : JPan area.update(x, y) } + fun reload(files: ConfigFiles = configFiles()) { + graph.reload(Graph.loadGraph(files.list(Settings["bots.nav.definitions"]), emptyList())) + } + fun updateLevel(level: Int) { if (this.level != level) { this.level = level diff --git a/tools/src/main/kotlin/world/gregs/voidps/tools/map/view/ui/OptionsPane.kt b/tools/src/main/kotlin/world/gregs/voidps/tools/map/view/ui/OptionsPane.kt index 6403704653..a9aa74ec60 100644 --- a/tools/src/main/kotlin/world/gregs/voidps/tools/map/view/ui/OptionsPane.kt +++ b/tools/src/main/kotlin/world/gregs/voidps/tools/map/view/ui/OptionsPane.kt @@ -55,5 +55,17 @@ class OptionsPane(private val view: MapView) : JPanel() { ) } add(levelControls) + + val refresh = JPanel().apply { + layout = BoxLayout(this, BoxLayout.X_AXIS) + add( + JButton("Refresh").apply { + addActionListener { + view.reload() + } + }, + ) + } + add(refresh) } } From 748c417a7464f21f209bbb2d27df6eaa9907acbe Mon Sep 17 00:00:00 2001 From: GregHib Date: Mon, 2 Feb 2026 14:05:51 +0000 Subject: [PATCH 030/101] Reload areas too --- .../kotlin/world/gregs/voidps/tools/map/view/MapViewer.kt | 5 +++-- .../world/gregs/voidps/tools/map/view/draw/GraphDrawer.kt | 2 +- .../world/gregs/voidps/tools/map/view/draw/MapView.kt | 8 +++++--- .../world/gregs/voidps/tools/map/view/graph/AreaSet.kt | 5 ++--- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/tools/src/main/kotlin/world/gregs/voidps/tools/map/view/MapViewer.kt b/tools/src/main/kotlin/world/gregs/voidps/tools/map/view/MapViewer.kt index 6b46ab3443..76abfa81c3 100644 --- a/tools/src/main/kotlin/world/gregs/voidps/tools/map/view/MapViewer.kt +++ b/tools/src/main/kotlin/world/gregs/voidps/tools/map/view/MapViewer.kt @@ -31,13 +31,14 @@ class MapViewer { val files = configFiles() ObjectDefinitions.init(decoder).load(files.list(Settings["definitions.objects"])) Areas.load(files.list(Settings["map.areas"])) - val graph = Graph.loadGraph(files.list(Settings["bots.nav.definitions"]), emptyList()) if (DISPLAY_AREA_COLLISIONS || DISPLAY_ALL_COLLISIONS) { ObjectDefinitions.init(ObjectDecoder(member = true, lowDetail = false).load(cache)) .load(files.list(Settings["definitions.objects"])) MapDefinitions(CollisionDecoder(), cache).load(files) } - frame.add(MapView(graph, files.list(Settings["map.areas"]))) + val view = MapView() + view.reload(files) + frame.add(view) frame.pack() frame.setLocationRelativeTo(null) frame.isVisible = true diff --git a/tools/src/main/kotlin/world/gregs/voidps/tools/map/view/draw/GraphDrawer.kt b/tools/src/main/kotlin/world/gregs/voidps/tools/map/view/draw/GraphDrawer.kt index d8ead0fb9e..f89f8e9b33 100644 --- a/tools/src/main/kotlin/world/gregs/voidps/tools/map/view/draw/GraphDrawer.kt +++ b/tools/src/main/kotlin/world/gregs/voidps/tools/map/view/draw/GraphDrawer.kt @@ -17,9 +17,9 @@ import kotlin.math.sqrt class GraphDrawer( private val view: MapView, - private var graph: Graph?, private val area: AreaSet, ) { + private var graph: Graph? = null private val steps: StepValidator = StepValidator(Collisions.map) private val linkColour = Color(0.0f, 0.0f, 1.0f, 0.5f) diff --git a/tools/src/main/kotlin/world/gregs/voidps/tools/map/view/draw/MapView.kt b/tools/src/main/kotlin/world/gregs/voidps/tools/map/view/draw/MapView.kt index 5e28f713ce..6f41932a8a 100644 --- a/tools/src/main/kotlin/world/gregs/voidps/tools/map/view/draw/MapView.kt +++ b/tools/src/main/kotlin/world/gregs/voidps/tools/map/view/draw/MapView.kt @@ -19,10 +19,10 @@ import java.awt.Graphics import javax.swing.JPanel import javax.swing.SwingUtilities -class MapView(graph: Graph, private val areaFiles: List) : JPanel() { +class MapView : JPanel() { private val options = OptionsPane(this) - private val areaSet = AreaSet.load(areaFiles) + private val areaSet = AreaSet() private val highlight = HighlightedTile(this, options) private val area = HighlightedArea(this, areaSet) @@ -31,7 +31,7 @@ class MapView(graph: Graph, private val areaFiles: List) : JPanel() { private val hover = MouseHover(highlight, area) private val map = WorldMap(this) private val resize = ResizeListener(map) - private val graph = GraphDrawer(this, graph, areaSet) + private val graph = GraphDrawer(this, areaSet) // private val click = MouseClick(this, nav, graph, area, areaSet) private val apc = AreaPointConnector(this, areaSet) @@ -91,6 +91,8 @@ class MapView(graph: Graph, private val areaFiles: List) : JPanel() { } fun reload(files: ConfigFiles = configFiles()) { + val areaFiles = files.list(Settings["map.areas"]) + AreaSet.load(areaFiles, areaSet) graph.reload(Graph.loadGraph(files.list(Settings["bots.nav.definitions"]), emptyList())) } diff --git a/tools/src/main/kotlin/world/gregs/voidps/tools/map/view/graph/AreaSet.kt b/tools/src/main/kotlin/world/gregs/voidps/tools/map/view/graph/AreaSet.kt index 4380ec5a84..6092f2cb61 100644 --- a/tools/src/main/kotlin/world/gregs/voidps/tools/map/view/graph/AreaSet.kt +++ b/tools/src/main/kotlin/world/gregs/voidps/tools/map/view/graph/AreaSet.kt @@ -69,8 +69,8 @@ class AreaSet { // writer.writeValue(File(path), set.areas) } - fun load(paths: List): AreaSet { - val set = AreaSet() + fun load(paths: List, set: AreaSet) { + set.areas.clear() val areas = mutableListOf() for (path in paths) { Config.fileReader(path) { @@ -113,7 +113,6 @@ class AreaSet { } } set.areas.addAll(areas) - return set } } } From 98e2bf6dba3361a19e08ce23f01feeafdfafe99c Mon Sep 17 00:00:00 2001 From: GregHib Date: Mon, 2 Feb 2026 14:10:56 +0000 Subject: [PATCH 031/101] Fix area name offset --- .../world/gregs/voidps/tools/map/view/draw/HighlightedArea.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/src/main/kotlin/world/gregs/voidps/tools/map/view/draw/HighlightedArea.kt b/tools/src/main/kotlin/world/gregs/voidps/tools/map/view/draw/HighlightedArea.kt index ec994fe149..b3e961a1d4 100644 --- a/tools/src/main/kotlin/world/gregs/voidps/tools/map/view/draw/HighlightedArea.kt +++ b/tools/src/main/kotlin/world/gregs/voidps/tools/map/view/draw/HighlightedArea.kt @@ -47,7 +47,7 @@ class HighlightedArea(private val view: MapView, private val area: AreaSet) { is Polygon -> g.drawPolygon(shape) is Rectangle -> g.drawRect(shape.x, shape.y, shape.width, shape.height) } - g.drawString(area.name ?: "", 5, 90 + index * 15) + g.drawString(area.name ?: "", 5, 120 + index * 15) } } } From 3973054a2820aeeebb3503c3efbf3c5a9747590a Mon Sep 17 00:00:00 2001 From: GregHib Date: Mon, 2 Feb 2026 16:59:58 +0000 Subject: [PATCH 032/101] Link up navigation graph with GoTo and GoToNearest actions Lots of behaviour fixes --- .../src/main/kotlin/content/bot/BotManager.kt | 69 +++-- .../content/bot/action/BehaviourFragment.kt | 18 ++ .../content/bot/action/BehaviourFrame.kt | 9 +- .../content/bot/action/BehaviourState.kt | 2 +- .../kotlin/content/bot/action/BotAction.kt | 269 ++++++++++++------ .../kotlin/content/bot/action/BotActivity.kt | 36 ++- .../main/kotlin/content/bot/action/Reason.kt | 2 + game/src/main/kotlin/content/bot/fact/Fact.kt | 12 +- .../kotlin/content/bot/interact/path/Graph.kt | 51 ++-- .../player/command/PathFindingCommands.kt | 24 +- .../entity/player/dialogue/DialogueInput.kt | 4 + .../bot/action/BehaviourFragmentTest.kt | 22 +- .../content/bot/interact/path/GraphTest.kt | 76 +++-- 13 files changed, 396 insertions(+), 198 deletions(-) diff --git a/game/src/main/kotlin/content/bot/BotManager.kt b/game/src/main/kotlin/content/bot/BotManager.kt index a25b2247c3..c8dc2d4402 100644 --- a/game/src/main/kotlin/content/bot/BotManager.kt +++ b/game/src/main/kotlin/content/bot/BotManager.kt @@ -3,8 +3,10 @@ package content.bot import com.github.michaelbull.logging.InlineLogger import content.bot.action.* import content.bot.fact.Condition +import content.bot.fact.Fact import content.bot.interact.path.Graph import content.bot.interact.path.Graph.Companion.loadGraph +import content.entity.player.bank.bank import world.gregs.voidps.engine.data.ConfigFiles import world.gregs.voidps.engine.data.Settings import world.gregs.voidps.engine.event.AuditLog @@ -94,6 +96,8 @@ class BotManager( return true } + private val idle = BotActivity("idle", 2048, actions = listOf(BotAction.Wait(50))) // 30s + private fun assignRandom(bot: Bot) { val activity = if (bot.previous != null && hasRequirements(bot, bot.previous!!)) { bot.previous @@ -102,11 +106,7 @@ class BotManager( val activity = activities[it] activity != null && hasRequirements(bot, activity) }.randomOrNull(random) - if (id == null) { - BotActivity("idle", 2048, actions = listOf(BotAction.Wait(50))) // 30s - } else { - activities[id] - } + activities[id] ?: idle } if (activity == null) { if (bot.player["debug", false]) { @@ -159,17 +159,35 @@ class BotManager( if (bot.player["debug", false]) { logger.info { "Starting activity: ${behaviour.id}." } } + bot.blocked.add(behaviour.id) frame.start(bot) } private fun pickResolver(bot: Bot, condition: Condition, frame: BehaviourFrame): Behaviour? { - if (condition is Condition.Area) { - return Resolver("go_to_${condition.area}", -1, actions = listOf(BotAction.GoTo(condition.area)), produces = setOf(condition)) - } // TODO actions should have retry policies? val options = mutableListOf() + // Go to area + if (condition is Condition.Area) { + options.add(Resolver("go_to_${condition.area}", -1, actions = listOf(BotAction.GoTo(condition.area)), produces = setOf(condition))) + } + // If in bank and needs inventory -> withdraw from bank + if (condition is Condition.AtLeast && condition.fact is Fact.InventoryCount) { + if (bot.player.bank.contains(condition.fact.id, condition.min)) { + options.add( + Resolver( + "withdraw_${condition.fact.id}", 20, actions = listOf( + BotAction.GoToNearest("bank"), + BotAction.InteractObject("Use-quickly", "bank_booth*"), + BotAction.InterfaceOption("Withdraw-x", "bank:inventory:${condition.fact.id}"), + BotAction.StringEntry("${condition.min}"), + ) + ) + ) + } + } + // TODO: If in inventory and needs equipped -> equip for (key in condition.keys()) { - for (resolver in resolvers[key] ?: return null) { + for (resolver in resolvers[key] ?: emptyList()) { if (frame.blocked.contains(resolver.id)) { continue } @@ -188,26 +206,37 @@ class BotManager( when (val state = frame.state) { BehaviourState.Running -> frame.update(bot) BehaviourState.Pending -> start(bot, behaviour, frame) - BehaviourState.Success -> if (!frame.next()) { - AuditLog.event(bot, "completed", frame.behaviour.id) - bot.frames.pop() - if (behaviour is BotActivity) { - slots.release(behaviour) + BehaviourState.Success -> { + val debug = bot.player["debug", false] + if (debug) { + logger.info { "Completed action: ${frame.action()} for ${behaviour.id}." } + } + if (!frame.next()) { + AuditLog.event(bot, "completed", frame.behaviour.id) + bot.frames.pop() + if (behaviour is BotActivity) { + slots.release(behaviour) + } + } else { + if (debug) { + logger.info { "Next action: ${frame.action()} for ${behaviour.id}." } + } + frame.start(bot) } } is BehaviourState.Failed -> { val action = frame.action() + if (bot.player["debug", false]) { + logger.info { "Failed action: ${action::class.simpleName} for ${behaviour.id}, reason: ${state.reason}." } + } if (action is BotAction.RetryableAction && action.retryMax > 0) { - frame.state = BehaviourState.Wait(action.retryTicks) + frame.state = BehaviourState.Wait(action.retryTicks, BehaviourState.Running) if (frame.retries++ < action.retryMax) { + frame.start(bot) AuditLog.event(bot, "retry", frame.behaviour.id, frame.index, frame.retries, action::class.simpleName) return } } - - if (bot.player["debug", false]) { - logger.info { "Failed action: ${action::class.simpleName} for ${behaviour.id}, reason: ${state.reason}." } - } AuditLog.event(bot, "failed", behaviour.id, state.reason, frame.index, frame.retries, action::class.simpleName) if (state.reason is HardReason) { stop(bot) @@ -221,7 +250,7 @@ class BotManager( } is BehaviourState.Wait -> { if (--state.ticks <= 0) { - frame.state = BehaviourState.Pending + frame.state = state.next } } } diff --git a/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt b/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt index 2688b390ff..2650f46e01 100644 --- a/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt +++ b/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt @@ -21,6 +21,7 @@ data class BehaviourFragment( val resolved = when (action) { is BotAction.Reference -> when (val copy = action.action) { is BotAction.GoTo -> BotAction.GoTo(resolve(action.references["go_to"], copy.target)) + is BotAction.GoToNearest -> BotAction.GoToNearest(resolve(action.references["go_to_nearest"], copy.tag)) is BotAction.InterfaceOption -> BotAction.InterfaceOption( id = resolve(action.references["interface"], copy.id), option = resolve(action.references["option"], copy.option), @@ -43,6 +44,12 @@ data class BehaviourFragment( x = resolve(action.references["x"], copy.x), y = resolve(action.references["y"], copy.y), ) + is BotAction.StringEntry -> BotAction.StringEntry( + value = resolve(action.references["value"], copy.value), + ) + is BotAction.IntEntry -> BotAction.IntEntry( + value = resolve(action.references["value"], copy.value), + ) is BotAction.Wait -> BotAction.Wait(resolve(action.references["wait"], copy.ticks)) is BotAction.WaitFullInventory -> BotAction.WaitFullInventory(resolve(action.references["timeout"], copy.timeout)) is BotAction.Clone, is BotAction.Reference -> throw IllegalArgumentException("Invalid reference action type: ${action.action::class.simpleName}.") @@ -88,6 +95,17 @@ data class BehaviourFragment( Condition.range(Fact.EquipCount(id), min, max) } } + "owns" -> { + val id = resolve(references[req.type], req.id) + val min = resolve(references["amount"], req.min) + if (id.contains(",")) { + Condition.Any(id.split(",").map { Condition.range(Fact.ItemCount(id), min, max) }) + } else if (id.any { it == '*' || it == '#' }) { + Condition.Any(Wildcards.get(id, Wildcard.Item).map { Condition.range(Fact.ItemCount(id), min, max) }) + } else { + Condition.range(Fact.ItemCount(id), min, max) + } + } "variable" -> { val id = resolve(references[req.type], req.id) val default = resolve(references["default"], req.default) diff --git a/game/src/main/kotlin/content/bot/action/BehaviourFrame.kt b/game/src/main/kotlin/content/bot/action/BehaviourFrame.kt index 1dc67811e1..175738a670 100644 --- a/game/src/main/kotlin/content/bot/action/BehaviourFrame.kt +++ b/game/src/main/kotlin/content/bot/action/BehaviourFrame.kt @@ -21,13 +21,16 @@ data class BehaviourFrame( fun update(bot: Bot) { val action = action() - state = action.update(bot) + state = action.update(bot) ?: return } fun next(): Boolean { + if (index >= behaviour.actions.lastIndex) { + return false + } index++ - state = BehaviourState.Pending - return index < behaviour.actions.size + state = BehaviourState.Running + return true } fun fail(reason: Reason) { diff --git a/game/src/main/kotlin/content/bot/action/BehaviourState.kt b/game/src/main/kotlin/content/bot/action/BehaviourState.kt index 48840324c7..ab82e399d6 100644 --- a/game/src/main/kotlin/content/bot/action/BehaviourState.kt +++ b/game/src/main/kotlin/content/bot/action/BehaviourState.kt @@ -5,5 +5,5 @@ sealed interface BehaviourState { object Running : BehaviourState object Success : BehaviourState data class Failed(val reason: Reason) : BehaviourState - data class Wait(var ticks: Int) : BehaviourState + data class Wait(var ticks: Int, val next: BehaviourState) : BehaviourState } \ No newline at end of file diff --git a/game/src/main/kotlin/content/bot/action/BotAction.kt b/game/src/main/kotlin/content/bot/action/BotAction.kt index e30577819d..51d468520e 100644 --- a/game/src/main/kotlin/content/bot/action/BotAction.kt +++ b/game/src/main/kotlin/content/bot/action/BotAction.kt @@ -1,19 +1,15 @@ package content.bot.action import content.bot.Bot -import content.bot.bot -import content.bot.interact.navigation.navigate -import content.bot.interact.navigation.updateGraph -import content.bot.interact.path.AreaStrategy -import content.bot.interact.path.Dijkstra -import content.bot.interact.path.EdgeTraversal -import content.entity.Movement +import content.bot.BotManager +import content.bot.interact.path.Graph import content.entity.combat.attackers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch +import content.entity.player.bank.bank +import world.gregs.voidps.engine.GameLoop import world.gregs.voidps.engine.data.definition.Areas import world.gregs.voidps.engine.data.definition.InterfaceDefinitions -import world.gregs.voidps.engine.entity.character.mode.interact.Interact +import world.gregs.voidps.engine.data.definition.ItemDefinitions +import world.gregs.voidps.engine.entity.character.mode.EmptyMode import world.gregs.voidps.engine.entity.character.mode.interact.PlayerOnObjectInteract import world.gregs.voidps.engine.entity.character.npc.NPC import world.gregs.voidps.engine.entity.character.npc.NPCs @@ -21,15 +17,15 @@ import world.gregs.voidps.engine.entity.obj.GameObject import world.gregs.voidps.engine.entity.obj.GameObjects import world.gregs.voidps.engine.event.wildcardEquals import world.gregs.voidps.engine.get +import world.gregs.voidps.engine.inv.equipment +import world.gregs.voidps.engine.inv.inventory import world.gregs.voidps.engine.map.Spiral -import world.gregs.voidps.network.client.instruction.InteractInterface -import world.gregs.voidps.network.client.instruction.InteractNPC -import world.gregs.voidps.network.client.instruction.Walk +import world.gregs.voidps.network.client.instruction.* import world.gregs.voidps.type.random sealed interface BotAction { fun start(bot: Bot): BehaviourState = BehaviourState.Running - fun update(bot: Bot): BehaviourState = BehaviourState.Running + fun update(bot: Bot): BehaviourState? = null sealed class RetryableAction : BotAction { abstract val retryTicks: Int @@ -40,39 +36,86 @@ sealed interface BotAction { override fun start(bot: Bot) = BehaviourState.Failed(Reason.Cancelled) override fun update(bot: Bot) = BehaviourState.Failed(Reason.Cancelled) } + data class Reference(val action: BotAction, val references: Map) : BotAction { override fun start(bot: Bot) = BehaviourState.Failed(Reason.Cancelled) override fun update(bot: Bot) = BehaviourState.Failed(Reason.Cancelled) } data class Wait(val ticks: Int) : BotAction { - override fun start(bot: Bot): BehaviourState { - bot.frame().state = BehaviourState.Wait(ticks) - return BehaviourState.Running - } + override fun start(bot: Bot) = BehaviourState.Wait(ticks, BehaviourState.Success) } data class GoTo(val target: String) : BotAction { override fun start(bot: Bot): BehaviourState { - val def = Areas.getOrNull(target) ?: return BehaviourState.Failed(Reason.NoRoute) + val def = Areas.getOrNull(target) ?: return BehaviourState.Failed(Reason.Invalid) if (bot.tile in def.area) { return BehaviourState.Success } - updateGraph(bot) - // TODO new format for nav graph - val strategy = AreaStrategy(def.area) - val result = get().find(bot.player, strategy, EdgeTraversal()) - bot["navigating"] = result == null - if (result != null) { - bot["area"] = def - GlobalScope.launch { bot.navigate() } + val manager = get() + val list = mutableListOf() + val graph = manager.graph + val success = graph.find(bot.player, list, target) + return queueRoute(success, list, graph, bot, target) + } + + companion object { + internal fun queueRoute(success: Boolean, list: MutableList, graph: Graph, bot: Bot, target: String): BehaviourState { + if (!success) { + return BehaviourState.Failed(Reason.NoRoute) + } + val actions = mutableListOf() + var nav: NavigationShortcut? = null + for (edge in list) { + val shortcut = graph.shortcuts[edge] + if (shortcut != null) { + nav = shortcut + } else { + actions.addAll(graph.actions[edge] ?: continue) + } + } + if (actions.isNotEmpty()) { + bot.queue(BehaviourFrame(Resolver("go_to_${target}", 0, actions = actions))) + } + if (nav != null) { + bot.queue(BehaviourFrame(nav)) + } + if (bot.frames.isEmpty()) { + return BehaviourState.Failed(Reason.NoRoute) + } return BehaviourState.Running - } else { - return BehaviourState.Failed(Reason.NoRoute) } } } + data class GoToNearest(val tag: String) : BotAction { + override fun start(bot: Bot): BehaviourState { + val set = Areas.tagged(tag) + if (set.isEmpty()) { + return BehaviourState.Failed(Reason.Invalid) + } + if (set.any { bot.tile in it.area }) { + return BehaviourState.Success + } + val manager = get() + val list = mutableListOf() + val graph = manager.graph + val success = graph.findNearest(bot.player, list, tag) + return GoTo.queueRoute(success, list, graph, bot, tag) + } + + override fun update(bot: Bot): BehaviourState? { + val set = Areas.tagged(tag) + if (set.isEmpty()) { + return BehaviourState.Failed(Reason.Invalid) + } + if (set.any { bot.tile in it.area }) { + return BehaviourState.Success + } + return null + } + } + data class InteractNpc( val option: String, val id: String, @@ -107,7 +150,7 @@ sealed interface BotAction { override val retryMax: Int = 0, val radius: Int = 10, ) : RetryableAction() { - override fun update(bot: Bot): BehaviourState { + override fun start(bot: Bot): BehaviourState { if (bot.mode is PlayerOnObjectInteract) { return BehaviourState.Running } @@ -122,7 +165,14 @@ sealed interface BotAction { val obj = objects.randomOrNull(random) ?: return BehaviourState.Failed(Reason.NoTarget) val index = obj.def(bot.player).options?.indexOf(option) ?: return BehaviourState.Failed(Reason.NoTarget) bot.player.instructions.trySend(world.gregs.voidps.network.client.instruction.InteractObject(obj.intId, obj.x, obj.y, index + 1)) - return BehaviourState.Running + return BehaviourState.Wait(2, BehaviourState.Success) + } + + override fun update(bot: Bot): BehaviourState { + if (bot.mode is PlayerOnObjectInteract) { + return BehaviourState.Running + } + return BehaviourState.Failed(Reason.NoTarget) } } @@ -135,87 +185,124 @@ sealed interface BotAction { val def = definitions.getOrNull(id) ?: return BehaviourState.Failed(Reason.NoTarget) val componentId = definitions.getComponentId(id, component) ?: return BehaviourState.Failed(Reason.NoTarget) val componentDef = definitions.getComponent(id, component) ?: return BehaviourState.Failed(Reason.NoTarget) - val index = componentDef.options?.indexOf(option) ?: return BehaviourState.Failed(Reason.NoTarget) + var options = componentDef.options + if (options == null) { + options = componentDef.getOrNull("options") ?: emptyArray() + } + val index = options.indexOf(option) + if (index == -1) { + return BehaviourState.Failed(Reason.NoTarget) + } + val itemDef = if (item != null) ItemDefinitions.getOrNull(item) else null + + val inv = when (id) { + "bank" -> bot.player.bank + "inventory" -> bot.player.inventory + "equipment" -> bot.player.equipment + // TODO link up with inv defs + else -> null + } + val itemSlot = if (item != null && inv != null) inv.indexOf(item) else -1 bot.player.instructions.trySend( InteractInterface( interfaceId = def.id, componentId = componentId, - itemId = -1, - itemSlot = -1, + itemId = itemDef?.id ?: -1, + itemSlot = itemSlot, option = index ) ) // TODO could await actual response, or something to get actual feedback - return BehaviourState.Success + return BehaviourState.Wait(1, BehaviourState.Success) } } data class WaitFullInventory(val timeout: Int) : BotAction - data class WalkTo(val x: Int, val y: Int): BotAction { + data class IntEntry(val value: Int) : BotAction { + override fun start(bot: Bot): BehaviourState { + bot.player.instructions.trySend(EnterInt(value)) + return BehaviourState.Wait(1, BehaviourState.Success) + } + } + + data class StringEntry(val value: String) : BotAction { + override fun start(bot: Bot): BehaviourState { + bot.player.instructions.trySend(EnterString(value)) + return BehaviourState.Wait(1, BehaviourState.Success) + } + } + + data class WalkTo(val x: Int, val y: Int, val radius: Int = 4) : BotAction { override fun start(bot: Bot): BehaviourState { bot.player.instructions.trySend(Walk(x, y)) - return BehaviourState.Success + return BehaviourState.Running + } + + override fun update(bot: Bot) = when { + bot.tile.within(x, y, bot.tile.level, radius) -> BehaviourState.Success + bot.mode is EmptyMode && GameLoop.tick - bot.steps.last > 10 -> BehaviourState.Failed(Reason.Stuck) + else -> BehaviourState.Running } } /** -TODO how to handle repeat actions e.g. repeat Chop-down trees until inv is full - These are actions Gathering, Skilling etc.. - more resolvers like bank all, drop cheap items - how to handle combat, one task or multiple? - One Fight action - frames should have tick(): State methods - Combat should be an action which has a state machine for eating, retargeting, looting etc.. - GatheringActivity - TravelActivity - how to handle navigation in a non-hacky way - navigation behaviours - make nav-graph points only? - combine nav-graph requirements with facts - Goal generators - Rather than check all req for all activities do it reactively - Received an item recently? Add relevant activities to that item to the list of posibilities - Been too long since you picked up an item, now remove that goal from the list - No possibilities? Now expand search wider - - Open questions: + TODO how to handle repeat actions e.g. repeat Chop-down trees until inv is full - These are actions Gathering, Skilling etc.. + more resolvers like bank all, drop cheap items + how to handle combat, one task or multiple? - One Fight action + frames should have tick(): State methods + Combat should be an action which has a state machine for eating, retargeting, looting etc.. + GatheringActivity + TravelActivity + how to handle navigation in a non-hacky way + navigation behaviours + make nav-graph points only? + combine nav-graph requirements with facts + Goal generators + Rather than check all req for all activities do it reactively + Received an item recently? Add relevant activities to that item to the list of posibilities + Been too long since you picked up an item, now remove that goal from the list + No possibilities? Now expand search wider + + Open questions: - Complex activities like minigames, quests - Minigames: - They are closed mechanical systems so they are actions. - JoinMinigameLobby - Success, Timeout, Kicked etc.. - PlayMinigame - Roll selection, objectives, movement, combat, scoring. Fails on game end or leaving/disconnect - Trading with players: - Outcomes are non-deterministic, waiting on player timing - SellingAction - TradeAction - inits trade - trade rules - max wait - accepted items - price bounds - reacts to - offer chances - cancellation - terminates with success/failure - Quests: - Some complex quest mechanics might need custom actions - Activities: - TalkToCook - GetBucket - GetMilk - GetEgg - ReturnToCook + Minigames: + They are closed mechanical systems so they are actions. + JoinMinigameLobby - Success, Timeout, Kicked etc.. + PlayMinigame - Roll selection, objectives, movement, combat, scoring. Fails on game end or leaving/disconnect + Trading with players: + Outcomes are non-deterministic, waiting on player timing + SellingAction + TradeAction + inits trade + trade rules + max wait + accepted items + price bounds + reacts to + offer chances + cancellation + terminates with success/failure + Quests: + Some complex quest mechanics might need custom actions + Activities: + TalkToCook + GetBucket + GetMilk + GetEgg + ReturnToCook - navigation + actions - Virtual nodes - Create a temp node - link the current tile as a weight = 0 - link any applicable teleports - run traversal from temp source node + Virtual nodes + Create a temp node + link the current tile as a weight = 0 + link any applicable teleports + run traversal from temp source node - targeting - policy + policy - activity generators - reactive loading - Separate mandatory and resolvable requirements - mandatory requirements become gates for if activities are in the current pool - Listeners wait for state changes on specific mandatory requirements (level up, skill changes) and revaluate adding to activity pool - These listeners also check current activity requirements and fail it if no longer gated + Separate mandatory and resolvable requirements + mandatory requirements become gates for if activities are in the current pool + Listeners wait for state changes on specific mandatory requirements (level up, skill changes) and revaluate adding to activity pool + These listeners also check current activity requirements and fail it if no longer gated */ -} \ No newline at end of file +} diff --git a/game/src/main/kotlin/content/bot/action/BotActivity.kt b/game/src/main/kotlin/content/bot/action/BotActivity.kt index 82198f622b..c5826ea876 100644 --- a/game/src/main/kotlin/content/bot/action/BotActivity.kt +++ b/game/src/main/kotlin/content/bot/action/BotActivity.kt @@ -77,7 +77,8 @@ fun loadActivities( } } } else if (type == "shortcut") { - shortcuts.add(NavigationShortcut(id, weight, requirements, resolvables, actions = actions, produces = produces.toSet())) + require(resolvables.isEmpty()) { "Shortcuts cannot have setup requirements" } + shortcuts.add(NavigationShortcut(id, weight, requirements, actions = actions, produces = produces.toSet())) } else { activities[id] = BotActivity(id, capacity, requirements, resolvables, actions = actions) } @@ -140,7 +141,7 @@ fun loadActivities( } } } - "shortcut" -> shortcuts.add(NavigationShortcut(id, fragment.weight, requirements, resolvables, actions = actions)) + "shortcut" -> shortcuts.add(NavigationShortcut(id, fragment.weight, requirements, actions = actions)) else -> activities[id] = BotActivity(id, fragment.capacity, requirements, resolvables, actions) } } @@ -155,7 +156,6 @@ fun loadActivities( groups.getOrPut(key) { mutableListOf() }.add(activity.id) } } - println(activity) } activities.size } @@ -192,7 +192,6 @@ fun ConfigReader.requirements(list: MutableList) { "amount" -> when (val value = value()) { is Int -> { min = value - max = value } is String if value.contains('$') -> references[key] = value else -> throw IllegalArgumentException("Invalid '$key' value: $value ${exception()}") @@ -251,6 +250,13 @@ private fun getRequirement(type: String, id: String, min: Int?, max: Int?, value } else { Condition.range(Fact.InventoryCount(id), min, max) } + "owns" -> if (id.contains(",")) { + Condition.Any(id.split(",").map { Condition.range(Fact.ItemCount(it), min, max) }) + } else if (id.any { it == '*' || it == '#' }) { + Condition.Any(Wildcards.get(id, Wildcard.Item).map { Condition.range(Fact.ItemCount(it), min, max) }) + } else { + Condition.range(Fact.ItemCount(id), min, max) + } "equips" -> if (id.contains(",")) { Condition.Any(id.split(",").map { Condition.range(Fact.EquipCount(it), min, max) }) } else if (id.any { it == '*' || it == '#' }) { @@ -279,6 +285,7 @@ fun ConfigReader.actions(list: MutableList) { var retryTicks = 0 var retryMax = 0 var timeout = 0 + var int = 0 var ticks = 0 var radius = 10 var x = 0 @@ -286,7 +293,7 @@ fun ConfigReader.actions(list: MutableList) { val references = mutableMapOf() while (nextEntry()) { when (val key = key()) { - "go_to", "wait_for", "interface", "npc", "object", "clone" -> { + "go_to", "go_to_nearest", "enter_string", "wait_for", "interface", "npc", "object", "clone" -> { type = key id = string() if (id.contains('$')) { @@ -294,7 +301,9 @@ fun ConfigReader.actions(list: MutableList) { } } "x" -> { - type = "tile" + if (type == "") { + type = "tile" + } val value = value() if (value is String && value.contains('$')) { references[key] = value @@ -303,7 +312,9 @@ fun ConfigReader.actions(list: MutableList) { } } "y" -> { - type = "tile" + if (type == "") { + type = "tile" + } val value = value() if (value is String && value.contains('$')) { references[key] = value @@ -352,11 +363,22 @@ fun ConfigReader.actions(list: MutableList) { is String if value.contains('$') -> references[key] = value else -> throw IllegalArgumentException("Invalid '$key' value: $value ${exception()}") } + "enter_int" -> { + type = key + when (val value = value()) { + is Int -> int = value + is String if value.contains('$') -> references[key] = value + else -> throw IllegalArgumentException("Invalid '$key' value: $value ${exception()}") + } + } else -> throw IllegalArgumentException("Unknown action key: $key ${exception()}") } } var action = when (type) { "go_to" -> BotAction.GoTo(id) + "go_to_nearest" -> BotAction.GoToNearest(id) + "enter_string" -> BotAction.StringEntry(id) + "enter_int" -> BotAction.IntEntry(int) "wait" -> BotAction.Wait(ticks) "npc" -> BotAction.InteractNpc(id = id, option = option, retryTicks = retryTicks, retryMax = retryMax, radius = radius) "tile" -> BotAction.WalkTo(x = x, y = y) diff --git a/game/src/main/kotlin/content/bot/action/Reason.kt b/game/src/main/kotlin/content/bot/action/Reason.kt index 00d2b38b97..e717f9384a 100644 --- a/game/src/main/kotlin/content/bot/action/Reason.kt +++ b/game/src/main/kotlin/content/bot/action/Reason.kt @@ -3,8 +3,10 @@ package content.bot.action import content.bot.fact.Condition interface Reason { + object Invalid : HardReason object Cancelled : HardReason object NoRoute : HardReason + object Stuck : SoftReason object NoTarget : SoftReason data class Requirement(val fact: Condition) : HardReason } diff --git a/game/src/main/kotlin/content/bot/fact/Fact.kt b/game/src/main/kotlin/content/bot/fact/Fact.kt index ba400b9d7f..60ace6a8e6 100644 --- a/game/src/main/kotlin/content/bot/fact/Fact.kt +++ b/game/src/main/kotlin/content/bot/fact/Fact.kt @@ -1,5 +1,6 @@ package content.bot.fact +import content.entity.player.bank.bank import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.entity.character.player.skill.Skill import world.gregs.voidps.engine.inv.equipment @@ -14,14 +15,19 @@ sealed class Fact(val priority: Int) { abstract fun getValue(player: Player): T open fun keys(): Set = emptySet() + object InventorySpace : Fact(10) { + override fun keys() = setOf("inv:inventory") + override fun getValue(player: Player) = player.inventory.spaces + } + data class InventoryCount(val id: String) : Fact(100) { override fun keys() = setOf("inv:inventory") override fun getValue(player: Player) = player.inventory.count(id) } - object InventorySpace : Fact(10) { - override fun keys() = setOf("inv:inventory") - override fun getValue(player: Player) = player.inventory.spaces + data class ItemCount(val id: String) : Fact(100) { + override fun keys() = setOf("inv:inventory", "inv:bank", "inv:equipment") + override fun getValue(player: Player) = player.inventory.count(id) + player.bank.count(id) + player.equipment.count(id) } data class EquipCount(val id: String) : Fact(100) { diff --git a/game/src/main/kotlin/content/bot/interact/path/Graph.kt b/game/src/main/kotlin/content/bot/interact/path/Graph.kt index 3fb8616b55..10b045048d 100644 --- a/game/src/main/kotlin/content/bot/interact/path/Graph.kt +++ b/game/src/main/kotlin/content/bot/interact/path/Graph.kt @@ -35,11 +35,11 @@ class Graph( return Tile(tiles[nodeIndex]) } - fun findNearest(player: Player, tag: String, output: MutableList): Boolean { + fun findNearest(player: Player, output: MutableList, tag: String): Boolean { val start = startingPoints(player) return find(player, output, start, target = { val tile = Tile(tiles[it]) - Areas.tagged(tag).any { a -> tile in a.area } + Areas.tagged(tag).any { a -> tile in a.area } // TODO store tags and/or areas per node }) } @@ -48,13 +48,17 @@ class Graph( return find(player, output, start, target = { Tile(tiles[it]) in Areas[area] }) } - internal fun startingPoints(player: Player): Set = buildSet { - for (index in tiles.indices) { - val tile = tiles[index] - if (!player.tile.within(Tile(tile), 25)) { + fun startingPoints(player: Player): Set = buildSet { + for (index in 1 until tiles.size) { + val tile = Tile(tiles[index]) + if (player.tile.level != tile.level) { continue } - add(index) + val distance = player.tile.distanceTo(tile) + if (distance > 10) { + continue + } + add(Node(index, distance.coerceAtLeast(0))) } val blocked = if (player.isBot) player.bot.blocked else emptySet() for (shortcut in shortcuts.values) { @@ -64,18 +68,18 @@ class Graph( if (shortcut.requires.any { !it.check(player) }) { continue } - add(0) + add(Node(0, 0)) break } } - fun find(player: Player, output: MutableList, start: Int, target: Int) = find(player, output, setOf(start)) { it == target } + fun find(player: Player, output: MutableList, start: Node, target: Int) = find(player, output, setOf(start)) { it == target } - fun find(player: Player, output: MutableList, start: Set, target: Int) = find(player, output, start) { it == target } + fun find(player: Player, output: MutableList, start: Set, target: Int) = find(player, output, start) { it == target } - fun find(player: Player, output: MutableList, start: Int, target: (Int) -> Boolean) = find(player, output, setOf(start), target) + fun find(player: Player, output: MutableList, start: Node, target: (Int) -> Boolean) = find(player, output, setOf(start), target) - fun find(player: Player, output: MutableList, startingPoints: Set, target: (Int) -> Boolean): Boolean { + fun find(player: Player, output: MutableList, startingPoints: Set, target: (Int) -> Boolean): Boolean { output.clear() val queue = PriorityQueue() val visited = BooleanArray(nodeCount) @@ -85,8 +89,8 @@ class Graph( val previousEdge = IntArray(nodeCount) for (start in startingPoints) { - distance[start] = 0 - queue.add(Node(start, 0)) + distance[start.index] = 0 + queue.add(start) } while (queue.isNotEmpty()) { val (node, cost) = queue.poll() @@ -125,7 +129,7 @@ class Graph( return false } - private data class Node(val index: Int, val cost: Int) : Comparable { + data class Node(val index: Int, val cost: Int = 0) : Comparable { override fun compareTo(other: Node) = cost.compareTo(other.cost) } @@ -181,11 +185,6 @@ class Graph( return tiles.indexOf(tile) } - fun addBiEdge(start: Int, end: Int, weight: Int) { - addEdge(start, end, weight) - addEdge(end, start, weight) - } - fun addEdge(start: Int, end: Int, weight: Int, actions: List? = null, conditions: List? = null): Int { val edgeIndex = edgeCount++ nodes.add(start) @@ -234,8 +233,10 @@ class Graph( while (nextElement()) { var fromX = 0 var fromY = 0 + var fromLevel = 0 var toX = 0 var toY = 0 + var toLevel = 0 var cost = 0 val actions: MutableList = mutableListOf() val requirements: MutableList = mutableListOf() @@ -243,8 +244,10 @@ class Graph( when (val key = key()) { "from_x" -> fromX = int() "from_y" -> fromY = int() + "from_level" -> fromLevel = int() "to_x" -> toX = int() "to_y" -> toY = int() + "to_level" -> toLevel = int() "cost" -> cost = int() "actions" -> actions(actions) "conditions" -> requirements(requirements) @@ -254,11 +257,11 @@ class Graph( when { actions.isEmpty() -> { val cost = Distance.manhattan(fromX, fromY, toX, toY) - actions.add(BotAction.WalkTo(toX, toY)) - builder.addBiEdge(Tile(fromX, fromY), Tile(toX, toY), cost, actions) + builder.addEdge(Tile(fromX, fromY, fromLevel), Tile(toX, toY, toLevel), cost, listOf(BotAction.WalkTo(toX, toY)), null) + builder.addEdge(Tile(toX, toY, toLevel), Tile(fromX, fromY, fromLevel), cost, listOf(BotAction.WalkTo(fromX, fromY)), null) } - requirements.isEmpty() -> builder.addEdge(Tile(fromX, fromY), Tile(toX, toY), cost, actions, null) - else -> builder.addEdge(Tile(fromX, fromY), Tile(toX, toY), cost, actions, requirements) + requirements.isEmpty() -> builder.addEdge(Tile(fromX, fromY, fromLevel), Tile(toX, toY, toLevel), cost, actions, null) + else -> builder.addEdge(Tile(fromX, fromY, fromLevel), Tile(toX, toY, toLevel), cost, actions, requirements) } } } diff --git a/game/src/main/kotlin/content/entity/player/command/PathFindingCommands.kt b/game/src/main/kotlin/content/entity/player/command/PathFindingCommands.kt index a49fea9fdd..999b0bd010 100644 --- a/game/src/main/kotlin/content/entity/player/command/PathFindingCommands.kt +++ b/game/src/main/kotlin/content/entity/player/command/PathFindingCommands.kt @@ -2,10 +2,14 @@ package content.entity.player.command import content.bot.Bot import content.bot.BotManager +import content.bot.action.BehaviourFrame +import content.bot.action.BotAction +import content.bot.action.Resolver import content.bot.bot import content.bot.interact.path.Dijkstra import content.bot.interact.path.EdgeTraversal import content.bot.interact.path.NodeTargetStrategy +import content.bot.isBot import content.entity.gfx.areaGfx import org.rsmod.game.pathfinder.PathFinder import org.rsmod.game.pathfinder.StepValidator @@ -14,6 +18,7 @@ import org.rsmod.game.pathfinder.flag.CollisionFlag import world.gregs.voidps.engine.Script import world.gregs.voidps.engine.client.command.adminCommand import world.gregs.voidps.engine.client.command.stringArg +import world.gregs.voidps.engine.data.definition.Areas import world.gregs.voidps.engine.data.definition.PatrolDefinitions import world.gregs.voidps.engine.entity.character.mode.Patrol import world.gregs.voidps.engine.entity.character.move.tele @@ -121,18 +126,15 @@ class PathFindingCommands(val patrols: PatrolDefinitions) : Script { // println(pf.findPath(3205, 3220, 3205, 3223, 2)) } - adminCommand("walk_test") { - val manager = get() - val list = mutableListOf() - set("bot", Bot(this)) - bot.blocked.add("teleport_varrock") - // TODO convert nav_graph.toml to n - println(manager.graph.find(this, list, "varrock_teleport")) - println(list) - for (edge in list) { - println(manager.graph.tile(edge)) + adminCommand("go_to", stringArg("area-id", autofill = Areas.getAll().map { it.stringId }.toSet(), optional = true), desc = "Bot walk to a location") { args -> + val area = args.getOrNull(0) ?: "varrock_teleport" + if (!isBot) { + val manager = get() + val bot = Bot(this) + set("bot", bot) + manager.add(bot) } - + bot.queue(BehaviourFrame(Resolver("bot_to_$area", 0, actions = listOf(BotAction.GoTo(area))))) } adminCommand("walk_to_bank") { diff --git a/game/src/main/kotlin/content/entity/player/dialogue/DialogueInput.kt b/game/src/main/kotlin/content/entity/player/dialogue/DialogueInput.kt index 4484d0f866..8f703073c7 100644 --- a/game/src/main/kotlin/content/entity/player/dialogue/DialogueInput.kt +++ b/game/src/main/kotlin/content/entity/player/dialogue/DialogueInput.kt @@ -2,6 +2,7 @@ package content.entity.player.dialogue import world.gregs.voidps.engine.Script import world.gregs.voidps.engine.client.instruction.instruction +import world.gregs.voidps.engine.client.sendScript import world.gregs.voidps.engine.client.ui.closeDialogue import world.gregs.voidps.engine.suspend.IntSuspension import world.gregs.voidps.engine.suspend.NameSuspension @@ -44,14 +45,17 @@ class DialogueInput : Script { instruction { player -> (player.dialogueSuspension as? IntSuspension)?.resume(value) + player.sendScript("close_entry") } instruction { player -> (player.dialogueSuspension as? StringSuspension)?.resume(value) + player.sendScript("close_entry") } instruction { player -> (player.dialogueSuspension as? NameSuspension)?.resume(value) + player.sendScript("close_entry") } continueDialogue("dialogue_confirm_destroy:*") { diff --git a/game/src/test/kotlin/content/bot/action/BehaviourFragmentTest.kt b/game/src/test/kotlin/content/bot/action/BehaviourFragmentTest.kt index abeff6bc3e..f6c0ed6902 100644 --- a/game/src/test/kotlin/content/bot/action/BehaviourFragmentTest.kt +++ b/game/src/test/kotlin/content/bot/action/BehaviourFragmentTest.kt @@ -23,25 +23,6 @@ class BehaviourFragmentTest { Actions */ - @Test - fun `Missing action field reference throws`() { - val fragment = fragment() - val template = BotActivity( - id = "a", - capacity = 1, - actions = listOf( - BotAction.Reference( - BotAction.GoTo("x"), - references = mapOf("go_to" to "missing") - ) - ) - ) - - assertThrows { - fragment.resolveActions(template, mutableListOf()) - } - } - @Test fun `Nested clone action throws`() { val fragment = fragment() @@ -135,6 +116,7 @@ class BehaviourFragmentTest { @TestFactory fun `Resolve action references`() = listOf( Triple(BotAction.GoTo("default"), mapOf("go_to" to "lumbridge"), BotAction.GoTo("lumbridge")), + Triple(BotAction.GoToNearest("default"), mapOf("go_to_nearest" to "lumbridge"), BotAction.GoToNearest("lumbridge")), Triple(BotAction.InterfaceOption(option = "click", id = "something"), mapOf("option" to "Open", "interface" to "bank"), BotAction.InterfaceOption(option = "Open", id = "bank")), Triple( BotAction.InteractNpc( @@ -238,7 +220,7 @@ class BehaviourFragmentTest { @TestFactory fun `Resolve requirement references`() = listOf( Triple(Condition.Reference("skill", "defence", min = 1, max = 120), mapOf("skill" to "attack", "min" to 5, "max" to 99), Condition.Range(Fact.AttackLevel, 5, 99)), - Triple(Condition.Reference("variable", "default", value = 1), mapOf("variable" to "test", "value" to true), Condition.Equals(Fact.BoolVariable("test"), true)), + Triple(Condition.Reference("variable", "default", value = 1), mapOf("variable" to "test", "value" to true), Condition.Equals(Fact.BoolVariable("test", null), true)), Triple(Condition.Reference("equips", "default", min = 1), mapOf("equips" to "item", "amount" to 10), Condition.AtLeast(Fact.EquipCount("item"), 10)), Triple(Condition.Reference("carries", "default", min = 1), mapOf("carries" to "item", "amount" to 10), Condition.AtLeast(Fact.InventoryCount("item"), 10)), Triple(Condition.Reference("inventory_space", min = 1), mapOf("inventory_space" to 10), Condition.AtLeast(Fact.InventorySpace, 10)), diff --git a/game/src/test/kotlin/content/bot/interact/path/GraphTest.kt b/game/src/test/kotlin/content/bot/interact/path/GraphTest.kt index 03daf203fb..79c825b6dc 100644 --- a/game/src/test/kotlin/content/bot/interact/path/GraphTest.kt +++ b/game/src/test/kotlin/content/bot/interact/path/GraphTest.kt @@ -36,7 +36,7 @@ class GraphTest { // builder.print() val output = mutableListOf() - val success = builder.build().find(Player(), output, a, b) + val success = builder.build().find(Player(), output, Graph.Node(a), b) assertTrue(success) assertEquals(listOf(1, 2), output) } @@ -71,7 +71,7 @@ class GraphTest { // builder.print() val output = mutableListOf() - val success = builder.build().find(Player(), output, a, c) + val success = builder.build().find(Player(), output, Graph.Node(a), c) assertTrue(success) assertEquals(listOf(ab, bd, df, fc), output) } @@ -109,7 +109,7 @@ class GraphTest { // builder.print() val output = mutableListOf() - val success = builder.build().find(Player(), output, f, d) + val success = builder.build().find(Player(), output, Graph.Node(f), d) assertTrue(success) assertEquals(listOf(fg, gb, bd), output) } @@ -141,7 +141,7 @@ class GraphTest { // builder.print() val output = mutableListOf() - val success = builder.build().find(Player(), output, a, d) + val success = builder.build().find(Player(), output, Graph.Node(a), d) assertTrue(success) assertEquals(listOf(ae, ec, cd), output) } @@ -160,7 +160,7 @@ class GraphTest { // builder.print() val output = mutableListOf() - val success = builder.build().find(Player(), output, b, a) + val success = builder.build().find(Player(), output, Graph.Node(b), a) assertFalse(success) } @@ -198,7 +198,7 @@ class GraphTest { // builder.print() val output = mutableListOf() - val success = builder.build().find(Player(), output, setOf(f, a), d) + val success = builder.build().find(Player(), output, setOf(Graph.Node(f), Graph.Node(a)), d) assertTrue(success) assertEquals(listOf(ab, bd), output) } @@ -220,10 +220,10 @@ class GraphTest { player.tile = a val path = mutableListOf() - val found = graph.find(player, path, start = 0, target = 2) + val found = graph.find(player, path, start = Graph.Node(0), target = 2) assertTrue(found) - assertEquals(2, path.size, "Shortest path should be two edges, not direct edge") + assertEquals(listOf(0, 2), path) } @Test @@ -246,7 +246,7 @@ class GraphTest { player.tile = a val path = mutableListOf() - val found = graph.find(player, path, start = 0, target = 1) + val found = graph.find(player, path, start = Graph.Node(0), target = 1) assertFalse(found, "Edge condition blocks traversal") assertTrue(path.isEmpty()) @@ -256,9 +256,9 @@ class GraphTest { fun `Starting points include nearby tiles`() { val builder = Graph.Builder() - val a = Tile(0) - val b = Tile(20) - val c = Tile(100) + val a = Tile(1, 1) + val b = Tile(20, 20) + val c = Tile(100, 100) builder.add(a) builder.add(b) @@ -266,13 +266,13 @@ class GraphTest { val graph = builder.build() val player = Player() - player.tile = Tile(10) + player.tile = Tile(10, 10) val starts = graph.startingPoints(player) - assertTrue(starts.contains(0)) - assertTrue(starts.contains(1)) - assertFalse(starts.contains(2)) + assertTrue(starts.contains(Graph.Node(1, 9))) + assertTrue(starts.contains(Graph.Node(2, 10))) + assertFalse(starts.contains(Graph.Node(3, 90))) } @Test @@ -287,6 +287,7 @@ class GraphTest { ) val builder = Graph.Builder() + builder.add(Tile(75, 75)) val edge = builder.add(shortcut) val graph = builder.build() @@ -295,7 +296,7 @@ class GraphTest { val starts = graph.startingPoints(player) - assertTrue(starts.contains(edge)) + assertTrue(starts.contains(Graph.Node(edge))) } @Test @@ -312,9 +313,48 @@ class GraphTest { player.tile = a val path = mutableListOf() - val found = graph.find(player, path, start = 0, target = 2) + val found = graph.find(player, path, start = Graph.Node(0), target = 2) assertTrue(found) assertEquals(listOf(e1, e2), path) } + + @Test + fun `Complex route`() { + /* + */ + val builder = Graph.Builder() + val a = 0 + val b = 1 + val c = 2 + val d = 3 + val e = 4 + val f = 5 + val g = 6 + val h = 7 + val i = 8 + val j = 9 + val k = 10 + val l = 11 + val m = 12 + builder.addEdge(a, b, 12) + builder.addEdge(b, c, 13) + builder.addEdge(c, d, 13) + builder.addEdge(d, e, 18) + builder.addEdge(e, f, 11) + builder.addEdge(f, g, 10) + builder.addEdge(g, h, 8) + builder.addEdge(h, i, 7) + val aj = builder.addEdge(a, j, 2) + val jk = builder.addEdge(j, k, 5) + val kl = builder.addEdge(k, l, 7) + val lm = builder.addEdge(l, m, 9) + val mh = builder.addEdge(m, h, 6) +// builder.print() + + val output = mutableListOf() + val success = builder.build().find(Player(), output, Graph.Node(a), h) + assertTrue(success) + assertEquals(listOf(aj, jk, kl, lm, mh), output) + } } \ No newline at end of file From b279b329281caafe26dbfa1866ed8f0ebf76a2cd Mon Sep 17 00:00:00 2001 From: GregHib Date: Mon, 2 Feb 2026 17:04:00 +0000 Subject: [PATCH 033/101] Add all nav-edges and update varrock teleport shortcut --- .../area/misthalin/varrock/varrock.areas.toml | 8 +++ data/bot/lumbridge.nav-edges.toml | 64 ++++++++++++++----- data/bot/teleport.bots.toml | 28 +++++++- 3 files changed, 80 insertions(+), 20 deletions(-) diff --git a/data/area/misthalin/varrock/varrock.areas.toml b/data/area/misthalin/varrock/varrock.areas.toml index 689a88697c..39d2843864 100644 --- a/data/area/misthalin/varrock/varrock.areas.toml +++ b/data/area/misthalin/varrock/varrock.areas.toml @@ -21,6 +21,14 @@ rocks = ["copper", "tin", "iron"] levels = "1-45" spaces = 4 +[varrock_south_west_mine] +x = [3171, 3184] +y = [3364, 3380] +tags = ["mine"] +rocks = ["copper", "tin", "clay", "iron", "silver"] +levels = "1-45" +spaces = 4 + [varrock_east_bank] x = [3250, 3257] y = [3416, 3424] diff --git a/data/bot/lumbridge.nav-edges.toml b/data/bot/lumbridge.nav-edges.toml index a1f1e5a626..b4b9e68b05 100644 --- a/data/bot/lumbridge.nav-edges.toml +++ b/data/bot/lumbridge.nav-edges.toml @@ -1,8 +1,8 @@ edges = [ - { from_x = 3234, from_y = 3218, to_x = 3236, to_y = 3205 }, # lumbridge_gate_south_to_village - { from_x = 3234, from_y = 3218, to_x = 3243, to_y = 3209 }, # lumbridge_gate_south_to_church + { from_x = 3236, from_y = 3218, to_x = 3236, to_y = 3205 }, # lumbridge_gate_south_to_village + { from_x = 3236, from_y = 3218, to_x = 3243, to_y = 3209 }, # lumbridge_gate_south_to_church { from_x = 3236, from_y = 3205, to_x = 3243, to_y = 3209 }, # lumbridge_south_village_to_church - { from_x = 3234, from_y = 3218, to_x = 3250, to_y = 3212 }, # lumbridge_gate_south_to_behind_church + { from_x = 3236, from_y = 3218, to_x = 3250, to_y = 3212 }, # lumbridge_gate_south_to_behind_church { from_x = 3250, from_y = 3212, to_x = 3258, to_y = 3206 }, # lumbridge_behind_church_to_church_fishing_spot { from_x = 3236, from_y = 3205, to_x = 3231, to_y = 3203, cost = 5, actions = [{ option = "Open", object = "door_720_closed", x = 3234, y = 3203 }, { x = 3231, y = 3203 }] }, # lumbridge_south_village_to_bobs_axes { from_x = 3231, from_y = 3203, to_x = 3236, to_y = 3205, cost = 5, actions = [{ option = "Open", object = "door_720_closed", x = 3234, y = 3203 }, { x = 3236, y = 3205 }] }, # lumbridge_bobs_axes_to_south_village @@ -12,33 +12,38 @@ edges = [ { from_x = 3244, from_y = 3190, to_x = 3253, to_y = 3200 }, # lumbridge_graveyard_exit_to_behind_graveyard { from_x = 3250, from_y = 3212, to_x = 3253, to_y = 3200 }, # lumbridge_behind_church_to_behind_graveyard { from_x = 3258, from_y = 3206, to_x = 3253, to_y = 3200 }, # lumbridge_church_fishing_spot_to_behind_graveyard - { from_x = 3205, from_y = 3209, to_x = 3205, to_y = 3209, cost = 1, actions = [{ option = "Climb-down", object = "lumbridge_staircase_top", x = 3204, y = 3207 }] }, # lumbridge_castle_2nd_floor_south_stairs_to_1st_floor - { from_x = 3208, from_y = 3219, to_x = 3205, to_y = 3209 }, # lumbridge_castle_2nd_floor_bank_to_south_stairs - { from_x = 3205, from_y = 3209, to_x = 3205, to_y = 3209, cost = 1, actions = [{ option = "Climb-up", object = "lumbridge_staircase", x = 3204, y = 3207 }] }, # lumbridge_castle_ground_floor_south_stairs_to_1st_floor + { from_x = 3205, from_y = 3209, from_level = 2, to_x = 3205, to_y = 3209, to_level = 1, cost = 1, actions = [{ option = "Climb-down", object = "lumbridge_staircase_top", x = 3204, y = 3207 }] }, # lumbridge_castle_2nd_floor_south_stairs_to_1st_floor + { from_x = 3208, from_y = 3219, from_level = 2, to_x = 3205, to_y = 3209, to_level = 2 }, # lumbridge_castle_2nd_floor_bank_to_south_stairs + { from_x = 3205, from_y = 3209, to_x = 3205, to_y = 3209, to_level = 1, cost = 1, actions = [{ option = "Climb-up", object = "lumbridge_staircase", x = 3204, y = 3207 }] }, # lumbridge_castle_ground_floor_south_stairs_to_1st_floor { from_x = 3205, from_y = 3209, to_x = 3208, to_y = 3210 }, # lumbridge_castle_ground_floor_south_stairs_to_kitchen_corridor { from_x = 3208, from_y = 3210, to_x = 3215, to_y = 3216 }, # lumbridge_castle_kitchen_corridor_to_castle_south_entrance { from_x = 3208, from_y = 3210, to_x = 3211, to_y = 3214 }, # lumbridge_castle_kitchen_corridor_to_kitchen { from_x = 3215, from_y = 3216, to_x = 3222, to_y = 3218 }, # lumbridge_castle_south_entrance_to_courtyard_south - { from_x = 3205, from_y = 3209, to_x = 3205, to_y = 3209, cost = 1, actions = [{ option = "Climb-down", object = "lumbridge_castle_staircase_south_middle", x = 3204, y = 3207 }] }, # lumbridge_castle_1st_floor_south_stairs_to_ground_floor - { from_x = 3205, from_y = 3209, to_x = 3205, to_y = 3209, cost = 1, actions = [{ option = "Climb-up", object = "lumbridge_castle_staircase_south_middle", x = 3204, y = 3207 }] }, # lumbridge_castle_1st_floor_south_stairs_to_2nd_floor + { from_x = 3205, from_y = 3209, from_level = 1, to_x = 3205, to_y = 3209, cost = 1, actions = [{ option = "Climb-down", object = "lumbridge_castle_staircase_south_middle", x = 3204, y = 3207 }] }, # lumbridge_castle_1st_floor_south_stairs_to_ground_floor + { from_x = 3205, from_y = 3209, from_level = 1, to_x = 3205, to_y = 3209, to_level = 2, cost = 1, actions = [{ option = "Climb-up", object = "lumbridge_castle_staircase_south_middle", x = 3204, y = 3207 }] }, # lumbridge_castle_1st_floor_south_stairs_to_2nd_floor { from_x = 3209, from_y = 3205, to_x = 3199, to_y = 3218 }, # lumbridge_castle_grounds_south_to_tower_west { from_x = 3199, from_y = 3218, to_x = 3184, to_y = 3225 }, # lumbridge_castle_tower_west_to_yew_trees { from_x = 3199, from_y = 3218, to_x = 3193, to_y = 3236 }, # lumbridge_castle_tower_west_to_tree_patch { from_x = 3184, from_y = 3225, to_x = 3168, to_y = 3221 }, # lumbridge_castle_yew_trees_to_yew_trees_west { from_x = 3184, from_y = 3225, to_x = 3193, to_y = 3236 }, # lumbridge_castle_yew_trees_to_tree_patch { from_x = 3226, from_y = 3214, to_x = 3227, to_y = 3214, cost = 3, actions = [{ option = "Open", object = "door_627_closed", x = 3226, y = 3214 }] }, # lumbridge_south_tower_to_ground_floor - { from_x = 3227, from_y = 3214, to_x = 3229, to_y = 3214, cost = 1, actions = [{ option = "Climb-up", object = "36768", x = 3229, y = 3213 }] }, # lumbridge_south_tower_ground_floor_to_1st_floor - { from_x = 3229, from_y = 3214, to_x = 3229, to_y = 3214, cost = 1, actions = [{ option = "Climb-up", object = "36769", x = 3229, y = 3213 }] }, # lumbridge_south_tower_1st_floor_to_2nd_floor - { from_x = 3229, from_y = 3214, to_x = 3229, to_y = 3214, cost = 1, actions = [{ option = "Climb-down", object = "36769", x = 3229, y = 3213 }] }, # lumbridge_south_tower_1st_floor_to_ground_floor - { from_x = 3229, from_y = 3214, to_x = 3229, to_y = 3214, cost = 1, actions = [{ option = "Climb-down", object = "36770", x = 3229, y = 3213 }] }, # lumbridge_south_tower_2nd_floor_to_1st_floor - { from_x = 3234, from_y = 3220, to_x = 3236, to_y = 3225 }, # lumbridge_gate_north_to_bridge_west + { from_x = 3227, from_y = 3214, to_x = 3229, to_y = 3214, to_level = 1, cost = 1, actions = [{ option = "Climb-up", object = "36768", x = 3229, y = 3213 }] }, # lumbridge_south_tower_ground_floor_to_1st_floor + { from_x = 3229, from_y = 3214, from_level = 1, to_x = 3229, to_y = 3214, to_level = 2, cost = 1, actions = [{ option = "Climb-up", object = "36769", x = 3229, y = 3213 }] }, # lumbridge_south_tower_1st_floor_to_2nd_floor + { from_x = 3229, from_y = 3214, from_level = 1, to_x = 3229, to_y = 3214, cost = 1, actions = [{ option = "Climb-down", object = "36769", x = 3229, y = 3213 }] }, # lumbridge_south_tower_1st_floor_to_ground_floor + { from_x = 3229, from_y = 3214, from_level = 2, to_x = 3229, to_y = 3214, to_level = 1, cost = 1, actions = [{ option = "Climb-down", object = "36770", x = 3229, y = 3213 }] }, # lumbridge_south_tower_2nd_floor_to_1st_floor + { from_x = 3236, from_y = 3220, to_x = 3236, to_y = 3225 }, # lumbridge_gate_north_to_bridge_west { from_x = 3236, from_y = 3225, to_x = 3230, to_y = 3232 }, # lumbridge_bridge_west_to_unstable_house { from_x = 3236, from_y = 3225, to_x = 3253, to_y = 3225 }, # lumbridge_bridge_west_to_bridge_east { from_x = 3253, from_y = 3225, to_x = 3263, to_y = 3222 }, # lumbridge_bridge_east_to_trees_east { from_x = 3253, from_y = 3225, to_x = 3260, to_y = 3228 }, # lumbridge_bridge_east_to_east_crossroad { from_x = 3260, from_y = 3228, to_x = 3263, to_y = 3222 }, # lumbridge_east_crossroad_to_trees_east + { from_x = 3260, from_y = 3228, to_x = 3259, to_y = 3239 }, + { from_x = 3259, from_y = 3239, to_x = 3258, to_y = 3250 }, + { from_x = 3259, from_y = 3239, to_x = 3256, to_y = 3247 }, + { from_x = 3256, from_y = 3247, to_x = 3251, to_y = 3252 }, { from_x = 3235, from_y = 3261, to_x = 3250, to_y = 3266 }, # lumbridge_bridge_north_to_cow_entrance { from_x = 3251, from_y = 3252, to_x = 3250, to_y = 3266 }, # lumbridge_goblin_path_north_to_cow_entrance + { from_x = 3251, from_y = 3252, to_x = 3258, to_y = 3250 }, { from_x = 3250, from_y = 3266, to_x = 3240, to_y = 3280 }, # lumbridge_cow_entrance_to_cow_path { from_x = 3240, from_y = 3280, to_x = 3238, to_y = 3295 }, # lumbridge_cow_path_to_chicken_entrance { from_x = 3238, from_y = 3295, to_x = 3235, to_y = 3295, cost = 0, actions = [{ option = "Open", object = "gate_235_closed", x = 3237, y = 3295 }] }, # lumbridge_chicken_entrance_to_chicken_pen @@ -73,9 +78,9 @@ edges = [ { from_x = 3136, from_y = 3219, to_x = 3119, to_y = 3228 }, # lumbridge_swamp_west_wall_to_draynor_jail_path_south { from_x = 3222, from_y = 3218, to_x = 3226, to_y = 3214 }, # lumbridge_courtyard_south_to_tower_door { from_x = 3222, from_y = 3218, to_x = 3222, to_y = 3220 }, # lumbridge_courtyard_south_to_north - { from_x = 3222, from_y = 3218, to_x = 3234, to_y = 3218 }, # lumbridge_courtyard_south_to_gate_north - { from_x = 3222, from_y = 3220, to_x = 3234, to_y = 3220 }, # lumbridge_courtyard_north_to_gate_north - { from_x = 3234, from_y = 3218, to_x = 3234, to_y = 3220 }, # lumbridge_courtyard_gate_south_to_gate_north + { from_x = 3222, from_y = 3218, to_x = 3236, to_y = 3218 }, # lumbridge_courtyard_south_to_gate_north + { from_x = 3222, from_y = 3220, to_x = 3236, to_y = 3220 }, # lumbridge_courtyard_north_to_gate_north + { from_x = 3236, from_y = 3218, to_x = 3236, to_y = 3220 }, # lumbridge_courtyard_gate_south_to_gate_north { from_x = 3222, from_y = 3218, to_x = 3209, to_y = 3205 }, # lumbridge_courtyard_south_to_grounds_south { from_x = 3230, from_y = 3232, to_x = 3222, to_y = 3241 }, # lumbridge_unstable_house_to_general_store_east { from_x = 3222, from_y = 3241, to_x = 3217, to_y = 3241, cost = 6, actions = [{ option = "Open", object = "door_720_closed", x = 3219, y = 3241 }, { x = 3217, y = 3241 }] }, # lumbridge_general_store_east_to_general_store @@ -102,6 +107,23 @@ edges = [ { from_x = 3225, from_y = 3252, to_x = 3222, to_y = 3255 }, # lumbridge_smiths_south_to_west { from_x = 3222, from_y = 3255, to_x = 3218, to_y = 3255 }, # lumbridge_smiths_west_to_smiths_crossroad { from_x = 3218, from_y = 3255, to_x = 3219, to_y = 3247 }, # lumbridge_smiths_crossroad_to_west_crossroad + { from_x = 3218, from_y = 3255, to_x = 3223, to_y = 3260 }, + { from_x = 3223, from_y = 3260, to_x = 3235, to_y = 3261 }, + { from_x = 3218, from_y = 3255, to_x = 3217, to_y = 3268 }, + { from_x = 3217, from_y = 3268, to_x = 3213, to_y = 3277 }, + { from_x = 3213, from_y = 3277, to_x = 3199, to_y = 3279 }, + { from_x = 3199, from_y = 3279, to_x = 3190, to_y = 3283 }, + { from_x = 3190, from_y = 3283, to_x = 3190, to_y = 3294 }, + { from_x = 3190, from_y = 3294, to_x = 3186, to_y = 3307 }, + { from_x = 3186, from_y = 3307, to_x = 3177, to_y = 3315 }, + { from_x = 3177, from_y = 3315, to_x = 3177, to_y = 3316, cost = 0, actions = [{ option = "Open", object = "gate_235_closed", x = 3177, y = 3316 }] }, + { from_x = 3177, from_y = 3316, to_x = 3177, to_y = 3315, cost = 0, actions = [{ option = "Open", object = "gate_235_closed", x = 3177, y = 3316 }] }, + { from_x = 3177, from_y = 3316, to_x = 3179, to_y = 3326 }, + { from_x = 3179, from_y = 3326, to_x = 3178, to_y = 3334 }, + { from_x = 3178, from_y = 3334, to_x = 3177, to_y = 3345 }, + { from_x = 3177, from_y = 3345, to_x = 3177, to_y = 3357 }, + { from_x = 3177, from_y = 3357, to_x = 3178, to_y = 3364 }, + { from_x = 3178, from_y = 3364, to_x = 3184, to_y = 3370 }, { from_x = 3225, from_y = 3252, to_x = 3219, to_y = 3247 }, # lumbridge_smiths_south_to_west_crossroad { from_x = 3253, from_y = 3225, to_x = 3245, to_y = 3238 }, # lumbridge_bridge_east_to_goblins_river { from_x = 3253, from_y = 3225, to_x = 3254, to_y = 3239 }, # lumbridge_bridge_east_to_goblins @@ -129,7 +151,7 @@ edges = [ { from_x = 3128, from_y = 3407, to_x = 2841, to_y = 4830, cost = 3, actions = [{ option = "null", object = "air_altar_ruins", x = 3126, y = 3404 }] }, # varrock_west_air_altar_ruins_to_air_altar { from_x = 2841, from_y = 4830, to_x = 2841, to_y = 4829 }, # varrock_west_air_altar_to_exit { from_x = 2841, from_y = 4829, to_x = 3128, to_y = 3407, cost = 3, actions = [{ option = "Enter", object = "air_altar_portal", x = 2841, y = 4828 }] }, # varrock_west_air_altar_exit_to_ruins - { from_x = 3304, from_y = 3474, to_x = 2655, to_y = 4831, cost = 3, actions = [{ option = "null", object = "earth_altar_ruins", x = 3305, y = 3473 }] }, # varrock_east_earth_altar_ruins_to_altar + { from_x = 3304, from_y = 3474, to_x = 2655, to_y = 4831, cost = 3, actions = [{ option = "Enter", object = "earth_altar_ruins", x = 3305, y = 3473 }] }, # varrock_east_earth_altar_ruins_to_altar { from_x = 2655, from_y = 4831, to_x = 2655, to_y = 4830 }, # varrock_east_earth_altar_to_exit { from_x = 2655, from_y = 4830, to_x = 3304, to_y = 3474, cost = 3, actions = [{ option = "Enter", object = "earth_altar_portal", x = 2655, y = 4829 }] }, # varrock_east_earth_altar_exit_to_ruins { from_x = 3254, from_y = 3422, to_x = 3265, to_y = 3428 }, # varrock_east_bank_to_dirt_crossroad @@ -147,6 +169,8 @@ edges = [ { from_x = 3230, from_y = 3430, to_x = 3223, to_y = 3429 }, # varrock_east_armour_shop_to_centre_east { from_x = 3238, from_y = 3295, to_x = 3238, to_y = 3304 }, # lumbridge_chicken_entrance_to_varrock_south_split { from_x = 3238, from_y = 3304, to_x = 3251, to_y = 3319 }, # varrock_south_split_to_al_kharid_path_west + { from_x = 3238, from_y = 3304, to_x = 3238, to_y = 3320 }, # varrock_south_split_to_al_kharid_path_west + { from_x = 3238, from_y = 3320, to_x = 3240, to_y = 3336 }, # varrock_south_split_to_al_kharid_path_west { from_x = 3251, from_y = 3319, to_x = 3268, to_y = 3331 }, # varrock_al_kharid_path_west_to_crossroads { from_x = 3283, from_y = 3331, to_x = 3295, to_y = 3334 }, # al_kharid_north_entrance_to_varrock_north_west { from_x = 3295, from_y = 3334, to_x = 3304, to_y = 3335 }, # varrock_al_kharid_north_west_to_crossroad @@ -162,6 +186,12 @@ edges = [ { from_x = 3211, from_y = 3395, to_x = 3211, to_y = 3381 }, # varrock_blue_moon_inn_to_south_border { from_x = 3211, from_y = 3381, to_x = 3214, to_y = 3367 }, # varrock_south_border_to_dark_mages { from_x = 3214, from_y = 3367, to_x = 3226, to_y = 3352 }, # varrock_south_dark_mages_to_south + { from_x = 3214, from_y = 3367, to_x = 3206, to_y = 3376 }, + { from_x = 3211, from_y = 3381, to_x = 3206, to_y = 3376 }, + { from_x = 3206, from_y = 3376, to_x = 3197, to_y = 3373 }, + { from_x = 3197, from_y = 3373, to_x = 3191, to_y = 3367 }, + { from_x = 3197, from_y = 3373, to_x = 3184, to_y = 3370 }, + { from_x = 3191, from_y = 3367, to_x = 3184, to_y = 3370 }, { from_x = 3226, from_y = 3352, to_x = 3228, to_y = 3337 }, # varrock_dark_mages_to_south_fields_path { from_x = 3228, from_y = 3337, to_x = 3240, to_y = 3336 }, # varrock_south_fields_path_to_stile { from_x = 3240, from_y = 3336, to_x = 3254, to_y = 3333 }, # varrock_south_stile_to_path diff --git a/data/bot/teleport.bots.toml b/data/bot/teleport.bots.toml index b95448ee10..a5c8ec3982 100644 --- a/data/bot/teleport.bots.toml +++ b/data/bot/teleport.bots.toml @@ -27,17 +27,39 @@ [teleport_varrock] type = "shortcut" -weight = 10 +weight = 20 requires = [ { variable = "spellbook_config", value = 0, default = 0 }, -] -setup = [ { carries = "fire_rune", amount = 1 }, { carries = "air_rune", amount = 3 }, { carries = "law_rune", amount = 1 }, ] actions = [ { option = "Cast", interface = "modern_spellbook:varrock_teleport" }, + { wait = 5 }, +] +produces = [ + { location = "varrock_teleport" } +] + +[teleport_varrock_via_bank] +type = "shortcut" +weight = 50 +requires = [ + { variable = "spellbook_config", value = 0, default = 0 }, + { owns = "fire_rune", amount = 1 }, + { owns = "air_rune", amount = 3 }, + { owns = "law_rune", amount = 1 }, +] +actions = [ + { go_to_nearest = "bank" }, + { option = "Use-quickly", object = "bank_booth*" }, + { option = "Withdraw-1", interface = "bank:inventory:fire_rune" }, + { option = "Withdraw-X", interface = "bank:inventory:air_rune" }, + { enter_int = 3 }, + { option = "Withdraw-1", interface = "bank:inventory:law_rune" }, + { option = "Cast", interface = "modern_spellbook:varrock_teleport" }, + { wait = 5 }, ] produces = [ { location = "varrock_teleport" } From f90abc6ba94680275cfc759bc55a63cc9bf66dbe Mon Sep 17 00:00:00 2001 From: GregHib Date: Mon, 2 Feb 2026 20:37:46 +0000 Subject: [PATCH 034/101] WIP --- data/bot/teleport.bots.toml | 5 +++-- game/src/main/kotlin/content/bot/action/BotAction.kt | 4 +++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/data/bot/teleport.bots.toml b/data/bot/teleport.bots.toml index a5c8ec3982..a08ca2668d 100644 --- a/data/bot/teleport.bots.toml +++ b/data/bot/teleport.bots.toml @@ -27,7 +27,7 @@ [teleport_varrock] type = "shortcut" -weight = 20 +weight = 75 requires = [ { variable = "spellbook_config", value = 0, default = 0 }, { carries = "fire_rune", amount = 1 }, @@ -44,7 +44,7 @@ produces = [ [teleport_varrock_via_bank] type = "shortcut" -weight = 50 +weight = 100 requires = [ { variable = "spellbook_config", value = 0, default = 0 }, { owns = "fire_rune", amount = 1 }, @@ -60,6 +60,7 @@ actions = [ { option = "Withdraw-1", interface = "bank:inventory:law_rune" }, { option = "Cast", interface = "modern_spellbook:varrock_teleport" }, { wait = 5 }, +# { wait = { location = "varrock_teleport" } }, ] produces = [ { location = "varrock_teleport" } diff --git a/game/src/main/kotlin/content/bot/action/BotAction.kt b/game/src/main/kotlin/content/bot/action/BotAction.kt index 51d468520e..89b6fadee6 100644 --- a/game/src/main/kotlin/content/bot/action/BotAction.kt +++ b/game/src/main/kotlin/content/bot/action/BotAction.kt @@ -42,6 +42,8 @@ sealed interface BotAction { override fun update(bot: Bot) = BehaviourState.Failed(Reason.Cancelled) } + // TODO actions for navigation, combat, gathering, equipping items + data class Wait(val ticks: Int) : BotAction { override fun start(bot: Bot) = BehaviourState.Wait(ticks, BehaviourState.Success) } @@ -164,7 +166,7 @@ sealed interface BotAction { } val obj = objects.randomOrNull(random) ?: return BehaviourState.Failed(Reason.NoTarget) val index = obj.def(bot.player).options?.indexOf(option) ?: return BehaviourState.Failed(Reason.NoTarget) - bot.player.instructions.trySend(world.gregs.voidps.network.client.instruction.InteractObject(obj.intId, obj.x, obj.y, index + 1)) + bot.player.instructions.trySend(InteractObject(obj.intId, obj.x, obj.y, index + 1)) return BehaviourState.Wait(2, BehaviourState.Success) } From 26a43ed1596360fc3b17a16c90a30dc9eb7a6b5c Mon Sep 17 00:00:00 2001 From: GregHib Date: Wed, 4 Feb 2026 10:27:24 +0000 Subject: [PATCH 035/101] Add clock and timer facts --- .../content/bot/action/BehaviourFragment.kt | 33 ++++++----------- .../kotlin/content/bot/action/BotActivity.kt | 37 ++++--------------- .../main/kotlin/content/bot/fact/Condition.kt | 9 +++++ game/src/main/kotlin/content/bot/fact/Fact.kt | 13 +++++++ .../bot/action/BehaviourFragmentTest.kt | 2 + 5 files changed, 43 insertions(+), 51 deletions(-) diff --git a/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt b/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt index 2650f46e01..f8d8644119 100644 --- a/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt +++ b/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt @@ -76,35 +76,26 @@ data class BehaviourFragment( "carries" -> { val id = resolve(references[req.type], req.id) val min = resolve(references["amount"], req.min) - if (id.contains(",")) { - Condition.Any(id.split(",").map { Condition.range(Fact.InventoryCount(it), min, max) }) - } else if (id.any { it == '*' || it == '#' }) { - Condition.Any(Wildcards.get(id, Wildcard.Item).map { Condition.range(Fact.InventoryCount(it), min, max) }) - } else { - Condition.range(Fact.InventoryCount(id), min, max) - } + Condition.split(id, min, max, Wildcard.Item) { Fact.InventoryCount(it) } } "equips" -> { val id = resolve(references[req.type], req.id) val min = resolve(references["amount"], req.min) - if (id.contains(",")) { - Condition.Any(id.split(",").map { Condition.range(Fact.EquipCount(id), min, max) }) - } else if (id.any { it == '*' || it == '#' }) { - Condition.Any(Wildcards.get(id, Wildcard.Item).map { Condition.range(Fact.EquipCount(id), min, max) }) - } else { - Condition.range(Fact.EquipCount(id), min, max) - } + Condition.split(id, min, max, Wildcard.Item) { Fact.EquipCount(it) } } "owns" -> { val id = resolve(references[req.type], req.id) val min = resolve(references["amount"], req.min) - if (id.contains(",")) { - Condition.Any(id.split(",").map { Condition.range(Fact.ItemCount(id), min, max) }) - } else if (id.any { it == '*' || it == '#' }) { - Condition.Any(Wildcards.get(id, Wildcard.Item).map { Condition.range(Fact.ItemCount(id), min, max) }) - } else { - Condition.range(Fact.ItemCount(id), min, max) - } + Condition.split(id, min, max, Wildcard.Item) { Fact.ItemCount(it) } + } + "clock" -> { + val id = resolve(references[req.type], req.id) + Condition.split(id, min, max, Wildcard.Variables) { Fact.ClockRemaining(it) } + } + "timer" -> { + val id = resolve(references[req.type], req.id) + val value = resolve(references["value"], req.value as? Boolean) + Condition.Equals(Fact.HasTimer(id), value as? Boolean ?: true) } "variable" -> { val id = resolve(references[req.type], req.id) diff --git a/game/src/main/kotlin/content/bot/action/BotActivity.kt b/game/src/main/kotlin/content/bot/action/BotActivity.kt index c5826ea876..e84cef6c40 100644 --- a/game/src/main/kotlin/content/bot/action/BotActivity.kt +++ b/game/src/main/kotlin/content/bot/action/BotActivity.kt @@ -182,21 +182,14 @@ fun ConfigReader.requirements(list: MutableList) { val references = mutableMapOf() while (nextEntry()) { when (val key = key()) { - "skill", "carries", "equips", "owns", "variable", "clone", "location" -> { + "skill", "carries", "equips", "owns", "clock", "variable", "clone", "location" -> { type = key id = string() if (id.contains('$')) { references[key] = id } } - "amount" -> when (val value = value()) { - is Int -> { - min = value - } - is String if value.contains('$') -> references[key] = value - else -> throw IllegalArgumentException("Invalid '$key' value: $value ${exception()}") - } - "min" -> when (val value = value()) { + "amount", "min" -> when (val value = value()) { is Int -> min = value is String if value.contains('$') -> references[key] = value else -> throw IllegalArgumentException("Invalid '$key' value: $value ${exception()}") @@ -243,27 +236,11 @@ fun ConfigReader.requirements(list: MutableList) { private fun getRequirement(type: String, id: String, min: Int?, max: Int?, value: Any?, default: Any?): Condition? = when (type) { "skill" -> Condition.range(Fact.SkillLevel.of(id), min, max) - "carries" -> if (id.contains(",")) { - Condition.Any(id.split(",").map { Condition.range(Fact.InventoryCount(it), min, max) }) - } else if (id.any { it == '*' || it == '#' }) { - Condition.Any(Wildcards.get(id, Wildcard.Item).map { Condition.range(Fact.InventoryCount(it), min, max) }) - } else { - Condition.range(Fact.InventoryCount(id), min, max) - } - "owns" -> if (id.contains(",")) { - Condition.Any(id.split(",").map { Condition.range(Fact.ItemCount(it), min, max) }) - } else if (id.any { it == '*' || it == '#' }) { - Condition.Any(Wildcards.get(id, Wildcard.Item).map { Condition.range(Fact.ItemCount(it), min, max) }) - } else { - Condition.range(Fact.ItemCount(id), min, max) - } - "equips" -> if (id.contains(",")) { - Condition.Any(id.split(",").map { Condition.range(Fact.EquipCount(it), min, max) }) - } else if (id.any { it == '*' || it == '#' }) { - Condition.Any(Wildcards.get(id, Wildcard.Item).map { Condition.range(Fact.EquipCount(it), min, max) }) - } else { - Condition.range(Fact.EquipCount(id), min, max) - } + "carries" -> Condition.split(id, min, max, Wildcard.Item) { Fact.InventoryCount(it) } + "owns" -> Condition.split(id, min, max, Wildcard.Item) { Fact.ItemCount(it) } + "equips" -> Condition.split(id, min, max, Wildcard.Item) { Fact.EquipCount(it) } + "clock" -> Condition.split(id, min, max, Wildcard.Variables) { Fact.ClockRemaining(it) } + "timer" -> Condition.Equals(Fact.HasTimer(id), value as? Boolean ?: true) "variable" -> when (value) { is Int -> Condition.Equals(Fact.IntVariable(id, default as? Int), value) is String -> Condition.Equals(Fact.StringVariable(id, default as? String), value) diff --git a/game/src/main/kotlin/content/bot/fact/Condition.kt b/game/src/main/kotlin/content/bot/fact/Condition.kt index f3890d5e6c..62f0185b1c 100644 --- a/game/src/main/kotlin/content/bot/fact/Condition.kt +++ b/game/src/main/kotlin/content/bot/fact/Condition.kt @@ -2,7 +2,10 @@ package content.bot.fact import world.gregs.voidps.engine.data.definition.Areas import world.gregs.voidps.engine.entity.character.player.Player +import world.gregs.voidps.engine.event.Wildcard +import world.gregs.voidps.engine.event.Wildcards import world.gregs.voidps.type.Tile +import kotlin.collections.map sealed interface Condition { fun check(player: Player): Boolean @@ -90,6 +93,12 @@ sealed interface Condition { } companion object { + fun split(id: String, min: Int?, max: Int?, wildcard: Wildcard, fact: (String) -> Fact): Condition = when { + id.contains(",") -> Any(id.split(",").flatMap { if (id.any { it == '*' || it == '#' }) Wildcards.get(id, wildcard).map { range(fact(id), min, max) } else listOf(range(fact(id), min, max)) }) + id.any { it == '*' || it == '#' } -> Any(Wildcards.get(id, wildcard).map { range(fact(id), min, max) }) + else -> range(fact(id), min, max) + } + fun range(fact: Fact, min: Int?, max: Int?) = when { min != null && max != null -> Range(fact, min, max) min != null -> AtLeast(fact, min) diff --git a/game/src/main/kotlin/content/bot/fact/Fact.kt b/game/src/main/kotlin/content/bot/fact/Fact.kt index 60ace6a8e6..4814c6e5d0 100644 --- a/game/src/main/kotlin/content/bot/fact/Fact.kt +++ b/game/src/main/kotlin/content/bot/fact/Fact.kt @@ -1,10 +1,13 @@ package content.bot.fact import content.entity.player.bank.bank +import world.gregs.voidps.engine.GameLoop +import world.gregs.voidps.engine.client.variable.remaining import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.entity.character.player.skill.Skill import world.gregs.voidps.engine.inv.equipment import world.gregs.voidps.engine.inv.inventory +import world.gregs.voidps.engine.timer.epochSeconds import world.gregs.voidps.type.Tile /** @@ -55,6 +58,16 @@ sealed class Fact(val priority: Int) { override fun getValue(player: Player) = player.variables.get(id) ?: default } + data class ClockRemaining(val clock: String, val seconds: Boolean = false) : Fact(1) { + override fun keys() = setOf("var:$clock") + override fun getValue(player: Player) = player.remaining(clock, if (seconds) epochSeconds() else GameLoop.tick) + } + + data class HasTimer(val timer: String) : Fact(1) { + override fun keys() = setOf("timer:$timer") + override fun getValue(player: Player) = player.timers.contains(timer) + } + object PlayerTile : Fact(1000) { override fun keys() = setOf("tile") override fun getValue(player: Player) = player.tile diff --git a/game/src/test/kotlin/content/bot/action/BehaviourFragmentTest.kt b/game/src/test/kotlin/content/bot/action/BehaviourFragmentTest.kt index f6c0ed6902..ac7cad9b4b 100644 --- a/game/src/test/kotlin/content/bot/action/BehaviourFragmentTest.kt +++ b/game/src/test/kotlin/content/bot/action/BehaviourFragmentTest.kt @@ -225,6 +225,8 @@ class BehaviourFragmentTest { Triple(Condition.Reference("carries", "default", min = 1), mapOf("carries" to "item", "amount" to 10), Condition.AtLeast(Fact.InventoryCount("item"), 10)), Triple(Condition.Reference("inventory_space", min = 1), mapOf("inventory_space" to 10), Condition.AtLeast(Fact.InventorySpace, 10)), Triple(Condition.Reference("location", "default"), mapOf("location" to "area"), Condition.Area(Fact.PlayerTile, "area")), + Triple(Condition.Reference("timer", "default"), mapOf("timer" to "tick"), Condition.Equals(Fact.HasTimer("tick"), true)), + Triple(Condition.Reference("clock", "default", min = 1), mapOf("clock" to "tock", "min" to 5), Condition.AtLeast(Fact.ClockRemaining("tock"), 5)), ).map { (reference, values, expected) -> dynamicTest("Resolve ${reference::class.simpleName} references") { val fields = values.mapKeys { "ref_${it.key}" } From 096bb59437a694661b7897e434bf54e8adb0975a Mon Sep 17 00:00:00 2001 From: GregHib Date: Wed, 4 Feb 2026 12:19:25 +0000 Subject: [PATCH 036/101] Replace retry-actions with success conditions --- data/bot/woodcutting.bots.toml | 2 +- .../src/main/kotlin/content/bot/BotManager.kt | 10 +- .../content/bot/action/BehaviourFragment.kt | 30 ++- .../content/bot/action/BehaviourFrame.kt | 6 +- .../kotlin/content/bot/action/BotAction.kt | 223 ++++++++++++++---- .../kotlin/content/bot/action/BotActivity.kt | 128 +++++----- .../main/kotlin/content/bot/action/Reason.kt | 1 + .../test/kotlin/content/bot/BotManagerTest.kt | 40 +--- .../bot/action/BehaviourFragmentTest.kt | 16 +- 9 files changed, 286 insertions(+), 170 deletions(-) diff --git a/data/bot/woodcutting.bots.toml b/data/bot/woodcutting.bots.toml index 23285ec352..2af951fd2e 100644 --- a/data/bot/woodcutting.bots.toml +++ b/data/bot/woodcutting.bots.toml @@ -6,7 +6,7 @@ setup = [ { inventory_space = 20 }, ] actions = [ - { option = "Chop down", object = "$tree" }, + { option = "Chop down", object = "$tree", delay = 5, success = { inventory_space = 0 }, timeout = 30 }, ] produces = [ { skill = "woodcutting" } diff --git a/game/src/main/kotlin/content/bot/BotManager.kt b/game/src/main/kotlin/content/bot/BotManager.kt index c8dc2d4402..84f4fee2e8 100644 --- a/game/src/main/kotlin/content/bot/BotManager.kt +++ b/game/src/main/kotlin/content/bot/BotManager.kt @@ -229,15 +229,7 @@ class BotManager( if (bot.player["debug", false]) { logger.info { "Failed action: ${action::class.simpleName} for ${behaviour.id}, reason: ${state.reason}." } } - if (action is BotAction.RetryableAction && action.retryMax > 0) { - frame.state = BehaviourState.Wait(action.retryTicks, BehaviourState.Running) - if (frame.retries++ < action.retryMax) { - frame.start(bot) - AuditLog.event(bot, "retry", frame.behaviour.id, frame.index, frame.retries, action::class.simpleName) - return - } - } - AuditLog.event(bot, "failed", behaviour.id, state.reason, frame.index, frame.retries, action::class.simpleName) + AuditLog.event(bot, "failed", behaviour.id, state.reason, frame.index, action::class.simpleName) if (state.reason is HardReason) { stop(bot) } else { diff --git a/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt b/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt index f8d8644119..fe069f7fa2 100644 --- a/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt +++ b/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt @@ -26,18 +26,36 @@ data class BehaviourFragment( id = resolve(action.references["interface"], copy.id), option = resolve(action.references["option"], copy.option), ) - is BotAction.InteractNpc -> BotAction.InteractNpc( - option = resolve(action.references["option"], copy.option), + is BotAction.InteractNpc -> { + val option = resolve(action.references["option"], copy.option) + if (option == "Attack") { + BotAction.FightNpc( + id = resolve(action.references["npc"], copy.id), + radius = resolve(action.references["radius"], copy.radius), + ) + } else { + BotAction.InteractNpc( + option = option, + id = resolve(action.references["npc"], copy.id), + delay = resolve(action.references["delay"], copy.delay), + successCondition = copy.successCondition, + radius = resolve(action.references["radius"], copy.radius), + ) + } + } + is BotAction.FightNpc -> BotAction.FightNpc( id = resolve(action.references["npc"], copy.id), - retryTicks = resolve(action.references["retry_ticks"], copy.retryTicks), - retryMax = resolve(action.references["retry_max"], copy.retryMax), + delay = resolve(action.references["delay"], copy.delay), + success = copy.success, + healPercentage = resolve(action.references["heal_percent"], copy.healPercentage), + lootOverValue = resolve(action.references["loot_over"], copy.lootOverValue), radius = resolve(action.references["radius"], copy.radius), ) is BotAction.InteractObject -> BotAction.InteractObject( option = resolve(action.references["option"], copy.option), id = resolve(action.references["object"], copy.id), - retryTicks = resolve(action.references["retry_ticks"], copy.retryTicks), - retryMax = resolve(action.references["retry_max"], copy.retryMax), + delay = resolve(action.references["delay"], copy.delay), + success = copy.success, radius = resolve(action.references["radius"], copy.radius), ) is BotAction.WalkTo -> BotAction.WalkTo( diff --git a/game/src/main/kotlin/content/bot/action/BehaviourFrame.kt b/game/src/main/kotlin/content/bot/action/BehaviourFrame.kt index 175738a670..22fd4ae170 100644 --- a/game/src/main/kotlin/content/bot/action/BehaviourFrame.kt +++ b/game/src/main/kotlin/content/bot/action/BehaviourFrame.kt @@ -6,8 +6,8 @@ data class BehaviourFrame( val behaviour: Behaviour, var state: BehaviourState = BehaviourState.Pending, var index: Int = 0, - var retries: Int = 0, var blocked: MutableSet = mutableSetOf(), + var timeout: Int = 0, ) { fun action(): BotAction = behaviour.actions[index] @@ -16,12 +16,12 @@ data class BehaviourFrame( fun start(bot: Bot) { val action = action() - state = action.start(bot) + state = action.start(bot, this) } fun update(bot: Bot) { val action = action() - state = action.update(bot) ?: return + state = action.update(bot, this) ?: return } fun next(): Boolean { diff --git a/game/src/main/kotlin/content/bot/action/BotAction.kt b/game/src/main/kotlin/content/bot/action/BotAction.kt index 89b6fadee6..42f0feef50 100644 --- a/game/src/main/kotlin/content/bot/action/BotAction.kt +++ b/game/src/main/kotlin/content/bot/action/BotAction.kt @@ -2,6 +2,7 @@ package content.bot.action import content.bot.Bot import content.bot.BotManager +import content.bot.fact.Condition import content.bot.interact.path.Graph import content.entity.combat.attackers import content.entity.player.bank.bank @@ -10,9 +11,14 @@ import world.gregs.voidps.engine.data.definition.Areas import world.gregs.voidps.engine.data.definition.InterfaceDefinitions import world.gregs.voidps.engine.data.definition.ItemDefinitions import world.gregs.voidps.engine.entity.character.mode.EmptyMode +import world.gregs.voidps.engine.entity.character.mode.interact.PlayerOnFloorItemInteract +import world.gregs.voidps.engine.entity.character.mode.interact.PlayerOnNPCInteract import world.gregs.voidps.engine.entity.character.mode.interact.PlayerOnObjectInteract import world.gregs.voidps.engine.entity.character.npc.NPC import world.gregs.voidps.engine.entity.character.npc.NPCs +import world.gregs.voidps.engine.entity.character.player.skill.Skill +import world.gregs.voidps.engine.entity.item.floor.FloorItem +import world.gregs.voidps.engine.entity.item.floor.FloorItems import world.gregs.voidps.engine.entity.obj.GameObject import world.gregs.voidps.engine.entity.obj.GameObjects import world.gregs.voidps.engine.event.wildcardEquals @@ -22,34 +28,28 @@ import world.gregs.voidps.engine.inv.inventory import world.gregs.voidps.engine.map.Spiral import world.gregs.voidps.network.client.instruction.* import world.gregs.voidps.type.random +import kotlin.collections.iterator sealed interface BotAction { - fun start(bot: Bot): BehaviourState = BehaviourState.Running - fun update(bot: Bot): BehaviourState? = null - - sealed class RetryableAction : BotAction { - abstract val retryTicks: Int - abstract val retryMax: Int - } + fun start(bot: Bot, frame: BehaviourFrame): BehaviourState = BehaviourState.Running + fun update(bot: Bot, frame: BehaviourFrame): BehaviourState? = null data class Clone(val id: String) : BotAction { - override fun start(bot: Bot) = BehaviourState.Failed(Reason.Cancelled) - override fun update(bot: Bot) = BehaviourState.Failed(Reason.Cancelled) + override fun start(bot: Bot, frame: BehaviourFrame) = BehaviourState.Failed(Reason.Cancelled) + override fun update(bot: Bot, frame: BehaviourFrame) = BehaviourState.Failed(Reason.Cancelled) } data class Reference(val action: BotAction, val references: Map) : BotAction { - override fun start(bot: Bot) = BehaviourState.Failed(Reason.Cancelled) - override fun update(bot: Bot) = BehaviourState.Failed(Reason.Cancelled) + override fun start(bot: Bot, frame: BehaviourFrame) = BehaviourState.Failed(Reason.Cancelled) + override fun update(bot: Bot, frame: BehaviourFrame) = BehaviourState.Failed(Reason.Cancelled) } - // TODO actions for navigation, combat, gathering, equipping items - - data class Wait(val ticks: Int) : BotAction { - override fun start(bot: Bot) = BehaviourState.Wait(ticks, BehaviourState.Success) + data class Wait(val ticks: Int, val state: BehaviourState = BehaviourState.Success) : BotAction { + override fun start(bot: Bot, frame: BehaviourFrame) = BehaviourState.Wait(ticks, state) } data class GoTo(val target: String) : BotAction { - override fun start(bot: Bot): BehaviourState { + override fun start(bot: Bot, frame: BehaviourFrame): BehaviourState { val def = Areas.getOrNull(target) ?: return BehaviourState.Failed(Reason.Invalid) if (bot.tile in def.area) { return BehaviourState.Success @@ -91,7 +91,7 @@ sealed interface BotAction { } data class GoToNearest(val tag: String) : BotAction { - override fun start(bot: Bot): BehaviourState { + override fun start(bot: Bot, frame: BehaviourFrame): BehaviourState { val set = Areas.tagged(tag) if (set.isEmpty()) { return BehaviourState.Failed(Reason.Invalid) @@ -106,7 +106,7 @@ sealed interface BotAction { return GoTo.queueRoute(success, list, graph, bot, tag) } - override fun update(bot: Bot): BehaviourState? { + override fun update(bot: Bot, frame: BehaviourFrame): BehaviourState? { val set = Areas.tagged(tag) if (set.isEmpty()) { return BehaviourState.Failed(Reason.Invalid) @@ -121,65 +121,194 @@ sealed interface BotAction { data class InteractNpc( val option: String, val id: String, - override val retryTicks: Int = 0, - override val retryMax: Int = 0, + val delay: Int = 0, + val successCondition: Condition? = null, val radius: Int = 10, - ) : RetryableAction() { - override fun start(bot: Bot): BehaviourState { + ) : BotAction { + + override fun start(bot: Bot, frame: BehaviourFrame): BehaviourState { + frame.timeout = 0 + return BehaviourState.Running + } + + override fun update(bot: Bot, frame: BehaviourFrame) = when { + bot.mode is PlayerOnNPCInteract -> if (successCondition == null) BehaviourState.Success else BehaviourState.Running + bot.mode is EmptyMode -> search(bot) + successCondition?.check(bot.player) == true -> BehaviourState.Success + else -> null + } + + private fun search(bot: Bot): BehaviourState { val npcs = mutableListOf() - for (tile in Spiral.spiral(bot.player.tile, radius)) { + val player = bot.player + for (tile in Spiral.spiral(player.tile, radius)) { for (npc in NPCs.at(tile)) { if (!wildcardEquals(id, npc.id)) { continue } - if (option == "Attack" && npc.attackers.isNotEmpty() && !npc.attackers.contains(bot.player)) { + if (!npc.def(player).options.contains(option)) { continue } npcs.add(npc) } } - val npc = npcs.randomOrNull(random) ?: return BehaviourState.Failed(Reason.NoTarget) - val index = npc.def(bot.player).options.indexOf(option) + val npc = npcs.randomOrNull(random) ?: return handleNoTarget() + val index = npc.def(player).options.indexOf(option) bot.player.instructions.trySend(InteractNPC(npc.index, index)) return BehaviourState.Running } + + private fun handleNoTarget(): BehaviourState { + if (successCondition == null) { + return BehaviourState.Failed(Reason.NoTarget) + } + if (delay > 0) { + return BehaviourState.Wait(delay, BehaviourState.Running) + } + return BehaviourState.Running + } } - data class InteractObject( - val option: String, + data class FightNpc( val id: String, - override val retryTicks: Int = 0, - override val retryMax: Int = 0, + val delay: Int = 0, + val success: Condition? = null, val radius: Int = 10, - ) : RetryableAction() { - override fun start(bot: Bot): BehaviourState { - if (bot.mode is PlayerOnObjectInteract) { + val healPercentage: Int = 20, + val lootOverValue: Int = 0, + ) : BotAction { + + override fun start(bot: Bot, frame: BehaviourFrame): BehaviourState { + frame.timeout = 0 + return BehaviourState.Running + } + + override fun update(bot: Bot, frame: BehaviourFrame) = when { + bot.levels.get(Skill.Constitution) <= bot.levels.getMax(Skill.Constitution) / healPercentage-> eat(bot) + bot.mode is PlayerOnNPCInteract -> if (success == null) BehaviourState.Success else BehaviourState.Running + bot.mode is PlayerOnFloorItemInteract -> BehaviourState.Running + bot.mode is EmptyMode -> search(bot) + success?.check(bot.player) == true -> BehaviourState.Success + else -> null + } + + private fun eat(bot: Bot): BehaviourState { + val inventory = bot.player.inventory + for (index in inventory.indices){ + val item = inventory[index] + val option = item.def.options.indexOf("Eat") + if (option == -1) { + continue + } + bot.player.instructions.trySend(InteractInterface(149, 0, item.def.id, index, option)) + return BehaviourState.Wait(1, BehaviourState.Running) + } + return BehaviourState.Running + } + + private fun search(bot: Bot): BehaviourState { + val npcs = mutableListOf() + val loot = mutableListOf() + val player = bot.player + for (tile in Spiral.spiral(player.tile, radius)) { + for (npc in NPCs.at(tile)) { + if (!wildcardEquals(id, npc.id)) { + continue + } + if (!npc.def(player).options.contains("Attack")) { + continue + } + if (npc.attackers.isNotEmpty() && !npc.attackers.contains(player)) { + continue + } + npcs.add(npc) + } + for (item in FloorItems.at(tile)) { + if (item.owner != player.accountName) { + continue + } + if (item.def.cost <= lootOverValue) { + continue + } + loot.add(item) + } + } + val item = loot.randomOrNull(random) + if (item != null) { + bot.player.instructions.trySend(InteractFloorItem(item.def.id, item.tile.x, item.tile.y, item.def.floorOptions.indexOf("Pick-up"))) return BehaviourState.Running } + val npc = npcs.randomOrNull(random) ?: return handleNoTarget() + val index = npc.def(player).options.indexOf("Attack") + bot.player.instructions.trySend(InteractNPC(npc.index, index)) + return BehaviourState.Running + } + + private fun handleNoTarget(): BehaviourState { + if (success == null) { + return BehaviourState.Failed(Reason.NoTarget) + } + if (delay > 0) { + return BehaviourState.Wait(delay, BehaviourState.Running) + } + return BehaviourState.Running + } + } + + data class InteractObject( + val option: String, + val id: String, + val delay: Int = 0, + val success: Condition? = null, + val radius: Int = 10, + ) : BotAction { + + override fun start(bot: Bot, frame: BehaviourFrame): BehaviourState { + frame.timeout = 0 + return BehaviourState.Running + } + + override fun update(bot: Bot, frame: BehaviourFrame) = when { + bot.mode is PlayerOnObjectInteract -> if (success == null) BehaviourState.Success else BehaviourState.Running + bot.mode is EmptyMode -> search(bot) + success?.check(bot.player) == true -> BehaviourState.Success + else -> null + } + + private fun search(bot: Bot): BehaviourState { val objects = mutableListOf() - for (tile in Spiral.spiral(bot.player.tile, radius)) { + val player = bot.player + for (tile in Spiral.spiral(player.tile, radius)) { for (obj in GameObjects.at(tile)) { - if (wildcardEquals(id, obj.id)) { - objects.add(obj) + if (!wildcardEquals(id, obj.id)) { + continue } + val options = obj.def(player).options + if (options == null || !options.contains(option)) { + continue + } + objects.add(obj) } } - val obj = objects.randomOrNull(random) ?: return BehaviourState.Failed(Reason.NoTarget) + val obj = objects.randomOrNull(random) ?: return handleNoTarget() val index = obj.def(bot.player).options?.indexOf(option) ?: return BehaviourState.Failed(Reason.NoTarget) bot.player.instructions.trySend(InteractObject(obj.intId, obj.x, obj.y, index + 1)) - return BehaviourState.Wait(2, BehaviourState.Success) + return BehaviourState.Running } - override fun update(bot: Bot): BehaviourState { - if (bot.mode is PlayerOnObjectInteract) { - return BehaviourState.Running + private fun handleNoTarget(): BehaviourState { + if (success == null) { + return BehaviourState.Failed(Reason.NoTarget) } - return BehaviourState.Failed(Reason.NoTarget) + if (delay > 0) { + return BehaviourState.Wait(delay, BehaviourState.Running) + } + return BehaviourState.Running } } data class InterfaceOption(val id: String, val option: String) : BotAction { - override fun start(bot: Bot): BehaviourState { + override fun start(bot: Bot, frame: BehaviourFrame): BehaviourState { val definitions = get() val split = id.split(":") val (id, component) = split @@ -221,26 +350,26 @@ sealed interface BotAction { data class WaitFullInventory(val timeout: Int) : BotAction data class IntEntry(val value: Int) : BotAction { - override fun start(bot: Bot): BehaviourState { + override fun start(bot: Bot, frame: BehaviourFrame): BehaviourState { bot.player.instructions.trySend(EnterInt(value)) return BehaviourState.Wait(1, BehaviourState.Success) } } data class StringEntry(val value: String) : BotAction { - override fun start(bot: Bot): BehaviourState { + override fun start(bot: Bot, frame: BehaviourFrame): BehaviourState { bot.player.instructions.trySend(EnterString(value)) return BehaviourState.Wait(1, BehaviourState.Success) } } data class WalkTo(val x: Int, val y: Int, val radius: Int = 4) : BotAction { - override fun start(bot: Bot): BehaviourState { + override fun start(bot: Bot, frame: BehaviourFrame): BehaviourState { bot.player.instructions.trySend(Walk(x, y)) return BehaviourState.Running } - override fun update(bot: Bot) = when { + override fun update(bot: Bot, frame: BehaviourFrame) = when { bot.tile.within(x, y, bot.tile.level, radius) -> BehaviourState.Success bot.mode is EmptyMode && GameLoop.tick - bot.steps.last > 10 -> BehaviourState.Failed(Reason.Stuck) else -> BehaviourState.Running diff --git a/game/src/main/kotlin/content/bot/action/BotActivity.kt b/game/src/main/kotlin/content/bot/action/BotActivity.kt index e84cef6c40..13e953c895 100644 --- a/game/src/main/kotlin/content/bot/action/BotActivity.kt +++ b/game/src/main/kotlin/content/bot/action/BotActivity.kt @@ -5,7 +5,6 @@ import content.bot.fact.Fact import world.gregs.config.Config import world.gregs.config.ConfigReader import world.gregs.voidps.engine.event.Wildcard -import world.gregs.voidps.engine.event.Wildcards import world.gregs.voidps.engine.timedLoad /** @@ -173,65 +172,69 @@ private fun ConfigReader.fields(): Map { fun ConfigReader.requirements(list: MutableList) { while (nextElement()) { - var type = "" - var id = "" - var value: Any? = null - var default: Any? = null - var min: Int? = null - var max: Int? = null - val references = mutableMapOf() - while (nextEntry()) { - when (val key = key()) { - "skill", "carries", "equips", "owns", "clock", "variable", "clone", "location" -> { - type = key - id = string() - if (id.contains('$')) { - references[key] = id - } + list.add(requirement()) + } + list.sortBy { it.priority() } +} + +private fun ConfigReader.requirement(): Condition { + var type = "" + var id = "" + var value: Any? = null + var default: Any? = null + var min: Int? = null + var max: Int? = null + val references = mutableMapOf() + while (nextEntry()) { + when (val key = key()) { + "skill", "carries", "equips", "owns", "clock", "variable", "clone", "location" -> { + type = key + id = string() + if (id.contains('$')) { + references[key] = id } - "amount", "min" -> when (val value = value()) { + } + "amount", "min" -> when (val value = value()) { + is Int -> min = value + is String if value.contains('$') -> references[key] = value + else -> throw IllegalArgumentException("Invalid '$key' value: $value ${exception()}") + } + "max" -> when (val value = value()) { + is Int -> max = value + is String if value.contains('$') -> references[key] = value + else -> throw IllegalArgumentException("Invalid '$key' value: $value ${exception()}") + } + "inventory_space" -> { + type = key + when (val value = value()) { is Int -> min = value is String if value.contains('$') -> references[key] = value else -> throw IllegalArgumentException("Invalid '$key' value: $value ${exception()}") } - "max" -> when (val value = value()) { - is Int -> max = value + } + "radius" -> { + type = "tile" + when (val value = value()) { + is Int -> min = value is String if value.contains('$') -> references[key] = value else -> throw IllegalArgumentException("Invalid '$key' value: $value ${exception()}") } - "inventory_space" -> { - type = key - when (val value = value()) { - is Int -> min = value - is String if value.contains('$') -> references[key] = value - else -> throw IllegalArgumentException("Invalid '$key' value: $value ${exception()}") - } - } - "radius" -> { - type = "tile" - when (val value = value()) { - is Int -> min = value - is String if value.contains('$') -> references[key] = value - else -> throw IllegalArgumentException("Invalid '$key' value: $value ${exception()}") - } - } - "value" -> value = value() - "default" -> default = value() } + "value" -> value = value() + "default" -> default = value() } - var requirement = getRequirement(type, id, min, max, value, default) - if (requirement == null) { - if (type == "holds") { - throw IllegalArgumentException("Unknown requirement type 'holds'; did you mean 'carries' or 'equips'? ${exception()}.") - } - throw IllegalArgumentException("Unknown requirement type: $type ${exception()}") - } - if (references.isNotEmpty()) { - requirement = Condition.Reference(type, id, value, default, min, max, references) + } + var requirement = getRequirement(type, id, min, max, value, default) + if (requirement == null) { + if (type == "holds") { + throw IllegalArgumentException("Unknown requirement type 'holds'; did you mean 'carries' or 'equips'? ${exception()}.") } - list.add(requirement) + throw IllegalArgumentException("Unknown requirement type: $type ${exception()}") } - list.sortBy { it.priority() } + if (references.isNotEmpty()) { + requirement = Condition.Reference(type, id, value, default, min, max, references) + } + return requirement } private fun getRequirement(type: String, id: String, min: Int?, max: Int?, value: Any?, default: Any?): Condition? = when (type) { @@ -259,14 +262,16 @@ fun ConfigReader.actions(list: MutableList) { var type = "" var id = "" var option = "" - var retryTicks = 0 - var retryMax = 0 var timeout = 0 var int = 0 var ticks = 0 var radius = 10 + var delay = 0 + var heal = 0 + var loot = 0 var x = 0 var y = 0 + var success: Condition? = null val references = mutableMapOf() while (nextEntry()) { when (val key = key()) { @@ -311,6 +316,7 @@ fun ConfigReader.actions(list: MutableList) { references[key] = option } } + "success" -> success = requirement() "wait" -> { type = key when (val value = value()) { @@ -324,16 +330,20 @@ fun ConfigReader.actions(list: MutableList) { is String if value.contains('$') -> references[key] = value else -> throw IllegalArgumentException("Invalid '$key' value: $value ${exception()}") } - "retry_ticks" -> when (val value = value()) { - is Int -> retryTicks = value + "heal_percent" -> when (val value = value()) { + is Int -> heal = value is String if value.contains('$') -> references[key] = value else -> throw IllegalArgumentException("Invalid '$key' value: $value ${exception()}") } - "retry_max" -> when (val value = value()) { - is Int -> retryMax = value + "loot_over" -> when (val value = value()) { + is Int -> loot = value + is String if value.contains('$') -> references[key] = value + else -> throw IllegalArgumentException("Invalid '$key' value: $value ${exception()}") + } + "delay" -> when (val value = value()) { + is Int -> delay = value is String if value.contains('$') -> references[key] = value else -> throw IllegalArgumentException("Invalid '$key' value: $value ${exception()}") - } "timeout" -> when (val value = value()) { is Int -> timeout = value @@ -357,9 +367,13 @@ fun ConfigReader.actions(list: MutableList) { "enter_string" -> BotAction.StringEntry(id) "enter_int" -> BotAction.IntEntry(int) "wait" -> BotAction.Wait(ticks) - "npc" -> BotAction.InteractNpc(id = id, option = option, retryTicks = retryTicks, retryMax = retryMax, radius = radius) + "npc" -> if (option == "Attack") { + BotAction.FightNpc(id = id, delay = delay, success = success, healPercentage = heal, lootOverValue = loot, radius = radius) + } else { + BotAction.InteractNpc(id = id, option = option, delay = delay, successCondition = success, radius = radius) + } "tile" -> BotAction.WalkTo(x = x, y = y) - "object" -> BotAction.InteractObject(id = id, option = option, retryTicks = retryTicks, retryMax = retryMax, radius = radius) + "object" -> BotAction.InteractObject(id = id, option = option, delay = delay, success = success, radius = radius) "interface" -> BotAction.InterfaceOption(id = id, option = option) "clone" -> BotAction.Clone(id) "wait_for" -> when (id) { diff --git a/game/src/main/kotlin/content/bot/action/Reason.kt b/game/src/main/kotlin/content/bot/action/Reason.kt index e717f9384a..39607acb19 100644 --- a/game/src/main/kotlin/content/bot/action/Reason.kt +++ b/game/src/main/kotlin/content/bot/action/Reason.kt @@ -6,6 +6,7 @@ interface Reason { object Invalid : HardReason object Cancelled : HardReason object NoRoute : HardReason + object Timeout : HardReason object Stuck : SoftReason object NoTarget : SoftReason data class Requirement(val fact: Condition) : HardReason diff --git a/game/src/test/kotlin/content/bot/BotManagerTest.kt b/game/src/test/kotlin/content/bot/BotManagerTest.kt index 3192b174fc..bcf28b0d91 100644 --- a/game/src/test/kotlin/content/bot/BotManagerTest.kt +++ b/game/src/test/kotlin/content/bot/BotManagerTest.kt @@ -49,11 +49,12 @@ class BotManagerTest { fun `Pending frame starts running`() { val activity = testActivity( id = "walk", - plan = listOf(BotAction.Wait(1)) + plan = listOf(BotAction.Wait(1, BehaviourState.Running)) ) val manager = BotManager(mutableMapOf(activity.id to activity)) val bot = testBot(activity) + manager.tick(bot) manager.tick(bot) manager.tick(bot) @@ -79,42 +80,7 @@ class BotManagerTest { assertTrue(advanced) assertEquals(1, frame.index) - assertEquals(BehaviourState.Pending, frame.state) - } - - @Test - fun `Retryable action retries before failing`() { - val action = BotAction.InteractNpc( - option = "Talk", - id = "npc", - retryTicks = 2, - retryMax = 2 - ) - - val activity = testActivity( - id = "talk", - plan = listOf(action) - ) - - val manager = BotManager(mutableMapOf(activity.id to activity)) - val bot = testBot(activity) - - manager.tick(bot) - manager.tick(bot) - - val frame = bot.frame() - repeat(3) { - frame.fail(Reason.Requirement(Condition.Clone(""))) - manager.tick(bot) - assertTrue(frame.state is BehaviourState.Wait) - manager.tick(bot) // Tick 1 - manager.tick(bot) // Tick 2 - manager.tick(bot) // Pending - } - - // after retries exhausted → popped - assertEquals("idle", bot.frames.first().behaviour.id) - assertTrue("talk" in bot.blocked) + assertEquals(BehaviourState.Running, frame.state) } @Test diff --git a/game/src/test/kotlin/content/bot/action/BehaviourFragmentTest.kt b/game/src/test/kotlin/content/bot/action/BehaviourFragmentTest.kt index ac7cad9b4b..a2aab21f9b 100644 --- a/game/src/test/kotlin/content/bot/action/BehaviourFragmentTest.kt +++ b/game/src/test/kotlin/content/bot/action/BehaviourFragmentTest.kt @@ -122,16 +122,14 @@ class BehaviourFragmentTest { BotAction.InteractNpc( option = "talk", id = "default", - retryTicks = 1, - retryMax = 2, + delay = 2, radius = 10 ), - mapOf("option" to "Talk-to", "npc" to "bob", "retry_ticks" to 2, "retry_max" to 5, "radius" to 5), + mapOf("option" to "Talk-to", "npc" to "bob", "delay" to 5, "radius" to 5), BotAction.InteractNpc( option = "Talk-to", id = "bob", - retryTicks = 2, - retryMax = 5, + delay = 5, radius = 5 ) ), @@ -139,16 +137,14 @@ class BehaviourFragmentTest { BotAction.InteractObject( option = "interact", id = "default", - retryTicks = 1, - retryMax = 2, + delay = 2, radius = 10 ), - mapOf("option" to "Open", "object" to "door", "retry_ticks" to 2, "retry_max" to 5, "radius" to 5), + mapOf("option" to "Open", "object" to "door", "delay" to 5, "radius" to 5), BotAction.InteractObject( option = "Open", id = "door", - retryTicks = 2, - retryMax = 5, + delay = 5, radius = 5 ) ), From 59bdf200deb5fb203f303f18c09a5de06f2c268c Mon Sep 17 00:00:00 2001 From: GregHib Date: Wed, 4 Feb 2026 17:13:56 +0000 Subject: [PATCH 037/101] Improvements and fixes --- data/bot/bank.bots.toml | 10 ++++ data/bot/woodcutting.bots.toml | 2 +- .../src/main/kotlin/content/bot/BotManager.kt | 57 ++++++++++++------- .../content/bot/action/BehaviourFragment.kt | 3 +- .../kotlin/content/bot/action/BotAction.kt | 39 +++++++++---- .../kotlin/content/bot/action/BotActivity.kt | 16 ++---- .../main/kotlin/content/bot/action/Reason.kt | 2 +- .../main/kotlin/content/bot/fact/Condition.kt | 14 +++-- .../bot/action/BehaviourFragmentTest.kt | 1 - 9 files changed, 95 insertions(+), 49 deletions(-) create mode 100644 data/bot/bank.bots.toml diff --git a/data/bot/bank.bots.toml b/data/bot/bank.bots.toml new file mode 100644 index 0000000000..1d5b4123d1 --- /dev/null +++ b/data/bot/bank.bots.toml @@ -0,0 +1,10 @@ +[deposit_all_bank] +type = "resolver" +actions = [ + { go_to_nearest = "bank" }, + { option = "Use-quickly", object = "bank_booth*" }, + { option = "Deposit carried items", interface = "bank:carried" }, +] +produces = [ + { inventory_space = 28 } +] diff --git a/data/bot/woodcutting.bots.toml b/data/bot/woodcutting.bots.toml index 2af951fd2e..c582108969 100644 --- a/data/bot/woodcutting.bots.toml +++ b/data/bot/woodcutting.bots.toml @@ -15,7 +15,7 @@ produces = [ [lumbridge_trees] template = "woodcutting_template" capacity = 4 -fields = { hatchet = "iron_hatchet,steel_hatchet", location = "lumbridge_north_trees", tree = "tree" } +fields = { hatchet = "iron_hatchet,steel_hatchet", location = "lumbridge_north_trees", tree = "tree*" } requires = [ { skill = "woodcutting", min = 1, max = 15 }, ] diff --git a/game/src/main/kotlin/content/bot/BotManager.kt b/game/src/main/kotlin/content/bot/BotManager.kt index 84f4fee2e8..5b68d79f51 100644 --- a/game/src/main/kotlin/content/bot/BotManager.kt +++ b/game/src/main/kotlin/content/bot/BotManager.kt @@ -164,28 +164,13 @@ class BotManager( } private fun pickResolver(bot: Bot, condition: Condition, frame: BehaviourFrame): Behaviour? { - // TODO actions should have retry policies? val options = mutableListOf() - // Go to area - if (condition is Condition.Area) { - options.add(Resolver("go_to_${condition.area}", -1, actions = listOf(BotAction.GoTo(condition.area)), produces = setOf(condition))) - } - // If in bank and needs inventory -> withdraw from bank - if (condition is Condition.AtLeast && condition.fact is Fact.InventoryCount) { - if (bot.player.bank.contains(condition.fact.id, condition.min)) { - options.add( - Resolver( - "withdraw_${condition.fact.id}", 20, actions = listOf( - BotAction.GoToNearest("bank"), - BotAction.InteractObject("Use-quickly", "bank_booth*"), - BotAction.InterfaceOption("Withdraw-x", "bank:inventory:${condition.fact.id}"), - BotAction.StringEntry("${condition.min}"), - ) - ) - ) + addDefaultResolvers(bot, options, condition) + if (condition is Condition.Any) { + for (condition in condition.conditions) { + addDefaultResolvers(bot, options, condition) } } - // TODO: If in inventory and needs equipped -> equip for (key in condition.keys()) { for (resolver in resolvers[key] ?: emptyList()) { if (frame.blocked.contains(resolver.id)) { @@ -200,9 +185,42 @@ class BotManager( return options.minByOrNull { it.weight } } + private fun addDefaultResolvers(bot: Bot, resolvers: MutableList, condition: Condition) { + if (condition is Condition.Area) { + resolvers.add(Resolver("go_to_${condition.area}", -1, actions = listOf(BotAction.GoTo(condition.area)), produces = setOf(condition))) + } else if (condition is Condition.AtLeast && condition.fact is Fact.InventoryCount && bot.player.bank.contains(condition.fact.id, condition.min)) { + if (condition.min == 1 || condition.min == 5 || condition.min == 10) { + resolvers.add( + Resolver( + "withdraw_${condition.fact.id}", 20, actions = listOf( + BotAction.GoToNearest("bank"), + BotAction.InteractObject("Use-quickly", "bank_booth*"), + BotAction.InterfaceOption("Withdraw-${condition.min}", "bank:inventory:${condition.fact.id}"), + ) + ) + ) + } else { + resolvers.add( + Resolver( + "withdraw_${condition.fact.id}", 20, actions = listOf( + BotAction.GoToNearest("bank"), + BotAction.InteractObject("Use-quickly", "bank_booth*"), + BotAction.InterfaceOption("Withdraw-X", "bank:inventory:${condition.fact.id}"), + BotAction.IntEntry(condition.min), + ) + ) + ) + } + } + // TODO: If in inventory and needs equipped -> equip + } + private fun execute(bot: Bot) { val frame = bot.frame() val behaviour = frame.behaviour + if (bot.player["debug", false]) { + logger.info { "Bot task: ${behaviour.id} state: ${frame.state} action: ${frame.action()}." } + } when (val state = frame.state) { BehaviourState.Running -> frame.update(bot) BehaviourState.Pending -> start(bot, behaviour, frame) @@ -215,6 +233,7 @@ class BotManager( AuditLog.event(bot, "completed", frame.behaviour.id) bot.frames.pop() if (behaviour is BotActivity) { + bot.blocked.remove(behaviour.id) slots.release(behaviour) } } else { diff --git a/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt b/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt index fe069f7fa2..4727d4ec9d 100644 --- a/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt +++ b/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt @@ -23,8 +23,8 @@ data class BehaviourFragment( is BotAction.GoTo -> BotAction.GoTo(resolve(action.references["go_to"], copy.target)) is BotAction.GoToNearest -> BotAction.GoToNearest(resolve(action.references["go_to_nearest"], copy.tag)) is BotAction.InterfaceOption -> BotAction.InterfaceOption( - id = resolve(action.references["interface"], copy.id), option = resolve(action.references["option"], copy.option), + id = resolve(action.references["interface"], copy.id), ) is BotAction.InteractNpc -> { val option = resolve(action.references["option"], copy.option) @@ -69,7 +69,6 @@ data class BehaviourFragment( value = resolve(action.references["value"], copy.value), ) is BotAction.Wait -> BotAction.Wait(resolve(action.references["wait"], copy.ticks)) - is BotAction.WaitFullInventory -> BotAction.WaitFullInventory(resolve(action.references["timeout"], copy.timeout)) is BotAction.Clone, is BotAction.Reference -> throw IllegalArgumentException("Invalid reference action type: ${action.action::class.simpleName}.") } is BotAction.Clone -> throw IllegalArgumentException("Unresolved clone action in template ${id}.") diff --git a/game/src/main/kotlin/content/bot/action/BotAction.kt b/game/src/main/kotlin/content/bot/action/BotAction.kt index 42f0feef50..97e847775f 100644 --- a/game/src/main/kotlin/content/bot/action/BotAction.kt +++ b/game/src/main/kotlin/content/bot/action/BotAction.kt @@ -50,7 +50,7 @@ sealed interface BotAction { data class GoTo(val target: String) : BotAction { override fun start(bot: Bot, frame: BehaviourFrame): BehaviourState { - val def = Areas.getOrNull(target) ?: return BehaviourState.Failed(Reason.Invalid) + val def = Areas.getOrNull(target) ?: return BehaviourState.Failed(Reason.Invalid("No areas found with id '$target'.")) if (bot.tile in def.area) { return BehaviourState.Success } @@ -61,6 +61,11 @@ sealed interface BotAction { return queueRoute(success, list, graph, bot, target) } + override fun update(bot: Bot, frame: BehaviourFrame): BehaviourState? { + val def = Areas.getOrNull(target) ?: return BehaviourState.Failed(Reason.Invalid("No areas found with id '$target'.")) + return if (bot.tile in def.area) BehaviourState.Success else null + } + companion object { internal fun queueRoute(success: Boolean, list: MutableList, graph: Graph, bot: Bot, target: String): BehaviourState { if (!success) { @@ -94,7 +99,7 @@ sealed interface BotAction { override fun start(bot: Bot, frame: BehaviourFrame): BehaviourState { val set = Areas.tagged(tag) if (set.isEmpty()) { - return BehaviourState.Failed(Reason.Invalid) + return BehaviourState.Failed(Reason.Invalid("No areas tagged with tag '$tag'.")) } if (set.any { bot.tile in it.area }) { return BehaviourState.Success @@ -109,7 +114,7 @@ sealed interface BotAction { override fun update(bot: Bot, frame: BehaviourFrame): BehaviourState? { val set = Areas.tagged(tag) if (set.isEmpty()) { - return BehaviourState.Failed(Reason.Invalid) + return BehaviourState.Failed(Reason.Invalid("No areas tagged with tag '$tag'.")) } if (set.any { bot.tile in it.area }) { return BehaviourState.Success @@ -132,9 +137,9 @@ sealed interface BotAction { } override fun update(bot: Bot, frame: BehaviourFrame) = when { + successCondition?.check(bot.player) == true -> BehaviourState.Success bot.mode is PlayerOnNPCInteract -> if (successCondition == null) BehaviourState.Success else BehaviourState.Running bot.mode is EmptyMode -> search(bot) - successCondition?.check(bot.player) == true -> BehaviourState.Success else -> null } @@ -184,11 +189,11 @@ sealed interface BotAction { } override fun update(bot: Bot, frame: BehaviourFrame) = when { - bot.levels.get(Skill.Constitution) <= bot.levels.getMax(Skill.Constitution) / healPercentage-> eat(bot) + bot.levels.get(Skill.Constitution) <= bot.levels.getMax(Skill.Constitution) / healPercentage -> eat(bot) + success?.check(bot.player) == true -> BehaviourState.Success bot.mode is PlayerOnNPCInteract -> if (success == null) BehaviourState.Success else BehaviourState.Running bot.mode is PlayerOnFloorItemInteract -> BehaviourState.Running bot.mode is EmptyMode -> search(bot) - success?.check(bot.player) == true -> BehaviourState.Success else -> null } @@ -269,9 +274,9 @@ sealed interface BotAction { } override fun update(bot: Bot, frame: BehaviourFrame) = when { + success?.check(bot.player) == true -> BehaviourState.Success bot.mode is PlayerOnObjectInteract -> if (success == null) BehaviourState.Success else BehaviourState.Running bot.mode is EmptyMode -> search(bot) - success?.check(bot.player) == true -> BehaviourState.Success else -> null } @@ -307,10 +312,13 @@ sealed interface BotAction { } } - data class InterfaceOption(val id: String, val option: String) : BotAction { + data class InterfaceOption(val option: String, val id: String, val success: Condition? = null) : BotAction { override fun start(bot: Bot, frame: BehaviourFrame): BehaviourState { val definitions = get() val split = id.split(":") + if (split.size < 2) { + return BehaviourState.Failed(Reason.Invalid("Invalid interface id '$id'.")) + } val (id, component) = split val item = split.getOrNull(2) val def = definitions.getOrNull(id) ?: return BehaviourState.Failed(Reason.NoTarget) @@ -343,11 +351,20 @@ sealed interface BotAction { option = index ) ) // TODO could await actual response, or something to get actual feedback - return BehaviourState.Wait(1, BehaviourState.Success) + return when { + success == null -> BehaviourState.Wait(1, BehaviourState.Success) + success.check(bot.player) -> BehaviourState.Success + else -> BehaviourState.Running + } } - } - data class WaitFullInventory(val timeout: Int) : BotAction + override fun update(bot: Bot, frame: BehaviourFrame): BehaviourState? { + if (success != null && success.check(bot.player)) { + return BehaviourState.Success + } + return super.update(bot, frame) + } + } data class IntEntry(val value: Int) : BotAction { override fun start(bot: Bot, frame: BehaviourFrame): BehaviourState { diff --git a/game/src/main/kotlin/content/bot/action/BotActivity.kt b/game/src/main/kotlin/content/bot/action/BotActivity.kt index 13e953c895..93dd60689c 100644 --- a/game/src/main/kotlin/content/bot/action/BotActivity.kt +++ b/game/src/main/kotlin/content/bot/action/BotActivity.kt @@ -177,7 +177,7 @@ fun ConfigReader.requirements(list: MutableList) { list.sortBy { it.priority() } } -private fun ConfigReader.requirement(): Condition { +private fun ConfigReader.requirement(greatThan: Boolean = true): Condition { var type = "" var id = "" var value: Any? = null @@ -224,7 +224,7 @@ private fun ConfigReader.requirement(): Condition { "default" -> default = value() } } - var requirement = getRequirement(type, id, min, max, value, default) + var requirement = getRequirement(type, id, min, max, value, default, greatThan) if (requirement == null) { if (type == "holds") { throw IllegalArgumentException("Unknown requirement type 'holds'; did you mean 'carries' or 'equips'? ${exception()}.") @@ -237,7 +237,7 @@ private fun ConfigReader.requirement(): Condition { return requirement } -private fun getRequirement(type: String, id: String, min: Int?, max: Int?, value: Any?, default: Any?): Condition? = when (type) { +private fun getRequirement(type: String, id: String, min: Int?, max: Int?, value: Any?, default: Any?, greatThan: Boolean): Condition? = when (type) { "skill" -> Condition.range(Fact.SkillLevel.of(id), min, max) "carries" -> Condition.split(id, min, max, Wildcard.Item) { Fact.InventoryCount(it) } "owns" -> Condition.split(id, min, max, Wildcard.Item) { Fact.ItemCount(it) } @@ -252,7 +252,7 @@ private fun getRequirement(type: String, id: String, min: Int?, max: Int?, value else -> null } "clone" -> Condition.Clone(id) - "inventory_space" -> Condition.range(Fact.InventorySpace, min, max) + "inventory_space" -> Condition.range(Fact.InventorySpace, min, max, greatThan) "location" -> Condition.Area(Fact.PlayerTile, id) else -> null } @@ -275,7 +275,7 @@ fun ConfigReader.actions(list: MutableList) { val references = mutableMapOf() while (nextEntry()) { when (val key = key()) { - "go_to", "go_to_nearest", "enter_string", "wait_for", "interface", "npc", "object", "clone" -> { + "go_to", "go_to_nearest", "enter_string", "interface", "npc", "object", "clone" -> { type = key id = string() if (id.contains('$')) { @@ -316,7 +316,7 @@ fun ConfigReader.actions(list: MutableList) { references[key] = option } } - "success" -> success = requirement() + "success" -> success = requirement(greatThan = false) "wait" -> { type = key when (val value = value()) { @@ -376,10 +376,6 @@ fun ConfigReader.actions(list: MutableList) { "object" -> BotAction.InteractObject(id = id, option = option, delay = delay, success = success, radius = radius) "interface" -> BotAction.InterfaceOption(id = id, option = option) "clone" -> BotAction.Clone(id) - "wait_for" -> when (id) { - "full_inventory" -> BotAction.WaitFullInventory(timeout) - else -> throw IllegalArgumentException("Unknown wait_for action: $id ${exception()}") - } else -> throw IllegalArgumentException("Unknown action type: $type ${exception()}") } if (references.isNotEmpty()) { diff --git a/game/src/main/kotlin/content/bot/action/Reason.kt b/game/src/main/kotlin/content/bot/action/Reason.kt index 39607acb19..01f7988aab 100644 --- a/game/src/main/kotlin/content/bot/action/Reason.kt +++ b/game/src/main/kotlin/content/bot/action/Reason.kt @@ -3,7 +3,7 @@ package content.bot.action import content.bot.fact.Condition interface Reason { - object Invalid : HardReason + data class Invalid(val message: String) : HardReason object Cancelled : HardReason object NoRoute : HardReason object Timeout : HardReason diff --git a/game/src/main/kotlin/content/bot/fact/Condition.kt b/game/src/main/kotlin/content/bot/fact/Condition.kt index 62f0185b1c..b998ba44dd 100644 --- a/game/src/main/kotlin/content/bot/fact/Condition.kt +++ b/game/src/main/kotlin/content/bot/fact/Condition.kt @@ -94,16 +94,22 @@ sealed interface Condition { companion object { fun split(id: String, min: Int?, max: Int?, wildcard: Wildcard, fact: (String) -> Fact): Condition = when { - id.contains(",") -> Any(id.split(",").flatMap { if (id.any { it == '*' || it == '#' }) Wildcards.get(id, wildcard).map { range(fact(id), min, max) } else listOf(range(fact(id), min, max)) }) - id.any { it == '*' || it == '#' } -> Any(Wildcards.get(id, wildcard).map { range(fact(id), min, max) }) + id.contains(",") -> Any(id.split(",").flatMap { individual -> + if (individual.any { char -> char == '*' || char == '#' }) { + Wildcards.get(individual, wildcard).map { resolved -> range(fact(resolved), min, max) } + } else { + listOf(range(fact(individual), min, max)) + } + }) + id.any { char -> char == '*' || char == '#' } -> Any(Wildcards.get(id, wildcard).map { resolved -> range(fact(resolved), min, max) }) else -> range(fact(id), min, max) } - fun range(fact: Fact, min: Int?, max: Int?) = when { + fun range(fact: Fact, min: Int?, max: Int?, greaterThan: Boolean = true) = when { min != null && max != null -> Range(fact, min, max) min != null -> AtLeast(fact, min) max != null -> AtMost(fact, max) - else -> Equals(fact, 1) + else -> if (greaterThan) AtLeast(fact, 1) else AtMost(fact, 1) } } diff --git a/game/src/test/kotlin/content/bot/action/BehaviourFragmentTest.kt b/game/src/test/kotlin/content/bot/action/BehaviourFragmentTest.kt index a2aab21f9b..ee49546a9d 100644 --- a/game/src/test/kotlin/content/bot/action/BehaviourFragmentTest.kt +++ b/game/src/test/kotlin/content/bot/action/BehaviourFragmentTest.kt @@ -149,7 +149,6 @@ class BehaviourFragmentTest { ) ), Triple(BotAction.Wait(4), mapOf("wait" to 5), BotAction.Wait(5)), - Triple(BotAction.WaitFullInventory(4), mapOf("timeout" to 5), BotAction.WaitFullInventory(5)), ).map { (default, values, expected) -> dynamicTest("Resolve ${default::class.simpleName} references") { val fields = values.mapKeys { "ref_${it.key}" } From ca855c74858b8e469813ca3037270c8368cbfb7d Mon Sep 17 00:00:00 2001 From: GregHib Date: Wed, 4 Feb 2026 17:28:47 +0000 Subject: [PATCH 038/101] Fixes --- data/bot/bank.bots.toml | 2 +- data/bot/woodcutting.bots.toml | 2 +- .../kotlin/content/bot/action/BotActivity.kt | 16 +++++----------- .../main/kotlin/content/bot/fact/Condition.kt | 4 ++-- 4 files changed, 9 insertions(+), 15 deletions(-) diff --git a/data/bot/bank.bots.toml b/data/bot/bank.bots.toml index 1d5b4123d1..eb4ec49502 100644 --- a/data/bot/bank.bots.toml +++ b/data/bot/bank.bots.toml @@ -3,7 +3,7 @@ type = "resolver" actions = [ { go_to_nearest = "bank" }, { option = "Use-quickly", object = "bank_booth*" }, - { option = "Deposit carried items", interface = "bank:carried" }, + { option = "Deposit carried items", interface = "bank:carried", success = { inventory_space = 28 } }, ] produces = [ { inventory_space = 28 } diff --git a/data/bot/woodcutting.bots.toml b/data/bot/woodcutting.bots.toml index c582108969..c1927ca845 100644 --- a/data/bot/woodcutting.bots.toml +++ b/data/bot/woodcutting.bots.toml @@ -6,7 +6,7 @@ setup = [ { inventory_space = 20 }, ] actions = [ - { option = "Chop down", object = "$tree", delay = 5, success = { inventory_space = 0 }, timeout = 30 }, + { option = "Chop down", object = "$tree", delay = 5, success = { inventory_space = 0 } }, ] produces = [ { skill = "woodcutting" } diff --git a/game/src/main/kotlin/content/bot/action/BotActivity.kt b/game/src/main/kotlin/content/bot/action/BotActivity.kt index 93dd60689c..d3b824839b 100644 --- a/game/src/main/kotlin/content/bot/action/BotActivity.kt +++ b/game/src/main/kotlin/content/bot/action/BotActivity.kt @@ -177,7 +177,7 @@ fun ConfigReader.requirements(list: MutableList) { list.sortBy { it.priority() } } -private fun ConfigReader.requirement(greatThan: Boolean = true): Condition { +private fun ConfigReader.requirement(exact: Boolean = false): Condition { var type = "" var id = "" var value: Any? = null @@ -224,7 +224,7 @@ private fun ConfigReader.requirement(greatThan: Boolean = true): Condition { "default" -> default = value() } } - var requirement = getRequirement(type, id, min, max, value, default, greatThan) + var requirement = getRequirement(type, id, min, max, value, default, exact) if (requirement == null) { if (type == "holds") { throw IllegalArgumentException("Unknown requirement type 'holds'; did you mean 'carries' or 'equips'? ${exception()}.") @@ -237,7 +237,7 @@ private fun ConfigReader.requirement(greatThan: Boolean = true): Condition { return requirement } -private fun getRequirement(type: String, id: String, min: Int?, max: Int?, value: Any?, default: Any?, greatThan: Boolean): Condition? = when (type) { +private fun getRequirement(type: String, id: String, min: Int?, max: Int?, value: Any?, default: Any?, exact: Boolean): Condition? = when (type) { "skill" -> Condition.range(Fact.SkillLevel.of(id), min, max) "carries" -> Condition.split(id, min, max, Wildcard.Item) { Fact.InventoryCount(it) } "owns" -> Condition.split(id, min, max, Wildcard.Item) { Fact.ItemCount(it) } @@ -252,7 +252,7 @@ private fun getRequirement(type: String, id: String, min: Int?, max: Int?, value else -> null } "clone" -> Condition.Clone(id) - "inventory_space" -> Condition.range(Fact.InventorySpace, min, max, greatThan) + "inventory_space" -> if (exact && min != null) Condition.Equals(Fact.InventorySpace, min) else Condition.range(Fact.InventorySpace, min, max) "location" -> Condition.Area(Fact.PlayerTile, id) else -> null } @@ -262,7 +262,6 @@ fun ConfigReader.actions(list: MutableList) { var type = "" var id = "" var option = "" - var timeout = 0 var int = 0 var ticks = 0 var radius = 10 @@ -316,7 +315,7 @@ fun ConfigReader.actions(list: MutableList) { references[key] = option } } - "success" -> success = requirement(greatThan = false) + "success" -> success = requirement(exact = true) "wait" -> { type = key when (val value = value()) { @@ -345,11 +344,6 @@ fun ConfigReader.actions(list: MutableList) { is String if value.contains('$') -> references[key] = value else -> throw IllegalArgumentException("Invalid '$key' value: $value ${exception()}") } - "timeout" -> when (val value = value()) { - is Int -> timeout = value - is String if value.contains('$') -> references[key] = value - else -> throw IllegalArgumentException("Invalid '$key' value: $value ${exception()}") - } "enter_int" -> { type = key when (val value = value()) { diff --git a/game/src/main/kotlin/content/bot/fact/Condition.kt b/game/src/main/kotlin/content/bot/fact/Condition.kt index b998ba44dd..63d5b35b54 100644 --- a/game/src/main/kotlin/content/bot/fact/Condition.kt +++ b/game/src/main/kotlin/content/bot/fact/Condition.kt @@ -105,11 +105,11 @@ sealed interface Condition { else -> range(fact(id), min, max) } - fun range(fact: Fact, min: Int?, max: Int?, greaterThan: Boolean = true) = when { + fun range(fact: Fact, min: Int?, max: Int?) = when { min != null && max != null -> Range(fact, min, max) min != null -> AtLeast(fact, min) max != null -> AtMost(fact, max) - else -> if (greaterThan) AtLeast(fact, 1) else AtMost(fact, 1) + else -> AtLeast(fact, 1) } } From 1f3c20203ab2b3549815928ca68483f92ef95dc9 Mon Sep 17 00:00:00 2001 From: GregHib Date: Wed, 4 Feb 2026 19:15:53 +0000 Subject: [PATCH 039/101] Fixes to shopping and banking, add bunch of woodcutting spots --- data/area/misthalin/draynor/draynor.bots.toml | 20 +++ .../misthalin/lumbridge/lumbridge.bots.toml | 77 +++++++++++ data/bot/axe_shop.bots.toml | 26 ++-- data/bot/bank.bots.toml | 2 +- data/bot/shop.bots.toml | 26 ++++ data/bot/woodcutting.bots.toml | 72 +++++++--- .../client/instruction/InterfaceHandler.kt | 51 ++++--- .../handle/InterfaceOptionHandler.kt | 2 +- .../instruction/handle/ObjectOptionHandler.kt | 2 +- .../src/main/kotlin/content/bot/BotManager.kt | 34 ++++- .../src/main/kotlin/content/bot/BotUpdates.kt | 2 +- .../content/bot/action/BehaviourFragment.kt | 130 ++++++++++-------- .../kotlin/content/bot/action/BotAction.kt | 43 +++--- .../kotlin/content/bot/action/BotActivity.kt | 26 ++-- .../main/kotlin/content/bot/fact/Condition.kt | 13 ++ game/src/main/kotlin/content/bot/fact/Fact.kt | 25 +++- .../bot/action/BehaviourFragmentTest.kt | 1 + 17 files changed, 392 insertions(+), 160 deletions(-) create mode 100644 data/area/misthalin/draynor/draynor.bots.toml create mode 100644 data/area/misthalin/lumbridge/lumbridge.bots.toml create mode 100644 data/bot/shop.bots.toml diff --git a/data/area/misthalin/draynor/draynor.bots.toml b/data/area/misthalin/draynor/draynor.bots.toml new file mode 100644 index 0000000000..d8ff9ddfcb --- /dev/null +++ b/data/area/misthalin/draynor/draynor.bots.toml @@ -0,0 +1,20 @@ +[draynor_east_trees] +template = "normal_tree_template" +fields = { location = "draynor_oak_trees" } + +[draynor_east_oak_trees] +template = "oak_tree_template" +fields = { location = "draynor_oak_trees" } + +[draynor_north_trees] +template = "normal_tree_template" +fields = { location = "draynor_north_trees" } + +[draynor_west_oak_trees] +template = "oak_tree_template" +fields = { location = "draynor_west_oak_trees" } + +[draynor_willow_trees] +template = "willow_tree_template" +capacity = 8 +fields = { location = "draynor_willow_trees" } diff --git a/data/area/misthalin/lumbridge/lumbridge.bots.toml b/data/area/misthalin/lumbridge/lumbridge.bots.toml new file mode 100644 index 0000000000..9f37672f99 --- /dev/null +++ b/data/area/misthalin/lumbridge/lumbridge.bots.toml @@ -0,0 +1,77 @@ +[lumbridge_north_trees] +template = "normal_tree_template" +fields = { location = "lumbridge_north_trees" } + +[lumbridge_cow_trees] +template = "normal_tree_template" +fields = { location = "lumbridge_cow_trees" } + +[lumbridge_west_trees] +template = "normal_tree_template" +fields = { location = "lumbridge_west_trees" } + +[lumbridge_east_trees] +template = "normal_tree_template" +fields = { location = "lumbridge_east_trees" } + +[lumbridge_oak_trees] +template = "oak_tree_template" +fields = { location = "lumbridge_north_trees" } + +[lumbridge_castle_yew_tree] +template = "yew_tree_template" +fields = { location = "lumbridge_north_west_yew_tree" } + +[lumbridge_west_yew_tree] +template = "yew_tree_template" +fields = { location = "lumbridge_west_yew_tree" } + +# Bobs Brilliant Axes + +[take_bronze_hatchet_bobs] +type = "resolver" +weight = 5 +template = "take_shop_sample" +fields = { shop_location = "bobs_axe_shop", shopkeeper = "bob", item = "bronze_hatchet" } + +[take_bronze_pickaxe_bobs] +type = "resolver" +weight = 5 +template = "take_shop_sample" +fields = { shop_location = "bobs_axe_shop", shopkeeper = "bob", item = "bronze_pickaxe" } + +[buy_bronze_pickaxe] +type = "resolver" +weight = 10 +template = "buy_from_shop" +fields = { cost = 1, shop_location = "bobs_axe_shop", shopkeeper = "bob", item = "bronze_pickaxe" } +requires = [ + { skill = "mining", min = 1 }, +] + +[buy_bronze_hatchet] +type = "resolver" +weight = 10 +template = "buy_from_shop" +fields = { cost = 16, shop_location = "bobs_axe_shop", shopkeeper = "bob", item = "bronze_hatchet" } +requires = [ + { skill = "woodcutting", min = 1 }, +] + +[buy_iron_hatchet] +type = "resolver" +weight = 10 +template = "buy_from_shop" +fields = { cost = 56, shop_location = "bobs_axe_shop", shopkeeper = "bob", item = "iron_hatchet" } +requires = [ + { skill = "woodcutting", min = 1 }, +] + +[buy_steel_hatchet] +type = "resolver" +weight = 10 +template = "buy_from_shop" +fields = { cost = 200, shop_location = "bobs_axe_shop", shopkeeper = "bob", item = "steel_hatchet" } +requires = [ + { skill = "woodcutting", min = 6 }, +] \ No newline at end of file diff --git a/data/bot/axe_shop.bots.toml b/data/bot/axe_shop.bots.toml index c6f6cf3054..33ca1a0cd9 100644 --- a/data/bot/axe_shop.bots.toml +++ b/data/bot/axe_shop.bots.toml @@ -1,35 +1,33 @@ -[buy_from_shop] -setup = [ - { carries = "coins", amount = "$cost" }, - { location = "$shop_location" }, - { inventory_space = 1 }, -] -actions = [ - { option = "Trade", npc = "$shopkeeper" }, - { option = "Buy 1", interface = "shop:inventory:$item" }, -] -produces = [ - { carries = "$item" } +[take_free_bronze_hatchet] +type = "resolver" +weight = 5 +template = "take_shop_sample" +fields = { shop_location = "bobs_axe_shop", shopkeeper = "bob", item = "bronze_hatchet" } +requires = [ + { skill = "woodcutting", min = 1, max = 10 }, ] [buy_bronze_hatchet] type = "resolver" +weight = 10 template = "buy_from_shop" fields = { cost = 16, shop_location = "bobs_axe_shop", shopkeeper = "bob", item = "bronze_hatchet" } requires = [ - { skill = "woodcutting", min = 1 }, + { skill = "woodcutting", min = 1, max = 10 }, ] [buy_iron_hatchet] type = "resolver" +weight = 10 template = "buy_from_shop" fields = { cost = 56, shop_location = "bobs_axe_shop", shopkeeper = "bob", item = "iron_hatchet" } requires = [ - { skill = "woodcutting", min = 1 }, + { skill = "woodcutting", min = 1, max = 15 }, ] [buy_steel_hatchet] type = "resolver" +weight = 10 template = "buy_from_shop" fields = { cost = 200, shop_location = "bobs_axe_shop", shopkeeper = "bob", item = "steel_hatchet" } requires = [ diff --git a/data/bot/bank.bots.toml b/data/bot/bank.bots.toml index eb4ec49502..37adbbc796 100644 --- a/data/bot/bank.bots.toml +++ b/data/bot/bank.bots.toml @@ -2,7 +2,7 @@ type = "resolver" actions = [ { go_to_nearest = "bank" }, - { option = "Use-quickly", object = "bank_booth*" }, + { option = "Use-quickly", object = "bank_booth*", success = { interface = "bank" } }, { option = "Deposit carried items", interface = "bank:carried", success = { inventory_space = 28 } }, ] produces = [ diff --git a/data/bot/shop.bots.toml b/data/bot/shop.bots.toml new file mode 100644 index 0000000000..07a1524392 --- /dev/null +++ b/data/bot/shop.bots.toml @@ -0,0 +1,26 @@ +[buy_from_shop] +setup = [ + { carries = "coins", amount = "$cost" }, + { location = "$shop_location" }, + { inventory_space = 1 }, +] +actions = [ + { option = "Trade", npc = "$shopkeeper", success = { interface = "shop" } }, + { option = "Buy-1", interface = "shop:stock:$item", success = { carries = "$item" } }, +] +produces = [ + { carries = "$item" } +] + +[take_shop_sample] +setup = [ + { location = "$shop_location" }, + { inventory_space = 1 }, +] +actions = [ + { option = "Trade", npc = "$shopkeeper", success = { interface = "shop" } }, + { option = "Take-1", interface = "shop:sample:$item", success = { carries = "$item" } }, +] +produces = [ + { carries = "$item" } +] \ No newline at end of file diff --git a/data/bot/woodcutting.bots.toml b/data/bot/woodcutting.bots.toml index c1927ca845..c55c2f60c7 100644 --- a/data/bot/woodcutting.bots.toml +++ b/data/bot/woodcutting.bots.toml @@ -3,7 +3,7 @@ type = "activity" setup = [ { carries = "$hatchet" }, { location = "$location" }, - { inventory_space = 20 }, + { inventory_space = 27 }, ] actions = [ { option = "Chop down", object = "$tree", delay = 5, success = { inventory_space = 0 } }, @@ -12,46 +12,78 @@ produces = [ { skill = "woodcutting" } ] -[lumbridge_trees] -template = "woodcutting_template" +[normal_tree_template] +type = "activity" capacity = 4 -fields = { hatchet = "iron_hatchet,steel_hatchet", location = "lumbridge_north_trees", tree = "tree*" } requires = [ { skill = "woodcutting", min = 1, max = 15 }, ] +setup = [ + { carries = "steel_hatchet,iron_hatchet,bronze_hatchet" }, + { location = "$location" }, + { inventory_space = 27 }, +] +actions = [ + { option = "Chop down", object = "tree*", delay = 5, success = { inventory_space = 0 } }, +] produces = [ - { carries = "logs" } + { carries = "logs" }, + { skill = "woodcutting" } ] -[lumbridge_oak_trees] -template = "woodcutting_template" +[oak_tree_template] +type = "activity" capacity = 2 -fields = { hatchet = "steel_hatchet", location = "lumbridge_north_trees", tree = "oak" } requires = [ { skill = "woodcutting", min = 15, max = 30 }, ] +setup = [ + { carries = "mithril_hatchet,steel_hatchet" }, + { location = "$location" }, + { inventory_space = 27 }, +] +actions = [ + { option = "Chop down", object = "oak*", delay = 5, success = { inventory_space = 0 } }, +] produces = [ - { carries = "oak_logs" } + { carries = "oak_logs" }, + { skill = "woodcutting" } ] -[draynor_oak_trees] -template = "woodcutting_template" +[willow_tree_template] +type = "activity" capacity = 2 -fields = { hatchet = "steel_hatchet", location = "draynor_oak_trees", tree = "oak" } requires = [ - { skill = "woodcutting", min = 15, max = 30 }, + { skill = "woodcutting", min = 30, max = 60 }, +] +setup = [ + { carries = "rune_hatchet,adamant_hatchet,mithril_hatchet,steel_hatchet" }, + { location = "$location" }, + { inventory_space = 27 }, +] +actions = [ + { option = "Chop down", object = "willow*", delay = 5, success = { inventory_space = 0 } }, ] produces = [ - { carries = "oak_logs" } + { carries = "willow_logs" }, + { skill = "woodcutting" } ] -[draynor_willow_trees] -template = "woodcutting_template" -capacity = 6 -fields = { hatchet = "steel_hatchet,mithril_hatchet", location = "draynor_willow_trees", tree = "willow*" } +[yew_tree_template] +type = "activity" +capacity = 2 requires = [ - { skill = "woodcutting", min = 30, max = 45 }, + { skill = "woodcutting", min = 60, max = 75 }, +] +setup = [ + { carries = "dragon_hatchet,rune_hatchet,adamant_hatchet" }, + { location = "$location" }, + { inventory_space = 27 }, +] +actions = [ + { option = "Chop down", object = "yew*", delay = 5, success = { inventory_space = 0 } }, ] produces = [ - { carries = "willow_logs" } + { carries = "yew_logs" }, + { skill = "woodcutting" } ] diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/InterfaceHandler.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/InterfaceHandler.kt index 9c3cacf636..75ae0f1969 100644 --- a/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/InterfaceHandler.kt +++ b/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/InterfaceHandler.kt @@ -15,14 +15,12 @@ class InterfaceHandler( private val inventoryDefinitions: InventoryDefinitions, private val enumDefinitions: EnumDefinitions, ) { - private val logger = InlineLogger() fun getInterfaceItem(player: Player, interfaceId: Int, componentId: Int, itemId: Int, itemSlot: Int): InterfaceData? { val id = getOpenInterface(player, interfaceId) ?: return null val componentDefinition = getComponentDefinition(player, interfaceId, componentId) ?: return null val component = componentDefinition.stringId var item = Item.EMPTY - var inventory = "" if (itemId != -1) { when { id.startsWith("summoning_") && id.endsWith("_creation") -> item = Item(ItemDefinitions.get(itemId).stringId) @@ -38,12 +36,12 @@ class InterfaceHandler( id == "common_item_costs" -> item = Item(ItemDefinitions.get(itemId).stringId) id == "farming_equipment_store" || id == "farming_equipment_store_side" -> {} else -> { - inventory = getInventory(player, id, component, componentDefinition) ?: return null + val inventory = getInventory(player, id, component, componentDefinition) ?: return null item = getInventoryItem(player, id, componentDefinition, inventory, itemId, itemSlot) ?: return null } } } - return InterfaceData(id, component, item, inventory, componentDefinition.options) + return InterfaceData(id, component, item, componentDefinition.options) } private fun getOpenInterface(player: Player, interfaceId: Int): String? { @@ -65,25 +63,6 @@ class InterfaceHandler( return componentDefinition } - private fun getInventory(player: Player, id: String, component: String, componentDefinition: InterfaceComponentDefinition): String? { - if (component.isEmpty()) { - logger.info { "No inventory component found [$player, interface=$id, inventory=$component]" } - return null - } - if (id == "shop") { - return player["shop"] - } - var inventory = componentDefinition["inventory", ""] - if (id == "grand_exchange") { - inventory = "collection_box_${player["grand_exchange_box", -1]}" - } - if (!player.inventories.contains(inventory)) { - logger.info { "Player doesn't have interface inventory [$player, interface=$id, inventory=$inventory]" } - return null - } - return inventory - } - private fun getInventoryItem(player: Player, id: String, componentDefinition: InterfaceComponentDefinition, inventoryId: String, item: Int, itemSlot: Int): Item? { val itemId = if (item == -1 || item > ItemDefinitions.size) "" else ItemDefinitions.get(item).stringId val slot = when { @@ -113,13 +92,35 @@ class InterfaceHandler( } return inventory[slot] } + + companion object { + private val logger = InlineLogger() + + fun getInventory(player: Player, id: String, component: String, componentDefinition: InterfaceComponentDefinition): String? { + if (component.isEmpty()) { + logger.info { "No inventory component found [$player, interface=$id, inventory=$component]" } + return null + } + if (id == "shop") { + return player["shop"] + } + var inventory = componentDefinition["inventory", ""] + if (id == "grand_exchange") { + inventory = "collection_box_${player["grand_exchange_box", -1]}" + } + if (!player.inventories.contains(inventory)) { + logger.info { "Player doesn't have interface inventory [$player, interface=$id, inventory=$inventory]" } + return null + } + return inventory + } + } } data class InterfaceData( val id: String, val component: String, val item: Item, - val inventory: String, val options: Array?, ) { override fun equals(other: Any?): Boolean { @@ -131,7 +132,6 @@ data class InterfaceData( if (id != other.id) return false if (component != other.component) return false if (item != other.item) return false - if (inventory != other.inventory) return false if (options != null) { if (other.options == null) return false if (!options.contentEquals(other.options)) return false @@ -146,7 +146,6 @@ data class InterfaceData( var result = id.hashCode() result = 31 * result + component.hashCode() result = 31 * result + item.hashCode() - result = 31 * result + inventory.hashCode() result = 31 * result + (options?.contentHashCode() ?: 0) return result } diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/InterfaceOptionHandler.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/InterfaceOptionHandler.kt index d5d3224cb1..1911db6935 100644 --- a/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/InterfaceOptionHandler.kt +++ b/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/InterfaceOptionHandler.kt @@ -20,7 +20,7 @@ class InterfaceOptionHandler( override fun validate(player: Player, instruction: InteractInterface) { val (interfaceId, componentId, itemId, itemSlot, option) = instruction - var (id, component, item, _, options) = handler.getInterfaceItem(player, interfaceId, componentId, itemId, itemSlot) ?: return + var (id, component, item, options) = handler.getInterfaceItem(player, interfaceId, componentId, itemId, itemSlot) ?: return if (options == null) { options = interfaceDefinitions.getComponent(id, component)?.getOrNull("options") ?: emptyArray() diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/ObjectOptionHandler.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/ObjectOptionHandler.kt index b5f2364f3a..1fb8214fbe 100644 --- a/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/ObjectOptionHandler.kt +++ b/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/ObjectOptionHandler.kt @@ -41,7 +41,7 @@ class ObjectOptionHandler : InstructionHandler() { val index = option - 1 val selectedOption = options.getOrNull(index) if (selectedOption == null) { - logger.warn { "Invalid object option $target $index ${definition.options.contentToString()}" } + logger.warn { "Invalid object option $target $index ${options.contentToString()}" } return } player.closeInterfaces() diff --git a/game/src/main/kotlin/content/bot/BotManager.kt b/game/src/main/kotlin/content/bot/BotManager.kt index 5b68d79f51..cf2158e804 100644 --- a/game/src/main/kotlin/content/bot/BotManager.kt +++ b/game/src/main/kotlin/content/bot/BotManager.kt @@ -102,10 +102,34 @@ class BotManager( val activity = if (bot.previous != null && hasRequirements(bot, bot.previous!!)) { bot.previous } else { + if (bot.player["debug", false]) { + logger.info { "Picking bot ${bot.player.accountName} new task from available: ${bot.available}." } + } val id = bot.available.filter { val activity = activities[it] activity != null && hasRequirements(bot, activity) }.randomOrNull(random) + if (id == null) { + if (bot.player["debug", false]) { + logger.info { "Failed to find activity for bot ${bot.player.accountName}. Reasons:" } + for (id in bot.available) { + val activity = activities[id] ?: continue + if (!slots.hasFree(activity)) { + logger.info { "Activity: $id - No available slots." } + } else if (bot.blocked.contains(activity.id)) { + logger.info { "Activity: $id - Blocked." } + } else { + for (requirement in activity.requires) { + if (!requirement.check(bot.player)) { + logger.info { "Activity: $id - Failed requirement: $requirement" } + break + } + } + } + } + logger.info { "Picking bot ${bot.player.accountName} new task from available: ${bot.available}." } + } + } activities[id] ?: idle } if (activity == null) { @@ -141,18 +165,20 @@ class BotManager( } val resolver = pickResolver(bot, requirement, frame) if (resolver == null) { + if (bot.player["debug", false]) { + logger.info { "No resolver found for for ${behaviour.id} keys: ${requirement.keys()} requirement: ${requirement}." } + } frame.fail(Reason.Requirement(requirement)) // No way to resolve return } // Attempt resolution AuditLog.event(bot, "start_resolver", resolver.id, behaviour.id) if (bot.player["debug", false]) { - logger.info { "Starting resolution: ${resolver.id} for ${behaviour.id} req ${requirement}." } + logger.info { "Starting resolution: ${resolver.id} for ${behaviour.id} requirement: ${requirement}." } } frame.blocked.add(resolver.id) val resolverFrame = BehaviourFrame(resolver) bot.queue(resolverFrame) - resolverFrame.start(bot) return } AuditLog.event(bot, "start_activity", behaviour.id) @@ -194,7 +220,7 @@ class BotManager( Resolver( "withdraw_${condition.fact.id}", 20, actions = listOf( BotAction.GoToNearest("bank"), - BotAction.InteractObject("Use-quickly", "bank_booth*"), + BotAction.InteractObject("Use-quickly", "bank_booth*", success = Condition.Equals(Fact.InterfaceOpen("bank"), true)), BotAction.InterfaceOption("Withdraw-${condition.min}", "bank:inventory:${condition.fact.id}"), ) ) @@ -204,7 +230,7 @@ class BotManager( Resolver( "withdraw_${condition.fact.id}", 20, actions = listOf( BotAction.GoToNearest("bank"), - BotAction.InteractObject("Use-quickly", "bank_booth*"), + BotAction.InteractObject("Use-quickly", "bank_booth*", success = Condition.Equals(Fact.InterfaceOpen("bank"), true)), BotAction.InterfaceOption("Withdraw-X", "bank:inventory:${condition.fact.id}"), BotAction.IntEntry(condition.min), ) diff --git a/game/src/main/kotlin/content/bot/BotUpdates.kt b/game/src/main/kotlin/content/bot/BotUpdates.kt index 65cb1bc460..e7c26d80fd 100644 --- a/game/src/main/kotlin/content/bot/BotUpdates.kt +++ b/game/src/main/kotlin/content/bot/BotUpdates.kt @@ -6,7 +6,7 @@ class BotUpdates(val manager: BotManager) : Script { init { levelChanged { skill, _, _ -> if (isBot) { - manager.update(bot, skill.name) + manager.update(bot, "skill:${skill.name.lowercase()}") } } diff --git a/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt b/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt index 4727d4ec9d..c120beb178 100644 --- a/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt +++ b/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt @@ -2,7 +2,6 @@ package content.bot.action import content.bot.fact.* import world.gregs.voidps.engine.event.Wildcard -import world.gregs.voidps.engine.event.Wildcards data class BehaviourFragment( override val id: String, @@ -31,14 +30,16 @@ data class BehaviourFragment( if (option == "Attack") { BotAction.FightNpc( id = resolve(action.references["npc"], copy.id), + success = resolveReference(copy.success), + delay = resolve(action.references["radius"], copy.delay), radius = resolve(action.references["radius"], copy.radius), ) } else { BotAction.InteractNpc( option = option, id = resolve(action.references["npc"], copy.id), + success = resolveReference(copy.success), delay = resolve(action.references["delay"], copy.delay), - successCondition = copy.successCondition, radius = resolve(action.references["radius"], copy.radius), ) } @@ -46,7 +47,7 @@ data class BehaviourFragment( is BotAction.FightNpc -> BotAction.FightNpc( id = resolve(action.references["npc"], copy.id), delay = resolve(action.references["delay"], copy.delay), - success = copy.success, + success = resolveReference(copy.success), healPercentage = resolve(action.references["heal_percent"], copy.healPercentage), lootOverValue = resolve(action.references["loot_over"], copy.lootOverValue), radius = resolve(action.references["radius"], copy.radius), @@ -54,8 +55,8 @@ data class BehaviourFragment( is BotAction.InteractObject -> BotAction.InteractObject( option = resolve(action.references["option"], copy.option), id = resolve(action.references["object"], copy.id), + success = resolveReference(copy.success), delay = resolve(action.references["delay"], copy.delay), - success = copy.success, radius = resolve(action.references["radius"], copy.radius), ) is BotAction.WalkTo -> BotAction.WalkTo( @@ -80,67 +81,74 @@ data class BehaviourFragment( fun resolveRequirements(requirements: MutableList, facts: List) { for (req in facts) { - val resolved = when (req) { - is Condition.Reference -> { - val references = req.references - val min = resolve(references["min"], req.min) - val max = resolve(references["max"], req.max) - when (req.type) { - "skill" -> { - val id = resolve(references[req.type], req.id) - Condition.range(Fact.SkillLevel.of(id), min, max) - } - "carries" -> { - val id = resolve(references[req.type], req.id) - val min = resolve(references["amount"], req.min) - Condition.split(id, min, max, Wildcard.Item) { Fact.InventoryCount(it) } - } - "equips" -> { - val id = resolve(references[req.type], req.id) - val min = resolve(references["amount"], req.min) - Condition.split(id, min, max, Wildcard.Item) { Fact.EquipCount(it) } - } - "owns" -> { - val id = resolve(references[req.type], req.id) - val min = resolve(references["amount"], req.min) - Condition.split(id, min, max, Wildcard.Item) { Fact.ItemCount(it) } - } - "clock" -> { - val id = resolve(references[req.type], req.id) - Condition.split(id, min, max, Wildcard.Variables) { Fact.ClockRemaining(it) } - } - "timer" -> { - val id = resolve(references[req.type], req.id) - val value = resolve(references["value"], req.value as? Boolean) - Condition.Equals(Fact.HasTimer(id), value as? Boolean ?: true) - } - "variable" -> { - val id = resolve(references[req.type], req.id) - val default = resolve(references["default"], req.default) - when (val value = resolve(references["value"], req.value)) { - is Int -> Condition.Equals(Fact.IntVariable(id, default as? Int), value) - is String -> Condition.Equals(Fact.StringVariable(id, default as? String), value) - is Double -> Condition.Equals(Fact.DoubleVariable(id, default as? Double), value) - is Boolean -> Condition.Equals(Fact.BoolVariable(id, default as? Boolean), value) - else -> null - } - } - "inventory_space" -> { - val min = resolve(references["inventory_space"], req.min) - Condition.range(Fact.InventorySpace, min, null) - } - "location" -> { - val id = resolve(references["location"], req.id) - Condition.Area(Fact.PlayerTile, id) - } + val resolved = resolveReference(req) ?: continue + requirements.add(resolved) + } + } + + private fun BehaviourFragment.resolveReference(req: Condition?): Condition? = when (req) { + is Condition.Reference -> { + val references = req.references + val min = resolve(references["min"], req.min) + val max = resolve(references["max"], req.max) + when (req.type) { + "skill" -> { + val id = resolve(references[req.type], req.id) + Condition.range(Fact.SkillLevel.of(id), min, max) + } + "carries" -> { + val id = resolve(references[req.type], req.id) + val min = resolve(references["amount"], req.min) + Condition.split(id, min, max, Wildcard.Item) { Fact.InventoryCount(it) } + } + "equips" -> { + val id = resolve(references[req.type], req.id) + val min = resolve(references["amount"], req.min) + Condition.split(id, min, max, Wildcard.Item) { Fact.EquipCount(it) } + } + "owns" -> { + val id = resolve(references[req.type], req.id) + val min = resolve(references["amount"], req.min) + Condition.split(id, min, max, Wildcard.Item) { Fact.ItemCount(it) } + } + "clock" -> { + val id = resolve(references[req.type], req.id) + Condition.split(id, min, max, Wildcard.Variables) { Fact.ClockRemaining(it) } + } + "timer" -> { + val id = resolve(references[req.type], req.id) + val value = resolve(references["value"], req.value as? Boolean) + Condition.Equals(Fact.HasTimer(id), value as? Boolean ?: true) + } + "interface" -> { + val id = resolve(references[req.type], req.id) + val value = resolve(references["value"], req.value as? Boolean) + Condition.Equals(Fact.InterfaceOpen(id), value as? Boolean ?: true) + } + "variable" -> { + val id = resolve(references[req.type], req.id) + val default = resolve(references["default"], req.default) + when (val value = resolve(references["value"], req.value)) { + is Int -> Condition.Equals(Fact.IntVariable(id, default as? Int), value) + is String -> Condition.Equals(Fact.StringVariable(id, default as? String), value) + is Double -> Condition.Equals(Fact.DoubleVariable(id, default as? Double), value) + is Boolean -> Condition.Equals(Fact.BoolVariable(id, default as? Boolean), value) else -> null } } - is Condition.Clone -> throw IllegalArgumentException("Unresolved clone requirement in template ${id}.") - else -> req - } ?: continue - requirements.add(resolved) + "inventory_space" -> { + val min = resolve(references["inventory_space"], req.min) + Condition.range(Fact.InventorySpace, min, null) + } + "location" -> { + val id = resolve(references["location"], req.id) + Condition.Area(Fact.PlayerTile, id) + } + else -> null + } } + is Condition.Clone -> throw IllegalArgumentException("Unresolved clone requirement in template ${id}.") + else -> req } private fun String.key(): String { diff --git a/game/src/main/kotlin/content/bot/action/BotAction.kt b/game/src/main/kotlin/content/bot/action/BotAction.kt index 97e847775f..ccab0b2c2b 100644 --- a/game/src/main/kotlin/content/bot/action/BotAction.kt +++ b/game/src/main/kotlin/content/bot/action/BotAction.kt @@ -7,6 +7,7 @@ import content.bot.interact.path.Graph import content.entity.combat.attackers import content.entity.player.bank.bank import world.gregs.voidps.engine.GameLoop +import world.gregs.voidps.engine.client.instruction.InterfaceHandler import world.gregs.voidps.engine.data.definition.Areas import world.gregs.voidps.engine.data.definition.InterfaceDefinitions import world.gregs.voidps.engine.data.definition.ItemDefinitions @@ -127,7 +128,7 @@ sealed interface BotAction { val option: String, val id: String, val delay: Int = 0, - val successCondition: Condition? = null, + val success: Condition? = null, val radius: Int = 10, ) : BotAction { @@ -137,8 +138,8 @@ sealed interface BotAction { } override fun update(bot: Bot, frame: BehaviourFrame) = when { - successCondition?.check(bot.player) == true -> BehaviourState.Success - bot.mode is PlayerOnNPCInteract -> if (successCondition == null) BehaviourState.Success else BehaviourState.Running + success?.check(bot.player) == true -> BehaviourState.Success + bot.mode is PlayerOnNPCInteract -> if (success == null) BehaviourState.Success else BehaviourState.Running bot.mode is EmptyMode -> search(bot) else -> null } @@ -159,12 +160,15 @@ sealed interface BotAction { } val npc = npcs.randomOrNull(random) ?: return handleNoTarget() val index = npc.def(player).options.indexOf(option) - bot.player.instructions.trySend(InteractNPC(npc.index, index)) + if (index == -1) { + return BehaviourState.Failed(Reason.NoTarget) + } + bot.player.instructions.trySend(InteractNPC(npc.index, index + 1)) return BehaviourState.Running } private fun handleNoTarget(): BehaviourState { - if (successCondition == null) { + if (success == null) { return BehaviourState.Failed(Reason.NoTarget) } if (delay > 0) { @@ -240,12 +244,15 @@ sealed interface BotAction { } val item = loot.randomOrNull(random) if (item != null) { - bot.player.instructions.trySend(InteractFloorItem(item.def.id, item.tile.x, item.tile.y, item.def.floorOptions.indexOf("Pick-up"))) + bot.player.instructions.trySend(InteractFloorItem(item.def.id, item.tile.x, item.tile.y, item.def.floorOptions.indexOf("Pick-up") + 1)) return BehaviourState.Running } val npc = npcs.randomOrNull(random) ?: return handleNoTarget() val index = npc.def(player).options.indexOf("Attack") - bot.player.instructions.trySend(InteractNPC(npc.index, index)) + if (index == -1) { + return BehaviourState.Failed(Reason.NoTarget) + } + bot.player.instructions.trySend(InteractNPC(npc.index, index + 1)) return BehaviourState.Running } @@ -297,6 +304,9 @@ sealed interface BotAction { } val obj = objects.randomOrNull(random) ?: return handleNoTarget() val index = obj.def(bot.player).options?.indexOf(option) ?: return BehaviourState.Failed(Reason.NoTarget) + if (index == -1) { + return BehaviourState.Failed(Reason.NoTarget) + } bot.player.instructions.trySend(InteractObject(obj.intId, obj.x, obj.y, index + 1)) return BehaviourState.Running } @@ -321,27 +331,24 @@ sealed interface BotAction { } val (id, component) = split val item = split.getOrNull(2) - val def = definitions.getOrNull(id) ?: return BehaviourState.Failed(Reason.NoTarget) - val componentId = definitions.getComponentId(id, component) ?: return BehaviourState.Failed(Reason.NoTarget) - val componentDef = definitions.getComponent(id, component) ?: return BehaviourState.Failed(Reason.NoTarget) + val def = definitions.getOrNull(id) ?: return BehaviourState.Failed(Reason.Invalid("Invalid interface id $id:${component}:${item}.")) + val componentId = definitions.getComponentId(id, component) ?: return BehaviourState.Failed(Reason.Invalid("Invalid interface component $id:${component}:${item}.")) + val componentDef = definitions.getComponent(id, component) ?: return BehaviourState.Failed(Reason.Invalid("Invalid interface component definition $id:${component}:${item}.")) var options = componentDef.options if (options == null) { options = componentDef.getOrNull("options") ?: emptyArray() } val index = options.indexOf(option) if (index == -1) { - return BehaviourState.Failed(Reason.NoTarget) + return BehaviourState.Failed(Reason.Invalid("No interface option $option for $id:$component:${item} options=${options.contentToString()}.")) } val itemDef = if (item != null) ItemDefinitions.getOrNull(item) else null - val inv = when (id) { - "bank" -> bot.player.bank - "inventory" -> bot.player.inventory - "equipment" -> bot.player.equipment - // TODO link up with inv defs - else -> null + val inv = InterfaceHandler.getInventory(bot.player, id, component, componentDef) + var itemSlot = if (item != null && inv != null) bot.player.inventories.inventory(inv).indexOf(item) else -1 + if (id == "shop") { + itemSlot *= 6 } - val itemSlot = if (item != null && inv != null) inv.indexOf(item) else -1 bot.player.instructions.trySend( InteractInterface( interfaceId = def.id, diff --git a/game/src/main/kotlin/content/bot/action/BotActivity.kt b/game/src/main/kotlin/content/bot/action/BotActivity.kt index d3b824839b..6307a6169c 100644 --- a/game/src/main/kotlin/content/bot/action/BotActivity.kt +++ b/game/src/main/kotlin/content/bot/action/BotActivity.kt @@ -68,18 +68,19 @@ fun loadActivities( } if (template != null) { - fragments[id] = BehaviourFragment(id, type, capacity, weight, template, requirements, resolvables, actions = actions, fields = fields) + fragments[id] = BehaviourFragment(id, type, capacity, weight, template, requirements, resolvables, actions = actions, fields = fields, produces = produces.toSet()) } else if (type == "resolver") { + val resolver = Resolver(id, weight, requirements, resolvables, actions = actions, produces = produces.toSet()) for (fact in produces) { for (key in fact.keys()) { - resolvers.getOrPut(key) { mutableListOf() }.add(Resolver(id, weight, requirements, resolvables, actions = actions)) + resolvers.getOrPut(key) { mutableListOf() }.add(resolver) } } } else if (type == "shortcut") { require(resolvables.isEmpty()) { "Shortcuts cannot have setup requirements" } shortcuts.add(NavigationShortcut(id, weight, requirements, actions = actions, produces = produces.toSet())) } else { - activities[id] = BotActivity(id, capacity, requirements, resolvables, actions = actions) + activities[id] = BotActivity(id, capacity, requirements, resolvables, actions = actions, produces = produces.toSet()) } } } @@ -129,19 +130,25 @@ fun loadActivities( fragment.resolveRequirements(resolvables, template.resolve) resolvables.sortBy { it.priority() } + val products = mutableListOf() + products.addAll(fragment.produces) + fragment.resolveRequirements(products, template.produces.toList()) + products.sortBy { it.priority() } + val actions = mutableListOf() actions.addAll(fragment.actions) fragment.resolveActions(template, actions) when (fragment.type) { "resolver" -> { - for (fact in fragment.produces) { + val resolver = Resolver(id, fragment.weight, requirements, resolvables, actions, products.toSet()) + for (fact in products) { for (key in fact.keys()) { - resolvers.getOrPut(key) { mutableListOf() }.add(Resolver(id, fragment.weight, requirements, resolvables, actions)) + resolvers.getOrPut(key) { mutableListOf() }.add(resolver) } } } "shortcut" -> shortcuts.add(NavigationShortcut(id, fragment.weight, requirements, actions = actions)) - else -> activities[id] = BotActivity(id, fragment.capacity, requirements, resolvables, actions) + else -> activities[id] = BotActivity(id, template.capacity, requirements, resolvables, actions) } } // Templates aren't selectable activities @@ -151,7 +158,7 @@ fun loadActivities( // Group activities by requirement types for (activity in activities.values) { for (fact in activity.requires) { - for (key in fact.keys()) { + for (key in fact.groups()) { groups.getOrPut(key) { mutableListOf() }.add(activity.id) } } @@ -187,7 +194,7 @@ private fun ConfigReader.requirement(exact: Boolean = false): Condition { val references = mutableMapOf() while (nextEntry()) { when (val key = key()) { - "skill", "carries", "equips", "owns", "clock", "variable", "clone", "location" -> { + "skill", "carries", "equips", "interface", "owns", "clock", "variable", "clone", "location" -> { type = key id = string() if (id.contains('$')) { @@ -244,6 +251,7 @@ private fun getRequirement(type: String, id: String, min: Int?, max: Int?, value "equips" -> Condition.split(id, min, max, Wildcard.Item) { Fact.EquipCount(it) } "clock" -> Condition.split(id, min, max, Wildcard.Variables) { Fact.ClockRemaining(it) } "timer" -> Condition.Equals(Fact.HasTimer(id), value as? Boolean ?: true) + "interface" -> Condition.Equals(Fact.InterfaceOpen(id), value as? Boolean ?: true) "variable" -> when (value) { is Int -> Condition.Equals(Fact.IntVariable(id, default as? Int), value) is String -> Condition.Equals(Fact.StringVariable(id, default as? String), value) @@ -364,7 +372,7 @@ fun ConfigReader.actions(list: MutableList) { "npc" -> if (option == "Attack") { BotAction.FightNpc(id = id, delay = delay, success = success, healPercentage = heal, lootOverValue = loot, radius = radius) } else { - BotAction.InteractNpc(id = id, option = option, delay = delay, successCondition = success, radius = radius) + BotAction.InteractNpc(id = id, option = option, delay = delay, success = success, radius = radius) } "tile" -> BotAction.WalkTo(x = x, y = y) "object" -> BotAction.InteractObject(id = id, option = option, delay = delay, success = success, radius = radius) diff --git a/game/src/main/kotlin/content/bot/fact/Condition.kt b/game/src/main/kotlin/content/bot/fact/Condition.kt index 63d5b35b54..f247576a57 100644 --- a/game/src/main/kotlin/content/bot/fact/Condition.kt +++ b/game/src/main/kotlin/content/bot/fact/Condition.kt @@ -10,66 +10,77 @@ import kotlin.collections.map sealed interface Condition { fun check(player: Player): Boolean fun keys(): Set + fun groups(): Set fun priority(): Int data class Equals(val fact: Fact, val value: T) : Condition { override fun check(player: Player) = fact.getValue(player) == value override fun priority() = fact.priority override fun keys() = fact.keys() + override fun groups() = fact.groups() } data class AtLeast(val fact: Fact, val min: Int) : Condition { override fun check(player: Player) = fact.getValue(player) >= min override fun priority() = fact.priority override fun keys() = fact.keys() + override fun groups() = fact.groups() } data class AtMost(val fact: Fact, val max: Int) : Condition { override fun check(player: Player) = fact.getValue(player) <= max override fun priority() = fact.priority override fun keys() = fact.keys() + override fun groups() = fact.groups() } data class Range(val fact: Fact, val min: Int, val max: Int) : Condition { override fun check(player: Player) = fact.getValue(player) in min..max override fun priority() = fact.priority override fun keys() = fact.keys() + override fun groups() = fact.groups() } data class Within(val fact: Fact, val tile: Tile, val radius: Int) : Condition { override fun check(player: Player) = fact.getValue(player).within(tile, radius) override fun priority() = fact.priority override fun keys() = fact.keys() + override fun groups() = fact.groups() } data class Area(val fact: Fact, val area: String) : Condition { // TODO make fact always PlayerTile? override fun check(player: Player) = fact.getValue(player) in Areas[area] override fun priority() = fact.priority override fun keys() = setOf("enter:$area") + override fun groups() = setOf("area") } data class OneOf(val fact: Fact, val values: Set) : Condition { override fun check(player: Player) = fact.getValue(player) in values override fun priority() = fact.priority override fun keys() = fact.keys() + override fun groups() = fact.groups() } data class Not(val inner: Condition) : Condition { override fun check(player: Player) = !inner.check(player) override fun priority() = inner.priority() override fun keys() = inner.keys() + override fun groups() = inner.groups() } data class All(val conditions: List) : Condition { override fun check(player: Player) = conditions.all { it.check(player) } override fun priority() = conditions.first().priority() override fun keys() = conditions.flatMap { it.keys() }.toSet() + override fun groups() = conditions.flatMap { it.groups() }.toSet() } data class Any(val conditions: List) : Condition { override fun check(player: Player) = conditions.any { it.check(player) } override fun priority() = conditions.first().priority() override fun keys() = conditions.flatMap { it.keys() }.toSet() + override fun groups() = conditions.flatMap { it.groups() }.toSet() } data class Reference( @@ -84,12 +95,14 @@ sealed interface Condition { override fun check(player: Player) = false override fun priority() = -1 override fun keys() = emptySet() + override fun groups() = emptySet() } class Clone(val id: String) : Condition { override fun check(player: Player) = false override fun priority() = -1 override fun keys() = emptySet() + override fun groups() = emptySet() } companion object { diff --git a/game/src/main/kotlin/content/bot/fact/Fact.kt b/game/src/main/kotlin/content/bot/fact/Fact.kt index 4814c6e5d0..3dac3e4071 100644 --- a/game/src/main/kotlin/content/bot/fact/Fact.kt +++ b/game/src/main/kotlin/content/bot/fact/Fact.kt @@ -16,25 +16,37 @@ import world.gregs.voidps.type.Tile */ sealed class Fact(val priority: Int) { abstract fun getValue(player: Player): T + + /** + * Fact specific identifiers for finding resolvers e.g. inv:bronze_hatchet, bank:coins etc... + */ open fun keys(): Set = emptySet() + /** + * Group types to listen for types of updates e.g. inv:bank, enter:area etc... + */ + open fun groups(): Set = keys() + object InventorySpace : Fact(10) { - override fun keys() = setOf("inv:inventory") + override fun keys() = setOf("inventory_space") override fun getValue(player: Player) = player.inventory.spaces } data class InventoryCount(val id: String) : Fact(100) { - override fun keys() = setOf("inv:inventory") + override fun keys() = setOf("inv:$id") + override fun groups() = setOf("inv:inventory") override fun getValue(player: Player) = player.inventory.count(id) } data class ItemCount(val id: String) : Fact(100) { - override fun keys() = setOf("inv:inventory", "inv:bank", "inv:equipment") + override fun keys() = setOf("inventory:$id", "bank:$id", "worn_equipment:$id") + override fun groups() = setOf("inv:inventory", "inv:bank", "inv:worn_equipment") override fun getValue(player: Player) = player.inventory.count(id) + player.bank.count(id) + player.equipment.count(id) } data class EquipCount(val id: String) : Fact(100) { - override fun keys() = setOf("inv:equipment") + override fun keys() = setOf("worn_equipment:$id") + override fun groups() = setOf("inv:worn_equipment") override fun getValue(player: Player) = player.equipment.count(id) } @@ -68,6 +80,11 @@ sealed class Fact(val priority: Int) { override fun getValue(player: Player) = player.timers.contains(timer) } + data class InterfaceOpen(val id: String) : Fact(1) { + override fun keys() = setOf("iface:$id") + override fun getValue(player: Player) = player.interfaces.contains(id) + } + object PlayerTile : Fact(1000) { override fun keys() = setOf("tile") override fun getValue(player: Player) = player.tile diff --git a/game/src/test/kotlin/content/bot/action/BehaviourFragmentTest.kt b/game/src/test/kotlin/content/bot/action/BehaviourFragmentTest.kt index ee49546a9d..119008e299 100644 --- a/game/src/test/kotlin/content/bot/action/BehaviourFragmentTest.kt +++ b/game/src/test/kotlin/content/bot/action/BehaviourFragmentTest.kt @@ -221,6 +221,7 @@ class BehaviourFragmentTest { Triple(Condition.Reference("inventory_space", min = 1), mapOf("inventory_space" to 10), Condition.AtLeast(Fact.InventorySpace, 10)), Triple(Condition.Reference("location", "default"), mapOf("location" to "area"), Condition.Area(Fact.PlayerTile, "area")), Triple(Condition.Reference("timer", "default"), mapOf("timer" to "tick"), Condition.Equals(Fact.HasTimer("tick"), true)), + Triple(Condition.Reference("interface", "default"), mapOf("interface" to "id"), Condition.Equals(Fact.InterfaceOpen("id"), true)), Triple(Condition.Reference("clock", "default", min = 1), mapOf("clock" to "tock", "min" to 5), Condition.AtLeast(Fact.ClockRemaining("tock"), 5)), ).map { (reference, values, expected) -> dynamicTest("Resolve ${reference::class.simpleName} references") { From 272343910c315c8e0210f5b80f4a88362d8134c1 Mon Sep 17 00:00:00 2001 From: GregHib Date: Wed, 4 Feb 2026 19:18:28 +0000 Subject: [PATCH 040/101] Remove some inappropriate bot names --- data/bot_names.txt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/data/bot_names.txt b/data/bot_names.txt index 5f07fb22fe..bee8541a41 100644 --- a/data/bot_names.txt +++ b/data/bot_names.txt @@ -1506,7 +1506,6 @@ Light LightDark99 LightningL Lily -LimpStiff LineCircle LinguineL Linus @@ -2561,9 +2560,6 @@ SteppeStan StewSteve StickShift StickyDoor -StiffFlexi -StiffLimp -StiffPliable StigStgmoch StillMoving StockStan From c60b9545ef20bd9cc1dfd46028e8c4599bd9a128 Mon Sep 17 00:00:00 2001 From: GregHib Date: Wed, 4 Feb 2026 19:48:36 +0000 Subject: [PATCH 041/101] Add combat level fact and more mining bots --- .../al_kharid/al_kharid.areas.toml | 4 + .../al_kharid/al_kharid.bots.toml | 56 +++++++ .../misthalin/lumbridge/lumbridge.bots.toml | 32 +++- data/area/misthalin/varrock/varrock.bots.toml | 35 ++++ data/bot/axe_shop.bots.toml | 35 ---- data/bot/lumbridge.nav-edges.toml | 2 + data/bot/mining_templates.bots.toml | 151 ++++++++++++++++++ ...s.toml => woodcutting_templates.bots.toml} | 14 -- .../content/bot/action/BehaviourFragment.kt | 3 + .../kotlin/content/bot/action/BotActivity.kt | 4 +- game/src/main/kotlin/content/bot/fact/Fact.kt | 6 + 11 files changed, 285 insertions(+), 57 deletions(-) create mode 100644 data/area/kharidian_desert/al_kharid/al_kharid.bots.toml create mode 100644 data/area/misthalin/varrock/varrock.bots.toml delete mode 100644 data/bot/axe_shop.bots.toml create mode 100644 data/bot/mining_templates.bots.toml rename data/bot/{woodcutting.bots.toml => woodcutting_templates.bots.toml} (85%) diff --git a/data/area/kharidian_desert/al_kharid/al_kharid.areas.toml b/data/area/kharidian_desert/al_kharid/al_kharid.areas.toml index ba4d5bd885..9bb68a1d73 100644 --- a/data/area/kharidian_desert/al_kharid/al_kharid.areas.toml +++ b/data/area/kharidian_desert/al_kharid/al_kharid.areas.toml @@ -34,6 +34,10 @@ y = [3121, 3125] x = [3291, 3308] y = [3281, 3320] +[al_kharid_mine_north] +x = [3291, 3308] +y = [3309, 3320] + [greater_al_kharid] x = [3264,3264,3391,3391,3423,3423,3455,3455] y = [3136,3327,3327,3263,3263,3182,3182,3136] diff --git a/data/area/kharidian_desert/al_kharid/al_kharid.bots.toml b/data/area/kharidian_desert/al_kharid/al_kharid.bots.toml new file mode 100644 index 0000000000..2dd950da40 --- /dev/null +++ b/data/area/kharidian_desert/al_kharid/al_kharid.bots.toml @@ -0,0 +1,56 @@ +# Mining +[al_kharid_iron_mining] +template = "iron_ore_template" +capacity = 2 +requires = [ + { combat_level = 10 } +] +fields = { location = "al_kharid_mine" } + +[al_kharid_coal_mining] +template = "coal_template" +capacity = 1 +requires = [ + { combat_level = 10 } +] +fields = { location = "al_kharid_mine" } + +[al_kharid_silver_mining] +template = "silver_ore_template" +capacity = 2 +requires = [ + { combat_level = 10 } +] +fields = { location = "al_kharid_mine" } + +[al_kharid_north_copper_mining] +template = "copper_ore_template" +capacity = 1 +requires = [ + { combat_level = 10 } +] +fields = { location = "al_kharid_mine_north" } + +[al_kharid_north_silver_mining] +template = "silver_ore_template" +capacity = 1 +requires = [ + { combat_level = 10 } +] +fields = { location = "al_kharid_mine_north" } + +[al_kharid_north_iron_mining] +template = "iron_ore_template" +capacity = 2 +requires = [ + { combat_level = 10 } +] +fields = { location = "al_kharid_mine_north" } + +[al_kharid_north_mithril_mining] +template = "mithril_ore_template" +capacity = 2 +requires = [ + { combat_level = 10 } +] +fields = { location = "al_kharid_mine_north" } diff --git a/data/area/misthalin/lumbridge/lumbridge.bots.toml b/data/area/misthalin/lumbridge/lumbridge.bots.toml index 9f37672f99..50c01ea1ed 100644 --- a/data/area/misthalin/lumbridge/lumbridge.bots.toml +++ b/data/area/misthalin/lumbridge/lumbridge.bots.toml @@ -1,31 +1,49 @@ -[lumbridge_north_trees] +# Woodcutting +[lumbridge_north_tree_cutting] template = "normal_tree_template" fields = { location = "lumbridge_north_trees" } -[lumbridge_cow_trees] +[lumbridge_cow_tree_cutting] template = "normal_tree_template" fields = { location = "lumbridge_cow_trees" } -[lumbridge_west_trees] +[lumbridge_west_tree_cutting] template = "normal_tree_template" fields = { location = "lumbridge_west_trees" } -[lumbridge_east_trees] +[lumbridge_east_tree_cutting] template = "normal_tree_template" fields = { location = "lumbridge_east_trees" } -[lumbridge_oak_trees] +[lumbridge_oak_tree_cutting] template = "oak_tree_template" fields = { location = "lumbridge_north_trees" } -[lumbridge_castle_yew_tree] +[lumbridge_castle_yew_tree_cutting] template = "yew_tree_template" fields = { location = "lumbridge_north_west_yew_tree" } -[lumbridge_west_yew_tree] +[lumbridge_west_yew_tree_cutting] template = "yew_tree_template" fields = { location = "lumbridge_west_yew_tree" } +# Mining +[lumbridge_copper_mining] +template = "copper_ore_template" +fields = { location = "lumbridge_swamp_east_copper_mine" } + +[lumbridge_tin_mining] +template = "tin_ore_template" +fields = { location = "lumbridge_swamp_east_tin_mine" } + +[lumbridge_coal_mining] +template = "coal_template" +fields = { location = "lumbridge_swamp_west_coal_mine" } + +[lumbridge_mithril_mining] +template = "mithril_ore_template" +fields = { location = "lumbridge_swamp_west_mithril_mine" } + # Bobs Brilliant Axes [take_bronze_hatchet_bobs] diff --git a/data/area/misthalin/varrock/varrock.bots.toml b/data/area/misthalin/varrock/varrock.bots.toml new file mode 100644 index 0000000000..518093de1f --- /dev/null +++ b/data/area/misthalin/varrock/varrock.bots.toml @@ -0,0 +1,35 @@ +# Mining +[varrock_copper_mining] +template = "copper_ore_template" +capacity = 4 +fields = { location = "varrock_south_east_mine" } + +[varrock_tin_mining] +template = "tin_ore_template" +capacity = 4 +fields = { location = "varrock_south_east_mine" } + +[varrock_iron_mining] +template = "iron_ore_template" +capacity = 2 +fields = { location = "varrock_south_east_mine" } + +[varrock_clay_mining] +template = "clay_template" +capacity = 2 +fields = { location = "varrock_south_west_mine" } + +[varrock_east_tin_mining] +template = "tin_ore_template" +capacity = 4 +fields = { location = "varrock_south_west_mine" } + +[varrock_silver_mining] +template = "silver_ore_template" +capacity = 3 +fields = { location = "varrock_south_west_mine" } + +[varrock_east_iron_mining] +template = "iron_ore_template" +capacity = 2 +fields = { location = "varrock_south_west_mine" } diff --git a/data/bot/axe_shop.bots.toml b/data/bot/axe_shop.bots.toml deleted file mode 100644 index 33ca1a0cd9..0000000000 --- a/data/bot/axe_shop.bots.toml +++ /dev/null @@ -1,35 +0,0 @@ -[take_free_bronze_hatchet] -type = "resolver" -weight = 5 -template = "take_shop_sample" -fields = { shop_location = "bobs_axe_shop", shopkeeper = "bob", item = "bronze_hatchet" } -requires = [ - { skill = "woodcutting", min = 1, max = 10 }, -] - -[buy_bronze_hatchet] -type = "resolver" -weight = 10 -template = "buy_from_shop" -fields = { cost = 16, shop_location = "bobs_axe_shop", shopkeeper = "bob", item = "bronze_hatchet" } -requires = [ - { skill = "woodcutting", min = 1, max = 10 }, -] - -[buy_iron_hatchet] -type = "resolver" -weight = 10 -template = "buy_from_shop" -fields = { cost = 56, shop_location = "bobs_axe_shop", shopkeeper = "bob", item = "iron_hatchet" } -requires = [ - { skill = "woodcutting", min = 1, max = 15 }, -] - -[buy_steel_hatchet] -type = "resolver" -weight = 10 -template = "buy_from_shop" -fields = { cost = 200, shop_location = "bobs_axe_shop", shopkeeper = "bob", item = "steel_hatchet" } -requires = [ - { skill = "woodcutting", min = 6 }, -] diff --git a/data/bot/lumbridge.nav-edges.toml b/data/bot/lumbridge.nav-edges.toml index b4b9e68b05..75b1e72fe1 100644 --- a/data/bot/lumbridge.nav-edges.toml +++ b/data/bot/lumbridge.nav-edges.toml @@ -217,6 +217,8 @@ edges = [ { from_x = 3283, from_y = 3331, to_x = 3284, to_y = 3313 }, # al_kharid_mine_north_entrance_to_west_path { from_x = 3284, from_y = 3313, to_x = 3287, to_y = 3294 }, # al_kharid_mine_west_path_to_south { from_x = 3287, from_y = 3294, to_x = 3298, to_y = 3280 }, # al_kharid_mine_west_path_to_entrance + { from_x = 3298, from_y = 3280, to_x = 3299, to_y = 3294 }, + { from_x = 3299, from_y = 3294, to_x = 3300, to_y = 3311 }, { from_x = 3298, from_y = 3280, to_x = 3299, to_y = 3263 }, # al_kharid_mine_entrance_to_mine_south { from_x = 3299, from_y = 3263, to_x = 3294, to_y = 3242 }, # al_kharid_mine_south_to_north_path { from_x = 3294, from_y = 3242, to_x = 3278, to_y = 3228 }, # al_kharid_north_path_to_crossroad diff --git a/data/bot/mining_templates.bots.toml b/data/bot/mining_templates.bots.toml new file mode 100644 index 0000000000..16e9404c4a --- /dev/null +++ b/data/bot/mining_templates.bots.toml @@ -0,0 +1,151 @@ +[copper_ore_template] +type = "activity" +capacity = 4 +requires = [ + { skill = "mining", min = 1, max = 15 }, +] +setup = [ + { carries = "steel_pickaxe,iron_pickaxe,bronze_pickaxe" }, + { location = "$location" }, + { inventory_space = 27 }, +] +actions = [ + { option = "Mine", object = "copper_rocks*", delay = 5, success = { inventory_space = 0 } }, +] +produces = [ + { carries = "copper_ore" }, + { skill = "mining" } +] + +[tin_ore_template] +type = "activity" +capacity = 4 +requires = [ + { skill = "mining", min = 1, max = 15 }, +] +setup = [ + { carries = "steel_pickaxe,iron_pickaxe,bronze_pickaxe" }, + { location = "$location" }, + { inventory_space = 27 }, +] +actions = [ + { option = "Mine", object = "tin_rocks*", delay = 5, success = { inventory_space = 0 } }, +] +produces = [ + { carries = "tin_ore" }, + { skill = "mining" } +] + +[clay_template] +type = "activity" +capacity = 2 +requires = [ + { skill = "mining", min = 1, max = 15 }, +] +setup = [ + { carries = "steel_pickaxe,iron_pickaxe,bronze_pickaxe" }, + { location = "$location" }, + { inventory_space = 27 }, +] +actions = [ + { option = "Mine", object = "clay_rocks*", delay = 5, success = { inventory_space = 0 } }, +] +produces = [ + { carries = "clay" }, + { skill = "mining" } +] + +[iron_ore_template] +type = "activity" +capacity = 2 +requires = [ + { skill = "mining", min = 15, max = 40 }, +] +setup = [ + { carries = "adamant_pickaxe,mithril_pickaxe,steel_pickaxe" }, + { location = "$location" }, + { inventory_space = 27 }, +] +actions = [ + { option = "Mine", object = "iron_rocks*", delay = 5, success = { inventory_space = 0 } }, +] +produces = [ + { carries = "iron_ore" }, + { skill = "mining" } +] + +[silver_ore_template] +type = "activity" +capacity = 2 +requires = [ + { skill = "mining", min = 20, max = 40 }, +] +setup = [ + { carries = "adamant_pickaxe,mithril_pickaxe,steel_pickaxe" }, + { location = "$location" }, + { inventory_space = 27 }, +] +actions = [ + { option = "Mine", object = "silver_rocks*", delay = 5, success = { inventory_space = 0 } }, +] +produces = [ + { carries = "silver_ore" }, + { skill = "mining" } +] + +[coal_template] +type = "activity" +capacity = 4 +requires = [ + { skill = "mining", min = 30, max = 55 }, +] +setup = [ + { carries = "rune_pickaxe,adamant_pickaxe,mithril_pickaxe" }, + { location = "$location" }, + { inventory_space = 27 }, +] +actions = [ + { option = "Mine", object = "coal_rocks*", delay = 10, success = { inventory_space = 0 } }, +] +produces = [ + { carries = "coal" }, + { skill = "mining" } +] + +[gold_ore_template] +type = "activity" +capacity = 3 +requires = [ + { skill = "mining", min = 40, max = 60 }, +] +setup = [ + { carries = "rune_pickaxe,adamant_pickaxe,mithril_pickaxe" }, + { location = "$location" }, + { inventory_space = 27 }, +] +actions = [ + { option = "Mine", object = "gold_rocks*", delay = 15, success = { inventory_space = 0 } }, +] +produces = [ + { carries = "gold_ore" }, + { skill = "mining" } +] + +[mithril_ore_template] +type = "activity" +capacity = 2 +requires = [ + { skill = "mining", min = 55, max = 70 }, +] +setup = [ + { carries = "dragon_pickaxe,rune_pickaxe,adamant_pickaxe" }, + { location = "$location" }, + { inventory_space = 27 }, +] +actions = [ + { option = "Mine", object = "mithril_rocks*", delay = 15, success = { inventory_space = 0 } }, +] +produces = [ + { carries = "mithril_ore" }, + { skill = "mining" } +] diff --git a/data/bot/woodcutting.bots.toml b/data/bot/woodcutting_templates.bots.toml similarity index 85% rename from data/bot/woodcutting.bots.toml rename to data/bot/woodcutting_templates.bots.toml index c55c2f60c7..808582e536 100644 --- a/data/bot/woodcutting.bots.toml +++ b/data/bot/woodcutting_templates.bots.toml @@ -1,17 +1,3 @@ -[woodcutting_template] -type = "activity" -setup = [ - { carries = "$hatchet" }, - { location = "$location" }, - { inventory_space = 27 }, -] -actions = [ - { option = "Chop down", object = "$tree", delay = 5, success = { inventory_space = 0 } }, -] -produces = [ - { skill = "woodcutting" } -] - [normal_tree_template] type = "activity" capacity = 4 diff --git a/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt b/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt index c120beb178..b339cce911 100644 --- a/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt +++ b/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt @@ -144,6 +144,9 @@ data class BehaviourFragment( val id = resolve(references["location"], req.id) Condition.Area(Fact.PlayerTile, id) } + "combat_level" -> { + Condition.AtLeast(Fact.CombatLevel, resolve(references["combat_level"], req.min) ?: 1) + } else -> null } } diff --git a/game/src/main/kotlin/content/bot/action/BotActivity.kt b/game/src/main/kotlin/content/bot/action/BotActivity.kt index 6307a6169c..e65caf79bf 100644 --- a/game/src/main/kotlin/content/bot/action/BotActivity.kt +++ b/game/src/main/kotlin/content/bot/action/BotActivity.kt @@ -6,6 +6,7 @@ import world.gregs.config.Config import world.gregs.config.ConfigReader import world.gregs.voidps.engine.event.Wildcard import world.gregs.voidps.engine.timedLoad +import kotlin.math.min /** * An activity with a limited number of slots that bots can perform @@ -211,7 +212,7 @@ private fun ConfigReader.requirement(exact: Boolean = false): Condition { is String if value.contains('$') -> references[key] = value else -> throw IllegalArgumentException("Invalid '$key' value: $value ${exception()}") } - "inventory_space" -> { + "combat_level", "inventory_space" -> { type = key when (val value = value()) { is Int -> min = value @@ -262,6 +263,7 @@ private fun getRequirement(type: String, id: String, min: Int?, max: Int?, value "clone" -> Condition.Clone(id) "inventory_space" -> if (exact && min != null) Condition.Equals(Fact.InventorySpace, min) else Condition.range(Fact.InventorySpace, min, max) "location" -> Condition.Area(Fact.PlayerTile, id) + "combat_level" -> Condition.AtLeast(Fact.CombatLevel, min ?: 1) else -> null } diff --git a/game/src/main/kotlin/content/bot/fact/Fact.kt b/game/src/main/kotlin/content/bot/fact/Fact.kt index 3dac3e4071..1fb2f45936 100644 --- a/game/src/main/kotlin/content/bot/fact/Fact.kt +++ b/game/src/main/kotlin/content/bot/fact/Fact.kt @@ -4,6 +4,7 @@ import content.entity.player.bank.bank import world.gregs.voidps.engine.GameLoop import world.gregs.voidps.engine.client.variable.remaining import world.gregs.voidps.engine.entity.character.player.Player +import world.gregs.voidps.engine.entity.character.player.combatLevel import world.gregs.voidps.engine.entity.character.player.skill.Skill import world.gregs.voidps.engine.inv.equipment import world.gregs.voidps.engine.inv.inventory @@ -90,6 +91,11 @@ sealed class Fact(val priority: Int) { override fun getValue(player: Player) = player.tile } + object CombatLevel : Fact(1) { + override fun keys() = setOf("combat") + override fun getValue(player: Player) = player.combatLevel + } + object AttackLevel : SkillLevel(Skill.Attack) object DefenceLevel : SkillLevel(Skill.Defence) object StrengthLevel : SkillLevel(Skill.Strength) From 0832cae4a45dd2a3e641ff538097f03b7f54968e Mon Sep 17 00:00:00 2001 From: GregHib Date: Wed, 4 Feb 2026 19:54:59 +0000 Subject: [PATCH 042/101] Fix lumbridge swamp lost city npc spawns --- .../lumbridge/swamp/lumbridge_swamp.npc-spawns.toml | 8 ++++---- .../misthalin/lumbridge/swamp/lumbridge_swamp.npcs.toml | 4 ++++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/data/area/misthalin/lumbridge/swamp/lumbridge_swamp.npc-spawns.toml b/data/area/misthalin/lumbridge/swamp/lumbridge_swamp.npc-spawns.toml index 2ac3e386e6..8db96feb6a 100644 --- a/data/area/misthalin/lumbridge/swamp/lumbridge_swamp.npc-spawns.toml +++ b/data/area/misthalin/lumbridge/swamp/lumbridge_swamp.npc-spawns.toml @@ -39,10 +39,10 @@ spawns = [ { id = "giant_spider", x = 3187, y = 3184 }, { id = "giant_spider", x = 3193, y = 3193 }, { id = "giant_spider", x = 3195, y = 3187 }, - { id = "archer_lumbridge", x = 3183, y = 3147 }, - { id = "warrior_lumbridge", x = 3186, y = 3145 }, - { id = "monk_lumbridge", x = 3185, y = 3146 }, - { id = "wizard_lumbridge", x = 3187, y = 3147 }, + { id = "archer_lumbridge", x = 3148, y = 3208 }, + { id = "warrior_lumbridge", x = 3150, y = 3208 }, + { id = "monk_lumbridge", x = 3152, y = 3204 }, + { id = "wizard_lumbridge", x = 3149, y = 3204 }, { id = "giant_rat_dark", x = 3201, y = 3174, members = true }, { id = "giant_rat_dark", x = 3227, y = 3193, members = true }, { id = "giant_rat_dark", x = 3232, y = 3170, members = true }, diff --git a/data/area/misthalin/lumbridge/swamp/lumbridge_swamp.npcs.toml b/data/area/misthalin/lumbridge/swamp/lumbridge_swamp.npcs.toml index 346bc68b66..c91940ae57 100644 --- a/data/area/misthalin/lumbridge/swamp/lumbridge_swamp.npcs.toml +++ b/data/area/misthalin/lumbridge/swamp/lumbridge_swamp.npcs.toml @@ -18,18 +18,22 @@ id = 5890 [archer_lumbridge] id = 649 +wander_range = 4 examine = "She looks quite experienced." [warrior_lumbridge] id = 650 +wander_range = 4 examine = "He looks big and dumb." [monk_lumbridge] id = 651 +wander_range = 4 examine = "He looks holy." [wizard_lumbridge] id = 652 +wander_range = 4 examine = "He looks kind of puny..." [sergeant_mossfists_2] From 2bd3a9da0fd3cccee6aa055c608c1fa2ac793705 Mon Sep 17 00:00:00 2001 From: GregHib Date: Wed, 4 Feb 2026 20:11:20 +0000 Subject: [PATCH 043/101] Re-weight buying vs banking --- .../al_kharid/al_kharid.bots.toml | 52 ++++++++++++------- .../misthalin/lumbridge/lumbridge.bots.toml | 12 ++--- .../src/main/kotlin/content/bot/BotManager.kt | 14 ++--- .../kotlin/content/bot/action/BotAction.kt | 35 ++++++++----- 4 files changed, 68 insertions(+), 45 deletions(-) diff --git a/data/area/kharidian_desert/al_kharid/al_kharid.bots.toml b/data/area/kharidian_desert/al_kharid/al_kharid.bots.toml index 2dd950da40..67da9d5fb7 100644 --- a/data/area/kharidian_desert/al_kharid/al_kharid.bots.toml +++ b/data/area/kharidian_desert/al_kharid/al_kharid.bots.toml @@ -2,55 +2,67 @@ [al_kharid_iron_mining] template = "iron_ore_template" capacity = 2 -requires = [ - { combat_level = 10 } -] fields = { location = "al_kharid_mine" } [al_kharid_coal_mining] template = "coal_template" capacity = 1 -requires = [ - { combat_level = 10 } -] fields = { location = "al_kharid_mine" } [al_kharid_silver_mining] template = "silver_ore_template" capacity = 2 -requires = [ - { combat_level = 10 } -] fields = { location = "al_kharid_mine" } [al_kharid_north_copper_mining] template = "copper_ore_template" capacity = 1 -requires = [ - { combat_level = 10 } -] fields = { location = "al_kharid_mine_north" } [al_kharid_north_silver_mining] template = "silver_ore_template" capacity = 1 -requires = [ - { combat_level = 10 } -] fields = { location = "al_kharid_mine_north" } [al_kharid_north_iron_mining] template = "iron_ore_template" capacity = 2 -requires = [ - { combat_level = 10 } -] fields = { location = "al_kharid_mine_north" } [al_kharid_north_mithril_mining] template = "mithril_ore_template" capacity = 2 +fields = { location = "al_kharid_mine_north" } + +# Zekes Superior Scimitars +[buy_bronze_scimitar] +type = "resolver" +template = "buy_from_shop" +fields = { cost = 32, shop_location = "zekes_scimitar_shop", shopkeeper = "zeke", item = "bronze_scimitar" } requires = [ - { combat_level = 10 } + { skill = "attack", min = 1 }, +] + +[buy_iron_scimitar] +type = "resolver" +template = "buy_from_shop" +fields = { cost = 112, shop_location = "zekes_scimitar_shop", shopkeeper = "zeke", item = "iron_scimitar" } +requires = [ + { skill = "attack", min = 1 }, +] + +[buy_steel_scimitar] +type = "resolver" +template = "buy_from_shop" +fields = { cost = 400, shop_location = "zekes_scimitar_shop", shopkeeper = "zeke", item = "steel_scimitar" } +requires = [ + { skill = "attack", min = 5 }, +] + +[buy_mithril_scimitar] +type = "resolver" +template = "buy_from_shop" +fields = { cost = 1040, shop_location = "zekes_scimitar_shop", shopkeeper = "zeke", item = "mithril_scimitar" } +requires = [ + { skill = "attack", min = 20 }, ] -fields = { location = "al_kharid_mine_north" } diff --git a/data/area/misthalin/lumbridge/lumbridge.bots.toml b/data/area/misthalin/lumbridge/lumbridge.bots.toml index 50c01ea1ed..53c86b9daa 100644 --- a/data/area/misthalin/lumbridge/lumbridge.bots.toml +++ b/data/area/misthalin/lumbridge/lumbridge.bots.toml @@ -48,19 +48,19 @@ fields = { location = "lumbridge_swamp_west_mithril_mine" } [take_bronze_hatchet_bobs] type = "resolver" -weight = 5 +weight = 30 template = "take_shop_sample" fields = { shop_location = "bobs_axe_shop", shopkeeper = "bob", item = "bronze_hatchet" } [take_bronze_pickaxe_bobs] type = "resolver" -weight = 5 +weight = 30 template = "take_shop_sample" fields = { shop_location = "bobs_axe_shop", shopkeeper = "bob", item = "bronze_pickaxe" } [buy_bronze_pickaxe] type = "resolver" -weight = 10 +weight = 35 template = "buy_from_shop" fields = { cost = 1, shop_location = "bobs_axe_shop", shopkeeper = "bob", item = "bronze_pickaxe" } requires = [ @@ -69,7 +69,7 @@ requires = [ [buy_bronze_hatchet] type = "resolver" -weight = 10 +weight = 35 template = "buy_from_shop" fields = { cost = 16, shop_location = "bobs_axe_shop", shopkeeper = "bob", item = "bronze_hatchet" } requires = [ @@ -78,7 +78,7 @@ requires = [ [buy_iron_hatchet] type = "resolver" -weight = 10 +weight = 45 template = "buy_from_shop" fields = { cost = 56, shop_location = "bobs_axe_shop", shopkeeper = "bob", item = "iron_hatchet" } requires = [ @@ -87,7 +87,7 @@ requires = [ [buy_steel_hatchet] type = "resolver" -weight = 10 +weight = 50 template = "buy_from_shop" fields = { cost = 200, shop_location = "bobs_axe_shop", shopkeeper = "bob", item = "steel_hatchet" } requires = [ diff --git a/game/src/main/kotlin/content/bot/BotManager.kt b/game/src/main/kotlin/content/bot/BotManager.kt index cf2158e804..7983309e0b 100644 --- a/game/src/main/kotlin/content/bot/BotManager.kt +++ b/game/src/main/kotlin/content/bot/BotManager.kt @@ -108,7 +108,7 @@ class BotManager( val id = bot.available.filter { val activity = activities[it] activity != null && hasRequirements(bot, activity) - }.randomOrNull(random) + }.randomOrNull(random) // TODO weight by distance? if (id == null) { if (bot.player["debug", false]) { logger.info { "Failed to find activity for bot ${bot.player.accountName}. Reasons:" } @@ -218,7 +218,7 @@ class BotManager( if (condition.min == 1 || condition.min == 5 || condition.min == 10) { resolvers.add( Resolver( - "withdraw_${condition.fact.id}", 20, actions = listOf( + "withdraw_${condition.fact.id}", weight = 20, actions = listOf( BotAction.GoToNearest("bank"), BotAction.InteractObject("Use-quickly", "bank_booth*", success = Condition.Equals(Fact.InterfaceOpen("bank"), true)), BotAction.InterfaceOption("Withdraw-${condition.min}", "bank:inventory:${condition.fact.id}"), @@ -228,7 +228,7 @@ class BotManager( } else { resolvers.add( Resolver( - "withdraw_${condition.fact.id}", 20, actions = listOf( + "withdraw_${condition.fact.id}", weight = 20, actions = listOf( BotAction.GoToNearest("bank"), BotAction.InteractObject("Use-quickly", "bank_booth*", success = Condition.Equals(Fact.InterfaceOpen("bank"), true)), BotAction.InterfaceOption("Withdraw-X", "bank:inventory:${condition.fact.id}"), @@ -245,7 +245,7 @@ class BotManager( val frame = bot.frame() val behaviour = frame.behaviour if (bot.player["debug", false]) { - logger.info { "Bot task: ${behaviour.id} state: ${frame.state} action: ${frame.action()}." } + logger.trace { "Bot task: ${behaviour.id} state: ${frame.state} action: ${frame.action()}." } } when (val state = frame.state) { BehaviourState.Running -> frame.update(bot) @@ -253,7 +253,7 @@ class BotManager( BehaviourState.Success -> { val debug = bot.player["debug", false] if (debug) { - logger.info { "Completed action: ${frame.action()} for ${behaviour.id}." } + logger.trace { "Completed action: ${frame.action()} for ${behaviour.id}." } } if (!frame.next()) { AuditLog.event(bot, "completed", frame.behaviour.id) @@ -264,7 +264,7 @@ class BotManager( } } else { if (debug) { - logger.info { "Next action: ${frame.action()} for ${behaviour.id}." } + logger.trace { "Next action: ${frame.action()} for ${behaviour.id}." } } frame.start(bot) } @@ -272,7 +272,7 @@ class BotManager( is BehaviourState.Failed -> { val action = frame.action() if (bot.player["debug", false]) { - logger.info { "Failed action: ${action::class.simpleName} for ${behaviour.id}, reason: ${state.reason}." } + logger.warn { "Failed action: ${action::class.simpleName} for ${behaviour.id}, reason: ${state.reason}." } } AuditLog.event(bot, "failed", behaviour.id, state.reason, frame.index, action::class.simpleName) if (state.reason is HardReason) { diff --git a/game/src/main/kotlin/content/bot/action/BotAction.kt b/game/src/main/kotlin/content/bot/action/BotAction.kt index ccab0b2c2b..8aee153444 100644 --- a/game/src/main/kotlin/content/bot/action/BotAction.kt +++ b/game/src/main/kotlin/content/bot/action/BotAction.kt @@ -15,6 +15,7 @@ import world.gregs.voidps.engine.entity.character.mode.EmptyMode import world.gregs.voidps.engine.entity.character.mode.interact.PlayerOnFloorItemInteract import world.gregs.voidps.engine.entity.character.mode.interact.PlayerOnNPCInteract import world.gregs.voidps.engine.entity.character.mode.interact.PlayerOnObjectInteract +import world.gregs.voidps.engine.entity.character.mode.move.Movement import world.gregs.voidps.engine.entity.character.npc.NPC import world.gregs.voidps.engine.entity.character.npc.NPCs import world.gregs.voidps.engine.entity.character.player.skill.Skill @@ -55,6 +56,18 @@ sealed interface BotAction { if (bot.tile in def.area) { return BehaviourState.Success } + return BehaviourState.Running + } + + override fun update(bot: Bot, frame: BehaviourFrame): BehaviourState { + if (bot.steps.isNotEmpty()) { + return BehaviourState.Running + } + val def = Areas.getOrNull(target) ?: return BehaviourState.Failed(Reason.Invalid("No areas found with id '$target'.")) + if (bot.tile in def.area) { + return BehaviourState.Success + } + val manager = get() val list = mutableListOf() val graph = manager.graph @@ -62,11 +75,6 @@ sealed interface BotAction { return queueRoute(success, list, graph, bot, target) } - override fun update(bot: Bot, frame: BehaviourFrame): BehaviourState? { - val def = Areas.getOrNull(target) ?: return BehaviourState.Failed(Reason.Invalid("No areas found with id '$target'.")) - return if (bot.tile in def.area) BehaviourState.Success else null - } - companion object { internal fun queueRoute(success: Boolean, list: MutableList, graph: Graph, bot: Bot, target: String): BehaviourState { if (!success) { @@ -105,14 +113,13 @@ sealed interface BotAction { if (set.any { bot.tile in it.area }) { return BehaviourState.Success } - val manager = get() - val list = mutableListOf() - val graph = manager.graph - val success = graph.findNearest(bot.player, list, tag) - return GoTo.queueRoute(success, list, graph, bot, tag) + return BehaviourState.Running } - override fun update(bot: Bot, frame: BehaviourFrame): BehaviourState? { + override fun update(bot: Bot, frame: BehaviourFrame): BehaviourState { + if (bot.steps.isNotEmpty()) { + return BehaviourState.Running + } val set = Areas.tagged(tag) if (set.isEmpty()) { return BehaviourState.Failed(Reason.Invalid("No areas tagged with tag '$tag'.")) @@ -120,7 +127,11 @@ sealed interface BotAction { if (set.any { bot.tile in it.area }) { return BehaviourState.Success } - return null + val manager = get() + val list = mutableListOf() + val graph = manager.graph + val success = graph.findNearest(bot.player, list, tag) + return GoTo.queueRoute(success, list, graph, bot, tag) } } From fda261951854347dfa74378fcde22e7ece1e0947 Mon Sep 17 00:00:00 2001 From: GregHib Date: Wed, 4 Feb 2026 21:17:46 +0000 Subject: [PATCH 044/101] Fix traversing graphs with zero weight --- .../kotlin/content/bot/interact/path/Graph.kt | 4 +-- .../content/bot/interact/path/GraphTest.kt | 28 +++++++++++++++---- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/game/src/main/kotlin/content/bot/interact/path/Graph.kt b/game/src/main/kotlin/content/bot/interact/path/Graph.kt index 10b045048d..fa31e61f90 100644 --- a/game/src/main/kotlin/content/bot/interact/path/Graph.kt +++ b/game/src/main/kotlin/content/bot/interact/path/Graph.kt @@ -89,7 +89,7 @@ class Graph( val previousEdge = IntArray(nodeCount) for (start in startingPoints) { - distance[start.index] = 0 + distance[start.index] = -1 queue.add(start) } while (queue.isNotEmpty()) { @@ -97,7 +97,7 @@ class Graph( if (target(node)) { // Reconstruct the path var previous = node - while (distance[previous] != 0) { + while (distance[previous] != -1) { output.add(0, previousEdge[previous]) previous = previousNode[previous] } diff --git a/game/src/test/kotlin/content/bot/interact/path/GraphTest.kt b/game/src/test/kotlin/content/bot/interact/path/GraphTest.kt index 79c825b6dc..4f48a6b9c7 100644 --- a/game/src/test/kotlin/content/bot/interact/path/GraphTest.kt +++ b/game/src/test/kotlin/content/bot/interact/path/GraphTest.kt @@ -23,22 +23,22 @@ class GraphTest { 10 / | A | 1 1 \ | - E + C */ val builder = Graph.Builder() val a = 0 val b = 1 val c = 2 - builder.addEdge(a, b, 10) - builder.addEdge(a, c, 1) - builder.addEdge(c, b, 1) + val ab = builder.addEdge(a, b, 10) + val ac = builder.addEdge(a, c, 1) + val cb = builder.addEdge(c, b, 1) // builder.print() val output = mutableListOf() val success = builder.build().find(Player(), output, Graph.Node(a), b) assertTrue(success) - assertEquals(listOf(1, 2), output) + assertEquals(listOf(ac, cb), output) } @Test @@ -164,6 +164,24 @@ class GraphTest { assertFalse(success) } + @Test + fun `Single edge path without weights`() { + /* + B + 1 | + A + */ + val builder = Graph.Builder() + val a = 0 + val b = 1 + val ab = builder.addEdge(a, b, 0) +// builder.print() + + val output = mutableListOf() + val success = builder.build().find(Player(), output, Graph.Node(a), b) + assertTrue(success) + assertEquals(listOf(ab), output) + } @Test fun `Multiple starting points`() { From 300dbf7aff3354dc1ef6bc9c57b12f4caaa1a180 Mon Sep 17 00:00:00 2001 From: GregHib Date: Wed, 4 Feb 2026 21:33:53 +0000 Subject: [PATCH 045/101] Fix edge-less traversals --- .../kotlin/content/bot/interact/path/Graph.kt | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/game/src/main/kotlin/content/bot/interact/path/Graph.kt b/game/src/main/kotlin/content/bot/interact/path/Graph.kt index fa31e61f90..5424d7fed8 100644 --- a/game/src/main/kotlin/content/bot/interact/path/Graph.kt +++ b/game/src/main/kotlin/content/bot/interact/path/Graph.kt @@ -83,12 +83,17 @@ class Graph( output.clear() val queue = PriorityQueue() val visited = BooleanArray(nodeCount) - val distance = IntArray(nodeCount) - distance.fill(Int.MAX_VALUE) - val previousNode = IntArray(nodeCount) - val previousEdge = IntArray(nodeCount) + val distance = IntArray(nodeCount) { Int.MAX_VALUE } + val parentNode = IntArray(nodeCount) { -1 } + val previousEdge = IntArray(nodeCount) { -1 } for (start in startingPoints) { + if (target(start.index)) { + // As we're queuing all nearby points we don't want select any starting points which are in + // the target, otherwise we'll end up with no edges to traverse. + // (if this were normal dijkstra we'd produce points not edges and this wouldn't be an issue) + continue + } distance[start.index] = -1 queue.add(start) } @@ -97,9 +102,9 @@ class Graph( if (target(node)) { // Reconstruct the path var previous = node - while (distance[previous] != -1) { + while (parentNode[previous] != -1) { output.add(0, previousEdge[previous]) - previous = previousNode[previous] + previous = parentNode[previous] } return true } @@ -121,7 +126,7 @@ class Graph( continue } distance[to] = cost + weight - previousNode[to] = node + parentNode[to] = node previousEdge[to] = edge queue.add(Node(to, cost + weight)) } From 1077d85e9a820a904f6d1807dad7e2dbe39d75d9 Mon Sep 17 00:00:00 2001 From: GregHib Date: Wed, 4 Feb 2026 22:18:47 +0000 Subject: [PATCH 046/101] Add equipping and combat bot --- .../misthalin/lumbridge/lumbridge.bots.toml | 23 +++++++++++++++++++ data/bot/lumbridge.nav-edges.toml | 8 +++---- .../src/main/kotlin/content/bot/BotManager.kt | 9 +++++++- .../kotlin/content/bot/action/BotAction.kt | 4 ++-- 4 files changed, 37 insertions(+), 7 deletions(-) diff --git a/data/area/misthalin/lumbridge/lumbridge.bots.toml b/data/area/misthalin/lumbridge/lumbridge.bots.toml index 53c86b9daa..6627e2123c 100644 --- a/data/area/misthalin/lumbridge/lumbridge.bots.toml +++ b/data/area/misthalin/lumbridge/lumbridge.bots.toml @@ -1,3 +1,26 @@ +[kill_chickens] +type = "activity" +capacity = 4 +requires = [ + { skill = "attack", min = 1, max = 5 }, +] +setup = [ + { equips = "bronze_sword,bronze_dagger,bronze_scimitar" }, + # TODO food? + { location = "lumbridge_chicken_pen" }, + { inventory_space = 27 }, +] +actions = [ + { option = "Select", interface = "combat_styles:style1" }, + { option = "Attack", npc = "chicken*", delay = 5, success = { inventory_space = 0 } }, +] +produces = [ + { carries = "feather" }, + { carries = "bones" }, + { carries = "raw_chicken" }, + { skill = "attack" } +] + # Woodcutting [lumbridge_north_tree_cutting] template = "normal_tree_template" diff --git a/data/bot/lumbridge.nav-edges.toml b/data/bot/lumbridge.nav-edges.toml index 75b1e72fe1..09847f0868 100644 --- a/data/bot/lumbridge.nav-edges.toml +++ b/data/bot/lumbridge.nav-edges.toml @@ -31,7 +31,7 @@ edges = [ { from_x = 3229, from_y = 3214, from_level = 1, to_x = 3229, to_y = 3214, to_level = 2, cost = 1, actions = [{ option = "Climb-up", object = "36769", x = 3229, y = 3213 }] }, # lumbridge_south_tower_1st_floor_to_2nd_floor { from_x = 3229, from_y = 3214, from_level = 1, to_x = 3229, to_y = 3214, cost = 1, actions = [{ option = "Climb-down", object = "36769", x = 3229, y = 3213 }] }, # lumbridge_south_tower_1st_floor_to_ground_floor { from_x = 3229, from_y = 3214, from_level = 2, to_x = 3229, to_y = 3214, to_level = 1, cost = 1, actions = [{ option = "Climb-down", object = "36770", x = 3229, y = 3213 }] }, # lumbridge_south_tower_2nd_floor_to_1st_floor - { from_x = 3236, from_y = 3220, to_x = 3236, to_y = 3225 }, # lumbridge_gate_north_to_bridge_west + { from_x = 3236, from_y = 3219, to_x = 3236, to_y = 3225 }, # lumbridge_gate_north_to_bridge_west { from_x = 3236, from_y = 3225, to_x = 3230, to_y = 3232 }, # lumbridge_bridge_west_to_unstable_house { from_x = 3236, from_y = 3225, to_x = 3253, to_y = 3225 }, # lumbridge_bridge_west_to_bridge_east { from_x = 3253, from_y = 3225, to_x = 3263, to_y = 3222 }, # lumbridge_bridge_east_to_trees_east @@ -77,10 +77,10 @@ edges = [ { from_x = 3136, from_y = 3219, to_x = 3138, to_y = 3227 }, # lumbridge_swamp_west_wall_to_draynor_east_path { from_x = 3136, from_y = 3219, to_x = 3119, to_y = 3228 }, # lumbridge_swamp_west_wall_to_draynor_jail_path_south { from_x = 3222, from_y = 3218, to_x = 3226, to_y = 3214 }, # lumbridge_courtyard_south_to_tower_door - { from_x = 3222, from_y = 3218, to_x = 3222, to_y = 3220 }, # lumbridge_courtyard_south_to_north + { from_x = 3222, from_y = 3218, to_x = 3222, to_y = 3219 }, # lumbridge_courtyard_south_to_north { from_x = 3222, from_y = 3218, to_x = 3236, to_y = 3218 }, # lumbridge_courtyard_south_to_gate_north - { from_x = 3222, from_y = 3220, to_x = 3236, to_y = 3220 }, # lumbridge_courtyard_north_to_gate_north - { from_x = 3236, from_y = 3218, to_x = 3236, to_y = 3220 }, # lumbridge_courtyard_gate_south_to_gate_north + { from_x = 3222, from_y = 3219, to_x = 3236, to_y = 3219 }, # lumbridge_courtyard_north_to_gate_north + { from_x = 3236, from_y = 3218, to_x = 3236, to_y = 3219 }, # lumbridge_courtyard_gate_south_to_gate_north { from_x = 3222, from_y = 3218, to_x = 3209, to_y = 3205 }, # lumbridge_courtyard_south_to_grounds_south { from_x = 3230, from_y = 3232, to_x = 3222, to_y = 3241 }, # lumbridge_unstable_house_to_general_store_east { from_x = 3222, from_y = 3241, to_x = 3217, to_y = 3241, cost = 6, actions = [{ option = "Open", object = "door_720_closed", x = 3219, y = 3241 }, { x = 3217, y = 3241 }] }, # lumbridge_general_store_east_to_general_store diff --git a/game/src/main/kotlin/content/bot/BotManager.kt b/game/src/main/kotlin/content/bot/BotManager.kt index 7983309e0b..465810bbb1 100644 --- a/game/src/main/kotlin/content/bot/BotManager.kt +++ b/game/src/main/kotlin/content/bot/BotManager.kt @@ -237,8 +237,15 @@ class BotManager( ) ) } + } else if (condition is Condition.AtLeast && condition.fact is Fact.EquipCount && bot.player.inventory.contains(condition.fact.id, condition.min)) { + resolvers.add( + Resolver( + "equip_${condition.fact.id}", weight = 0, + resolve = listOf(Condition.AtLeast(Fact.InventoryCount(condition.fact.id), condition.min)), + actions = listOf(BotAction.InterfaceOption("Equip", "inventory:inventory:${condition.fact.id}")) + ) + ) } - // TODO: If in inventory and needs equipped -> equip } private fun execute(bot: Bot) { diff --git a/game/src/main/kotlin/content/bot/action/BotAction.kt b/game/src/main/kotlin/content/bot/action/BotAction.kt index 8aee153444..7ce4cfe51c 100644 --- a/game/src/main/kotlin/content/bot/action/BotAction.kt +++ b/game/src/main/kotlin/content/bot/action/BotAction.kt @@ -204,7 +204,7 @@ sealed interface BotAction { } override fun update(bot: Bot, frame: BehaviourFrame) = when { - bot.levels.get(Skill.Constitution) <= bot.levels.getMax(Skill.Constitution) / healPercentage -> eat(bot) + healPercentage > 0 && bot.levels.get(Skill.Constitution) <= bot.levels.getMax(Skill.Constitution) / healPercentage -> eat(bot) success?.check(bot.player) == true -> BehaviourState.Success bot.mode is PlayerOnNPCInteract -> if (success == null) BehaviourState.Success else BehaviourState.Running bot.mode is PlayerOnFloorItemInteract -> BehaviourState.Running @@ -255,7 +255,7 @@ sealed interface BotAction { } val item = loot.randomOrNull(random) if (item != null) { - bot.player.instructions.trySend(InteractFloorItem(item.def.id, item.tile.x, item.tile.y, item.def.floorOptions.indexOf("Pick-up") + 1)) + bot.player.instructions.trySend(InteractFloorItem(item.def.id, item.tile.x, item.tile.y, item.def.floorOptions.indexOf("Take"))) return BehaviourState.Running } val npc = npcs.randomOrNull(random) ?: return handleNoTarget() From a9f63b326ca459e806a7abc300f6220f92a93c10 Mon Sep 17 00:00:00 2001 From: GregHib Date: Wed, 4 Feb 2026 22:24:44 +0000 Subject: [PATCH 047/101] Target nearest applicable entity first --- data/bot/teleport.bots.toml | 4 +- .../kotlin/content/bot/action/BotAction.kt | 69 +++++++------------ 2 files changed, 26 insertions(+), 47 deletions(-) diff --git a/data/bot/teleport.bots.toml b/data/bot/teleport.bots.toml index a08ca2668d..80cb988d0b 100644 --- a/data/bot/teleport.bots.toml +++ b/data/bot/teleport.bots.toml @@ -27,7 +27,7 @@ [teleport_varrock] type = "shortcut" -weight = 75 +weight = 125 requires = [ { variable = "spellbook_config", value = 0, default = 0 }, { carries = "fire_rune", amount = 1 }, @@ -44,7 +44,7 @@ produces = [ [teleport_varrock_via_bank] type = "shortcut" -weight = 100 +weight = 150 requires = [ { variable = "spellbook_config", value = 0, default = 0 }, { owns = "fire_rune", amount = 1 }, diff --git a/game/src/main/kotlin/content/bot/action/BotAction.kt b/game/src/main/kotlin/content/bot/action/BotAction.kt index 7ce4cfe51c..90f7fd44df 100644 --- a/game/src/main/kotlin/content/bot/action/BotAction.kt +++ b/game/src/main/kotlin/content/bot/action/BotAction.kt @@ -156,26 +156,21 @@ sealed interface BotAction { } private fun search(bot: Bot): BehaviourState { - val npcs = mutableListOf() val player = bot.player for (tile in Spiral.spiral(player.tile, radius)) { for (npc in NPCs.at(tile)) { if (!wildcardEquals(id, npc.id)) { continue } - if (!npc.def(player).options.contains(option)) { + val index = npc.def(player).options.indexOf(option) + if (index == -1) { continue } - npcs.add(npc) + bot.player.instructions.trySend(InteractNPC(npc.index, index + 1)) + return BehaviourState.Running } } - val npc = npcs.randomOrNull(random) ?: return handleNoTarget() - val index = npc.def(player).options.indexOf(option) - if (index == -1) { - return BehaviourState.Failed(Reason.NoTarget) - } - bot.player.instructions.trySend(InteractNPC(npc.index, index + 1)) - return BehaviourState.Running + return handleNoTarget() } private fun handleNoTarget(): BehaviourState { @@ -227,44 +222,34 @@ sealed interface BotAction { } private fun search(bot: Bot): BehaviourState { - val npcs = mutableListOf() - val loot = mutableListOf() val player = bot.player for (tile in Spiral.spiral(player.tile, radius)) { - for (npc in NPCs.at(tile)) { - if (!wildcardEquals(id, npc.id)) { + for (item in FloorItems.at(tile)) { + if (item.owner != player.accountName) { continue } - if (!npc.def(player).options.contains("Attack")) { + if (item.def.cost <= lootOverValue) { continue } - if (npc.attackers.isNotEmpty() && !npc.attackers.contains(player)) { + bot.player.instructions.trySend(InteractFloorItem(item.def.id, item.tile.x, item.tile.y, item.def.floorOptions.indexOf("Take"))) + return BehaviourState.Running + } + for (npc in NPCs.at(tile)) { + if (!wildcardEquals(id, npc.id)) { continue } - npcs.add(npc) - } - for (item in FloorItems.at(tile)) { - if (item.owner != player.accountName) { + val index = npc.def(player).options.indexOf("Attack") + if (index == -1) { continue } - if (item.def.cost <= lootOverValue) { + if (npc.attackers.isNotEmpty() && !npc.attackers.contains(player)) { continue } - loot.add(item) + bot.player.instructions.trySend(InteractNPC(npc.index, index + 1)) + return BehaviourState.Running } } - val item = loot.randomOrNull(random) - if (item != null) { - bot.player.instructions.trySend(InteractFloorItem(item.def.id, item.tile.x, item.tile.y, item.def.floorOptions.indexOf("Take"))) - return BehaviourState.Running - } - val npc = npcs.randomOrNull(random) ?: return handleNoTarget() - val index = npc.def(player).options.indexOf("Attack") - if (index == -1) { - return BehaviourState.Failed(Reason.NoTarget) - } - bot.player.instructions.trySend(InteractNPC(npc.index, index + 1)) - return BehaviourState.Running + return handleNoTarget() } private fun handleNoTarget(): BehaviourState { @@ -299,27 +284,21 @@ sealed interface BotAction { } private fun search(bot: Bot): BehaviourState { - val objects = mutableListOf() val player = bot.player for (tile in Spiral.spiral(player.tile, radius)) { for (obj in GameObjects.at(tile)) { if (!wildcardEquals(id, obj.id)) { continue } - val options = obj.def(player).options - if (options == null || !options.contains(option)) { + val index = obj.def(player).options?.indexOf(option) + if (index == null || index == -1) { continue } - objects.add(obj) + bot.player.instructions.trySend(InteractObject(obj.intId, obj.x, obj.y, index + 1)) + return BehaviourState.Running } } - val obj = objects.randomOrNull(random) ?: return handleNoTarget() - val index = obj.def(bot.player).options?.indexOf(option) ?: return BehaviourState.Failed(Reason.NoTarget) - if (index == -1) { - return BehaviourState.Failed(Reason.NoTarget) - } - bot.player.instructions.trySend(InteractObject(obj.intId, obj.x, obj.y, index + 1)) - return BehaviourState.Running + return handleNoTarget() } private fun handleNoTarget(): BehaviourState { From 0fab8a46799c14b04d146e7bf5ba3d9568f22348 Mon Sep 17 00:00:00 2001 From: GregHib Date: Wed, 4 Feb 2026 22:55:55 +0000 Subject: [PATCH 048/101] Tweak resolvers and combat --- data/bot/bank.bots.toml | 13 +++++++++++- .../src/main/kotlin/content/bot/BotManager.kt | 21 ++++++++++++++++--- .../content/bot/action/BehaviourFragment.kt | 5 +++++ .../kotlin/content/bot/action/BotAction.kt | 11 ++-------- .../kotlin/content/bot/action/BotActivity.kt | 1 + game/src/main/kotlin/content/bot/fact/Fact.kt | 6 ++++++ 6 files changed, 44 insertions(+), 13 deletions(-) diff --git a/data/bot/bank.bots.toml b/data/bot/bank.bots.toml index 37adbbc796..48d02e77a6 100644 --- a/data/bot/bank.bots.toml +++ b/data/bot/bank.bots.toml @@ -1,4 +1,4 @@ -[deposit_all_bank] +[deposit_carried_items] type = "resolver" actions = [ { go_to_nearest = "bank" }, @@ -8,3 +8,14 @@ actions = [ produces = [ { inventory_space = 28 } ] + +#[deposit_worn_items] +#type = "resolver" +#actions = [ +# { go_to_nearest = "bank" }, +# { option = "Use-quickly", object = "bank_booth*", success = { interface = "bank" } }, +# { option = "Deposit worn items", interface = "bank:worn", success = { equipment_space = 28 } }, +#] +#produces = [ +# { equipment_space = 28 } +#] diff --git a/game/src/main/kotlin/content/bot/BotManager.kt b/game/src/main/kotlin/content/bot/BotManager.kt index 465810bbb1..3a1bf4b74d 100644 --- a/game/src/main/kotlin/content/bot/BotManager.kt +++ b/game/src/main/kotlin/content/bot/BotManager.kt @@ -197,6 +197,7 @@ class BotManager( addDefaultResolvers(bot, options, condition) } } + options.removeAll { frame.blocked.contains(it.id) || it.requires.any { fact -> !fact.check(bot.player) } } for (key in condition.keys()) { for (resolver in resolvers[key] ?: emptyList()) { if (frame.blocked.contains(resolver.id)) { @@ -237,22 +238,36 @@ class BotManager( ) ) } - } else if (condition is Condition.AtLeast && condition.fact is Fact.EquipCount && bot.player.inventory.contains(condition.fact.id, condition.min)) { + } else if (condition is Condition.AtLeast && condition.fact is Fact.EquipCount) { resolvers.add( Resolver( "equip_${condition.fact.id}", weight = 0, - resolve = listOf(Condition.AtLeast(Fact.InventoryCount(condition.fact.id), condition.min)), + requires = listOf(Condition.AtLeast(Fact.InventoryCount(condition.fact.id), condition.min)), actions = listOf(BotAction.InterfaceOption("Equip", "inventory:inventory:${condition.fact.id}")) ) ) + resolvers.add( + Resolver( + "withdraw_and_equip_${condition.fact.id}", weight = 0, + requires = listOf(Condition.AtLeast(Fact.BankCount(condition.fact.id), condition.min)), + actions = listOf( + BotAction.GoToNearest("bank"), + BotAction.InteractObject("Use-quickly", "bank_booth*", success = Condition.Equals(Fact.InterfaceOpen("bank"), true)), + BotAction.InterfaceOption("Withdraw-X", "bank:inventory:${condition.fact.id}"), + BotAction.IntEntry(condition.min), + BotAction.InterfaceOption("Equip", "inventory:inventory:${condition.fact.id}") + ) + ) + ) } } private fun execute(bot: Bot) { val frame = bot.frame() val behaviour = frame.behaviour - if (bot.player["debug", false]) { + if (bot.player["debug", false] && frame.state != bot.get("previous_state")) { logger.trace { "Bot task: ${behaviour.id} state: ${frame.state} action: ${frame.action()}." } + bot["previous_state"] = frame.state } when (val state = frame.state) { BehaviourState.Running -> frame.update(bot) diff --git a/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt b/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt index b339cce911..6651696304 100644 --- a/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt +++ b/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt @@ -111,6 +111,11 @@ data class BehaviourFragment( val min = resolve(references["amount"], req.min) Condition.split(id, min, max, Wildcard.Item) { Fact.ItemCount(it) } } + "banked" -> { + val id = resolve(references[req.type], req.id) + val min = resolve(references["amount"], req.min) + Condition.split(id, min, max, Wildcard.Item) { Fact.BankCount(it) } + } "clock" -> { val id = resolve(references[req.type], req.id) Condition.split(id, min, max, Wildcard.Variables) { Fact.ClockRemaining(it) } diff --git a/game/src/main/kotlin/content/bot/action/BotAction.kt b/game/src/main/kotlin/content/bot/action/BotAction.kt index 90f7fd44df..df3991b81d 100644 --- a/game/src/main/kotlin/content/bot/action/BotAction.kt +++ b/game/src/main/kotlin/content/bot/action/BotAction.kt @@ -5,7 +5,7 @@ import content.bot.BotManager import content.bot.fact.Condition import content.bot.interact.path.Graph import content.entity.combat.attackers -import content.entity.player.bank.bank +import content.entity.combat.dead import world.gregs.voidps.engine.GameLoop import world.gregs.voidps.engine.client.instruction.InterfaceHandler import world.gregs.voidps.engine.data.definition.Areas @@ -15,22 +15,15 @@ import world.gregs.voidps.engine.entity.character.mode.EmptyMode import world.gregs.voidps.engine.entity.character.mode.interact.PlayerOnFloorItemInteract import world.gregs.voidps.engine.entity.character.mode.interact.PlayerOnNPCInteract import world.gregs.voidps.engine.entity.character.mode.interact.PlayerOnObjectInteract -import world.gregs.voidps.engine.entity.character.mode.move.Movement -import world.gregs.voidps.engine.entity.character.npc.NPC import world.gregs.voidps.engine.entity.character.npc.NPCs import world.gregs.voidps.engine.entity.character.player.skill.Skill -import world.gregs.voidps.engine.entity.item.floor.FloorItem import world.gregs.voidps.engine.entity.item.floor.FloorItems -import world.gregs.voidps.engine.entity.obj.GameObject import world.gregs.voidps.engine.entity.obj.GameObjects import world.gregs.voidps.engine.event.wildcardEquals import world.gregs.voidps.engine.get -import world.gregs.voidps.engine.inv.equipment import world.gregs.voidps.engine.inv.inventory import world.gregs.voidps.engine.map.Spiral import world.gregs.voidps.network.client.instruction.* -import world.gregs.voidps.type.random -import kotlin.collections.iterator sealed interface BotAction { fun start(bot: Bot, frame: BehaviourFrame): BehaviourState = BehaviourState.Running @@ -242,7 +235,7 @@ sealed interface BotAction { if (index == -1) { continue } - if (npc.attackers.isNotEmpty() && !npc.attackers.contains(player)) { + if (npc.dead || npc.attackers.isNotEmpty() && !npc.attackers.contains(player)) { continue } bot.player.instructions.trySend(InteractNPC(npc.index, index + 1)) diff --git a/game/src/main/kotlin/content/bot/action/BotActivity.kt b/game/src/main/kotlin/content/bot/action/BotActivity.kt index e65caf79bf..d658211769 100644 --- a/game/src/main/kotlin/content/bot/action/BotActivity.kt +++ b/game/src/main/kotlin/content/bot/action/BotActivity.kt @@ -249,6 +249,7 @@ private fun getRequirement(type: String, id: String, min: Int?, max: Int?, value "skill" -> Condition.range(Fact.SkillLevel.of(id), min, max) "carries" -> Condition.split(id, min, max, Wildcard.Item) { Fact.InventoryCount(it) } "owns" -> Condition.split(id, min, max, Wildcard.Item) { Fact.ItemCount(it) } + "banked" -> Condition.split(id, min, max, Wildcard.Item) { Fact.BankCount(it) } "equips" -> Condition.split(id, min, max, Wildcard.Item) { Fact.EquipCount(it) } "clock" -> Condition.split(id, min, max, Wildcard.Variables) { Fact.ClockRemaining(it) } "timer" -> Condition.Equals(Fact.HasTimer(id), value as? Boolean ?: true) diff --git a/game/src/main/kotlin/content/bot/fact/Fact.kt b/game/src/main/kotlin/content/bot/fact/Fact.kt index 1fb2f45936..531d316702 100644 --- a/game/src/main/kotlin/content/bot/fact/Fact.kt +++ b/game/src/main/kotlin/content/bot/fact/Fact.kt @@ -45,6 +45,12 @@ sealed class Fact(val priority: Int) { override fun getValue(player: Player) = player.inventory.count(id) + player.bank.count(id) + player.equipment.count(id) } + data class BankCount(val id: String) : Fact(100) { + override fun keys() = setOf("bank:$id") + override fun groups() = setOf("inv:bank") + override fun getValue(player: Player) = player.bank.count(id) + } + data class EquipCount(val id: String) : Fact(100) { override fun keys() = setOf("worn_equipment:$id") override fun groups() = setOf("inv:worn_equipment") From 6b931f654409da1c9ed4203c78a8a550cdb4ec33 Mon Sep 17 00:00:00 2001 From: GregHib Date: Thu, 5 Feb 2026 11:55:06 +0000 Subject: [PATCH 049/101] Add boolean return to validators for immediate feedback --- .../client/instruction/InstructionHandler.kt | 2 +- .../client/instruction/InstructionHandlers.kt | 36 ++++++++++--------- .../handle/DialogueContinueHandler.kt | 7 ++-- .../handle/DialogueContinueKeyHandler.kt | 9 +++-- .../handle/DialogueItemContinueHandler.kt | 5 +-- .../handle/ExecuteCommandHandler.kt | 7 ++-- .../handle/FloorItemOptionHandler.kt | 13 +++---- .../handle/InterfaceClosedHandler.kt | 4 ++- .../InterfaceOnFloorItemOptionHandler.kt | 7 ++-- .../InterfaceOnInterfaceOptionHandler.kt | 7 ++-- .../handle/InterfaceOnNPCOptionHandler.kt | 7 ++-- .../handle/InterfaceOnObjectOptionHandler.kt | 7 ++-- .../handle/InterfaceOnPlayerOptionHandler.kt | 7 ++-- .../handle/InterfaceOptionHandler.kt | 7 ++-- .../handle/InterfaceSwitchHandler.kt | 7 ++-- .../instruction/handle/NPCOptionHandler.kt | 13 +++---- .../instruction/handle/ObjectOptionHandler.kt | 11 +++--- .../instruction/handle/PlayerOptionHandler.kt | 9 ++--- 18 files changed, 91 insertions(+), 74 deletions(-) diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/InstructionHandler.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/InstructionHandler.kt index f70b3be489..0ff209eb32 100644 --- a/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/InstructionHandler.kt +++ b/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/InstructionHandler.kt @@ -8,5 +8,5 @@ abstract class InstructionHandler { /** * Validates the [instruction] information is correct and emits a [Player] event with the relevant data */ - abstract fun validate(player: Player, instruction: T) + abstract fun validate(player: Player, instruction: T): Boolean } diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/InstructionHandlers.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/InstructionHandlers.kt index 6d3ff9f394..e689435f1a 100644 --- a/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/InstructionHandlers.kt +++ b/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/InstructionHandlers.kt @@ -59,23 +59,24 @@ class InstructionHandlers( } } - fun handle(player: Player, instruction: Instruction) { + fun handle(player: Player, instruction: Instruction): Boolean { when (instruction) { - is InteractInterfaceItem -> interactInterfaceItem.validate(player, instruction) - is InteractInterfacePlayer -> interactInterfacePlayer.validate(player, instruction) - is InteractInterfaceObject -> interactInterfaceObject.validate(player, instruction) - is InteractInterfaceNPC -> interactInterfaceNPC.validate(player, instruction) - is InteractInterfaceFloorItem -> interactInterfaceFloorItem.validate(player, instruction) - is InteractFloorItem -> interactFloorItem.validate(player, instruction) - is InteractDialogue -> interactDialogue.validate(player, instruction) - is ContinueKey -> continueKey.validate(player, instruction) - is InteractDialogueItem -> interactDialogueItem.validate(player, instruction) - is InterfaceClosedInstruction -> closeInterface.validate(player, instruction) - is InteractInterface -> interactInterface.validate(player, instruction) - is MoveInventoryItem -> moveInventoryItem.validate(player, instruction) - is InteractNPC -> interactNPC.validate(player, instruction) - is InteractObject -> interactObject.validate(player, instruction) - is InteractPlayer -> interactPlayer.validate(player, instruction) + is InteractInterfaceItem -> return interactInterfaceItem.validate(player, instruction) + is InteractInterfacePlayer -> return interactInterfacePlayer.validate(player, instruction) + is InteractInterfaceObject -> return interactInterfaceObject.validate(player, instruction) + is InteractInterfaceNPC -> return interactInterfaceNPC.validate(player, instruction) + is InteractInterfaceFloorItem -> return interactInterfaceFloorItem.validate(player, instruction) + is InteractFloorItem -> return interactFloorItem.validate(player, instruction) + is InteractDialogue -> return interactDialogue.validate(player, instruction) + is ContinueKey -> return continueKey.validate(player, instruction) + is InteractDialogueItem -> return interactDialogueItem.validate(player, instruction) + is InterfaceClosedInstruction -> return closeInterface.validate(player, instruction) + is InteractInterface -> return interactInterface.validate(player, instruction) + is MoveInventoryItem -> return moveInventoryItem.validate(player, instruction) + is InteractNPC -> return interactNPC.validate(player, instruction) + is InteractObject -> return interactObject.validate(player, instruction) + is InteractPlayer -> return interactPlayer.validate(player, instruction) + is ExecuteCommand -> return executeCommand.validate(player, instruction) is ExamineItem -> examineItem.invoke(instruction, player) is ExamineNpc -> examineNPC.invoke(instruction, player) is ExamineObject -> examineObject.invoke(instruction, player) @@ -83,7 +84,6 @@ class InstructionHandlers( is Walk -> walk.invoke(instruction, player) is WorldMapClick -> worldMapClick.invoke(instruction, player) is FinishRegionLoad -> finishRegionLoad.invoke(instruction, player) - is ExecuteCommand -> executeCommand.validate(player, instruction) is EnterString -> enterString.invoke(instruction, player) is EnterName -> enterName.invoke(instruction, player) is EnterInt -> enterInt.invoke(instruction, player) @@ -100,7 +100,9 @@ class InstructionHandlers( is ClanChatKick -> clanChatKickHandler.invoke(instruction, player) is ClanChatRank -> clanChatRankHandler.invoke(instruction, player) is SongEnd -> songEndHandler.invoke(instruction, player) + else -> return false } + return true } } diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/DialogueContinueHandler.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/DialogueContinueHandler.kt index d7599e1f55..30685db9ed 100644 --- a/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/DialogueContinueHandler.kt +++ b/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/DialogueContinueHandler.kt @@ -13,20 +13,21 @@ class DialogueContinueHandler( private val logger = InlineLogger() - override fun validate(player: Player, instruction: InteractDialogue) { + override fun validate(player: Player, instruction: InteractDialogue): Boolean { val (interfaceId, componentId) = instruction val id = definitions.get(interfaceId).stringId if (!player.interfaces.contains(id)) { logger.debug { "Dialogue $interfaceId not found for player $player" } - return + return false } val component = definitions.get(id).components?.get(componentId) if (component == null) { logger.debug { "Dialogue $interfaceId component $componentId not found for player $player" } - return + return false } Dialogues.continueDialogue(player, "$id:${component.stringId}") + return true } } diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/DialogueContinueKeyHandler.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/DialogueContinueKeyHandler.kt index 4ab9ccf47b..46120503e3 100644 --- a/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/DialogueContinueKeyHandler.kt +++ b/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/DialogueContinueKeyHandler.kt @@ -13,15 +13,14 @@ import world.gregs.voidps.network.client.instruction.ContinueKey class DialogueContinueKeyHandler( private val definitions: InterfaceDefinitions, ) : InstructionHandler() { - override fun validate(player: Player, instruction: ContinueKey) { - val dialogue = player.dialogue - if (dialogue == null) { - return - } + override fun validate(player: Player, instruction: ContinueKey): Boolean { + val dialogue = player.dialogue ?: return false val option = if (instruction.button == -1) "continue" else "line${instruction.button}" if (definitions.get(dialogue).components?.values?.any { it.stringId == option } == true) { Dialogues.continueDialogue(player, "$dialogue:$option") + return true } + return false } } diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/DialogueItemContinueHandler.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/DialogueItemContinueHandler.kt index 9e7de16f72..9d7f5c7632 100644 --- a/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/DialogueItemContinueHandler.kt +++ b/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/DialogueItemContinueHandler.kt @@ -11,12 +11,13 @@ class DialogueItemContinueHandler : InstructionHandler() { private val logger = InlineLogger() - override fun validate(player: Player, instruction: InteractDialogueItem) { + override fun validate(player: Player, instruction: InteractDialogueItem): Boolean { val definition = ItemDefinitions.getOrNull(instruction.item) if (definition == null) { logger.debug { "Item ${instruction.item} not found for player $player." } - return + return false } Dialogues.continueItem(player, definition.stringId) + return true } } diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/ExecuteCommandHandler.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/ExecuteCommandHandler.kt index ab007261bf..c5bed6d08f 100644 --- a/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/ExecuteCommandHandler.kt +++ b/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/ExecuteCommandHandler.kt @@ -10,10 +10,10 @@ import world.gregs.voidps.network.client.instruction.ExecuteCommand class ExecuteCommandHandler : InstructionHandler() { - override fun validate(player: Player, instruction: ExecuteCommand) { + override fun validate(player: Player, instruction: ExecuteCommand): Boolean { if (instruction.tab) { Commands.autofill(player, instruction.command) - return + return true } val parts = instruction.command.split(" ") val prefix = parts[0] @@ -26,11 +26,12 @@ class ExecuteCommandHandler : InstructionHandler() { player.tele(x, y, level) player["world_map_centre"] = player.tile.id player["world_map_marker_player"] = player.tile.id - return + return true } Script.launch { AuditLog.event(player, "command", "\"${instruction.command}\"") Commands.call(player, instruction.command) } + return true } } diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/FloorItemOptionHandler.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/FloorItemOptionHandler.kt index 8c7ef6b791..f5df58756a 100644 --- a/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/FloorItemOptionHandler.kt +++ b/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/FloorItemOptionHandler.kt @@ -17,29 +17,30 @@ class FloorItemOptionHandler : InstructionHandler() { private val logger = InlineLogger() - override fun validate(player: Player, instruction: InteractFloorItem) { + override fun validate(player: Player, instruction: InteractFloorItem): Boolean { if (player.contains("delay")) { - return + return false } val (id, x, y, optionIndex) = instruction val tile = player.tile.copy(x, y) val floorItem = FloorItems.at(tile).firstOrNull { it.def.id == id } if (floorItem == null) { logger.warn { "Invalid floor item $id $tile" } - return + return false } val options = floorItem.def.floorOptions val selectedOption = options.getOrNull(optionIndex) if (selectedOption == null) { logger.warn { "Invalid floor item option $optionIndex ${options.contentToString()}" } - return + return false } if (selectedOption == "Examine") { - player.message(floorItem.def.getOrNull("examine") ?: return, ChatType.ItemExamine) - return + player.message(floorItem.def.getOrNull("examine") ?: return false, ChatType.ItemExamine) + return false } player.closeInterfaces() player.interactFloorItem(floorItem, selectedOption, -1) + return true } } diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/InterfaceClosedHandler.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/InterfaceClosedHandler.kt index 793ec9ec86..f4899635d3 100644 --- a/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/InterfaceClosedHandler.kt +++ b/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/InterfaceClosedHandler.kt @@ -6,10 +6,12 @@ import world.gregs.voidps.network.client.instruction.InterfaceClosedInstruction class InterfaceClosedHandler : InstructionHandler() { - override fun validate(player: Player, instruction: InterfaceClosedInstruction) { + override fun validate(player: Player, instruction: InterfaceClosedInstruction): Boolean { val id = player.interfaces.get("main_screen") ?: player.interfaces.get("wide_screen") ?: player.interfaces.get("underlay") if (id != null) { player.interfaces.close(id) + return true } + return false } } diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/InterfaceOnFloorItemOptionHandler.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/InterfaceOnFloorItemOptionHandler.kt index 4eb98262f4..39d816c53c 100644 --- a/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/InterfaceOnFloorItemOptionHandler.kt +++ b/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/InterfaceOnFloorItemOptionHandler.kt @@ -16,21 +16,22 @@ class InterfaceOnFloorItemOptionHandler(private val handler: InterfaceHandler) : private val logger = InlineLogger() - override fun validate(player: Player, instruction: InteractInterfaceFloorItem) { + override fun validate(player: Player, instruction: InteractInterfaceFloorItem): Boolean { val (floorItemId, x, y, interfaceId, componentId, itemId, itemSlot) = instruction val tile = player.tile.copy(x, y) val floorItem = FloorItems.at(tile).firstOrNull { it.def.id == floorItemId } if (floorItem == null) { logger.warn { "Invalid floor item $itemId $tile" } - return + return false } - val (id, component, item) = handler.getInterfaceItem(player, interfaceId, componentId, itemId, itemSlot) ?: return + val (id, component, item) = handler.getInterfaceItem(player, interfaceId, componentId, itemId, itemSlot) ?: return false player.closeInterfaces() if (item.isEmpty()) { player.interactOn(floorItem, id, component, itemSlot, approachRange = -1) } else { player.interactItemOn(floorItem, id, component, item, itemSlot, approachRange = -1) } + return true } } diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/InterfaceOnInterfaceOptionHandler.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/InterfaceOnInterfaceOptionHandler.kt index 8d65d91376..0df17ba8b5 100644 --- a/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/InterfaceOnInterfaceOptionHandler.kt +++ b/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/InterfaceOnInterfaceOptionHandler.kt @@ -11,11 +11,11 @@ class InterfaceOnInterfaceOptionHandler( private val handler: InterfaceHandler, ) : InstructionHandler() { - override fun validate(player: Player, instruction: InteractInterfaceItem) { + override fun validate(player: Player, instruction: InteractInterfaceItem): Boolean { val (fromItemId, toItemId, fromSlot, toSlot, fromInterfaceId, fromComponentId, toInterfaceId, toComponentId) = instruction - val (fromId, fromComponent, fromItem) = handler.getInterfaceItem(player, fromInterfaceId, fromComponentId, fromItemId, fromSlot) ?: return - val (_, _, toItem) = handler.getInterfaceItem(player, toInterfaceId, toComponentId, toItemId, toSlot) ?: return + val (fromId, fromComponent, fromItem) = handler.getInterfaceItem(player, fromInterfaceId, fromComponentId, fromItemId, fromSlot) ?: return false + val (_, _, toItem) = handler.getInterfaceItem(player, toInterfaceId, toComponentId, toItemId, toSlot) ?: return false player.closeInterfaces() player.queue.clearWeak() @@ -25,5 +25,6 @@ class InterfaceOnInterfaceOptionHandler( } else { InterfaceApi.itemOnItem(player, fromItem, toItem, fromSlot, toSlot) } + return true } } diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/InterfaceOnNPCOptionHandler.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/InterfaceOnNPCOptionHandler.kt index 517b4fc97a..22cb6dcbf3 100644 --- a/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/InterfaceOnNPCOptionHandler.kt +++ b/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/InterfaceOnNPCOptionHandler.kt @@ -14,11 +14,11 @@ import world.gregs.voidps.network.client.instruction.InteractInterfaceNPC class InterfaceOnNPCOptionHandler(private val handler: InterfaceHandler) : InstructionHandler() { - override fun validate(player: Player, instruction: InteractInterfaceNPC) { + override fun validate(player: Player, instruction: InteractInterfaceNPC): Boolean { val (npcIndex, interfaceId, componentId, itemId, itemSlot) = instruction - val npc = NPCs.indexed(npcIndex) ?: return + val npc = NPCs.indexed(npcIndex) ?: return false - val (id, component, item) = handler.getInterfaceItem(player, interfaceId, componentId, itemId, itemSlot) ?: return + val (id, component, item) = handler.getInterfaceItem(player, interfaceId, componentId, itemId, itemSlot) ?: return false player.closeInterfaces() player.talkWith(npc) @@ -27,6 +27,7 @@ class InterfaceOnNPCOptionHandler(private val handler: InterfaceHandler) : Instr } else { player.interactItemOn(npc, id, component, item, itemSlot) } + return true } } diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/InterfaceOnObjectOptionHandler.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/InterfaceOnObjectOptionHandler.kt index e03c01e63b..b75e6708b8 100644 --- a/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/InterfaceOnObjectOptionHandler.kt +++ b/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/InterfaceOnObjectOptionHandler.kt @@ -17,22 +17,23 @@ class InterfaceOnObjectOptionHandler( private val handler: InterfaceHandler, ) : InstructionHandler() { - override fun validate(player: Player, instruction: InteractInterfaceObject) { + override fun validate(player: Player, instruction: InteractInterfaceObject): Boolean { val (objectId, x, y, interfaceId, componentId, itemId, itemSlot) = instruction val tile = Tile(x, y, player.tile.level) val obj = GameObjects.findOrNull(tile, objectId) if (obj == null) { player.noInterest() - return + return false } - val (id, component, item) = handler.getInterfaceItem(player, interfaceId, componentId, itemId, itemSlot) ?: return + val (id, component, item) = handler.getInterfaceItem(player, interfaceId, componentId, itemId, itemSlot) ?: return false player.closeInterfaces() if (item.isEmpty()) { player.interactOn(obj, id, component, itemSlot) } else { player.interactItemOn(obj, id, component, item, itemSlot) } + return true } } diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/InterfaceOnPlayerOptionHandler.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/InterfaceOnPlayerOptionHandler.kt index 857724be10..a48c61fc67 100644 --- a/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/InterfaceOnPlayerOptionHandler.kt +++ b/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/InterfaceOnPlayerOptionHandler.kt @@ -12,12 +12,13 @@ class InterfaceOnPlayerOptionHandler( private val handler: InterfaceHandler, ) : InstructionHandler() { - override fun validate(player: Player, instruction: InteractInterfacePlayer) { + override fun validate(player: Player, instruction: InteractInterfacePlayer): Boolean { val (playerIndex, interfaceId, componentId, itemId, itemSlot) = instruction - val target = Players.indexed(playerIndex) ?: return + val target = Players.indexed(playerIndex) ?: return false - val (id, component, item) = handler.getInterfaceItem(player, interfaceId, componentId, itemId, itemSlot) ?: return + val (id, component, item) = handler.getInterfaceItem(player, interfaceId, componentId, itemId, itemSlot) ?: return false player.closeInterfaces() player.mode = ItemOnPlayerInteract(target, "$id:$component", item, itemSlot, player) + return true } } diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/InterfaceOptionHandler.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/InterfaceOptionHandler.kt index 1911db6935..a404acb31f 100644 --- a/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/InterfaceOptionHandler.kt +++ b/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/InterfaceOptionHandler.kt @@ -17,10 +17,10 @@ class InterfaceOptionHandler( private val logger = InlineLogger() - override fun validate(player: Player, instruction: InteractInterface) { + override fun validate(player: Player, instruction: InteractInterface): Boolean { val (interfaceId, componentId, itemId, itemSlot, option) = instruction - var (id, component, item, options) = handler.getInterfaceItem(player, interfaceId, componentId, itemId, itemSlot) ?: return + var (id, component, item, options) = handler.getInterfaceItem(player, interfaceId, componentId, itemId, itemSlot) ?: return false if (options == null) { options = interfaceDefinitions.getComponent(id, component)?.getOrNull("options") ?: emptyArray() @@ -28,7 +28,7 @@ class InterfaceOptionHandler( if (option !in options.indices) { logger.info { "Interface option not found [$player, interface=$interfaceId, component=$componentId, option=$option, options=${options.toList()}]" } - return + return false } val selectedOption = options.getOrNull(option) ?: "" @@ -42,5 +42,6 @@ class InterfaceOptionHandler( Script.launch { InterfaceApi.option(player, event) } + return true } } diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/InterfaceSwitchHandler.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/InterfaceSwitchHandler.kt index 25a0e3e5c2..1c71d9f4db 100644 --- a/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/InterfaceSwitchHandler.kt +++ b/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/InterfaceSwitchHandler.kt @@ -10,7 +10,7 @@ class InterfaceSwitchHandler( private val handler: InterfaceHandler, ) : InstructionHandler() { - override fun validate(player: Player, instruction: MoveInventoryItem) { + override fun validate(player: Player, instruction: MoveInventoryItem): Boolean { var (fromInterfaceId, fromComponentId, fromItemId, fromSlot, toInterfaceId, toComponentId, toItemId, toSlot) = instruction if (toInterfaceId == 149) { toSlot -= 28 @@ -18,8 +18,9 @@ class InterfaceSwitchHandler( fromItemId = toItemId toItemId = temp } - val (fromId, fromComponent) = handler.getInterfaceItem(player, fromInterfaceId, fromComponentId, fromItemId, fromSlot) ?: return - val (toId, toComponent) = handler.getInterfaceItem(player, toInterfaceId, toComponentId, toItemId, toSlot) ?: return + val (fromId, fromComponent) = handler.getInterfaceItem(player, fromInterfaceId, fromComponentId, fromItemId, fromSlot) ?: return false + val (toId, toComponent) = handler.getInterfaceItem(player, toInterfaceId, toComponentId, toItemId, toSlot) ?: return false InterfaceApi.swap(player, "$fromId:$fromComponent", "$toId:$toComponent", fromSlot, toSlot) + return true } } diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/NPCOptionHandler.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/NPCOptionHandler.kt index d95cbe1eec..784e4ab97d 100644 --- a/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/NPCOptionHandler.kt +++ b/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/NPCOptionHandler.kt @@ -21,11 +21,11 @@ class NPCOptionHandler : InstructionHandler() { private val logger = InlineLogger() - override fun validate(player: Player, instruction: InteractNPC) { + override fun validate(player: Player, instruction: InteractNPC): Boolean { if (player.contains("delay")) { - return + return false } - val npc = NPCs.indexed(instruction.npcIndex) ?: return + val npc = NPCs.indexed(instruction.npcIndex) ?: return false var def = npc.def val transform = npc["transform_id", ""] if (transform.isNotBlank()) { @@ -38,19 +38,20 @@ class NPCOptionHandler : InstructionHandler() { if (selectedOption == null) { player.noInterest() logger.warn { "Invalid npc option $npc $index ${options.contentToString()}" } - return + return false } if (selectedOption == "Listen-to" && player["movement", "walk"] == "music") { player.message("You are already resting.") - return + return false } if (player.hasClock("stunned")) { player.message("You're stunned!", ChatType.Filter) - return + return false } player.closeInterfaces() player.talkWith(npc, definition) player.interactNpc(npc, selectedOption) + return true } } diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/ObjectOptionHandler.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/ObjectOptionHandler.kt index 1fb8214fbe..20960ce2bd 100644 --- a/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/ObjectOptionHandler.kt +++ b/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/ObjectOptionHandler.kt @@ -21,31 +21,32 @@ class ObjectOptionHandler : InstructionHandler() { private val logger = InlineLogger() - override fun validate(player: Player, instruction: InteractObject) { + override fun validate(player: Player, instruction: InteractObject): Boolean { if (player.contains("delay")) { - return + return false } val (objectId, x, y, option) = instruction val tile = player.tile.copy(x = x, y = y) val target = getObject(tile, objectId) if (target == null) { logger.warn { "Invalid object $objectId $tile" } - return + return false } val definition = getDefinition(player, ObjectDefinitions, target.def, target.def) val options = definition.options if (options == null) { logger.warn { "Invalid object interaction $target $option ${definition.options.contentToString()}" } - return + return false } val index = option - 1 val selectedOption = options.getOrNull(index) if (selectedOption == null) { logger.warn { "Invalid object option $target $index ${options.contentToString()}" } - return + return false } player.closeInterfaces() player.interactObject(target, selectedOption) + return true } private fun getObject(tile: Tile, objectId: Int): GameObject? { diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/PlayerOptionHandler.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/PlayerOptionHandler.kt index 6502d34712..31515006a8 100644 --- a/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/PlayerOptionHandler.kt +++ b/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/PlayerOptionHandler.kt @@ -16,16 +16,16 @@ class PlayerOptionHandler : InstructionHandler() { private val logger = InlineLogger() - override fun validate(player: Player, instruction: InteractPlayer) { + override fun validate(player: Player, instruction: InteractPlayer): Boolean { if (player.contains("delay")) { - return + return false } - val target = Players.indexed(instruction.playerIndex) ?: return + val target = Players.indexed(instruction.playerIndex) ?: return false val optionIndex = instruction.option val option = player.options.get(optionIndex) if (option == PlayerOptions.EMPTY_OPTION) { logger.info { "Invalid player option $optionIndex ${player.options.get(optionIndex)} for $player on $target" } - return + return false } player.closeInterfaces() if (option == "Follow") { @@ -33,6 +33,7 @@ class PlayerOptionHandler : InstructionHandler() { } else { player.interactPlayer(target, option) } + return true } } From e8521cbb99c0c0340ec27a09a357d6b60fd31f0c Mon Sep 17 00:00:00 2001 From: GregHib Date: Thu, 5 Feb 2026 12:04:19 +0000 Subject: [PATCH 050/101] Use interaction validators directly for faster feedback --- .../kotlin/content/bot/action/BotAction.kt | 56 ++++++++++++++----- 1 file changed, 42 insertions(+), 14 deletions(-) diff --git a/game/src/main/kotlin/content/bot/action/BotAction.kt b/game/src/main/kotlin/content/bot/action/BotAction.kt index df3991b81d..29c745fc50 100644 --- a/game/src/main/kotlin/content/bot/action/BotAction.kt +++ b/game/src/main/kotlin/content/bot/action/BotAction.kt @@ -7,6 +7,7 @@ import content.bot.interact.path.Graph import content.entity.combat.attackers import content.entity.combat.dead import world.gregs.voidps.engine.GameLoop +import world.gregs.voidps.engine.client.instruction.InstructionHandlers import world.gregs.voidps.engine.client.instruction.InterfaceHandler import world.gregs.voidps.engine.data.definition.Areas import world.gregs.voidps.engine.data.definition.InterfaceDefinitions @@ -159,7 +160,10 @@ sealed interface BotAction { if (index == -1) { continue } - bot.player.instructions.trySend(InteractNPC(npc.index, index + 1)) + val valid = get().handle(bot.player, InteractNPC(npc.index, index + 1)) + if (!valid) { + return BehaviourState.Failed(Reason.Invalid("Invalid npc interaction: ${npc.index} ${index + 1}")) + } return BehaviourState.Running } } @@ -208,7 +212,10 @@ sealed interface BotAction { if (option == -1) { continue } - bot.player.instructions.trySend(InteractInterface(149, 0, item.def.id, index, option)) + val valid = get().handle(bot.player, InteractInterface(149, 0, item.def.id, index, option)) + if (!valid) { + return BehaviourState.Failed(Reason.Invalid("Invalid inventory interaction: ${item.def.id} $index $option")) + } return BehaviourState.Wait(1, BehaviourState.Running) } return BehaviourState.Running @@ -224,7 +231,11 @@ sealed interface BotAction { if (item.def.cost <= lootOverValue) { continue } - bot.player.instructions.trySend(InteractFloorItem(item.def.id, item.tile.x, item.tile.y, item.def.floorOptions.indexOf("Take"))) + val index = item.def.floorOptions.indexOf("Take") + val valid = get().handle(bot.player, InteractFloorItem(item.def.id, item.tile.x, item.tile.y, index)) + if (!valid) { + return BehaviourState.Failed(Reason.Invalid("Invalid floor item interaction: $item $index")) + } return BehaviourState.Running } for (npc in NPCs.at(tile)) { @@ -238,7 +249,10 @@ sealed interface BotAction { if (npc.dead || npc.attackers.isNotEmpty() && !npc.attackers.contains(player)) { continue } - bot.player.instructions.trySend(InteractNPC(npc.index, index + 1)) + val valid = get().handle(bot.player, InteractNPC(npc.index, index + 1)) + if (!valid) { + return BehaviourState.Failed(Reason.Invalid("Invalid npc interaction: ${npc.index} ${index + 1}")) + } return BehaviourState.Running } } @@ -287,7 +301,10 @@ sealed interface BotAction { if (index == null || index == -1) { continue } - bot.player.instructions.trySend(InteractObject(obj.intId, obj.x, obj.y, index + 1)) + val valid = get().handle(bot.player, InteractObject(obj.intId, obj.x, obj.y, index + 1)) + if (!valid) { + return BehaviourState.Failed(Reason.Invalid("Invalid object interaction: $obj ${index + 1}")) + } return BehaviourState.Running } } @@ -332,16 +349,15 @@ sealed interface BotAction { if (id == "shop") { itemSlot *= 6 } - bot.player.instructions.trySend( - InteractInterface( - interfaceId = def.id, - componentId = componentId, - itemId = itemDef?.id ?: -1, - itemSlot = itemSlot, - option = index - ) - ) // TODO could await actual response, or something to get actual feedback + val valid = get().handle(bot.player, InteractInterface( + interfaceId = def.id, + componentId = componentId, + itemId = itemDef?.id ?: -1, + itemSlot = itemSlot, + option = index + )) return when { + !valid -> BehaviourState.Failed(Reason.Invalid("Invalid interaction: ${def.id}:${componentId}:${itemDef?.id} slot $itemSlot option ${index}.")) success == null -> BehaviourState.Wait(1, BehaviourState.Success) success.check(bot.player) -> BehaviourState.Success else -> BehaviourState.Running @@ -384,6 +400,18 @@ sealed interface BotAction { } /** + * TODO + * firemaking bot + * thieving bot + * bone burying bot + * cooking bot + * cow bot + * fletching bot + * rune mysteries quest bot + * bot saving? + * bot setups + * bot spawning in other locations + * TODO how to handle repeat actions e.g. repeat Chop-down trees until inv is full - These are actions Gathering, Skilling etc.. more resolvers like bank all, drop cheap items how to handle combat, one task or multiple? - One Fight action From 840cc93fc370f6e5c4b00b396b57a5b3d2401108 Mon Sep 17 00:00:00 2001 From: GregHib Date: Thu, 5 Feb 2026 12:32:12 +0000 Subject: [PATCH 051/101] Add item on item and dialogue continue actions --- .../content/bot/action/BehaviourFragment.kt | 8 ++ .../kotlin/content/bot/action/BotAction.kt | 77 +++++++++++++++++++ .../kotlin/content/bot/action/BotActivity.kt | 6 +- .../entity/player/command/MetaCommands.kt | 3 - .../bot/action/BehaviourFragmentTest.kt | 2 + 5 files changed, 91 insertions(+), 5 deletions(-) diff --git a/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt b/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt index 6651696304..0b709a2619 100644 --- a/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt +++ b/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt @@ -25,6 +25,14 @@ data class BehaviourFragment( option = resolve(action.references["option"], copy.option), id = resolve(action.references["interface"], copy.id), ) + is BotAction.DialogueContinue -> BotAction.DialogueContinue( + option = resolve(action.references["option"], copy.option), + id = resolve(action.references["continue"], copy.id), + ) + is BotAction.ItemOnItem -> BotAction.ItemOnItem( + item = resolve(action.references["item"], copy.item), + on = resolve(action.references["on"], copy.on), + ) is BotAction.InteractNpc -> { val option = resolve(action.references["option"], copy.option) if (option == "Attack") { diff --git a/game/src/main/kotlin/content/bot/action/BotAction.kt b/game/src/main/kotlin/content/bot/action/BotAction.kt index 29c745fc50..c54360fea4 100644 --- a/game/src/main/kotlin/content/bot/action/BotAction.kt +++ b/game/src/main/kotlin/content/bot/action/BotAction.kt @@ -322,6 +322,45 @@ sealed interface BotAction { } } + data class ItemOnItem(val item: String, val on: String, val success: Condition? = null) : BotAction { + override fun start(bot: Bot, frame: BehaviourFrame): BehaviourState { + val inventory = bot.player.inventory + val fromSlot = inventory.indexOf(item) + if (fromSlot == -1) { + return BehaviourState.Failed(Reason.Invalid("No inventory item '$item'.")) + } + val toSlot = inventory.indexOf(on) + if (toSlot == -1) { + return BehaviourState.Failed(Reason.Invalid("No inventory item '$on'.")) + } + val from = inventory[fromSlot] + val to = inventory[toSlot] + val valid = get().handle(bot.player, InteractInterfaceItem( + from.def.id, + to.def.id, + fromSlot, + toSlot, + 149, + 0, + 149, + 0 + )) + return when { + !valid -> BehaviourState.Failed(Reason.Invalid("Invalid item on item: ${from.def.id}:${fromSlot} -> ${to.def.id}:${toSlot}.")) + success == null -> BehaviourState.Wait(1, BehaviourState.Success) + success.check(bot.player) -> BehaviourState.Success + else -> BehaviourState.Running + } + } + + override fun update(bot: Bot, frame: BehaviourFrame): BehaviourState? { + if (success != null && success.check(bot.player)) { + return BehaviourState.Success + } + return super.update(bot, frame) + } + } + data class InterfaceOption(val option: String, val id: String, val success: Condition? = null) : BotAction { override fun start(bot: Bot, frame: BehaviourFrame): BehaviourState { val definitions = get() @@ -372,6 +411,44 @@ sealed interface BotAction { } } + data class DialogueContinue(val option: String, val id: String, val success: Condition? = null) : BotAction { + override fun start(bot: Bot, frame: BehaviourFrame): BehaviourState { + val definitions = get() + val split = id.split(":") + if (split.size < 2) { + return BehaviourState.Failed(Reason.Invalid("Invalid interface id '$id'.")) + } + val (id, component) = split + val item = split.getOrNull(2) + val def = definitions.getOrNull(id) ?: return BehaviourState.Failed(Reason.Invalid("Invalid interface id $id:${component}:${item}.")) + val componentId = definitions.getComponentId(id, component) ?: return BehaviourState.Failed(Reason.Invalid("Invalid interface component $id:${component}:${item}.")) + val componentDef = definitions.getComponent(id, component) ?: return BehaviourState.Failed(Reason.Invalid("Invalid interface component definition $id:${component}:${item}.")) + var options = componentDef.options + if (options == null) { + options = componentDef.getOrNull("options") ?: emptyArray() + } + val index = options.indexOf(option) + val valid = get().handle(bot.player, InteractDialogue( + interfaceId = def.id, + componentId = componentId, + option = index + )) + return when { + !valid -> BehaviourState.Failed(Reason.Invalid("Invalid interaction: ${def.id}:${componentId} option=${index}.")) + success == null -> BehaviourState.Wait(1, BehaviourState.Success) + success.check(bot.player) -> BehaviourState.Success + else -> BehaviourState.Running + } + } + + override fun update(bot: Bot, frame: BehaviourFrame): BehaviourState? { + if (success != null && success.check(bot.player)) { + return BehaviourState.Success + } + return super.update(bot, frame) + } + } + data class IntEntry(val value: Int) : BotAction { override fun start(bot: Bot, frame: BehaviourFrame): BehaviourState { bot.player.instructions.trySend(EnterInt(value)) diff --git a/game/src/main/kotlin/content/bot/action/BotActivity.kt b/game/src/main/kotlin/content/bot/action/BotActivity.kt index d658211769..57e2d24167 100644 --- a/game/src/main/kotlin/content/bot/action/BotActivity.kt +++ b/game/src/main/kotlin/content/bot/action/BotActivity.kt @@ -285,7 +285,7 @@ fun ConfigReader.actions(list: MutableList) { val references = mutableMapOf() while (nextEntry()) { when (val key = key()) { - "go_to", "go_to_nearest", "enter_string", "interface", "npc", "object", "clone" -> { + "go_to", "go_to_nearest", "enter_string", "interface", "npc", "object", "clone", "item", "continue" -> { type = key id = string() if (id.contains('$')) { @@ -320,7 +320,7 @@ fun ConfigReader.actions(list: MutableList) { references[key] = id } } - "option" -> { + "option", "on" -> { option = string() if (option.contains('$')) { references[key] = option @@ -380,6 +380,8 @@ fun ConfigReader.actions(list: MutableList) { "tile" -> BotAction.WalkTo(x = x, y = y) "object" -> BotAction.InteractObject(id = id, option = option, delay = delay, success = success, radius = radius) "interface" -> BotAction.InterfaceOption(id = id, option = option) + "continue" -> BotAction.DialogueContinue(id = id, option = option) + "item" -> BotAction.ItemOnItem(item = id, on = option) "clone" -> BotAction.Clone(id) else -> throw IllegalArgumentException("Unknown action type: $type ${exception()}") } diff --git a/game/src/main/kotlin/content/entity/player/command/MetaCommands.kt b/game/src/main/kotlin/content/entity/player/command/MetaCommands.kt index e43d47dc1f..287a7f0704 100644 --- a/game/src/main/kotlin/content/entity/player/command/MetaCommands.kt +++ b/game/src/main/kotlin/content/entity/player/command/MetaCommands.kt @@ -224,9 +224,6 @@ class MetaCommands( } } } - for (line in list) { - println(line) - } player.questJournal("Commands List", list) } diff --git a/game/src/test/kotlin/content/bot/action/BehaviourFragmentTest.kt b/game/src/test/kotlin/content/bot/action/BehaviourFragmentTest.kt index 119008e299..c7b9397100 100644 --- a/game/src/test/kotlin/content/bot/action/BehaviourFragmentTest.kt +++ b/game/src/test/kotlin/content/bot/action/BehaviourFragmentTest.kt @@ -118,6 +118,8 @@ class BehaviourFragmentTest { Triple(BotAction.GoTo("default"), mapOf("go_to" to "lumbridge"), BotAction.GoTo("lumbridge")), Triple(BotAction.GoToNearest("default"), mapOf("go_to_nearest" to "lumbridge"), BotAction.GoToNearest("lumbridge")), Triple(BotAction.InterfaceOption(option = "click", id = "something"), mapOf("option" to "Open", "interface" to "bank"), BotAction.InterfaceOption(option = "Open", id = "bank")), + Triple(BotAction.DialogueContinue(option = "click", id = "something"), mapOf("option" to "Option", "continue" to "now"), BotAction.DialogueContinue(option = "Option", id = "now")), + Triple(BotAction.ItemOnItem(item = "default", on = "on"), mapOf("item" to "item", "on" to "another"), BotAction.ItemOnItem(item = "item", on = "another")), Triple( BotAction.InteractNpc( option = "talk", From 6e555e56e5243d52f621c2f3370794d8533fe2b9 Mon Sep 17 00:00:00 2001 From: GregHib Date: Thu, 5 Feb 2026 13:31:40 +0000 Subject: [PATCH 052/101] Fix bot disconnecting by making client required and viewport optional --- .../engine/client/PlayerAccountLoader.kt | 10 +++++----- .../gregs/voidps/engine/data/AccountManager.kt | 11 ++++++----- .../voidps/engine/data/AccountManagerTest.kt | 3 ++- game/src/main/kotlin/content/bot/BotSpawns.kt | 18 +++++++++++++----- .../entity/player/command/ServerCommands.kt | 2 ++ game/src/test/kotlin/WorldTest.kt | 3 ++- 6 files changed, 30 insertions(+), 17 deletions(-) diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/client/PlayerAccountLoader.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/client/PlayerAccountLoader.kt index ec8b6a5cda..1c49ce6d54 100644 --- a/engine/src/main/kotlin/world/gregs/voidps/engine/client/PlayerAccountLoader.kt +++ b/engine/src/main/kotlin/world/gregs/voidps/engine/client/PlayerAccountLoader.kt @@ -73,16 +73,16 @@ class PlayerAccountLoader( } } - suspend fun connect(player: Player, client: Client? = null, displayMode: Int = 0) { - if (!accounts.setup(player, client, displayMode)) { + suspend fun connect(player: Player, client: Client, displayMode: Int = 0, viewport: Boolean = true) { + if (!accounts.setup(player, client, displayMode, viewport)) { logger.warn { "Error setting up account" } - client?.disconnect(Response.WORLD_FULL) + client.disconnect(Response.WORLD_FULL) return } withContext(gameContext) { queue.await() - logger.info { "${if (client != null) "Player" else "Bot"} logged in ${player.accountName} index ${player.index}." } - client?.login(player.name, player.index, player.rights.ordinal, member = World.members, membersWorld = World.members) + logger.info { "${if (viewport) "Player" else "Bot"} logged in ${player.accountName} index ${player.index}." } + client.login(player.name, player.index, player.rights.ordinal, member = World.members, membersWorld = World.members) accounts.spawn(player, client) AuditLog.event(player, "connected", player.tile) } diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/data/AccountManager.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/data/AccountManager.kt index bd54863a37..fea091feb9 100644 --- a/engine/src/main/kotlin/world/gregs/voidps/engine/data/AccountManager.kt +++ b/engine/src/main/kotlin/world/gregs/voidps/engine/data/AccountManager.kt @@ -1,5 +1,6 @@ package world.gregs.voidps.engine.data +import world.gregs.voidps.engine.GameLoop import world.gregs.voidps.engine.client.message import world.gregs.voidps.engine.client.ui.InterfaceOptions import world.gregs.voidps.engine.client.ui.Interfaces @@ -48,7 +49,7 @@ class AccountManager( this["new_player"] = true } - fun setup(player: Player, client: Client?, displayMode: Int): Boolean { + fun setup(player: Player, client: Client, displayMode: Int, viewport: Boolean = true): Boolean { player.index = Players.index() ?: return false player.visuals.hits.self = player.index player.interfaces = Interfaces(player, interfaceDefinitions) @@ -70,10 +71,10 @@ class AccountManager( accountDefinitions.add(player) } player.interfaces.displayMode = displayMode - if (client != null) { + player.client = client + (player.variables as PlayerVariables).client = client + if (viewport) { player.viewport = Viewport() - player.client = client - (player.variables as PlayerVariables).client = client } player.collision = CollisionStrategyProvider.get(character = player) return true @@ -120,7 +121,7 @@ class AccountManager( } player.client?.disconnect() connectionQueue.disconnect { - World.queue("logout", 1) { + World.queue("logout_${player.accountName}", 1) { Players.remove(player) } val offset = player.get("instance_offset")?.let { Delta(it) } ?: Delta.EMPTY diff --git a/engine/src/test/kotlin/world/gregs/voidps/engine/data/AccountManagerTest.kt b/engine/src/test/kotlin/world/gregs/voidps/engine/data/AccountManagerTest.kt index 1642290d1f..ff5c13797f 100644 --- a/engine/src/test/kotlin/world/gregs/voidps/engine/data/AccountManagerTest.kt +++ b/engine/src/test/kotlin/world/gregs/voidps/engine/data/AccountManagerTest.kt @@ -26,6 +26,7 @@ import world.gregs.voidps.engine.entity.character.player.equip.AppearanceOverrid import world.gregs.voidps.engine.script.KoinMock import world.gregs.voidps.network.client.Client import world.gregs.voidps.network.client.ConnectionQueue +import world.gregs.voidps.network.client.DummyClient import world.gregs.voidps.network.login.protocol.encode.logout import world.gregs.voidps.type.Tile import world.gregs.voidps.type.area.Rectangle @@ -99,7 +100,7 @@ class AccountManagerTest : KoinMock() { @Test fun `Initialise player`() { val player = Player(0) - manager.setup(player, null, 0) + manager.setup(player, DummyClient(), 0, false) assertNotNull(player.visuals) assertNotNull(player.interfaces) assertNotNull(player.interfaceOptions) diff --git a/game/src/main/kotlin/content/bot/BotSpawns.kt b/game/src/main/kotlin/content/bot/BotSpawns.kt index 45cb3df657..ea5d96743f 100644 --- a/game/src/main/kotlin/content/bot/BotSpawns.kt +++ b/game/src/main/kotlin/content/bot/BotSpawns.kt @@ -20,7 +20,6 @@ import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.entity.character.player.appearance import world.gregs.voidps.engine.entity.character.player.chat.ChatType import world.gregs.voidps.engine.entity.character.player.sex -import world.gregs.voidps.engine.get import world.gregs.voidps.engine.inv.add import world.gregs.voidps.engine.inv.inventory import world.gregs.voidps.engine.timer.* @@ -99,12 +98,21 @@ class BotSpawns( } fun clear(player: Player, args: List) { - val count = args[0].toIntOrNull() ?: MAX_PLAYERS + val count = args.getOrNull(0)?.toIntOrNull() ?: MAX_PLAYERS World.queue("bot_clear") { runBlocking { - for (bot in manager.bots.take(count)) { + var removed = 0 + for (bot in manager.bots) { + if (bot.player.client != null && bot.player.client !is DummyClient) { + continue + } manager.remove(bot) - accounts.logout(bot.player, false) + Script.launch { + accounts.logout(bot.player, true) + } + if (++removed >= count) { + break + } } } } @@ -153,7 +161,7 @@ class BotSpawns( } val player = Player(tile = Areas["lumbridge_teleport"].random(), accountName = name) val bot = player.initBot() - loader.connect(player, if (Settings["development.bots.live", false]) DummyClient() else null) + loader.connect(player, DummyClient(), viewport = Settings["development.bots.live", false]) setAppearance(player) if (player.inventory.isEmpty()) { player.inventory.add("coins", 10000) diff --git a/game/src/main/kotlin/content/entity/player/command/ServerCommands.kt b/game/src/main/kotlin/content/entity/player/command/ServerCommands.kt index 38f7db82b3..6202b60782 100644 --- a/game/src/main/kotlin/content/entity/player/command/ServerCommands.kt +++ b/game/src/main/kotlin/content/entity/player/command/ServerCommands.kt @@ -1,5 +1,6 @@ package content.entity.player.command +import content.bot.BotManager import content.bot.interact.navigation.graph.NavigationGraph import content.entity.obj.ObjectTeleports import content.entity.obj.ship.CharterShips @@ -129,6 +130,7 @@ class ServerCommands(val accountLoader: PlayerAccountLoader) : Script { Settings.load() SettingsReload.now() } + "bots" -> get().load(files) } } diff --git a/game/src/test/kotlin/WorldTest.kt b/game/src/test/kotlin/WorldTest.kt index 7ffd426c49..063ba30cd8 100644 --- a/game/src/test/kotlin/WorldTest.kt +++ b/game/src/test/kotlin/WorldTest.kt @@ -48,6 +48,7 @@ import world.gregs.voidps.engine.map.instance.Instances import world.gregs.voidps.engine.timer.setCurrentTime import world.gregs.voidps.network.client.Client import world.gregs.voidps.network.client.ConnectionQueue +import world.gregs.voidps.network.client.DummyClient import world.gregs.voidps.type.Tile import world.gregs.voidps.type.setRandom import java.io.File @@ -99,7 +100,7 @@ abstract class WorldTest : KoinTest { fun createPlayer(tile: Tile = Tile.EMPTY, name: String = "player"): Player { val player = Player(tile = tile, accountName = name, passwordHash = "") - assertTrue(accounts.setup(player, null, 0)) + assertTrue(accounts.setup(player, DummyClient(), 0, false)) accountDefs.add(player) tick() player["creation"] = -1 From 36c5a880a73ffdf89939687748cd5764b6b1e1f3 Mon Sep 17 00:00:00 2001 From: GregHib Date: Thu, 5 Feb 2026 20:54:24 +0000 Subject: [PATCH 053/101] Fix invalid inventory messages and bot restart actions --- .../client/instruction/InterfaceHandler.kt | 3 + .../src/main/kotlin/content/bot/BotManager.kt | 41 +++--- game/src/main/kotlin/content/bot/BotSpawns.kt | 2 + .../content/bot/action/BehaviourFragment.kt | 10 ++ .../kotlin/content/bot/action/BotAction.kt | 117 ++++++++++-------- .../kotlin/content/bot/action/BotActivity.kt | 11 +- game/src/main/kotlin/content/bot/fact/Fact.kt | 5 + .../entity/player/dialogue/DialogueInput.kt | 1 + 8 files changed, 125 insertions(+), 65 deletions(-) diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/InterfaceHandler.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/InterfaceHandler.kt index 75ae0f1969..926afdb061 100644 --- a/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/InterfaceHandler.kt +++ b/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/InterfaceHandler.kt @@ -108,6 +108,9 @@ class InterfaceHandler( if (id == "grand_exchange") { inventory = "collection_box_${player["grand_exchange_box", -1]}" } + if (inventory == "") { + return null + } if (!player.inventories.contains(inventory)) { logger.info { "Player doesn't have interface inventory [$player, interface=$id, inventory=$inventory]" } return null diff --git a/game/src/main/kotlin/content/bot/BotManager.kt b/game/src/main/kotlin/content/bot/BotManager.kt index 3a1bf4b74d..1124064d4d 100644 --- a/game/src/main/kotlin/content/bot/BotManager.kt +++ b/game/src/main/kotlin/content/bot/BotManager.kt @@ -10,7 +10,9 @@ import content.entity.player.bank.bank import world.gregs.voidps.engine.data.ConfigFiles import world.gregs.voidps.engine.data.Settings import world.gregs.voidps.engine.event.AuditLog +import world.gregs.voidps.engine.timer.toTicks import world.gregs.voidps.type.random +import java.util.concurrent.TimeUnit /** * Each tick checks @@ -96,14 +98,14 @@ class BotManager( return true } - private val idle = BotActivity("idle", 2048, actions = listOf(BotAction.Wait(50))) // 30s + private val idle = BotActivity("idle", 2048, actions = listOf(BotAction.Wait(TimeUnit.SECONDS.toTicks(30)))) private fun assignRandom(bot: Bot) { val activity = if (bot.previous != null && hasRequirements(bot, bot.previous!!)) { bot.previous } else { if (bot.player["debug", false]) { - logger.info { "Picking bot ${bot.player.accountName} new task from available: ${bot.available}." } + logger.trace { "Picking bot ${bot.player.accountName} new task from available: ${bot.available}." } } val id = bot.available.filter { val activity = activities[it] @@ -111,23 +113,22 @@ class BotManager( }.randomOrNull(random) // TODO weight by distance? if (id == null) { if (bot.player["debug", false]) { - logger.info { "Failed to find activity for bot ${bot.player.accountName}. Reasons:" } + logger.info { "Failed to find activity for bot ${bot.player.accountName}." } for (id in bot.available) { val activity = activities[id] ?: continue if (!slots.hasFree(activity)) { - logger.info { "Activity: $id - No available slots." } + logger.trace { "Activity: $id - No available slots." } } else if (bot.blocked.contains(activity.id)) { - logger.info { "Activity: $id - Blocked." } + logger.trace { "Activity: $id - Blocked." } } else { for (requirement in activity.requires) { if (!requirement.check(bot.player)) { - logger.info { "Activity: $id - Failed requirement: $requirement" } + logger.trace { "Activity: $id - Failed requirement: $requirement" } break } } } } - logger.info { "Picking bot ${bot.player.accountName} new task from available: ${bot.available}." } } } activities[id] ?: idle @@ -143,9 +144,9 @@ class BotManager( private fun assign(bot: Bot, activity: BotActivity) { AuditLog.event(bot, "assigned", activity.id) - if (bot.player["debug", false]) { +// if (bot.player["debug", false]) { logger.info { "Assigned bot: '${bot.player.accountName}' task '${activity.id}'." } - } +// } slots.occupy(activity) bot.previous = activity bot.queue(BehaviourFrame(activity)) @@ -219,21 +220,31 @@ class BotManager( if (condition.min == 1 || condition.min == 5 || condition.min == 10) { resolvers.add( Resolver( - "withdraw_${condition.fact.id}", weight = 20, actions = listOf( + "withdraw_${condition.fact.id}", weight = 20, + resolve = listOf( + Condition.AtLeast(Fact.InventorySpace, condition.min), + ), + actions = listOf( BotAction.GoToNearest("bank"), BotAction.InteractObject("Use-quickly", "bank_booth*", success = Condition.Equals(Fact.InterfaceOpen("bank"), true)), BotAction.InterfaceOption("Withdraw-${condition.min}", "bank:inventory:${condition.fact.id}"), + BotAction.CloseInterface, ) ) ) } else { resolvers.add( Resolver( - "withdraw_${condition.fact.id}", weight = 20, actions = listOf( + "withdraw_${condition.fact.id}", weight = 20, + resolve = listOf( + Condition.AtLeast(Fact.InventorySpace, condition.min), + ), + actions = listOf( BotAction.GoToNearest("bank"), BotAction.InteractObject("Use-quickly", "bank_booth*", success = Condition.Equals(Fact.InterfaceOpen("bank"), true)), BotAction.InterfaceOption("Withdraw-X", "bank:inventory:${condition.fact.id}"), BotAction.IntEntry(condition.min), + BotAction.CloseInterface, ) ) ) @@ -250,11 +261,13 @@ class BotManager( Resolver( "withdraw_and_equip_${condition.fact.id}", weight = 0, requires = listOf(Condition.AtLeast(Fact.BankCount(condition.fact.id), condition.min)), + resolve = listOf(Condition.AtLeast(Fact.InventorySpace, condition.min)), actions = listOf( BotAction.GoToNearest("bank"), BotAction.InteractObject("Use-quickly", "bank_booth*", success = Condition.Equals(Fact.InterfaceOpen("bank"), true)), BotAction.InterfaceOption("Withdraw-X", "bank:inventory:${condition.fact.id}"), BotAction.IntEntry(condition.min), + BotAction.CloseInterface, BotAction.InterfaceOption("Equip", "inventory:inventory:${condition.fact.id}") ) ) @@ -265,7 +278,7 @@ class BotManager( private fun execute(bot: Bot) { val frame = bot.frame() val behaviour = frame.behaviour - if (bot.player["debug", false] && frame.state != bot.get("previous_state")) { + if (bot.player["debug", false]) { logger.trace { "Bot task: ${behaviour.id} state: ${frame.state} action: ${frame.action()}." } bot["previous_state"] = frame.state } @@ -275,7 +288,7 @@ class BotManager( BehaviourState.Success -> { val debug = bot.player["debug", false] if (debug) { - logger.trace { "Completed action: ${frame.action()} for ${behaviour.id}." } + logger.debug { "Completed action: ${frame.action()} for ${behaviour.id}." } } if (!frame.next()) { AuditLog.event(bot, "completed", frame.behaviour.id) @@ -286,7 +299,7 @@ class BotManager( } } else { if (debug) { - logger.trace { "Next action: ${frame.action()} for ${behaviour.id}." } + logger.debug { "Next action: ${frame.action()} for ${behaviour.id}." } } frame.start(bot) } diff --git a/game/src/main/kotlin/content/bot/BotSpawns.kt b/game/src/main/kotlin/content/bot/BotSpawns.kt index ea5d96743f..23d5945b67 100644 --- a/game/src/main/kotlin/content/bot/BotSpawns.kt +++ b/game/src/main/kotlin/content/bot/BotSpawns.kt @@ -135,6 +135,8 @@ class BotSpawns( val bot = player.initBot() manager.add(bot) if (args.getOrNull(0)?.isNotBlank() == true) { + bot.available.clear() + bot.available.add(args[0]) manager.assign(bot, args[0]) } Bots.start(player) diff --git a/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt b/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt index 0b709a2619..b09c5bc5b7 100644 --- a/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt +++ b/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt @@ -77,6 +77,11 @@ data class BehaviourFragment( is BotAction.IntEntry -> BotAction.IntEntry( value = resolve(action.references["value"], copy.value), ) + is BotAction.Restart -> BotAction.Restart( + resolveReference(copy.check), + resolveReference(copy.success) ?: throw IllegalArgumentException("Restart action must have success condition.") + ) + is BotAction.CloseInterface -> BotAction.CloseInterface is BotAction.Wait -> BotAction.Wait(resolve(action.references["wait"], copy.ticks)) is BotAction.Clone, is BotAction.Reference -> throw IllegalArgumentException("Invalid reference action type: ${action.action::class.simpleName}.") } @@ -133,6 +138,11 @@ data class BehaviourFragment( val value = resolve(references["value"], req.value as? Boolean) Condition.Equals(Fact.HasTimer(id), value as? Boolean ?: true) } + "queue" -> { + val id = resolve(references[req.type], req.id) + val value = resolve(references["value"], req.value as? Boolean) + Condition.Equals(Fact.HasQueue(id), value as? Boolean ?: true) + } "interface" -> { val id = resolve(references[req.type], req.id) val value = resolve(references["value"], req.value as? Boolean) diff --git a/game/src/main/kotlin/content/bot/action/BotAction.kt b/game/src/main/kotlin/content/bot/action/BotAction.kt index c54360fea4..a76d241836 100644 --- a/game/src/main/kotlin/content/bot/action/BotAction.kt +++ b/game/src/main/kotlin/content/bot/action/BotAction.kt @@ -7,8 +7,11 @@ import content.bot.interact.path.Graph import content.entity.combat.attackers import content.entity.combat.dead import world.gregs.voidps.engine.GameLoop +import world.gregs.voidps.engine.client.command.playerCommand import world.gregs.voidps.engine.client.instruction.InstructionHandlers import world.gregs.voidps.engine.client.instruction.InterfaceHandler +import world.gregs.voidps.engine.client.ui.dialogue +import world.gregs.voidps.engine.client.ui.menu import world.gregs.voidps.engine.data.definition.Areas import world.gregs.voidps.engine.data.definition.InterfaceDefinitions import world.gregs.voidps.engine.data.definition.ItemDefinitions @@ -17,6 +20,7 @@ import world.gregs.voidps.engine.entity.character.mode.interact.PlayerOnFloorIte import world.gregs.voidps.engine.entity.character.mode.interact.PlayerOnNPCInteract import world.gregs.voidps.engine.entity.character.mode.interact.PlayerOnObjectInteract import world.gregs.voidps.engine.entity.character.npc.NPCs +import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.entity.character.player.skill.Skill import world.gregs.voidps.engine.entity.item.floor.FloorItems import world.gregs.voidps.engine.entity.obj.GameObjects @@ -25,6 +29,7 @@ import world.gregs.voidps.engine.get import world.gregs.voidps.engine.inv.inventory import world.gregs.voidps.engine.map.Spiral import world.gregs.voidps.network.client.instruction.* +import kotlin.to sealed interface BotAction { fun start(bot: Bot, frame: BehaviourFrame): BehaviourState = BehaviourState.Running @@ -206,7 +211,7 @@ sealed interface BotAction { private fun eat(bot: Bot): BehaviourState { val inventory = bot.player.inventory - for (index in inventory.indices){ + for (index in inventory.indices) { val item = inventory[index] val option = item.def.options.indexOf("Eat") if (option == -1) { @@ -323,7 +328,10 @@ sealed interface BotAction { } data class ItemOnItem(val item: String, val on: String, val success: Condition? = null) : BotAction { - override fun start(bot: Bot, frame: BehaviourFrame): BehaviourState { + override fun update(bot: Bot, frame: BehaviourFrame): BehaviourState? { + if (success != null && success.check(bot.player)) { + return BehaviourState.Success + } val inventory = bot.player.inventory val fromSlot = inventory.indexOf(item) if (fromSlot == -1) { @@ -335,16 +343,7 @@ sealed interface BotAction { } val from = inventory[fromSlot] val to = inventory[toSlot] - val valid = get().handle(bot.player, InteractInterfaceItem( - from.def.id, - to.def.id, - fromSlot, - toSlot, - 149, - 0, - 149, - 0 - )) + val valid = get().handle(bot.player, InteractInterfaceItem(from.def.id, to.def.id, fromSlot, toSlot, 149, 0, 149, 0)) return when { !valid -> BehaviourState.Failed(Reason.Invalid("Invalid item on item: ${from.def.id}:${fromSlot} -> ${to.def.id}:${toSlot}.")) success == null -> BehaviourState.Wait(1, BehaviourState.Success) @@ -352,17 +351,13 @@ sealed interface BotAction { else -> BehaviourState.Running } } + } + data class InterfaceOption(val option: String, val id: String, val success: Condition? = null) : BotAction { override fun update(bot: Bot, frame: BehaviourFrame): BehaviourState? { if (success != null && success.check(bot.player)) { return BehaviourState.Success } - return super.update(bot, frame) - } - } - - data class InterfaceOption(val option: String, val id: String, val success: Condition? = null) : BotAction { - override fun start(bot: Bot, frame: BehaviourFrame): BehaviourState { val definitions = get() val split = id.split(":") if (split.size < 2) { @@ -388,13 +383,15 @@ sealed interface BotAction { if (id == "shop") { itemSlot *= 6 } - val valid = get().handle(bot.player, InteractInterface( - interfaceId = def.id, - componentId = componentId, - itemId = itemDef?.id ?: -1, - itemSlot = itemSlot, - option = index - )) + val valid = get().handle( + bot.player, InteractInterface( + interfaceId = def.id, + componentId = componentId, + itemId = itemDef?.id ?: -1, + itemSlot = itemSlot, + option = index + ) + ) return when { !valid -> BehaviourState.Failed(Reason.Invalid("Invalid interaction: ${def.id}:${componentId}:${itemDef?.id} slot $itemSlot option ${index}.")) success == null -> BehaviourState.Wait(1, BehaviourState.Success) @@ -402,17 +399,13 @@ sealed interface BotAction { else -> BehaviourState.Running } } + } - override fun update(bot: Bot, frame: BehaviourFrame): BehaviourState? { + data class DialogueContinue(val option: String, val id: String, val success: Condition? = null) : BotAction { + override fun update(bot: Bot, frame: BehaviourFrame): BehaviourState { if (success != null && success.check(bot.player)) { return BehaviourState.Success } - return super.update(bot, frame) - } - } - - data class DialogueContinue(val option: String, val id: String, val success: Condition? = null) : BotAction { - override fun start(bot: Bot, frame: BehaviourFrame): BehaviourState { val definitions = get() val split = id.split(":") if (split.size < 2) { @@ -428,24 +421,17 @@ sealed interface BotAction { options = componentDef.getOrNull("options") ?: emptyArray() } val index = options.indexOf(option) - val valid = get().handle(bot.player, InteractDialogue( - interfaceId = def.id, - componentId = componentId, - option = index - )) - return when { - !valid -> BehaviourState.Failed(Reason.Invalid("Invalid interaction: ${def.id}:${componentId} option=${index}.")) - success == null -> BehaviourState.Wait(1, BehaviourState.Success) - success.check(bot.player) -> BehaviourState.Success - else -> BehaviourState.Running + val valid = get().handle( + bot.player, InteractDialogue( + interfaceId = def.id, + componentId = componentId, + option = index + ) + ) + if (!valid) { + return BehaviourState.Failed(Reason.Invalid("Invalid interaction: ${def.id}:${componentId} option=${index}.")) } - } - - override fun update(bot: Bot, frame: BehaviourFrame): BehaviourState? { - if (success != null && success.check(bot.player)) { - return BehaviourState.Success - } - return super.update(bot, frame) + return BehaviourState.Wait(1, BehaviourState.Success) } } @@ -456,6 +442,39 @@ sealed interface BotAction { } } + object CloseInterface : BotAction { + override fun update(bot: Bot, frame: BehaviourFrame): BehaviourState { + if (bot.player.menu == null) { + return BehaviourState.Success + } + if (get().handle(bot.player, InterfaceClosedInstruction)) { + return BehaviourState.Success + } + return BehaviourState.Failed(Reason.NoTarget) + } + } + + /** + * Restarts the current action when [check] doesn't hold true (or bot has no mode) and success state isn't matched. + */ + data class Restart( + val check: Condition?, + val success: Condition + ) : BotAction { + override fun update(bot: Bot, frame: BehaviourFrame): BehaviourState { + if (success.check(bot.player)) { + return BehaviourState.Success + } + if (check == null && bot.mode !is EmptyMode) { + return BehaviourState.Running + } else if (check != null && check.check(bot.player)) { + return BehaviourState.Running + } + frame.index = 0 + return BehaviourState.Running + } + } + data class StringEntry(val value: String) : BotAction { override fun start(bot: Bot, frame: BehaviourFrame): BehaviourState { bot.player.instructions.trySend(EnterString(value)) diff --git a/game/src/main/kotlin/content/bot/action/BotActivity.kt b/game/src/main/kotlin/content/bot/action/BotActivity.kt index 57e2d24167..a97c498013 100644 --- a/game/src/main/kotlin/content/bot/action/BotActivity.kt +++ b/game/src/main/kotlin/content/bot/action/BotActivity.kt @@ -6,7 +6,6 @@ import world.gregs.config.Config import world.gregs.config.ConfigReader import world.gregs.voidps.engine.event.Wildcard import world.gregs.voidps.engine.timedLoad -import kotlin.math.min /** * An activity with a limited number of slots that bots can perform @@ -195,7 +194,7 @@ private fun ConfigReader.requirement(exact: Boolean = false): Condition { val references = mutableMapOf() while (nextEntry()) { when (val key = key()) { - "skill", "carries", "equips", "interface", "owns", "clock", "variable", "clone", "location" -> { + "skill", "carries", "equips", "interface", "owns", "clock", "variable", "clone", "location", "queue" -> { type = key id = string() if (id.contains('$')) { @@ -253,6 +252,7 @@ private fun getRequirement(type: String, id: String, min: Int?, max: Int?, value "equips" -> Condition.split(id, min, max, Wildcard.Item) { Fact.EquipCount(it) } "clock" -> Condition.split(id, min, max, Wildcard.Variables) { Fact.ClockRemaining(it) } "timer" -> Condition.Equals(Fact.HasTimer(id), value as? Boolean ?: true) + "queue" -> Condition.Equals(Fact.HasQueue(id), value as? Boolean ?: true) "interface" -> Condition.Equals(Fact.InterfaceOpen(id), value as? Boolean ?: true) "variable" -> when (value) { is Int -> Condition.Equals(Fact.IntVariable(id, default as? Int), value) @@ -282,6 +282,7 @@ fun ConfigReader.actions(list: MutableList) { var x = 0 var y = 0 var success: Condition? = null + var check: Condition? = null val references = mutableMapOf() while (nextEntry()) { when (val key = key()) { @@ -326,7 +327,12 @@ fun ConfigReader.actions(list: MutableList) { references[key] = option } } + "restart" -> { + require(boolean()) { "Can't have restart = false ${exception()}" } + type = key + } "success" -> success = requirement(exact = true) + "check" -> check = requirement(exact = true) "wait" -> { type = key when (val value = value()) { @@ -372,6 +378,7 @@ fun ConfigReader.actions(list: MutableList) { "enter_string" -> BotAction.StringEntry(id) "enter_int" -> BotAction.IntEntry(int) "wait" -> BotAction.Wait(ticks) + "restart" -> BotAction.Restart(check, success ?: throw IllegalArgumentException("Restart must have success condition.")) "npc" -> if (option == "Attack") { BotAction.FightNpc(id = id, delay = delay, success = success, healPercentage = heal, lootOverValue = loot, radius = radius) } else { diff --git a/game/src/main/kotlin/content/bot/fact/Fact.kt b/game/src/main/kotlin/content/bot/fact/Fact.kt index 531d316702..cc45d1597c 100644 --- a/game/src/main/kotlin/content/bot/fact/Fact.kt +++ b/game/src/main/kotlin/content/bot/fact/Fact.kt @@ -87,6 +87,11 @@ sealed class Fact(val priority: Int) { override fun getValue(player: Player) = player.timers.contains(timer) } + data class HasQueue(val queue: String) : Fact(1) { + override fun keys() = setOf("queue:$queue") + override fun getValue(player: Player) = player.queue.contains(queue) + } + data class InterfaceOpen(val id: String) : Fact(1) { override fun keys() = setOf("iface:$id") override fun getValue(player: Player) = player.interfaces.contains(id) diff --git a/game/src/main/kotlin/content/entity/player/dialogue/DialogueInput.kt b/game/src/main/kotlin/content/entity/player/dialogue/DialogueInput.kt index 8f703073c7..cbb0b07aab 100644 --- a/game/src/main/kotlin/content/entity/player/dialogue/DialogueInput.kt +++ b/game/src/main/kotlin/content/entity/player/dialogue/DialogueInput.kt @@ -65,6 +65,7 @@ class DialogueInput : Script { continueDialogue("dialogue_skill_creation:choice*") { val choice = it.substringAfter(":choice").toIntOrNull() ?: 0 (dialogueSuspension as? IntSuspension)?.resume(choice - 1) + closeDialogue() } } } From 099e293f5f4cbe4b939b7c424d63bcc63d5a98c3 Mon Sep 17 00:00:00 2001 From: GregHib Date: Thu, 5 Feb 2026 20:55:08 +0000 Subject: [PATCH 054/101] Add fletching bots --- .../misthalin/lumbridge/lumbridge.bots.toml | 30 ++++++++++ data/bot/fletching.bots.toml | 58 +++++++++++++++++++ 2 files changed, 88 insertions(+) create mode 100644 data/bot/fletching.bots.toml diff --git a/data/area/misthalin/lumbridge/lumbridge.bots.toml b/data/area/misthalin/lumbridge/lumbridge.bots.toml index 6627e2123c..d210e8c11b 100644 --- a/data/area/misthalin/lumbridge/lumbridge.bots.toml +++ b/data/area/misthalin/lumbridge/lumbridge.bots.toml @@ -21,6 +21,36 @@ produces = [ { skill = "attack" } ] +# Fletching +[lumbridge_fletch_arrow_shafts] +template = "fletching_arrow_shafts_template" +capacity = 2 +fields = { location = "lumbridge_castle_bank", logs = "logs" } +requires = [ + { skill = "fletching", min = 1 } +] + +[lumbridge_fletch_shortbows] +template = "fletching_shortbow_template" +capacity = 2 +fields = { location = "lumbridge_castle_bank", logs = "logs" } +requires = [ + { skill = "fletching", min = 5 } +] +produces = [ + { carries = "shortbow_u" } +] + +[lumbridge_fletch_longbows] +template = "fletching_longbow_template" +fields = { location = "lumbridge_castle_bank", logs = "logs" } +requires = [ + { skill = "fletching", min = 10 } +] +produces = [ + { carries = "longbow_u" } +] + # Woodcutting [lumbridge_north_tree_cutting] template = "normal_tree_template" diff --git a/data/bot/fletching.bots.toml b/data/bot/fletching.bots.toml new file mode 100644 index 0000000000..00d6e5c5d8 --- /dev/null +++ b/data/bot/fletching.bots.toml @@ -0,0 +1,58 @@ +[fletching_arrow_shafts_template] +capacity = 2 +requires = [ + { owns = "$logs", min = 27 } +] +setup = [ + { location = "$location" }, + { carries = "knife" }, + { carries = "$logs", min = 27 } +] +actions = [ + { item = "knife", on = "$logs", success = { interface = "dialogue_skill_creation" } }, + { option = "All", interface = "skill_creation_amount:all" }, + { continue = "dialogue_skill_creation:choice1" }, + { restart = true, check = { queue = "fletching" }, success = { inventory_space = 26 } } +] +produces = [ + { carries = "arrow_shafts" }, + { skill = "fletching" } +] + +[fletching_shortbow_template] +requires = [ + { owns = "$logs", min = 27 } +] +setup = [ + { location = "$location" }, + { carries = "knife" }, + { carries = "$logs", min = 27 } +] +actions = [ + { item = "knife", on = "$logs" }, + { option = "All", interface = "skill_creation_amount:all" }, + { continue = "dialogue_skill_creation:choice2", success = { queue = "fletching" } }, + { restart = true, success = { inventory_space = 26 } } +] +produces = [ + { skill = "fletching" } +] + +[fletching_longbow_template] +requires = [ + { owns = "$logs", min = 27 } +] +setup = [ + { location = "$location" }, + { carries = "knife" }, + { carries = "$logs", min = 27 } +] +actions = [ + { item = "knife", on = "$logs", success = { queue = "fletching_make_dialog" } }, + { option = "All", interface = "skill_creation_amount:all" }, + { continue = "dialogue_skill_creation:choice3", success = { queue = "fletching" } }, + { restart = true, success = { inventory_space = 26 } } +] +produces = [ + { skill = "fletching" } +] From 3c3dcdd16932c7451959a4c3877fa8d828bc0463 Mon Sep 17 00:00:00 2001 From: GregHib Date: Fri, 6 Feb 2026 11:39:27 +0000 Subject: [PATCH 055/101] WIP --- data/area/misthalin/lumbridge/lumbridge.bots.toml | 9 +++++++++ data/bot/thieving_templates.bots.toml | 14 ++++++++++++++ .../main/kotlin/content/bot/action/BotAction.kt | 3 ++- 3 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 data/bot/thieving_templates.bots.toml diff --git a/data/area/misthalin/lumbridge/lumbridge.bots.toml b/data/area/misthalin/lumbridge/lumbridge.bots.toml index d210e8c11b..3c156898e0 100644 --- a/data/area/misthalin/lumbridge/lumbridge.bots.toml +++ b/data/area/misthalin/lumbridge/lumbridge.bots.toml @@ -21,6 +21,15 @@ produces = [ { skill = "attack" } ] +# Thieving +[lumbridge_pickpocket_men] +template = "pickpocketting_template" +capacity = 2 +fields = { location = "lumbridge_castle", npc = "men*,women*" } +requires = [ + { skill = "thieving", min = 1 } +] + # Fletching [lumbridge_fletch_arrow_shafts] template = "fletching_arrow_shafts_template" diff --git a/data/bot/thieving_templates.bots.toml b/data/bot/thieving_templates.bots.toml new file mode 100644 index 0000000000..7dbcf40f58 --- /dev/null +++ b/data/bot/thieving_templates.bots.toml @@ -0,0 +1,14 @@ +[pickpocketting_template] +capacity = 3 +setup = [ + { inventory_space = 1 }, + { location = "$location" }, +] +actions = [ + { option = "Pickpocket", npc = "$npc" }, + { restart = true, check = { skill = "constitution", min = 20 }, success = { skill = "thieving", min = 5 } } +] +produces = [ + { carries = "arrow_shafts" }, + { skill = "fletching" } +] \ No newline at end of file diff --git a/game/src/main/kotlin/content/bot/action/BotAction.kt b/game/src/main/kotlin/content/bot/action/BotAction.kt index a76d241836..2e419e2c48 100644 --- a/game/src/main/kotlin/content/bot/action/BotAction.kt +++ b/game/src/main/kotlin/content/bot/action/BotAction.kt @@ -502,12 +502,13 @@ sealed interface BotAction { * bone burying bot * cooking bot * cow bot - * fletching bot * rune mysteries quest bot * bot saving? * bot setups * bot spawning in other locations * + * TODO need a better way of managing inventory. Removing unneeded items in favour of required ones vs emptying full inventory every time etc... + * TODO how to handle repeat actions e.g. repeat Chop-down trees until inv is full - These are actions Gathering, Skilling etc.. more resolvers like bank all, drop cheap items how to handle combat, one task or multiple? - One Fight action From 96371af453183caa8ea04c3a76795ca671d2c809 Mon Sep 17 00:00:00 2001 From: GregHib Date: Fri, 6 Feb 2026 14:43:04 +0000 Subject: [PATCH 056/101] Tweak reset waiting conditions and thieving, add bone burying --- .../misthalin/lumbridge/lumbridge.areas.toml | 6 ++- .../misthalin/lumbridge/lumbridge.bots.toml | 4 +- data/bot/fletching.bots.toml | 2 +- data/bot/prayer.bots.toml | 16 +++++++ data/bot/thieving_templates.bots.toml | 10 ++-- .../src/main/kotlin/content/bot/BotManager.kt | 48 ++++++++++++------- game/src/main/kotlin/content/bot/BotSpawns.kt | 8 ++-- .../content/bot/action/BehaviourFragment.kt | 18 ++++--- .../kotlin/content/bot/action/BotAction.kt | 11 +++-- .../kotlin/content/bot/action/BotActivity.kt | 28 ++++++----- game/src/main/kotlin/content/bot/fact/Fact.kt | 2 +- 11 files changed, 103 insertions(+), 50 deletions(-) create mode 100644 data/bot/prayer.bots.toml diff --git a/data/area/misthalin/lumbridge/lumbridge.areas.toml b/data/area/misthalin/lumbridge/lumbridge.areas.toml index 28b42433ed..92fb68f325 100644 --- a/data/area/misthalin/lumbridge/lumbridge.areas.toml +++ b/data/area/misthalin/lumbridge/lumbridge.areas.toml @@ -207,4 +207,8 @@ y = [3270, 3275] x = [3136, 3263] y = [3136, 3327] tags = ["penguin_area"] -hint = "in the south of the kingdom of Misthalin." \ No newline at end of file +hint = "in the south of the kingdom of Misthalin." + +[lumbridge_thieving_area] +x = [3220, 3225] +y = [3236, 3242] \ No newline at end of file diff --git a/data/area/misthalin/lumbridge/lumbridge.bots.toml b/data/area/misthalin/lumbridge/lumbridge.bots.toml index 3c156898e0..4db52b224b 100644 --- a/data/area/misthalin/lumbridge/lumbridge.bots.toml +++ b/data/area/misthalin/lumbridge/lumbridge.bots.toml @@ -22,10 +22,10 @@ produces = [ ] # Thieving -[lumbridge_pickpocket_men] +[lumbridge_pickpocketing] template = "pickpocketting_template" capacity = 2 -fields = { location = "lumbridge_castle", npc = "men*,women*" } +fields = { location = "lumbridge_thieving_area", npc = "lumbridge_man*,lumbridge_woman*" } requires = [ { skill = "thieving", min = 1 } ] diff --git a/data/bot/fletching.bots.toml b/data/bot/fletching.bots.toml index 00d6e5c5d8..45c5b802a8 100644 --- a/data/bot/fletching.bots.toml +++ b/data/bot/fletching.bots.toml @@ -12,7 +12,7 @@ actions = [ { item = "knife", on = "$logs", success = { interface = "dialogue_skill_creation" } }, { option = "All", interface = "skill_creation_amount:all" }, { continue = "dialogue_skill_creation:choice1" }, - { restart = true, check = { queue = "fletching" }, success = { inventory_space = 26 } } + { restart = true, wait_if = [{ queue = "fletching" }], success = { inventory_space = 26 } } ] produces = [ { carries = "arrow_shafts" }, diff --git a/data/bot/prayer.bots.toml b/data/bot/prayer.bots.toml new file mode 100644 index 0000000000..b5d60e5436 --- /dev/null +++ b/data/bot/prayer.bots.toml @@ -0,0 +1,16 @@ +[bury_bones] +type = "activity" +capacity = 5 +requires = [ + { owns = "bones", amount = 28 }, +] +setup = [ + { carries = "bones", amount = 28 }, +] +actions = [ + { option = "Bury", interface = "inventory:inventory:bones" }, + { restart = true, wait_if = [{ variable = "bone_delay", min = 0, default = -1 }], success = { inventory_space = 28 } } +] +produces = [ + { skill = "prayer" } +] diff --git a/data/bot/thieving_templates.bots.toml b/data/bot/thieving_templates.bots.toml index 7dbcf40f58..8a57103e87 100644 --- a/data/bot/thieving_templates.bots.toml +++ b/data/bot/thieving_templates.bots.toml @@ -1,14 +1,18 @@ [pickpocketting_template] capacity = 3 +requires = [ + { skill = "constitution", min = 100 }, + { skill = "thieving", min = 1, max = 10 }, +] setup = [ { inventory_space = 1 }, { location = "$location" }, ] actions = [ { option = "Pickpocket", npc = "$npc" }, - { restart = true, check = { skill = "constitution", min = 20 }, success = { skill = "thieving", min = 5 } } + { restart = true, wait_if = [{ variable = "delay", min = 0, default = -1 }, { clock = "stunned", min = 0 }], success = { skill = "constitution", max = 20 } } ] produces = [ - { carries = "arrow_shafts" }, - { skill = "fletching" } + { carries = "coins" }, + { skill = "thieving" } ] \ No newline at end of file diff --git a/game/src/main/kotlin/content/bot/BotManager.kt b/game/src/main/kotlin/content/bot/BotManager.kt index 1124064d4d..548a38a3af 100644 --- a/game/src/main/kotlin/content/bot/BotManager.kt +++ b/game/src/main/kotlin/content/bot/BotManager.kt @@ -30,6 +30,9 @@ class BotManager( val bots = mutableListOf() private val logger = InlineLogger("BotManager") + val activityNames: Set + get() = activities.keys + fun add(bot: Bot) { bots.add(bot) for (activity in activities.values) { @@ -85,7 +88,11 @@ class BotManager( assignRandom(bot) return } - execute(bot) + try { + execute(bot) + } catch (exception: Exception) { + logger.error(exception) { "Error in bot '${bot.player.accountName}' tick ${bot.frames.map { it.behaviour.id }}." } + } } private fun hasRequirements(bot: Bot, activity: BotActivity): Boolean { @@ -144,9 +151,9 @@ class BotManager( private fun assign(bot: Bot, activity: BotActivity) { AuditLog.event(bot, "assigned", activity.id) -// if (bot.player["debug", false]) { + if (bot.player["debug", false]) { logger.info { "Assigned bot: '${bot.player.accountName}' task '${activity.id}'." } -// } + } slots.occupy(activity) bot.previous = activity bot.queue(BehaviourFrame(activity)) @@ -164,10 +171,25 @@ class BotManager( if (requirement.check(bot.player)) { continue } - val resolver = pickResolver(bot, requirement, frame) + val resolvers = availableResolvers(bot, requirement) + val resolver = resolvers + .filter { !frame.blocked.contains(it.id) && it.requires.none { fact -> !fact.check(bot.player) } } + .minByOrNull { it.weight } if (resolver == null) { if (bot.player["debug", false]) { logger.info { "No resolver found for for ${behaviour.id} keys: ${requirement.keys()} requirement: ${requirement}." } + for (resolver in resolvers) { + if (frame.blocked.contains(resolver.id)) { + logger.debug { "Resolver: ${resolver.id} - Blocked by frame behaviour: ${frame.behaviour.id}." } + break + } + for (requirement in resolver.requires) { + if (!requirement.check(bot.player)) { + logger.debug { "Resolver: ${resolver.id} - Failed requirement: $requirement." } + break + } + } + } } frame.fail(Reason.Requirement(requirement)) // No way to resolve return @@ -190,7 +212,7 @@ class BotManager( frame.start(bot) } - private fun pickResolver(bot: Bot, condition: Condition, frame: BehaviourFrame): Behaviour? { + private fun availableResolvers(bot: Bot, condition: Condition): MutableList { val options = mutableListOf() addDefaultResolvers(bot, options, condition) if (condition is Condition.Any) { @@ -198,19 +220,10 @@ class BotManager( addDefaultResolvers(bot, options, condition) } } - options.removeAll { frame.blocked.contains(it.id) || it.requires.any { fact -> !fact.check(bot.player) } } for (key in condition.keys()) { - for (resolver in resolvers[key] ?: emptyList()) { - if (frame.blocked.contains(resolver.id)) { - continue - } - if (resolver.requires.any { fact -> !fact.check(bot.player) }) { - continue - } - options.add(resolver) - } + options.addAll(resolvers[key] ?: continue) } - return options.minByOrNull { it.weight } + return options } private fun addDefaultResolvers(bot: Bot, resolvers: MutableList, condition: Condition) { @@ -293,6 +306,9 @@ class BotManager( if (!frame.next()) { AuditLog.event(bot, "completed", frame.behaviour.id) bot.frames.pop() + if (!bot.noTask()) { + bot.frame().blocked.remove(behaviour.id) + } if (behaviour is BotActivity) { bot.blocked.remove(behaviour.id) slots.release(behaviour) diff --git a/game/src/main/kotlin/content/bot/BotSpawns.kt b/game/src/main/kotlin/content/bot/BotSpawns.kt index 23d5945b67..be7cc2ee70 100644 --- a/game/src/main/kotlin/content/bot/BotSpawns.kt +++ b/game/src/main/kotlin/content/bot/BotSpawns.kt @@ -67,7 +67,7 @@ class BotSpawns( adminCommand("bots", intArg("count", optional = true), desc = "Spawn (count) number of bots", handler = ::spawn) adminCommand("clear_bots", intArg("count", optional = true), desc = "Clear all or some amount of bots", handler = ::clear) - adminCommand("bot", stringArg("task", optional = true, autofill = tasks.names), desc = "Toggle yourself on/off as a bot player", handler = ::toggle) + adminCommand("bot", stringArg("task", optional = true, autofill = manager.activityNames), desc = "Toggle yourself on/off as a bot player", handler = ::toggle) adminCommand("bot_info", desc = "Print bot info", handler = ::info) } @@ -136,8 +136,10 @@ class BotSpawns( manager.add(bot) if (args.getOrNull(0)?.isNotBlank() == true) { bot.available.clear() - bot.available.add(args[0]) - manager.assign(bot, args[0]) + val name = args[0] + bot.blocked.remove(name) + bot.available.add(name) + manager.assign(bot, name) } Bots.start(player) player.message("Bot enabled.") diff --git a/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt b/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt index b09c5bc5b7..c61bcf9d99 100644 --- a/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt +++ b/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt @@ -78,7 +78,7 @@ data class BehaviourFragment( value = resolve(action.references["value"], copy.value), ) is BotAction.Restart -> BotAction.Restart( - resolveReference(copy.check), + copy.wait.map { resolveReference(it) ?: throw IllegalArgumentException("Restart wait must have success condition.") }, resolveReference(copy.success) ?: throw IllegalArgumentException("Restart action must have success condition.") ) is BotAction.CloseInterface -> BotAction.CloseInterface @@ -151,12 +151,16 @@ data class BehaviourFragment( "variable" -> { val id = resolve(references[req.type], req.id) val default = resolve(references["default"], req.default) - when (val value = resolve(references["value"], req.value)) { - is Int -> Condition.Equals(Fact.IntVariable(id, default as? Int), value) - is String -> Condition.Equals(Fact.StringVariable(id, default as? String), value) - is Double -> Condition.Equals(Fact.DoubleVariable(id, default as? Double), value) - is Boolean -> Condition.Equals(Fact.BoolVariable(id, default as? Boolean), value) - else -> null + if (min != null || max != null) { + Condition.range(Fact.IntVariable(id, default as Int), min, max) + } else { + when (val value = resolve(references["value"], req.value)) { + is Int -> Condition.Equals(Fact.IntVariable(id, default as Int), value) + is String -> Condition.Equals(Fact.StringVariable(id, default as? String), value) + is Double -> Condition.Equals(Fact.DoubleVariable(id, default as? Double), value) + is Boolean -> Condition.Equals(Fact.BoolVariable(id, default as? Boolean), value) + else -> null + } } } "inventory_space" -> { diff --git a/game/src/main/kotlin/content/bot/action/BotAction.kt b/game/src/main/kotlin/content/bot/action/BotAction.kt index 2e419e2c48..5774e7b78c 100644 --- a/game/src/main/kotlin/content/bot/action/BotAction.kt +++ b/game/src/main/kotlin/content/bot/action/BotAction.kt @@ -156,9 +156,10 @@ sealed interface BotAction { private fun search(bot: Bot): BehaviourState { val player = bot.player + val ids = if (id.contains(",")) id.split(",") else listOf(id) for (tile in Spiral.spiral(player.tile, radius)) { for (npc in NPCs.at(tile)) { - if (!wildcardEquals(id, npc.id)) { + if (ids.none { wildcardEquals(it, npc.id) }) { continue } val index = npc.def(player).options.indexOf(option) @@ -458,16 +459,16 @@ sealed interface BotAction { * Restarts the current action when [check] doesn't hold true (or bot has no mode) and success state isn't matched. */ data class Restart( - val check: Condition?, - val success: Condition + val wait: List, + val success: Condition, ) : BotAction { override fun update(bot: Bot, frame: BehaviourFrame): BehaviourState { if (success.check(bot.player)) { return BehaviourState.Success } - if (check == null && bot.mode !is EmptyMode) { + if (wait.isEmpty() && bot.mode !is EmptyMode) { return BehaviourState.Running - } else if (check != null && check.check(bot.player)) { + } else if (wait.any { it.check(bot.player) }) { return BehaviourState.Running } frame.index = 0 diff --git a/game/src/main/kotlin/content/bot/action/BotActivity.kt b/game/src/main/kotlin/content/bot/action/BotActivity.kt index a97c498013..3ef6f2aea7 100644 --- a/game/src/main/kotlin/content/bot/action/BotActivity.kt +++ b/game/src/main/kotlin/content/bot/action/BotActivity.kt @@ -177,9 +177,9 @@ private fun ConfigReader.fields(): Map { return map } -fun ConfigReader.requirements(list: MutableList) { +fun ConfigReader.requirements(list: MutableList, exact: Boolean = false) { while (nextElement()) { - list.add(requirement()) + list.add(requirement(exact)) } list.sortBy { it.priority() } } @@ -254,12 +254,18 @@ private fun getRequirement(type: String, id: String, min: Int?, max: Int?, value "timer" -> Condition.Equals(Fact.HasTimer(id), value as? Boolean ?: true) "queue" -> Condition.Equals(Fact.HasQueue(id), value as? Boolean ?: true) "interface" -> Condition.Equals(Fact.InterfaceOpen(id), value as? Boolean ?: true) - "variable" -> when (value) { - is Int -> Condition.Equals(Fact.IntVariable(id, default as? Int), value) - is String -> Condition.Equals(Fact.StringVariable(id, default as? String), value) - is Double -> Condition.Equals(Fact.DoubleVariable(id, default as? Double), value) - is Boolean -> Condition.Equals(Fact.BoolVariable(id, default as? Boolean), value) - else -> null + "variable" -> { + if (min != null || max != null) { + Condition.range(Fact.IntVariable(id, default as Int), min, max) + } else { + when (value) { + is Int -> Condition.Equals(Fact.IntVariable(id, default as Int), value) + is String -> Condition.Equals(Fact.StringVariable(id, default as? String), value) + is Double -> Condition.Equals(Fact.DoubleVariable(id, default as? Double), value) + is Boolean -> Condition.Equals(Fact.BoolVariable(id, default as? Boolean), value) + else -> null + } + } } "clone" -> Condition.Clone(id) "inventory_space" -> if (exact && min != null) Condition.Equals(Fact.InventorySpace, min) else Condition.range(Fact.InventorySpace, min, max) @@ -282,8 +288,8 @@ fun ConfigReader.actions(list: MutableList) { var x = 0 var y = 0 var success: Condition? = null - var check: Condition? = null val references = mutableMapOf() + val wait = mutableListOf() while (nextEntry()) { when (val key = key()) { "go_to", "go_to_nearest", "enter_string", "interface", "npc", "object", "clone", "item", "continue" -> { @@ -332,7 +338,7 @@ fun ConfigReader.actions(list: MutableList) { type = key } "success" -> success = requirement(exact = true) - "check" -> check = requirement(exact = true) + "wait_if" -> requirements(wait, exact = true) "wait" -> { type = key when (val value = value()) { @@ -378,7 +384,7 @@ fun ConfigReader.actions(list: MutableList) { "enter_string" -> BotAction.StringEntry(id) "enter_int" -> BotAction.IntEntry(int) "wait" -> BotAction.Wait(ticks) - "restart" -> BotAction.Restart(check, success ?: throw IllegalArgumentException("Restart must have success condition.")) + "restart" -> BotAction.Restart(wait, success ?: throw IllegalArgumentException("Restart must have success condition.")) "npc" -> if (option == "Attack") { BotAction.FightNpc(id = id, delay = delay, success = success, healPercentage = heal, lootOverValue = loot, radius = radius) } else { diff --git a/game/src/main/kotlin/content/bot/fact/Fact.kt b/game/src/main/kotlin/content/bot/fact/Fact.kt index cc45d1597c..f3d528883b 100644 --- a/game/src/main/kotlin/content/bot/fact/Fact.kt +++ b/game/src/main/kotlin/content/bot/fact/Fact.kt @@ -57,7 +57,7 @@ sealed class Fact(val priority: Int) { override fun getValue(player: Player) = player.equipment.count(id) } - data class IntVariable(val id: String, val default: Int?) : Fact(1) { + data class IntVariable(val id: String, val default: Int) : Fact(1) { override fun keys() = setOf("var:${id}") override fun getValue(player: Player) = player.variables.get(id) ?: default } From c51163da6660a8cf590f39b5c2d704fab624e4c1 Mon Sep 17 00:00:00 2001 From: GregHib Date: Fri, 6 Feb 2026 15:03:13 +0000 Subject: [PATCH 057/101] Add cow bots --- .../misthalin/lumbridge/lumbridge.areas.toml | 6 +- .../misthalin/lumbridge/lumbridge.bots.toml | 66 ++++++++++++------- .../area/misthalin/varrock/varrock.areas.toml | 4 ++ data/area/misthalin/varrock/varrock.bots.toml | 59 +++++++++++++++++ data/bot/combat_template.bots.toml | 42 ++++++++++++ data/bot/lumbridge.nav-edges.toml | 4 ++ .../kotlin/content/bot/action/BotAction.kt | 2 - 7 files changed, 158 insertions(+), 25 deletions(-) create mode 100644 data/bot/combat_template.bots.toml diff --git a/data/area/misthalin/lumbridge/lumbridge.areas.toml b/data/area/misthalin/lumbridge/lumbridge.areas.toml index 92fb68f325..191cb6b6ab 100644 --- a/data/area/misthalin/lumbridge/lumbridge.areas.toml +++ b/data/area/misthalin/lumbridge/lumbridge.areas.toml @@ -211,4 +211,8 @@ hint = "in the south of the kingdom of Misthalin." [lumbridge_thieving_area] x = [3220, 3225] -y = [3236, 3242] \ No newline at end of file +y = [3236, 3242] + +[lumbridge_west_cow_field] +x = [3156, 3202] +y = [3316, 3341] diff --git a/data/area/misthalin/lumbridge/lumbridge.bots.toml b/data/area/misthalin/lumbridge/lumbridge.bots.toml index 4db52b224b..2d4085a6b0 100644 --- a/data/area/misthalin/lumbridge/lumbridge.bots.toml +++ b/data/area/misthalin/lumbridge/lumbridge.bots.toml @@ -1,25 +1,47 @@ -[kill_chickens] -type = "activity" -capacity = 4 -requires = [ - { skill = "attack", min = 1, max = 5 }, -] -setup = [ - { equips = "bronze_sword,bronze_dagger,bronze_scimitar" }, - # TODO food? - { location = "lumbridge_chicken_pen" }, - { inventory_space = 27 }, -] -actions = [ - { option = "Select", interface = "combat_styles:style1" }, - { option = "Attack", npc = "chicken*", delay = 5, success = { inventory_space = 0 } }, -] -produces = [ - { carries = "feather" }, - { carries = "bones" }, - { carries = "raw_chicken" }, - { skill = "attack" } -] +[kill_freds_chickens_attack] +template = "kill_chickens" +capacity = 2 +fields = { skill = "attack", location = "lumbridge_chicken_pen", style = "style1" } + +[kill_freds_chickens_strength] +template = "kill_chickens" +capacity = 2 +fields = { skill = "strength", location = "lumbridge_chicken_pen", style = "style2" } + +[kill_freds_chickens_defence] +template = "kill_chickens" +capacity = 2 +fields = { skill = "defence", location = "lumbridge_chicken_pen", style = "style4" } + +[kill_millies_cows_attack] +template = "kill_cows" +capacity = 2 +fields = { skill = "attack", location = "lumbridge_east_cow_field", style = "style1" } + +[kill_millies_cows_strength] +template = "kill_cows" +capacity = 2 +fields = { skill = "strength", location = "lumbridge_east_cow_field", style = "style2" } + +[kill_millies_cows_defence] +template = "kill_cows" +capacity = 2 +fields = { skill = "defence", location = "lumbridge_east_cow_field", style = "style4" } + +[kill_bills_cows_attack] +template = "kill_cows" +capacity = 2 +fields = { skill = "attack", location = "lumbridge_west_cow_field", style = "style1" } + +[kill_bills_cows_strength] +template = "kill_cows" +capacity = 2 +fields = { skill = "strength", location = "lumbridge_west_cow_field", style = "style2" } + +[kill_bills_cows_defence] +template = "kill_cows" +capacity = 2 +fields = { skill = "defence", location = "lumbridge_west_cow_field", style = "style4" } # Thieving [lumbridge_pickpocketing] diff --git a/data/area/misthalin/varrock/varrock.areas.toml b/data/area/misthalin/varrock/varrock.areas.toml index 39d2843864..347c0a6356 100644 --- a/data/area/misthalin/varrock/varrock.areas.toml +++ b/data/area/misthalin/varrock/varrock.areas.toml @@ -63,3 +63,7 @@ x = [3136, 3391] y = [3328, 3519] tags = ["penguin_area"] hint = "in the north of the kingdom of Misthalin." + +[varrock_sword_shop] +x = [3202, 3209] +y = [3395, 3403] diff --git a/data/area/misthalin/varrock/varrock.bots.toml b/data/area/misthalin/varrock/varrock.bots.toml index 518093de1f..0d225847fe 100644 --- a/data/area/misthalin/varrock/varrock.bots.toml +++ b/data/area/misthalin/varrock/varrock.bots.toml @@ -33,3 +33,62 @@ fields = { location = "varrock_south_west_mine" } template = "iron_ore_template" capacity = 2 fields = { location = "varrock_south_west_mine" } + +# Varrock sword shop + +[take_bronze_sword] +type = "resolver" +weight = 30 +template = "take_shop_sample" +fields = { shop_location = "varrock_sword_shop", shopkeeper = "sword_shop*,dealga", item = "bronze_sword" } + +[buy_bronze_sword] +type = "resolver" +weight = 35 +template = "buy_from_shop" +fields = { cost = 26, shop_location = "varrock_sword_shop", shopkeeper = "sword_shop*,dealga", item = "bronze_sword" } + +[buy_iron_sword] +type = "resolver" +weight = 35 +template = "buy_from_shop" +fields = { cost = 91, shop_location = "varrock_sword_shop", shopkeeper = "sword_shop*,dealga", item = "iron_sword" } + +[buy_steel_sword] +type = "resolver" +weight = 35 +template = "buy_from_shop" +fields = { cost = 325, shop_location = "varrock_sword_shop", shopkeeper = "sword_shop*,dealga", item = "steel_sword" } +requires = [ + { skill = "attack", min = 5 }, +] + +[buy_black_sword] +type = "resolver" +weight = 35 +template = "buy_from_shop" +fields = { cost = 624, shop_location = "varrock_sword_shop", shopkeeper = "sword_shop*,dealga", item = "black_sword" } +requires = [ + { skill = "attack", min = 10 }, +] + +[buy_bronze_dagger] +type = "resolver" +weight = 35 +template = "buy_from_shop" +fields = { cost = 10, shop_location = "varrock_sword_shop", shopkeeper = "sword_shop*,dealga", item = "bronze_dagger" } + +[buy_iron_dagger] +type = "resolver" +weight = 35 +template = "buy_from_shop" +fields = { cost = 35, shop_location = "varrock_sword_shop", shopkeeper = "sword_shop*,dealga", item = "iron_dagger" } + +[buy_steel_dagger] +type = "resolver" +weight = 35 +template = "buy_from_shop" +fields = { cost = 125, shop_location = "varrock_sword_shop", shopkeeper = "sword_shop*,dealga", item = "steel_dagger" } +requires = [ + { skill = "attack", min = 5 }, +] diff --git a/data/bot/combat_template.bots.toml b/data/bot/combat_template.bots.toml new file mode 100644 index 0000000000..dc5da11e78 --- /dev/null +++ b/data/bot/combat_template.bots.toml @@ -0,0 +1,42 @@ +[kill_chickens] +requires = [ + { skill = "$skill", min = 1, max = 5 }, +] +setup = [ + { equips = "bronze_sword,bronze_dagger,bronze_scimitar" }, + { location = "$location" }, + { inventory_space = 27 }, +] +actions = [ + { option = "Select", interface = "combat_styles:$style" }, + { option = "Attack", npc = "chicken*", delay = 5, success = { inventory_space = 0 } }, +] +produces = [ + { carries = "feather" }, + { carries = "bones" }, + { carries = "raw_chicken" }, + { skill = "$skill" } +] + +[kill_cows] +type = "activity" +capacity = 4 +requires = [ + { skill = "combat", min = 10 }, + { skill = "$skill", min = 5, max = 10 }, +] +setup = [ + { equips = "iron_sword,iron_dagger,iron_scimitar" }, + { location = "$location" }, + { inventory_space = 27 }, +] +actions = [ + { option = "Select", interface = "combat_styles:style1" }, + { option = "Attack", npc = "cow*", delay = 5, success = { inventory_space = 0 } }, +] +produces = [ + { carries = "bones" }, + { carries = "cowhide" }, + { carries = "raw_beef" }, + { skill = "$skill" } +] \ No newline at end of file diff --git a/data/bot/lumbridge.nav-edges.toml b/data/bot/lumbridge.nav-edges.toml index 09847f0868..a43a44edb0 100644 --- a/data/bot/lumbridge.nav-edges.toml +++ b/data/bot/lumbridge.nav-edges.toml @@ -183,6 +183,10 @@ edges = [ { from_x = 3295, from_y = 3372, to_x = 3293, to_y = 3384 }, # varrock_south_east_mine_path_to_border { from_x = 3211, from_y = 3420, to_x = 3210, to_y = 3407 }, # varrock_centre_south_to_dirt_crossroads { from_x = 3210, from_y = 3407, to_x = 3211, to_y = 3395 }, # varrock_south_dirt_crossroads_to_blue_moon_inn + { from_x = 3210, from_y = 3407, to_x = 3209, to_y = 3399 }, + { from_x = 3210, from_y = 3407, to_x = 3209, to_y = 3399 }, + { from_x = 3209, from_y = 3399, to_x = 3207, to_y = 3399, cost = 1, actions = [{ option = "Open", object = "door_444_closed", x = 3209, y = 3399 }, { x = 3207, y = 3399 }] }, + { from_x = 3211, from_y = 3395, to_x = 3209, to_y = 3399 }, # varrock_sword_shop { from_x = 3211, from_y = 3395, to_x = 3211, to_y = 3381 }, # varrock_blue_moon_inn_to_south_border { from_x = 3211, from_y = 3381, to_x = 3214, to_y = 3367 }, # varrock_south_border_to_dark_mages { from_x = 3214, from_y = 3367, to_x = 3226, to_y = 3352 }, # varrock_south_dark_mages_to_south diff --git a/game/src/main/kotlin/content/bot/action/BotAction.kt b/game/src/main/kotlin/content/bot/action/BotAction.kt index 5774e7b78c..baa381d690 100644 --- a/game/src/main/kotlin/content/bot/action/BotAction.kt +++ b/game/src/main/kotlin/content/bot/action/BotAction.kt @@ -499,8 +499,6 @@ sealed interface BotAction { /** * TODO * firemaking bot - * thieving bot - * bone burying bot * cooking bot * cow bot * rune mysteries quest bot From 145cf171b160f8ec6be08e4a013cbb78860817f1 Mon Sep 17 00:00:00 2001 From: GregHib Date: Fri, 6 Feb 2026 16:05:11 +0000 Subject: [PATCH 058/101] Add cooking bots --- .../misthalin/lumbridge/lumbridge.bots.toml | 15 ++++++ ...e.bots.toml => combat_templates.bots.toml} | 2 +- data/bot/cooking_templates.bots.toml | 45 ++++++++++++++++++ data/bot/lumbridge.nav-edges.toml | 16 +++---- data/bot/mining_templates.bots.toml | 1 - .../content/bot/action/BehaviourFragment.kt | 8 ++++ .../kotlin/content/bot/action/BotAction.kt | 46 +++++++++++++++++-- .../kotlin/content/bot/action/BotActivity.kt | 35 ++++++++++---- game/src/main/kotlin/content/bot/fact/Fact.kt | 14 ++++-- 9 files changed, 155 insertions(+), 27 deletions(-) rename data/bot/{combat_template.bots.toml => combat_templates.bots.toml} (96%) create mode 100644 data/bot/cooking_templates.bots.toml diff --git a/data/area/misthalin/lumbridge/lumbridge.bots.toml b/data/area/misthalin/lumbridge/lumbridge.bots.toml index 2d4085a6b0..7a0877ff9f 100644 --- a/data/area/misthalin/lumbridge/lumbridge.bots.toml +++ b/data/area/misthalin/lumbridge/lumbridge.bots.toml @@ -114,20 +114,35 @@ fields = { location = "lumbridge_west_yew_tree" } # Mining [lumbridge_copper_mining] template = "copper_ore_template" +capacity = 4 fields = { location = "lumbridge_swamp_east_copper_mine" } [lumbridge_tin_mining] template = "tin_ore_template" +capacity = 4 fields = { location = "lumbridge_swamp_east_tin_mine" } [lumbridge_coal_mining] template = "coal_template" +capacity = 4 fields = { location = "lumbridge_swamp_west_coal_mine" } [lumbridge_mithril_mining] template = "mithril_ore_template" +capacity = 2 fields = { location = "lumbridge_swamp_west_mithril_mine" } +# Cooking +[lumbridge_cook_chicken] +template = "cooking_template" +capacity = 4 +fields = { raw = "raw_chicken", cooked = "chicken", level = 1, location = "lumbridge_kitchen", obj = "cooking_range_lumbridge_castle" } + +[lumbridge_cook_beef] +template = "beef_template" +capacity = 4 +fields = { location = "lumbridge_kitchen", obj = "cooking_range_lumbridge_castle" } + # Bobs Brilliant Axes [take_bronze_hatchet_bobs] diff --git a/data/bot/combat_template.bots.toml b/data/bot/combat_templates.bots.toml similarity index 96% rename from data/bot/combat_template.bots.toml rename to data/bot/combat_templates.bots.toml index dc5da11e78..dcc27a0605 100644 --- a/data/bot/combat_template.bots.toml +++ b/data/bot/combat_templates.bots.toml @@ -22,7 +22,7 @@ produces = [ type = "activity" capacity = 4 requires = [ - { skill = "combat", min = 10 }, + { combat_level = 10 }, { skill = "$skill", min = 5, max = 10 }, ] setup = [ diff --git a/data/bot/cooking_templates.bots.toml b/data/bot/cooking_templates.bots.toml new file mode 100644 index 0000000000..765faf6c2d --- /dev/null +++ b/data/bot/cooking_templates.bots.toml @@ -0,0 +1,45 @@ +[cooking_template] +type = "activity" +capacity = 2 +requires = [ + { owns = "$raw", amount = 28 }, + { skill = "cooking", min = "$level" }, +] +setup = [ + { carries = "$raw", amount = 28 }, + { location = "$location" }, +] +actions = [ + { item = "$raw", on_object = "$obj", success = { interface = "dialogue_skill_creation" } }, + { option = "All", interface = "skill_creation_amount:all" }, + { continue = "dialogue_skill_creation:choice1" }, + { restart = true, wait_if = [{ queue = "cooking" }], success = { carries = "$raw", max = 0 } } +] +produces = [ + { skill = "cooking" }, + { carries = "$cooked" } +] + +# Beef has a different popup so can't use normal template +[beef_template] +type = "activity" +capacity = 2 +requires = [ + { owns = "raw_beef", amount = 28 }, + { skill = "cooking", min = 1 }, +] +setup = [ + { carries = "raw_beef", amount = 28 }, + { location = "$location" }, +] +actions = [ + { item = "raw_beef", on_object = "$obj", success = { interface = "dialogue_multi2" } }, + { option = "Continue", continue = "dialogue_multi2:line2", success = { interface = "dialogue_skill_creation" } }, + { option = "All", interface = "skill_creation_amount:all" }, + { continue = "dialogue_skill_creation:choice1" }, + { restart = true, wait_if = [{ queue = "cooking" }], success = { carries = "raw_beef", max = 0 } } +] +produces = [ + { skill = "cooking" }, + { carries = "beef" } +] \ No newline at end of file diff --git a/data/bot/lumbridge.nav-edges.toml b/data/bot/lumbridge.nav-edges.toml index a43a44edb0..aba98a2c94 100644 --- a/data/bot/lumbridge.nav-edges.toml +++ b/data/bot/lumbridge.nav-edges.toml @@ -12,25 +12,25 @@ edges = [ { from_x = 3244, from_y = 3190, to_x = 3253, to_y = 3200 }, # lumbridge_graveyard_exit_to_behind_graveyard { from_x = 3250, from_y = 3212, to_x = 3253, to_y = 3200 }, # lumbridge_behind_church_to_behind_graveyard { from_x = 3258, from_y = 3206, to_x = 3253, to_y = 3200 }, # lumbridge_church_fishing_spot_to_behind_graveyard - { from_x = 3205, from_y = 3209, from_level = 2, to_x = 3205, to_y = 3209, to_level = 1, cost = 1, actions = [{ option = "Climb-down", object = "lumbridge_staircase_top", x = 3204, y = 3207 }] }, # lumbridge_castle_2nd_floor_south_stairs_to_1st_floor + { from_x = 3205, from_y = 3209, from_level = 2, to_x = 3205, to_y = 3209, to_level = 1, cost = 1, actions = [{ option = "Climb-down", object = "lumbridge_staircase_top", x = 3204, y = 3207, success = { level = 1 } }] }, # lumbridge_castle_2nd_floor_south_stairs_to_1st_floor { from_x = 3208, from_y = 3219, from_level = 2, to_x = 3205, to_y = 3209, to_level = 2 }, # lumbridge_castle_2nd_floor_bank_to_south_stairs - { from_x = 3205, from_y = 3209, to_x = 3205, to_y = 3209, to_level = 1, cost = 1, actions = [{ option = "Climb-up", object = "lumbridge_staircase", x = 3204, y = 3207 }] }, # lumbridge_castle_ground_floor_south_stairs_to_1st_floor + { from_x = 3205, from_y = 3209, to_x = 3205, to_y = 3209, to_level = 1, cost = 1, actions = [{ option = "Climb-up", object = "lumbridge_staircase", x = 3204, y = 3207, success = { level = 1 } }] }, # lumbridge_castle_ground_floor_south_stairs_to_1st_floor { from_x = 3205, from_y = 3209, to_x = 3208, to_y = 3210 }, # lumbridge_castle_ground_floor_south_stairs_to_kitchen_corridor { from_x = 3208, from_y = 3210, to_x = 3215, to_y = 3216 }, # lumbridge_castle_kitchen_corridor_to_castle_south_entrance { from_x = 3208, from_y = 3210, to_x = 3211, to_y = 3214 }, # lumbridge_castle_kitchen_corridor_to_kitchen { from_x = 3215, from_y = 3216, to_x = 3222, to_y = 3218 }, # lumbridge_castle_south_entrance_to_courtyard_south - { from_x = 3205, from_y = 3209, from_level = 1, to_x = 3205, to_y = 3209, cost = 1, actions = [{ option = "Climb-down", object = "lumbridge_castle_staircase_south_middle", x = 3204, y = 3207 }] }, # lumbridge_castle_1st_floor_south_stairs_to_ground_floor - { from_x = 3205, from_y = 3209, from_level = 1, to_x = 3205, to_y = 3209, to_level = 2, cost = 1, actions = [{ option = "Climb-up", object = "lumbridge_castle_staircase_south_middle", x = 3204, y = 3207 }] }, # lumbridge_castle_1st_floor_south_stairs_to_2nd_floor + { from_x = 3205, from_y = 3209, from_level = 1, to_x = 3205, to_y = 3209, cost = 1, actions = [{ option = "Climb-down", object = "lumbridge_castle_staircase_south_middle", x = 3204, y = 3207, success = { level = 0 } }] }, # lumbridge_castle_1st_floor_south_stairs_to_ground_floor + { from_x = 3205, from_y = 3209, from_level = 1, to_x = 3205, to_y = 3209, to_level = 2, cost = 1, actions = [{ option = "Climb-up", object = "lumbridge_castle_staircase_south_middle", x = 3204, y = 3207, success = { level = 2 } }] }, # lumbridge_castle_1st_floor_south_stairs_to_2nd_floor { from_x = 3209, from_y = 3205, to_x = 3199, to_y = 3218 }, # lumbridge_castle_grounds_south_to_tower_west { from_x = 3199, from_y = 3218, to_x = 3184, to_y = 3225 }, # lumbridge_castle_tower_west_to_yew_trees { from_x = 3199, from_y = 3218, to_x = 3193, to_y = 3236 }, # lumbridge_castle_tower_west_to_tree_patch { from_x = 3184, from_y = 3225, to_x = 3168, to_y = 3221 }, # lumbridge_castle_yew_trees_to_yew_trees_west { from_x = 3184, from_y = 3225, to_x = 3193, to_y = 3236 }, # lumbridge_castle_yew_trees_to_tree_patch { from_x = 3226, from_y = 3214, to_x = 3227, to_y = 3214, cost = 3, actions = [{ option = "Open", object = "door_627_closed", x = 3226, y = 3214 }] }, # lumbridge_south_tower_to_ground_floor - { from_x = 3227, from_y = 3214, to_x = 3229, to_y = 3214, to_level = 1, cost = 1, actions = [{ option = "Climb-up", object = "36768", x = 3229, y = 3213 }] }, # lumbridge_south_tower_ground_floor_to_1st_floor - { from_x = 3229, from_y = 3214, from_level = 1, to_x = 3229, to_y = 3214, to_level = 2, cost = 1, actions = [{ option = "Climb-up", object = "36769", x = 3229, y = 3213 }] }, # lumbridge_south_tower_1st_floor_to_2nd_floor - { from_x = 3229, from_y = 3214, from_level = 1, to_x = 3229, to_y = 3214, cost = 1, actions = [{ option = "Climb-down", object = "36769", x = 3229, y = 3213 }] }, # lumbridge_south_tower_1st_floor_to_ground_floor - { from_x = 3229, from_y = 3214, from_level = 2, to_x = 3229, to_y = 3214, to_level = 1, cost = 1, actions = [{ option = "Climb-down", object = "36770", x = 3229, y = 3213 }] }, # lumbridge_south_tower_2nd_floor_to_1st_floor + { from_x = 3227, from_y = 3214, to_x = 3229, to_y = 3214, to_level = 1, cost = 1, actions = [{ option = "Climb-up", object = "36768", x = 3229, y = 3213, success = { level = 1 } }] }, # lumbridge_south_tower_ground_floor_to_1st_floor + { from_x = 3229, from_y = 3214, from_level = 1, to_x = 3229, to_y = 3214, to_level = 2, cost = 1, actions = [{ option = "Climb-up", object = "36769", x = 3229, y = 3213, success = { level = 2 } }] }, # lumbridge_south_tower_1st_floor_to_2nd_floor + { from_x = 3229, from_y = 3214, from_level = 1, to_x = 3229, to_y = 3214, cost = 1, actions = [{ option = "Climb-down", object = "36769", x = 3229, y = 3213, success = { level = 0 } }] }, # lumbridge_south_tower_1st_floor_to_ground_floor + { from_x = 3229, from_y = 3214, from_level = 2, to_x = 3229, to_y = 3214, to_level = 1, cost = 1, actions = [{ option = "Climb-down", object = "36770", x = 3229, y = 3213, success = { level = 1 } }] }, # lumbridge_south_tower_2nd_floor_to_1st_floor { from_x = 3236, from_y = 3219, to_x = 3236, to_y = 3225 }, # lumbridge_gate_north_to_bridge_west { from_x = 3236, from_y = 3225, to_x = 3230, to_y = 3232 }, # lumbridge_bridge_west_to_unstable_house { from_x = 3236, from_y = 3225, to_x = 3253, to_y = 3225 }, # lumbridge_bridge_west_to_bridge_east diff --git a/data/bot/mining_templates.bots.toml b/data/bot/mining_templates.bots.toml index 16e9404c4a..01eb22be58 100644 --- a/data/bot/mining_templates.bots.toml +++ b/data/bot/mining_templates.bots.toml @@ -113,7 +113,6 @@ produces = [ ] [gold_ore_template] -type = "activity" capacity = 3 requires = [ { skill = "mining", min = 40, max = 60 }, diff --git a/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt b/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt index c61bcf9d99..4ac3fd7ccd 100644 --- a/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt +++ b/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt @@ -24,14 +24,22 @@ data class BehaviourFragment( is BotAction.InterfaceOption -> BotAction.InterfaceOption( option = resolve(action.references["option"], copy.option), id = resolve(action.references["interface"], copy.id), + success = resolveReference(copy.success), ) is BotAction.DialogueContinue -> BotAction.DialogueContinue( option = resolve(action.references["option"], copy.option), id = resolve(action.references["continue"], copy.id), + success = resolveReference(copy.success), ) is BotAction.ItemOnItem -> BotAction.ItemOnItem( item = resolve(action.references["item"], copy.item), on = resolve(action.references["on"], copy.on), + success = resolveReference(copy.success), + ) + is BotAction.ItemOnObject -> BotAction.ItemOnObject( + item = resolve(action.references["item"], copy.item), + id = resolve(action.references["on_object"], copy.id), + success = resolveReference(copy.success), ) is BotAction.InteractNpc -> { val option = resolve(action.references["option"], copy.option) diff --git a/game/src/main/kotlin/content/bot/action/BotAction.kt b/game/src/main/kotlin/content/bot/action/BotAction.kt index baa381d690..08dbc7ed11 100644 --- a/game/src/main/kotlin/content/bot/action/BotAction.kt +++ b/game/src/main/kotlin/content/bot/action/BotAction.kt @@ -7,10 +7,9 @@ import content.bot.interact.path.Graph import content.entity.combat.attackers import content.entity.combat.dead import world.gregs.voidps.engine.GameLoop -import world.gregs.voidps.engine.client.command.playerCommand import world.gregs.voidps.engine.client.instruction.InstructionHandlers import world.gregs.voidps.engine.client.instruction.InterfaceHandler -import world.gregs.voidps.engine.client.ui.dialogue +import world.gregs.voidps.engine.client.instruction.handle.ObjectOptionHandler import world.gregs.voidps.engine.client.ui.menu import world.gregs.voidps.engine.data.definition.Areas import world.gregs.voidps.engine.data.definition.InterfaceDefinitions @@ -20,8 +19,8 @@ import world.gregs.voidps.engine.entity.character.mode.interact.PlayerOnFloorIte import world.gregs.voidps.engine.entity.character.mode.interact.PlayerOnNPCInteract import world.gregs.voidps.engine.entity.character.mode.interact.PlayerOnObjectInteract import world.gregs.voidps.engine.entity.character.npc.NPCs -import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.entity.character.player.skill.Skill +import world.gregs.voidps.engine.entity.item.Item import world.gregs.voidps.engine.entity.item.floor.FloorItems import world.gregs.voidps.engine.entity.obj.GameObjects import world.gregs.voidps.engine.event.wildcardEquals @@ -29,7 +28,6 @@ import world.gregs.voidps.engine.get import world.gregs.voidps.engine.inv.inventory import world.gregs.voidps.engine.map.Spiral import world.gregs.voidps.network.client.instruction.* -import kotlin.to sealed interface BotAction { fun start(bot: Bot, frame: BehaviourFrame): BehaviourState = BehaviourState.Running @@ -354,6 +352,45 @@ sealed interface BotAction { } } + data class ItemOnObject(val item: String, val id: String, val success: Condition? = null) : BotAction { + override fun update(bot: Bot, frame: BehaviourFrame): BehaviourState { + if (success != null && success.check(bot.player)) { + return BehaviourState.Success + } + val inventory = bot.player.inventory + val slot = inventory.indexOf(this@ItemOnObject.item) + if (slot == -1) { + return BehaviourState.Failed(Reason.Invalid("No inventory item '${this@ItemOnObject.item}'.")) + } + val item = inventory[slot] + return search(bot, item, slot) + } + + private fun search(bot: Bot, item: Item, slot: Int): BehaviourState { + val player = bot.player + val ids = if (id.contains(",")) id.split(",") else listOf(id) + for (tile in Spiral.spiral(player.tile, 10)) { + for (obj in GameObjects.at(tile)) { + if (ids.none { wildcardEquals(it, obj.id) }) { + continue + } + val valid = get().handle(bot.player, InteractInterfaceObject(obj.intId, obj.x, obj.y, 149, 0, item.def.id, slot)) + if (!valid) { + return BehaviourState.Failed(Reason.Invalid("Invalid item on object: ${item.def.id}:${slot} -> ${obj}.")) + } + return BehaviourState.Running + } + } + if (success == null) { + return BehaviourState.Failed(Reason.NoTarget) + } + if (success.check(bot.player)) { + return BehaviourState.Success + } + return BehaviourState.Running + } + } + data class InterfaceOption(val option: String, val id: String, val success: Condition? = null) : BotAction { override fun update(bot: Bot, frame: BehaviourFrame): BehaviourState? { if (success != null && success.check(bot.player)) { @@ -500,7 +537,6 @@ sealed interface BotAction { * TODO * firemaking bot * cooking bot - * cow bot * rune mysteries quest bot * bot saving? * bot setups diff --git a/game/src/main/kotlin/content/bot/action/BotActivity.kt b/game/src/main/kotlin/content/bot/action/BotActivity.kt index 3ef6f2aea7..7b2d961e59 100644 --- a/game/src/main/kotlin/content/bot/action/BotActivity.kt +++ b/game/src/main/kotlin/content/bot/action/BotActivity.kt @@ -162,6 +162,7 @@ fun loadActivities( groups.getOrPut(key) { mutableListOf() }.add(activity.id) } } + println(activity) } activities.size } @@ -177,14 +178,14 @@ private fun ConfigReader.fields(): Map { return map } -fun ConfigReader.requirements(list: MutableList, exact: Boolean = false) { +fun ConfigReader.requirements(list: MutableList, parentReferences: MutableMap? = null, exact: Boolean = false) { while (nextElement()) { - list.add(requirement(exact)) + list.add(requirement(parentReferences = parentReferences, exact = exact)) } list.sortBy { it.priority() } } -private fun ConfigReader.requirement(exact: Boolean = false): Condition { +private fun ConfigReader.requirement(parentReferences: MutableMap? = null, exact: Boolean = false): Condition { var type = "" var id = "" var value: Any? = null @@ -227,6 +228,14 @@ private fun ConfigReader.requirement(exact: Boolean = false): Condition { else -> throw IllegalArgumentException("Invalid '$key' value: $value ${exception()}") } } + "level" -> { + type = key + when (val v = value()) { + is Int -> value = v + is String if v.contains('$') -> references[key] = v + else -> throw IllegalArgumentException("Invalid '$key' value: $v ${exception()}") + } + } "value" -> value = value() "default" -> default = value() } @@ -240,6 +249,7 @@ private fun ConfigReader.requirement(exact: Boolean = false): Condition { } if (references.isNotEmpty()) { requirement = Condition.Reference(type, id, value, default, min, max, references) + parentReferences?.putAll(references) } return requirement } @@ -270,6 +280,7 @@ private fun getRequirement(type: String, id: String, min: Int?, max: Int?, value "clone" -> Condition.Clone(id) "inventory_space" -> if (exact && min != null) Condition.Equals(Fact.InventorySpace, min) else Condition.range(Fact.InventorySpace, min, max) "location" -> Condition.Area(Fact.PlayerTile, id) + "level" -> Condition.Equals(Fact.PlayerLevel, value as Int) "combat_level" -> Condition.AtLeast(Fact.CombatLevel, min ?: 1) else -> null } @@ -333,12 +344,19 @@ fun ConfigReader.actions(list: MutableList) { references[key] = option } } + "on_object" -> { + type = "${type}_on_object" + option = string() + if (option.contains('$')) { + references[key] = option + } + } "restart" -> { require(boolean()) { "Can't have restart = false ${exception()}" } type = key } - "success" -> success = requirement(exact = true) - "wait_if" -> requirements(wait, exact = true) + "success" -> success = requirement(references, exact = true) + "wait_if" -> requirements(wait, references, exact = true) "wait" -> { type = key when (val value = value()) { @@ -392,9 +410,10 @@ fun ConfigReader.actions(list: MutableList) { } "tile" -> BotAction.WalkTo(x = x, y = y) "object" -> BotAction.InteractObject(id = id, option = option, delay = delay, success = success, radius = radius) - "interface" -> BotAction.InterfaceOption(id = id, option = option) - "continue" -> BotAction.DialogueContinue(id = id, option = option) - "item" -> BotAction.ItemOnItem(item = id, on = option) + "interface" -> BotAction.InterfaceOption(id = id, option = option, success = success) + "continue" -> BotAction.DialogueContinue(id = id, option = option, success = success) + "item" -> BotAction.ItemOnItem(item = id, on = option, success = success) + "item_on_object" -> BotAction.ItemOnObject(item = id, id = option, success = success) "clone" -> BotAction.Clone(id) else -> throw IllegalArgumentException("Unknown action type: $type ${exception()}") } diff --git a/game/src/main/kotlin/content/bot/fact/Fact.kt b/game/src/main/kotlin/content/bot/fact/Fact.kt index f3d528883b..4297a494a4 100644 --- a/game/src/main/kotlin/content/bot/fact/Fact.kt +++ b/game/src/main/kotlin/content/bot/fact/Fact.kt @@ -102,6 +102,11 @@ sealed class Fact(val priority: Int) { override fun getValue(player: Player) = player.tile } + object PlayerLevel : Fact(1000) { + override fun keys() = setOf("level") + override fun getValue(player: Player) = player.tile.level + } + object CombatLevel : Fact(1) { override fun keys() = setOf("combat") override fun getValue(player: Player) = player.combatLevel @@ -132,12 +137,13 @@ sealed class Fact(val priority: Int) { object ConstructionLevel : SkillLevel(Skill.Construction) object SummoningLevel : SkillLevel(Skill.Summoning) object DungeoneeringLevel : SkillLevel(Skill.Dungeoneering) + data class ReferenceLevel(val key: String) : SkillLevel(null) abstract class SkillLevel( - val skill: Skill, + val skill: Skill?, ) : Fact(0) { - override fun keys() = setOf("skill:${skill.name.lowercase()}") - override fun getValue(player: Player) = player.levels.get(skill) + override fun keys() = setOf("skill:${skill!!.name.lowercase()}") + override fun getValue(player: Player) = player.levels.get(skill!!) companion object { fun of(skill: String): SkillLevel = when (skill.lowercase()) { @@ -166,7 +172,7 @@ sealed class Fact(val priority: Int) { "construction" -> ConstructionLevel "summoning" -> SummoningLevel "dungeoneering" -> DungeoneeringLevel - else -> throw IllegalArgumentException("Unknown skill: $skill") + else -> if (skill.startsWith("$")) ReferenceLevel(skill) else throw IllegalArgumentException("Unknown skill: $skill") } } } From adfce1157ff43a08288bd340c2f606f2070bfe29 Mon Sep 17 00:00:00 2001 From: GregHib Date: Sat, 7 Feb 2026 17:01:49 +0000 Subject: [PATCH 059/101] Requirement Refactor --- .../al_kharid/al_kharid.bots.toml | 33 - .../al_kharid/al_kharid.setups.toml | 28 + .../misthalin/lumbridge/lumbridge.bots.toml | 63 +- .../misthalin/lumbridge/lumbridge.setups.toml | 42 ++ .../misthalin/varrock/varrock.bot-setups.toml | 58 ++ data/area/misthalin/varrock/varrock.bots.toml | 59 -- data/bot/{bank.bots.toml => bank.setups.toml} | 3 +- data/bot/combat.templates.toml | 40 ++ data/bot/combat_templates.bots.toml | 42 -- ...lates.bots.toml => cooking.templates.toml} | 28 +- ...ing.bots.toml => fletching.templates.toml} | 33 +- data/bot/lumbridge.nav-edges.toml | 8 +- data/bot/mining.templates.toml | 135 ++++ data/bot/mining_templates.bots.toml | 150 ----- data/bot/prayer.bots.toml | 7 +- .../{shop.bots.toml => shop.templates.toml} | 14 +- ....bots.toml => teleport.bot-shortcuts.toml} | 3 - ...ates.bots.toml => thieving.templates.toml} | 13 +- ...{walking.bots.toml => walking.setups.toml} | 5 +- data/bot/woodcutting.templates.toml | 67 ++ data/bot/woodcutting_templates.bots.toml | 75 --- .../src/main/kotlin/content/bot/BotManager.kt | 68 +- .../kotlin/content/bot/action/Behaviour.kt | 7 +- .../content/bot/action/BehaviourFragment.kt | 6 +- .../kotlin/content/bot/action/BotAction.kt | 13 +- .../kotlin/content/bot/action/BotActivity.kt | 579 +++++++----------- .../content/bot/action/NavigationShortcut.kt | 8 +- .../main/kotlin/content/bot/action/Reason.kt | 4 +- .../kotlin/content/bot/action/Resolver.kt | 8 +- game/src/main/kotlin/content/bot/fact/Fact.kt | 1 + .../kotlin/content/bot/fact/FactParser.kt | 153 +++++ .../main/kotlin/content/bot/fact/Matcher.kt | 8 + .../main/kotlin/content/bot/fact/Outcome.kt | 7 + .../main/kotlin/content/bot/fact/Predicate.kt | 99 +++ .../content/bot/fact/PredicateParser.kt | 57 ++ .../main/kotlin/content/bot/fact/Produces.kt | 3 + .../main/kotlin/content/bot/fact/Reference.kt | 9 + .../kotlin/content/bot/fact/Requirement.kt | 70 +++ .../kotlin/content/bot/interact/path/Graph.kt | 32 +- game/src/main/resources/game.properties | 9 + 40 files changed, 1155 insertions(+), 892 deletions(-) create mode 100644 data/area/kharidian_desert/al_kharid/al_kharid.setups.toml create mode 100644 data/area/misthalin/lumbridge/lumbridge.setups.toml create mode 100644 data/area/misthalin/varrock/varrock.bot-setups.toml rename data/bot/{bank.bots.toml => bank.setups.toml} (92%) create mode 100644 data/bot/combat.templates.toml delete mode 100644 data/bot/combat_templates.bots.toml rename data/bot/{cooking_templates.bots.toml => cooking.templates.toml} (65%) rename data/bot/{fletching.bots.toml => fletching.templates.toml} (63%) create mode 100644 data/bot/mining.templates.toml delete mode 100644 data/bot/mining_templates.bots.toml rename data/bot/{shop.bots.toml => shop.templates.toml} (62%) rename data/bot/{teleport.bots.toml => teleport.bot-shortcuts.toml} (97%) rename data/bot/{thieving_templates.bots.toml => thieving.templates.toml} (52%) rename data/bot/{walking.bots.toml => walking.setups.toml} (70%) create mode 100644 data/bot/woodcutting.templates.toml delete mode 100644 data/bot/woodcutting_templates.bots.toml create mode 100644 game/src/main/kotlin/content/bot/fact/FactParser.kt create mode 100644 game/src/main/kotlin/content/bot/fact/Matcher.kt create mode 100644 game/src/main/kotlin/content/bot/fact/Outcome.kt create mode 100644 game/src/main/kotlin/content/bot/fact/Predicate.kt create mode 100644 game/src/main/kotlin/content/bot/fact/PredicateParser.kt create mode 100644 game/src/main/kotlin/content/bot/fact/Produces.kt create mode 100644 game/src/main/kotlin/content/bot/fact/Reference.kt create mode 100644 game/src/main/kotlin/content/bot/fact/Requirement.kt diff --git a/data/area/kharidian_desert/al_kharid/al_kharid.bots.toml b/data/area/kharidian_desert/al_kharid/al_kharid.bots.toml index 67da9d5fb7..9257e6438d 100644 --- a/data/area/kharidian_desert/al_kharid/al_kharid.bots.toml +++ b/data/area/kharidian_desert/al_kharid/al_kharid.bots.toml @@ -33,36 +33,3 @@ fields = { location = "al_kharid_mine_north" } template = "mithril_ore_template" capacity = 2 fields = { location = "al_kharid_mine_north" } - -# Zekes Superior Scimitars -[buy_bronze_scimitar] -type = "resolver" -template = "buy_from_shop" -fields = { cost = 32, shop_location = "zekes_scimitar_shop", shopkeeper = "zeke", item = "bronze_scimitar" } -requires = [ - { skill = "attack", min = 1 }, -] - -[buy_iron_scimitar] -type = "resolver" -template = "buy_from_shop" -fields = { cost = 112, shop_location = "zekes_scimitar_shop", shopkeeper = "zeke", item = "iron_scimitar" } -requires = [ - { skill = "attack", min = 1 }, -] - -[buy_steel_scimitar] -type = "resolver" -template = "buy_from_shop" -fields = { cost = 400, shop_location = "zekes_scimitar_shop", shopkeeper = "zeke", item = "steel_scimitar" } -requires = [ - { skill = "attack", min = 5 }, -] - -[buy_mithril_scimitar] -type = "resolver" -template = "buy_from_shop" -fields = { cost = 1040, shop_location = "zekes_scimitar_shop", shopkeeper = "zeke", item = "mithril_scimitar" } -requires = [ - { skill = "attack", min = 20 }, -] diff --git a/data/area/kharidian_desert/al_kharid/al_kharid.setups.toml b/data/area/kharidian_desert/al_kharid/al_kharid.setups.toml new file mode 100644 index 0000000000..c609763da9 --- /dev/null +++ b/data/area/kharidian_desert/al_kharid/al_kharid.setups.toml @@ -0,0 +1,28 @@ +# Zekes Superior Scimitars +[buy_bronze_scimitar] +template = "buy_from_shop" +fields = { cost = 32, shop_location = "zekes_scimitar_shop", shopkeeper = "zeke", item = "bronze_scimitar" } +requires = [ + { skill = { id = "attack", min = 1 } }, +] + +[buy_iron_scimitar] +template = "buy_from_shop" +fields = { cost = 112, shop_location = "zekes_scimitar_shop", shopkeeper = "zeke", item = "iron_scimitar" } +requires = [ + { skill = { id = "attack", min = 1 } }, +] + +[buy_steel_scimitar] +template = "buy_from_shop" +fields = { cost = 400, shop_location = "zekes_scimitar_shop", shopkeeper = "zeke", item = "steel_scimitar" } +requires = [ + { skill = { id = "attack", min = 5 } }, +] + +[buy_mithril_scimitar] +template = "buy_from_shop" +fields = { cost = 1040, shop_location = "zekes_scimitar_shop", shopkeeper = "zeke", item = "mithril_scimitar" } +requires = [ + { skill = { id = "attack", min = 20 } }, +] diff --git a/data/area/misthalin/lumbridge/lumbridge.bots.toml b/data/area/misthalin/lumbridge/lumbridge.bots.toml index 7a0877ff9f..09912a7e62 100644 --- a/data/area/misthalin/lumbridge/lumbridge.bots.toml +++ b/data/area/misthalin/lumbridge/lumbridge.bots.toml @@ -1,5 +1,4 @@ [kill_freds_chickens_attack] -template = "kill_chickens" capacity = 2 fields = { skill = "attack", location = "lumbridge_chicken_pen", style = "style1" } @@ -49,7 +48,7 @@ template = "pickpocketting_template" capacity = 2 fields = { location = "lumbridge_thieving_area", npc = "lumbridge_man*,lumbridge_woman*" } requires = [ - { skill = "thieving", min = 1 } + { skill = { id = "thieving", min = 1 } } ] # Fletching @@ -58,7 +57,7 @@ template = "fletching_arrow_shafts_template" capacity = 2 fields = { location = "lumbridge_castle_bank", logs = "logs" } requires = [ - { skill = "fletching", min = 1 } + { skill = { id = "fletching", min = 1 } } ] [lumbridge_fletch_shortbows] @@ -66,20 +65,20 @@ template = "fletching_shortbow_template" capacity = 2 fields = { location = "lumbridge_castle_bank", logs = "logs" } requires = [ - { skill = "fletching", min = 5 } + { skill = { id = "fletching", min = 5 } } ] produces = [ - { carries = "shortbow_u" } + { carries = { id = "shortbow_u" } } ] [lumbridge_fletch_longbows] template = "fletching_longbow_template" fields = { location = "lumbridge_castle_bank", logs = "logs" } requires = [ - { skill = "fletching", min = 10 } + { skill = { id = "fletching", min = 10 } } ] produces = [ - { carries = "longbow_u" } + { carries = { id = "longbow_u" } } ] # Woodcutting @@ -142,53 +141,3 @@ fields = { raw = "raw_chicken", cooked = "chicken", level = 1, location = "lumbr template = "beef_template" capacity = 4 fields = { location = "lumbridge_kitchen", obj = "cooking_range_lumbridge_castle" } - -# Bobs Brilliant Axes - -[take_bronze_hatchet_bobs] -type = "resolver" -weight = 30 -template = "take_shop_sample" -fields = { shop_location = "bobs_axe_shop", shopkeeper = "bob", item = "bronze_hatchet" } - -[take_bronze_pickaxe_bobs] -type = "resolver" -weight = 30 -template = "take_shop_sample" -fields = { shop_location = "bobs_axe_shop", shopkeeper = "bob", item = "bronze_pickaxe" } - -[buy_bronze_pickaxe] -type = "resolver" -weight = 35 -template = "buy_from_shop" -fields = { cost = 1, shop_location = "bobs_axe_shop", shopkeeper = "bob", item = "bronze_pickaxe" } -requires = [ - { skill = "mining", min = 1 }, -] - -[buy_bronze_hatchet] -type = "resolver" -weight = 35 -template = "buy_from_shop" -fields = { cost = 16, shop_location = "bobs_axe_shop", shopkeeper = "bob", item = "bronze_hatchet" } -requires = [ - { skill = "woodcutting", min = 1 }, -] - -[buy_iron_hatchet] -type = "resolver" -weight = 45 -template = "buy_from_shop" -fields = { cost = 56, shop_location = "bobs_axe_shop", shopkeeper = "bob", item = "iron_hatchet" } -requires = [ - { skill = "woodcutting", min = 1 }, -] - -[buy_steel_hatchet] -type = "resolver" -weight = 50 -template = "buy_from_shop" -fields = { cost = 200, shop_location = "bobs_axe_shop", shopkeeper = "bob", item = "steel_hatchet" } -requires = [ - { skill = "woodcutting", min = 6 }, -] \ No newline at end of file diff --git a/data/area/misthalin/lumbridge/lumbridge.setups.toml b/data/area/misthalin/lumbridge/lumbridge.setups.toml new file mode 100644 index 0000000000..2794888c43 --- /dev/null +++ b/data/area/misthalin/lumbridge/lumbridge.setups.toml @@ -0,0 +1,42 @@ +# Bobs Brilliant Axes +[take_bronze_hatchet_bobs] +template = "take_shop_sample" +weight = 30 +fields = { shop_location = "bobs_axe_shop", shopkeeper = "bob", item = "bronze_hatchet" } + +[take_bronze_pickaxe_bobs] +template = "take_shop_sample" +weight = 30 +fields = { shop_location = "bobs_axe_shop", shopkeeper = "bob", item = "bronze_pickaxe" } + +[buy_bronze_pickaxe] +template = "buy_from_shop" +weight = 35 +fields = { cost = 1, shop_location = "bobs_axe_shop", shopkeeper = "bob", item = "bronze_pickaxe" } +requires = [ + { skill = { id = "mining", min = 1 } }, +] + +[buy_bronze_hatchet] +template = "buy_from_shop" +weight = 35 +fields = { cost = 16, shop_location = "bobs_axe_shop", shopkeeper = "bob", item = "bronze_hatchet" } +requires = [ + { skill = { id = "woodcutting", min = 1 } }, +] + +[buy_iron_hatchet] +template = "buy_from_shop" +weight = 45 +fields = { cost = 56, shop_location = "bobs_axe_shop", shopkeeper = "bob", item = "iron_hatchet" } +requires = [ + { skill = { id = "woodcutting", min = 1 } }, +] + +[buy_steel_hatchet] +template = "buy_from_shop" +weight = 50 +fields = { cost = 200, shop_location = "bobs_axe_shop", shopkeeper = "bob", item = "steel_hatchet" } +requires = [ + { skill = { id = "woodcutting", min = 6 } }, +] diff --git a/data/area/misthalin/varrock/varrock.bot-setups.toml b/data/area/misthalin/varrock/varrock.bot-setups.toml new file mode 100644 index 0000000000..263e4c7e2f --- /dev/null +++ b/data/area/misthalin/varrock/varrock.bot-setups.toml @@ -0,0 +1,58 @@ +# Varrock sword shop + +[take_bronze_sword] +template = "take_shop_sample" +type = "resolver" +weight = 30 +fields = { shop_location = "varrock_sword_shop", shopkeeper = "sword_shop*,dealga", item = "bronze_sword" } + +[buy_bronze_sword] +template = "buy_from_shop" +type = "resolver" +weight = 35 +fields = { cost = 26, shop_location = "varrock_sword_shop", shopkeeper = "sword_shop*,dealga", item = "bronze_sword" } + +[buy_iron_sword] +template = "buy_from_shop" +type = "resolver" +weight = 35 +fields = { cost = 91, shop_location = "varrock_sword_shop", shopkeeper = "sword_shop*,dealga", item = "iron_sword" } + +[buy_steel_sword] +template = "buy_from_shop" +type = "resolver" +weight = 35 +fields = { cost = 325, shop_location = "varrock_sword_shop", shopkeeper = "sword_shop*,dealga", item = "steel_sword" } +requires = [ + { skill = { id = "attack", min = 5 } }, +] + +[buy_black_sword] +template = "buy_from_shop" +type = "resolver" +weight = 35 +fields = { cost = 624, shop_location = "varrock_sword_shop", shopkeeper = "sword_shop*,dealga", item = "black_sword" } +requires = [ + { skill = { id = "attack", min = 10 } }, +] + +[buy_bronze_dagger] +template = "buy_from_shop" +type = "resolver" +weight = 35 +fields = { cost = 10, shop_location = "varrock_sword_shop", shopkeeper = "sword_shop*,dealga", item = "bronze_dagger" } + +[buy_iron_dagger] +template = "buy_from_shop" +type = "resolver" +weight = 35 +fields = { cost = 35, shop_location = "varrock_sword_shop", shopkeeper = "sword_shop*,dealga", item = "iron_dagger" } + +[buy_steel_dagger] +template = "buy_from_shop" +type = "resolver" +weight = 35 +fields = { cost = 125, shop_location = "varrock_sword_shop", shopkeeper = "sword_shop*,dealga", item = "steel_dagger" } +requires = [ + { skill = { id = "attack", min = 5 } }, +] diff --git a/data/area/misthalin/varrock/varrock.bots.toml b/data/area/misthalin/varrock/varrock.bots.toml index 0d225847fe..518093de1f 100644 --- a/data/area/misthalin/varrock/varrock.bots.toml +++ b/data/area/misthalin/varrock/varrock.bots.toml @@ -33,62 +33,3 @@ fields = { location = "varrock_south_west_mine" } template = "iron_ore_template" capacity = 2 fields = { location = "varrock_south_west_mine" } - -# Varrock sword shop - -[take_bronze_sword] -type = "resolver" -weight = 30 -template = "take_shop_sample" -fields = { shop_location = "varrock_sword_shop", shopkeeper = "sword_shop*,dealga", item = "bronze_sword" } - -[buy_bronze_sword] -type = "resolver" -weight = 35 -template = "buy_from_shop" -fields = { cost = 26, shop_location = "varrock_sword_shop", shopkeeper = "sword_shop*,dealga", item = "bronze_sword" } - -[buy_iron_sword] -type = "resolver" -weight = 35 -template = "buy_from_shop" -fields = { cost = 91, shop_location = "varrock_sword_shop", shopkeeper = "sword_shop*,dealga", item = "iron_sword" } - -[buy_steel_sword] -type = "resolver" -weight = 35 -template = "buy_from_shop" -fields = { cost = 325, shop_location = "varrock_sword_shop", shopkeeper = "sword_shop*,dealga", item = "steel_sword" } -requires = [ - { skill = "attack", min = 5 }, -] - -[buy_black_sword] -type = "resolver" -weight = 35 -template = "buy_from_shop" -fields = { cost = 624, shop_location = "varrock_sword_shop", shopkeeper = "sword_shop*,dealga", item = "black_sword" } -requires = [ - { skill = "attack", min = 10 }, -] - -[buy_bronze_dagger] -type = "resolver" -weight = 35 -template = "buy_from_shop" -fields = { cost = 10, shop_location = "varrock_sword_shop", shopkeeper = "sword_shop*,dealga", item = "bronze_dagger" } - -[buy_iron_dagger] -type = "resolver" -weight = 35 -template = "buy_from_shop" -fields = { cost = 35, shop_location = "varrock_sword_shop", shopkeeper = "sword_shop*,dealga", item = "iron_dagger" } - -[buy_steel_dagger] -type = "resolver" -weight = 35 -template = "buy_from_shop" -fields = { cost = 125, shop_location = "varrock_sword_shop", shopkeeper = "sword_shop*,dealga", item = "steel_dagger" } -requires = [ - { skill = "attack", min = 5 }, -] diff --git a/data/bot/bank.bots.toml b/data/bot/bank.setups.toml similarity index 92% rename from data/bot/bank.bots.toml rename to data/bot/bank.setups.toml index 48d02e77a6..1ab7cdb659 100644 --- a/data/bot/bank.bots.toml +++ b/data/bot/bank.setups.toml @@ -1,12 +1,11 @@ [deposit_carried_items] -type = "resolver" actions = [ { go_to_nearest = "bank" }, { option = "Use-quickly", object = "bank_booth*", success = { interface = "bank" } }, { option = "Deposit carried items", interface = "bank:carried", success = { inventory_space = 28 } }, ] produces = [ - { inventory_space = 28 } + { inventory_space = { min = 28 } } ] #[deposit_worn_items] diff --git a/data/bot/combat.templates.toml b/data/bot/combat.templates.toml new file mode 100644 index 0000000000..f7ceaa6dc5 --- /dev/null +++ b/data/bot/combat.templates.toml @@ -0,0 +1,40 @@ +[kill_chickens] +requires = [ + { skill = { id = "$skill", min = 1, max = 5 } }, +] +setup = [ + { equips = { id = "bronze_sword,bronze_dagger,bronze_scimitar" } }, + { area = { id = "$location" } }, + { inventory_space = { min = 27 } }, +] +actions = [ + { option = "Select", interface = "combat_styles:$style" }, + { option = "Attack", npc = "chicken*", delay = 5, success = { inventory_space = 0 } }, +] +produces = [ + { carries = { id = "feather" } }, + { carries = { id = "bones" } }, + { carries = { id = "raw_chicken" } }, + { skill = { id = "$skill" } } +] + +[kill_cows] +requires = [ + { combat_level = { min = 10 } }, + { skill = { id = "$skill", min = 5, max = 10 } }, +] +setup = [ + { equips = { id = "iron_sword,iron_dagger,iron_scimitar" } }, + { area = { id = "$location" } }, + { inventory_space = { min = 27 } }, +] +actions = [ + { option = "Select", interface = "combat_styles:style1" }, + { option = "Attack", npc = "cow*", delay = 5, success = { inventory_space = 0 } }, +] +produces = [ + { carries = { id = "bones" } }, + { carries = { id = "cowhide" } }, + { carries = { id = "raw_beef" } }, + { skill = { id = "$skill" } } +] \ No newline at end of file diff --git a/data/bot/combat_templates.bots.toml b/data/bot/combat_templates.bots.toml deleted file mode 100644 index dcc27a0605..0000000000 --- a/data/bot/combat_templates.bots.toml +++ /dev/null @@ -1,42 +0,0 @@ -[kill_chickens] -requires = [ - { skill = "$skill", min = 1, max = 5 }, -] -setup = [ - { equips = "bronze_sword,bronze_dagger,bronze_scimitar" }, - { location = "$location" }, - { inventory_space = 27 }, -] -actions = [ - { option = "Select", interface = "combat_styles:$style" }, - { option = "Attack", npc = "chicken*", delay = 5, success = { inventory_space = 0 } }, -] -produces = [ - { carries = "feather" }, - { carries = "bones" }, - { carries = "raw_chicken" }, - { skill = "$skill" } -] - -[kill_cows] -type = "activity" -capacity = 4 -requires = [ - { combat_level = 10 }, - { skill = "$skill", min = 5, max = 10 }, -] -setup = [ - { equips = "iron_sword,iron_dagger,iron_scimitar" }, - { location = "$location" }, - { inventory_space = 27 }, -] -actions = [ - { option = "Select", interface = "combat_styles:style1" }, - { option = "Attack", npc = "cow*", delay = 5, success = { inventory_space = 0 } }, -] -produces = [ - { carries = "bones" }, - { carries = "cowhide" }, - { carries = "raw_beef" }, - { skill = "$skill" } -] \ No newline at end of file diff --git a/data/bot/cooking_templates.bots.toml b/data/bot/cooking.templates.toml similarity index 65% rename from data/bot/cooking_templates.bots.toml rename to data/bot/cooking.templates.toml index 765faf6c2d..06e6b98f80 100644 --- a/data/bot/cooking_templates.bots.toml +++ b/data/bot/cooking.templates.toml @@ -1,13 +1,11 @@ [cooking_template] -type = "activity" -capacity = 2 requires = [ - { owns = "$raw", amount = 28 }, - { skill = "cooking", min = "$level" }, + { owns = { id = "$raw", amount = 28 } }, + { skill = { id = "cooking", min = "$level" } }, ] setup = [ - { carries = "$raw", amount = 28 }, - { location = "$location" }, + { carries = { id = "$raw", amount = 28 } }, + { area = { id = "$location" } }, ] actions = [ { item = "$raw", on_object = "$obj", success = { interface = "dialogue_skill_creation" } }, @@ -16,21 +14,19 @@ actions = [ { restart = true, wait_if = [{ queue = "cooking" }], success = { carries = "$raw", max = 0 } } ] produces = [ - { skill = "cooking" }, - { carries = "$cooked" } + { skill = { id = "cooking" } }, + { carries = { id = "$cooked" } } ] # Beef has a different popup so can't use normal template [beef_template] -type = "activity" -capacity = 2 requires = [ - { owns = "raw_beef", amount = 28 }, - { skill = "cooking", min = 1 }, + { owns = { id = "raw_beef", amount = 28 } }, + { skill = { id = "cooking", min = 1 } }, ] setup = [ - { carries = "raw_beef", amount = 28 }, - { location = "$location" }, + { carries = { id = "raw_beef", amount = 28 } }, + { area = { id = "$location" } }, ] actions = [ { item = "raw_beef", on_object = "$obj", success = { interface = "dialogue_multi2" } }, @@ -40,6 +36,6 @@ actions = [ { restart = true, wait_if = [{ queue = "cooking" }], success = { carries = "raw_beef", max = 0 } } ] produces = [ - { skill = "cooking" }, - { carries = "beef" } + { skill = { id = "cooking" } }, + { carries = { id = "beef" } } ] \ No newline at end of file diff --git a/data/bot/fletching.bots.toml b/data/bot/fletching.templates.toml similarity index 63% rename from data/bot/fletching.bots.toml rename to data/bot/fletching.templates.toml index 45c5b802a8..ea00c8b467 100644 --- a/data/bot/fletching.bots.toml +++ b/data/bot/fletching.templates.toml @@ -1,12 +1,11 @@ [fletching_arrow_shafts_template] -capacity = 2 requires = [ - { owns = "$logs", min = 27 } + { owns = { id = "$logs", min = 27 } } ] setup = [ - { location = "$location" }, - { carries = "knife" }, - { carries = "$logs", min = 27 } + { area = { id = "$location" } }, + { carries = { id = "knife" } }, + { carries = { id = "$logs", min = 27 } } ] actions = [ { item = "knife", on = "$logs", success = { interface = "dialogue_skill_creation" } }, @@ -15,18 +14,18 @@ actions = [ { restart = true, wait_if = [{ queue = "fletching" }], success = { inventory_space = 26 } } ] produces = [ - { carries = "arrow_shafts" }, - { skill = "fletching" } + { carries = { id = "arrow_shafts" } }, + { skill = { id = "fletching" } } ] [fletching_shortbow_template] requires = [ - { owns = "$logs", min = 27 } + { owns = { id = "$logs", min = 27 } } ] setup = [ - { location = "$location" }, - { carries = "knife" }, - { carries = "$logs", min = 27 } + { area = { id = "$location" } }, + { carries = { id = "knife" } }, + { carries = { id = "$logs", min = 27 } } ] actions = [ { item = "knife", on = "$logs" }, @@ -35,17 +34,17 @@ actions = [ { restart = true, success = { inventory_space = 26 } } ] produces = [ - { skill = "fletching" } + { skill = { id = "fletching" } } ] [fletching_longbow_template] requires = [ - { owns = "$logs", min = 27 } + { owns = { id = "$logs", min = 27 } } ] setup = [ - { location = "$location" }, - { carries = "knife" }, - { carries = "$logs", min = 27 } + { area = { id = "$location" } }, + { carries = { id = "knife" } }, + { carries = { id = "$logs", min = 27 } } ] actions = [ { item = "knife", on = "$logs", success = { queue = "fletching_make_dialog" } }, @@ -54,5 +53,5 @@ actions = [ { restart = true, success = { inventory_space = 26 } } ] produces = [ - { skill = "fletching" } + { skill = { id = "fletching" } } ] diff --git a/data/bot/lumbridge.nav-edges.toml b/data/bot/lumbridge.nav-edges.toml index aba98a2c94..f88a020382 100644 --- a/data/bot/lumbridge.nav-edges.toml +++ b/data/bot/lumbridge.nav-edges.toml @@ -229,10 +229,10 @@ edges = [ { from_x = 3278, from_y = 3228, to_x = 3268, to_y = 3228 }, # al_kharid_crossroad_to_tollgate_north { from_x = 3278, from_y = 3228, to_x = 3268, to_y = 3227 }, # al_kharid_crossroad_to_tollgate { from_x = 3278, from_y = 3228, to_x = 3280, to_y = 3216 }, # al_kharid_crossroad_to_glider - { from_x = 3267, from_y = 3228, to_x = 3268, to_y = 3228, cost = 1, actions = [{ option = "Pay-toll(10gp)", object = "toll_gate_al_kharid_north", x = 3268, y = 3228 }], conditions = [{ carries = "coins", amount = 10 }] }, # lumbridge_tollgate_north_to_al_kharid_tollgate - { from_x = 3268, from_y = 3228, to_x = 3267, to_y = 3228, cost = 1, actions = [{ option = "Pay-toll(10gp)", object = "toll_gate_al_kharid_north", x = 3268, y = 3228 }], conditions = [{ carries = "coins", amount = 10 }] }, # al_kharid_tollgate_north_to_lumbridge_tollgate - { from_x = 3267, from_y = 3227, to_x = 3268, to_y = 3227, cost = 1, actions = [{ option = "Pay-toll(10gp)", object = "toll_gate_al_kharid_south", x = 3268, y = 3227 }], conditions = [{ carries = "coins", amount = 10 }] }, # lumbridge_tollgate_to_al_kharid - { from_x = 3268, from_y = 3227, to_x = 3267, to_y = 3227, cost = 1, actions = [{ option = "Pay-toll(10gp)", object = "toll_gate_al_kharid_south", x = 3268, y = 3227 }], conditions = [{ carries = "coins", amount = 10 }] }, # al_kharid_tollgate_to_lumbridge + { from_x = 3267, from_y = 3228, to_x = 3268, to_y = 3228, cost = 1, actions = [{ option = "Pay-toll(10gp)", object = "toll_gate_al_kharid_north", x = 3268, y = 3228 }], conditions = [{ carries = { id = "coins", amount = 10 } }] }, # lumbridge_tollgate_north_to_al_kharid_tollgate + { from_x = 3268, from_y = 3228, to_x = 3267, to_y = 3228, cost = 1, actions = [{ option = "Pay-toll(10gp)", object = "toll_gate_al_kharid_north", x = 3268, y = 3228 }], conditions = [{ carries = { id = "coins", amount = 10 } }] }, # al_kharid_tollgate_north_to_lumbridge_tollgate + { from_x = 3267, from_y = 3227, to_x = 3268, to_y = 3227, cost = 1, actions = [{ option = "Pay-toll(10gp)", object = "toll_gate_al_kharid_south", x = 3268, y = 3227 }], conditions = [{ carries = { id = "coins", amount = 10 } }] }, # lumbridge_tollgate_to_al_kharid + { from_x = 3268, from_y = 3227, to_x = 3267, to_y = 3227, cost = 1, actions = [{ option = "Pay-toll(10gp)", object = "toll_gate_al_kharid_south", x = 3268, y = 3227 }], conditions = [{ carries = { id = "coins", amount = 10 } }] }, # al_kharid_tollgate_to_lumbridge { from_x = 3268, from_y = 3227, to_x = 3280, to_y = 3216 }, # al_kharid_tollgate_to_glider { from_x = 3268, from_y = 3228, to_x = 3280, to_y = 3216 }, # al_kharid_tollgate_north_to_glider { from_x = 3280, from_y = 3216, to_x = 3292, to_y = 3215 }, # al_kharid_glider_to_musician diff --git a/data/bot/mining.templates.toml b/data/bot/mining.templates.toml new file mode 100644 index 0000000000..caa965981a --- /dev/null +++ b/data/bot/mining.templates.toml @@ -0,0 +1,135 @@ +[copper_ore_template] +requires = [ + { skill = { id = "mining", min = 1, max = 15 }}, +] +setup = [ + { carries = { id = "steel_pickaxe,iron_pickaxe,bronze_pickaxe" }}, + { area = { id = "$location" }}, + { inventory_space = { min = 27 }}, +] +actions = [ + { option = "Mine", object = "copper_rocks*", delay = 5, success = { inventory_space = 0 } }, +] +produces = [ + { carries = { id = "copper_ore" }}, + { skill = { id = "mining" } } +] + +[tin_ore_template] +requires = [ + { skill = { id = "mining", min = 1, max = 15 }}, +] +setup = [ + { carries = { id = "steel_pickaxe,iron_pickaxe,bronze_pickaxe" }}, + { area = { id = "$location" }}, + { inventory_space = { min = 27 }}, +] +actions = [ + { option = "Mine", object = "tin_rocks*", delay = 5, success = { inventory_space = 0 } }, +] +produces = [ + { carries = { id = "tin_ore" }}, + { skill = { id = "mining" } } +] + +[clay_template] +requires = [ + { skill = { id = "mining", min = 1, max = 15 }}, +] +setup = [ + { carries = { id = "steel_pickaxe,iron_pickaxe,bronze_pickaxe" }}, + { area = { id = "$location" }}, + { inventory_space = { min = 27 }}, +] +actions = [ + { option = "Mine", object = "clay_rocks*", delay = 5, success = { inventory_space = 0 } }, +] +produces = [ + { carries = { id = "clay" }}, + { skill = { id = "mining" } } +] + +[iron_ore_template] +requires = [ + { skill = { id = "mining", min = 15, max = 40 }}, +] +setup = [ + { carries = { id = "adamant_pickaxe,mithril_pickaxe,steel_pickaxe" }}, + { area = { id = "$location" }}, + { inventory_space = { min = 27 }}, +] +actions = [ + { option = "Mine", object = "iron_rocks*", delay = 5, success = { inventory_space = 0 } }, +] +produces = [ + { carries = { id = "iron_ore" }}, + { skill = { id = "mining" } } +] + +[silver_ore_template] +requires = [ + { skill = { id = "mining", min = 20, max = 40 }}, +] +setup = [ + { carries = { id = "adamant_pickaxe,mithril_pickaxe,steel_pickaxe" }}, + { area = { id = "$location" }}, + { inventory_space = { min = 27 }}, +] +actions = [ + { option = "Mine", object = "silver_rocks*", delay = 5, success = { inventory_space = 0 } }, +] +produces = [ + { carries = { id = "silver_ore" }}, + { skill = { id = "mining" } } +] + +[coal_template] +requires = [ + { skill = { id = "mining", min = 30, max = 55 }}, +] +setup = [ + { carries = { id = "rune_pickaxe,adamant_pickaxe,mithril_pickaxe" }}, + { area = { id = "$location" }}, + { inventory_space = { min = 27 }}, +] +actions = [ + { option = "Mine", object = "coal_rocks*", delay = 10, success = { inventory_space = 0 } }, +] +produces = [ + { carries = { id = "coal" }}, + { skill = { id = "mining" } } +] + +[gold_ore_template] +requires = [ + { skill = { id = "mining", min = 40, max = 60 }}, +] +setup = [ + { carries = { id = "rune_pickaxe,adamant_pickaxe,mithril_pickaxe" }}, + { area = { id = "$location" }}, + { inventory_space = { min = 27 }}, +] +actions = [ + { option = "Mine", object = "gold_rocks*", delay = 15, success = { inventory_space = 0 } }, +] +produces = [ + { carries = { id = "gold_ore" }}, + { skill = { id = "mining" } } +] + +[mithril_ore_template] +requires = [ + { skill = { id = "mining", min = 55, max = 70 }}, +] +setup = [ + { carries = { id = "dragon_pickaxe,rune_pickaxe,adamant_pickaxe" }}, + { area = { id = "$location" }}, + { inventory_space = { min = 27 }}, +] +actions = [ + { option = "Mine", object = "mithril_rocks*", delay = 15, success = { inventory_space = 0 } }, +] +produces = [ + { carries = { id = "mithril_ore" }}, + { skill = { id = "mining" } } +] diff --git a/data/bot/mining_templates.bots.toml b/data/bot/mining_templates.bots.toml deleted file mode 100644 index 01eb22be58..0000000000 --- a/data/bot/mining_templates.bots.toml +++ /dev/null @@ -1,150 +0,0 @@ -[copper_ore_template] -type = "activity" -capacity = 4 -requires = [ - { skill = "mining", min = 1, max = 15 }, -] -setup = [ - { carries = "steel_pickaxe,iron_pickaxe,bronze_pickaxe" }, - { location = "$location" }, - { inventory_space = 27 }, -] -actions = [ - { option = "Mine", object = "copper_rocks*", delay = 5, success = { inventory_space = 0 } }, -] -produces = [ - { carries = "copper_ore" }, - { skill = "mining" } -] - -[tin_ore_template] -type = "activity" -capacity = 4 -requires = [ - { skill = "mining", min = 1, max = 15 }, -] -setup = [ - { carries = "steel_pickaxe,iron_pickaxe,bronze_pickaxe" }, - { location = "$location" }, - { inventory_space = 27 }, -] -actions = [ - { option = "Mine", object = "tin_rocks*", delay = 5, success = { inventory_space = 0 } }, -] -produces = [ - { carries = "tin_ore" }, - { skill = "mining" } -] - -[clay_template] -type = "activity" -capacity = 2 -requires = [ - { skill = "mining", min = 1, max = 15 }, -] -setup = [ - { carries = "steel_pickaxe,iron_pickaxe,bronze_pickaxe" }, - { location = "$location" }, - { inventory_space = 27 }, -] -actions = [ - { option = "Mine", object = "clay_rocks*", delay = 5, success = { inventory_space = 0 } }, -] -produces = [ - { carries = "clay" }, - { skill = "mining" } -] - -[iron_ore_template] -type = "activity" -capacity = 2 -requires = [ - { skill = "mining", min = 15, max = 40 }, -] -setup = [ - { carries = "adamant_pickaxe,mithril_pickaxe,steel_pickaxe" }, - { location = "$location" }, - { inventory_space = 27 }, -] -actions = [ - { option = "Mine", object = "iron_rocks*", delay = 5, success = { inventory_space = 0 } }, -] -produces = [ - { carries = "iron_ore" }, - { skill = "mining" } -] - -[silver_ore_template] -type = "activity" -capacity = 2 -requires = [ - { skill = "mining", min = 20, max = 40 }, -] -setup = [ - { carries = "adamant_pickaxe,mithril_pickaxe,steel_pickaxe" }, - { location = "$location" }, - { inventory_space = 27 }, -] -actions = [ - { option = "Mine", object = "silver_rocks*", delay = 5, success = { inventory_space = 0 } }, -] -produces = [ - { carries = "silver_ore" }, - { skill = "mining" } -] - -[coal_template] -type = "activity" -capacity = 4 -requires = [ - { skill = "mining", min = 30, max = 55 }, -] -setup = [ - { carries = "rune_pickaxe,adamant_pickaxe,mithril_pickaxe" }, - { location = "$location" }, - { inventory_space = 27 }, -] -actions = [ - { option = "Mine", object = "coal_rocks*", delay = 10, success = { inventory_space = 0 } }, -] -produces = [ - { carries = "coal" }, - { skill = "mining" } -] - -[gold_ore_template] -capacity = 3 -requires = [ - { skill = "mining", min = 40, max = 60 }, -] -setup = [ - { carries = "rune_pickaxe,adamant_pickaxe,mithril_pickaxe" }, - { location = "$location" }, - { inventory_space = 27 }, -] -actions = [ - { option = "Mine", object = "gold_rocks*", delay = 15, success = { inventory_space = 0 } }, -] -produces = [ - { carries = "gold_ore" }, - { skill = "mining" } -] - -[mithril_ore_template] -type = "activity" -capacity = 2 -requires = [ - { skill = "mining", min = 55, max = 70 }, -] -setup = [ - { carries = "dragon_pickaxe,rune_pickaxe,adamant_pickaxe" }, - { location = "$location" }, - { inventory_space = 27 }, -] -actions = [ - { option = "Mine", object = "mithril_rocks*", delay = 15, success = { inventory_space = 0 } }, -] -produces = [ - { carries = "mithril_ore" }, - { skill = "mining" } -] diff --git a/data/bot/prayer.bots.toml b/data/bot/prayer.bots.toml index b5d60e5436..82afb9c84c 100644 --- a/data/bot/prayer.bots.toml +++ b/data/bot/prayer.bots.toml @@ -1,16 +1,15 @@ [bury_bones] -type = "activity" capacity = 5 requires = [ - { owns = "bones", amount = 28 }, + { owns = { id = "bones", min = 28 } }, ] setup = [ - { carries = "bones", amount = 28 }, + { carries = { id = "bones", min = 28 } }, ] actions = [ { option = "Bury", interface = "inventory:inventory:bones" }, { restart = true, wait_if = [{ variable = "bone_delay", min = 0, default = -1 }], success = { inventory_space = 28 } } ] produces = [ - { skill = "prayer" } + { skill = { id = "prayer" } } ] diff --git a/data/bot/shop.bots.toml b/data/bot/shop.templates.toml similarity index 62% rename from data/bot/shop.bots.toml rename to data/bot/shop.templates.toml index 07a1524392..d61eb82776 100644 --- a/data/bot/shop.bots.toml +++ b/data/bot/shop.templates.toml @@ -1,26 +1,26 @@ [buy_from_shop] setup = [ - { carries = "coins", amount = "$cost" }, - { location = "$shop_location" }, - { inventory_space = 1 }, + { carries = { id = "coins", min = "$cost" } }, + { area = { id = "$shop_location" } }, + { inventory_space = { min = 1 } }, ] actions = [ { option = "Trade", npc = "$shopkeeper", success = { interface = "shop" } }, { option = "Buy-1", interface = "shop:stock:$item", success = { carries = "$item" } }, ] produces = [ - { carries = "$item" } + { carries = { id = "$item" } } ] [take_shop_sample] setup = [ - { location = "$shop_location" }, - { inventory_space = 1 }, + { area = { id = "$shop_location" } }, + { inventory_space = { min = 1 } }, ] actions = [ { option = "Trade", npc = "$shopkeeper", success = { interface = "shop" } }, { option = "Take-1", interface = "shop:sample:$item", success = { carries = "$item" } }, ] produces = [ - { carries = "$item" } + { carries = { id = "$item" } } ] \ No newline at end of file diff --git a/data/bot/teleport.bots.toml b/data/bot/teleport.bot-shortcuts.toml similarity index 97% rename from data/bot/teleport.bots.toml rename to data/bot/teleport.bot-shortcuts.toml index 80cb988d0b..bdcfe8299b 100644 --- a/data/bot/teleport.bots.toml +++ b/data/bot/teleport.bot-shortcuts.toml @@ -26,7 +26,6 @@ #] [teleport_varrock] -type = "shortcut" weight = 125 requires = [ { variable = "spellbook_config", value = 0, default = 0 }, @@ -43,7 +42,6 @@ produces = [ ] [teleport_varrock_via_bank] -type = "shortcut" weight = 150 requires = [ { variable = "spellbook_config", value = 0, default = 0 }, @@ -67,7 +65,6 @@ produces = [ ] [teleport_lumbridge] -type = "shortcut" weight = 5 requires = [ { variable = "spellbook_config", value = 0, default = 0 }, diff --git a/data/bot/thieving_templates.bots.toml b/data/bot/thieving.templates.toml similarity index 52% rename from data/bot/thieving_templates.bots.toml rename to data/bot/thieving.templates.toml index 8a57103e87..7fc2d9e0ad 100644 --- a/data/bot/thieving_templates.bots.toml +++ b/data/bot/thieving.templates.toml @@ -1,18 +1,17 @@ [pickpocketting_template] -capacity = 3 requires = [ - { skill = "constitution", min = 100 }, - { skill = "thieving", min = 1, max = 10 }, + { skill = { id = "constitution", min = 100 } }, + { skill = { id = "thieving", min = 1, max = 10 } }, ] setup = [ - { inventory_space = 1 }, - { location = "$location" }, + { inventory_space = { min = 1 } }, + { area = { id = "$location" } }, ] actions = [ { option = "Pickpocket", npc = "$npc" }, { restart = true, wait_if = [{ variable = "delay", min = 0, default = -1 }, { clock = "stunned", min = 0 }], success = { skill = "constitution", max = 20 } } ] produces = [ - { carries = "coins" }, - { skill = "thieving" } + { carries = { id = "coins" } }, + { skill = { id = "thieving" } } ] \ No newline at end of file diff --git a/data/bot/walking.bots.toml b/data/bot/walking.setups.toml similarity index 70% rename from data/bot/walking.bots.toml rename to data/bot/walking.setups.toml index a2d067ca66..89c483da66 100644 --- a/data/bot/walking.bots.toml +++ b/data/bot/walking.setups.toml @@ -12,13 +12,12 @@ #] [toggle_run] -type = "resolver" requires = [ - { variable = "movement", value = "walk" } + { variable = { id = "movement", equals = "walk", default = "walk" } } ] actions = [ { option = "Turn Run mode on", interface = "energy_orb:run_background" } ] produces = [ - { variable = "movement", value = "run" } + { variable = { id = "movement", equals = "run", default = "walk" } } ] diff --git a/data/bot/woodcutting.templates.toml b/data/bot/woodcutting.templates.toml new file mode 100644 index 0000000000..5bc57e194a --- /dev/null +++ b/data/bot/woodcutting.templates.toml @@ -0,0 +1,67 @@ +[normal_tree_template] +requires = [ + { skill = { id = "woodcutting", min = 1, max = 15 } }, +] +setup = [ + { carries = { id = "steel_hatchet,iron_hatchet,bronze_hatchet" } }, + { area = { id = "$location" } }, + { inventory_space = { min = 27 } }, +] +actions = [ + { option = "Chop down", object = "tree*", delay = 5, success = { inventory_space = 0 } }, +] +produces = [ + { carries = { id = "logs" } }, + { skill = { id = "woodcutting" } } +] + +[oak_tree_template] +requires = [ + { skill = { id = "woodcutting", min = 15, max = 30 } }, +] +setup = [ + { carries = { id = "mithril_hatchet,steel_hatchet" } }, + { area = { id = "$location" } }, + { inventory_space = { min = 27 } }, +] +actions = [ + { option = "Chop down", object = "oak*", delay = 5, success = { inventory_space = 0 } }, +] +produces = [ + { carries = { id = "oak_logs" } }, + { skill = { id = "woodcutting" } } +] + +[willow_tree_template] +requires = [ + { skill = { id = "woodcutting", min = 30, max = 60 } }, +] +setup = [ + { carries = { id = "rune_hatchet,adamant_hatchet,mithril_hatchet,steel_hatchet" } }, + { area = { id = "$location" } }, + { inventory_space = { min = 27 } }, +] +actions = [ + { option = "Chop down", object = "willow*", delay = 5, success = { inventory_space = 0 } }, +] +produces = [ + { carries = { id = "willow_logs" } }, + { skill = { id = "woodcutting" } } +] + +[yew_tree_template] +requires = [ + { skill = { id = "woodcutting", min = 60, max = 75 } }, +] +setup = [ + { carries = { id = "dragon_hatchet,rune_hatchet,adamant_hatchet" } }, + { area = { id = "$location" } }, + { inventory_space = { min = 27 } }, +] +actions = [ + { option = "Chop down", object = "yew*", delay = 5, success = { inventory_space = 0 } }, +] +produces = [ + { carries = { id = "yew_logs" } }, + { skill = { id = "woodcutting" } } +] diff --git a/data/bot/woodcutting_templates.bots.toml b/data/bot/woodcutting_templates.bots.toml deleted file mode 100644 index 808582e536..0000000000 --- a/data/bot/woodcutting_templates.bots.toml +++ /dev/null @@ -1,75 +0,0 @@ -[normal_tree_template] -type = "activity" -capacity = 4 -requires = [ - { skill = "woodcutting", min = 1, max = 15 }, -] -setup = [ - { carries = "steel_hatchet,iron_hatchet,bronze_hatchet" }, - { location = "$location" }, - { inventory_space = 27 }, -] -actions = [ - { option = "Chop down", object = "tree*", delay = 5, success = { inventory_space = 0 } }, -] -produces = [ - { carries = "logs" }, - { skill = "woodcutting" } -] - -[oak_tree_template] -type = "activity" -capacity = 2 -requires = [ - { skill = "woodcutting", min = 15, max = 30 }, -] -setup = [ - { carries = "mithril_hatchet,steel_hatchet" }, - { location = "$location" }, - { inventory_space = 27 }, -] -actions = [ - { option = "Chop down", object = "oak*", delay = 5, success = { inventory_space = 0 } }, -] -produces = [ - { carries = "oak_logs" }, - { skill = "woodcutting" } -] - -[willow_tree_template] -type = "activity" -capacity = 2 -requires = [ - { skill = "woodcutting", min = 30, max = 60 }, -] -setup = [ - { carries = "rune_hatchet,adamant_hatchet,mithril_hatchet,steel_hatchet" }, - { location = "$location" }, - { inventory_space = 27 }, -] -actions = [ - { option = "Chop down", object = "willow*", delay = 5, success = { inventory_space = 0 } }, -] -produces = [ - { carries = "willow_logs" }, - { skill = "woodcutting" } -] - -[yew_tree_template] -type = "activity" -capacity = 2 -requires = [ - { skill = "woodcutting", min = 60, max = 75 }, -] -setup = [ - { carries = "dragon_hatchet,rune_hatchet,adamant_hatchet" }, - { location = "$location" }, - { inventory_space = 27 }, -] -actions = [ - { option = "Chop down", object = "yew*", delay = 5, success = { inventory_space = 0 } }, -] -produces = [ - { carries = "yew_logs" }, - { skill = "woodcutting" } -] diff --git a/game/src/main/kotlin/content/bot/BotManager.kt b/game/src/main/kotlin/content/bot/BotManager.kt index 548a38a3af..46064faa3c 100644 --- a/game/src/main/kotlin/content/bot/BotManager.kt +++ b/game/src/main/kotlin/content/bot/BotManager.kt @@ -4,6 +4,8 @@ import com.github.michaelbull.logging.InlineLogger import content.bot.action.* import content.bot.fact.Condition import content.bot.fact.Fact +import content.bot.fact.Predicate +import content.bot.fact.Requirement import content.bot.interact.path.Graph import content.bot.interact.path.Graph.Companion.loadGraph import content.entity.player.bank.bank @@ -72,7 +74,7 @@ class BotManager( fun load(files: ConfigFiles): BotManager { val shortcuts = mutableListOf() - loadActivities(files.list(Settings["bots.definitions"]), activities, groups, resolvers, shortcuts) + loadActivities(files, activities, groups, resolvers, shortcuts) graph = loadGraph(files.list(Settings["bots.nav.definitions"]), shortcuts) return this } @@ -167,7 +169,7 @@ class BotManager( frame.fail(Reason.Requirement(requirement)) return } - for (requirement in behaviour.resolve) { + for (requirement in behaviour.setup) { if (requirement.check(bot.player)) { continue } @@ -177,7 +179,7 @@ class BotManager( .minByOrNull { it.weight } if (resolver == null) { if (bot.player["debug", false]) { - logger.info { "No resolver found for for ${behaviour.id} keys: ${requirement.keys()} requirement: ${requirement}." } + logger.info { "No resolver found for for ${behaviour.id} keys: ${requirement.fact.keys()} requirement: ${requirement}." } for (resolver in resolvers) { if (frame.blocked.contains(resolver.id)) { logger.debug { "Resolver: ${resolver.id} - Blocked by frame behaviour: ${frame.behaviour.id}." } @@ -212,35 +214,31 @@ class BotManager( frame.start(bot) } - private fun availableResolvers(bot: Bot, condition: Condition): MutableList { + private fun availableResolvers(bot: Bot, condition: Requirement<*>): MutableList { val options = mutableListOf() addDefaultResolvers(bot, options, condition) - if (condition is Condition.Any) { - for (condition in condition.conditions) { - addDefaultResolvers(bot, options, condition) - } - } - for (key in condition.keys()) { + for (key in condition.fact.keys()) { options.addAll(resolvers[key] ?: continue) } return options } - private fun addDefaultResolvers(bot: Bot, resolvers: MutableList, condition: Condition) { - if (condition is Condition.Area) { - resolvers.add(Resolver("go_to_${condition.area}", -1, actions = listOf(BotAction.GoTo(condition.area)), produces = setOf(condition))) - } else if (condition is Condition.AtLeast && condition.fact is Fact.InventoryCount && bot.player.bank.contains(condition.fact.id, condition.min)) { - if (condition.min == 1 || condition.min == 5 || condition.min == 10) { + private fun addDefaultResolvers(bot: Bot, resolvers: MutableList, requirement: Requirement<*>) { + val predicate = requirement.predicate + if (predicate is Predicate.InArea) { + resolvers.add(Resolver("go_to_${predicate.name}", -1, actions = listOf(BotAction.GoTo(predicate.name)), produces = setOf(requirement))) + } else if (predicate is Predicate.IntRange && requirement.fact is Fact.InventoryCount && bot.player.bank.contains(requirement.fact.id, predicate.min!!)) { + if (predicate.min == 1 || predicate.min == 5 || predicate.min == 10) { resolvers.add( Resolver( - "withdraw_${condition.fact.id}", weight = 20, - resolve = listOf( - Condition.AtLeast(Fact.InventorySpace, condition.min), + "withdraw_${requirement.fact.id}", weight = 20, + setup = listOf( + Requirement(Fact.InventorySpace, Predicate.IntRange(predicate.min)) ), actions = listOf( BotAction.GoToNearest("bank"), BotAction.InteractObject("Use-quickly", "bank_booth*", success = Condition.Equals(Fact.InterfaceOpen("bank"), true)), - BotAction.InterfaceOption("Withdraw-${condition.min}", "bank:inventory:${condition.fact.id}"), + BotAction.InterfaceOption("Withdraw-${predicate.min}", "bank:inventory:${requirement.fact.id}"), BotAction.CloseInterface, ) ) @@ -248,40 +246,42 @@ class BotManager( } else { resolvers.add( Resolver( - "withdraw_${condition.fact.id}", weight = 20, - resolve = listOf( - Condition.AtLeast(Fact.InventorySpace, condition.min), + "withdraw_${requirement.fact.id}", weight = 20, + setup = listOf( + Requirement(Fact.InventorySpace, Predicate.IntRange(predicate.min)) ), actions = listOf( BotAction.GoToNearest("bank"), BotAction.InteractObject("Use-quickly", "bank_booth*", success = Condition.Equals(Fact.InterfaceOpen("bank"), true)), - BotAction.InterfaceOption("Withdraw-X", "bank:inventory:${condition.fact.id}"), - BotAction.IntEntry(condition.min), + BotAction.InterfaceOption("Withdraw-X", "bank:inventory:${requirement.fact.id}"), + BotAction.IntEntry(predicate.min), BotAction.CloseInterface, ) ) ) } - } else if (condition is Condition.AtLeast && condition.fact is Fact.EquipCount) { + } else if (predicate is Predicate.IntRange && requirement.fact is Fact.EquipCount) { resolvers.add( Resolver( - "equip_${condition.fact.id}", weight = 0, - requires = listOf(Condition.AtLeast(Fact.InventoryCount(condition.fact.id), condition.min)), - actions = listOf(BotAction.InterfaceOption("Equip", "inventory:inventory:${condition.fact.id}")) + "equip_${requirement.fact.id}", weight = 0, + setup = listOf( + Requirement(Fact.InventoryCount(requirement.fact.id), Predicate.IntRange(predicate.min)) + ), + actions = listOf(BotAction.InterfaceOption("Equip", "inventory:inventory:${requirement.fact.id}")) ) ) resolvers.add( Resolver( - "withdraw_and_equip_${condition.fact.id}", weight = 0, - requires = listOf(Condition.AtLeast(Fact.BankCount(condition.fact.id), condition.min)), - resolve = listOf(Condition.AtLeast(Fact.InventorySpace, condition.min)), + "withdraw_and_equip_${requirement.fact.id}", weight = 0, + requires = listOf(Requirement(Fact.BankCount(requirement.fact.id), Predicate.IntRange(predicate.min))), + setup = listOf(Requirement(Fact.InventorySpace, Predicate.IntRange(predicate.min))), actions = listOf( BotAction.GoToNearest("bank"), BotAction.InteractObject("Use-quickly", "bank_booth*", success = Condition.Equals(Fact.InterfaceOpen("bank"), true)), - BotAction.InterfaceOption("Withdraw-X", "bank:inventory:${condition.fact.id}"), - BotAction.IntEntry(condition.min), + BotAction.InterfaceOption("Withdraw-X", "bank:inventory:${requirement.fact.id}"), + BotAction.IntEntry(predicate.min!!), BotAction.CloseInterface, - BotAction.InterfaceOption("Equip", "inventory:inventory:${condition.fact.id}") + BotAction.InterfaceOption("Equip", "inventory:inventory:${requirement.fact.id}") ) ) ) diff --git a/game/src/main/kotlin/content/bot/action/Behaviour.kt b/game/src/main/kotlin/content/bot/action/Behaviour.kt index 941c2fa40d..84ffbd40e8 100644 --- a/game/src/main/kotlin/content/bot/action/Behaviour.kt +++ b/game/src/main/kotlin/content/bot/action/Behaviour.kt @@ -1,11 +1,12 @@ package content.bot.action import content.bot.fact.Condition +import content.bot.fact.Requirement interface Behaviour { val id: String - val requires: List - val resolve: List + val requires: List> + val setup: List> val actions: List - val produces: Set + val produces: Set> } \ No newline at end of file diff --git a/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt b/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt index 4ac3fd7ccd..2647d2ce17 100644 --- a/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt +++ b/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt @@ -9,10 +9,10 @@ data class BehaviourFragment( val capacity: Int, val weight: Int, var template: String, - override val requires: List = emptyList(), - override val resolve: List = emptyList(), + override val requires: List> = emptyList(), + override val setup: List> = emptyList(), override val actions: List = emptyList(), - override val produces: Set = emptySet(), + override val produces: Set> = emptySet(), val fields: Map = emptyMap(), ) : Behaviour { fun resolveActions(template: BotActivity, actions: MutableList) { diff --git a/game/src/main/kotlin/content/bot/action/BotAction.kt b/game/src/main/kotlin/content/bot/action/BotAction.kt index 08dbc7ed11..0604b113e1 100644 --- a/game/src/main/kotlin/content/bot/action/BotAction.kt +++ b/game/src/main/kotlin/content/bot/action/BotAction.kt @@ -535,15 +535,26 @@ sealed interface BotAction { /** * TODO + * Split edges into separate files + * combat training dummy bots * firemaking bot - * cooking bot * rune mysteries quest bot * bot saving? * bot setups * bot spawning in other locations + * tidy up old bot code + * move tags into edges not areas * * TODO need a better way of managing inventory. Removing unneeded items in favour of required ones vs emptying full inventory every time etc... * + * Idea: Reactions? + * A separate queue that runs "reactions" e.g. + * if my hp is low and I have food - eat it + * if I have bones and am not in combat - bury them + * if I have raw food and there's a fire nearby - cook it + * + * TODO behaviour loop detection + * TODO how to handle repeat actions e.g. repeat Chop-down trees until inv is full - These are actions Gathering, Skilling etc.. more resolvers like bank all, drop cheap items how to handle combat, one task or multiple? - One Fight action diff --git a/game/src/main/kotlin/content/bot/action/BotActivity.kt b/game/src/main/kotlin/content/bot/action/BotActivity.kt index 7b2d961e59..44be06353d 100644 --- a/game/src/main/kotlin/content/bot/action/BotActivity.kt +++ b/game/src/main/kotlin/content/bot/action/BotActivity.kt @@ -1,11 +1,12 @@ package content.bot.action -import content.bot.fact.Condition -import content.bot.fact.Fact +import content.bot.fact.Requirement import world.gregs.config.Config import world.gregs.config.ConfigReader -import world.gregs.voidps.engine.event.Wildcard +import world.gregs.voidps.engine.data.ConfigFiles +import world.gregs.voidps.engine.data.Settings import world.gregs.voidps.engine.timedLoad +import kotlin.collections.set /** * An activity with a limited number of slots that bots can perform @@ -14,412 +15,290 @@ import world.gregs.voidps.engine.timedLoad data class BotActivity( override val id: String, val capacity: Int, - override val requires: List = emptyList(), - override val resolve: List = emptyList(), + override val requires: List> = emptyList(), + override val setup: List> = emptyList(), override val actions: List = emptyList(), - override val produces: Set = emptySet(), + override val produces: Set> = emptySet(), ) : Behaviour fun loadActivities( - paths: List, + files: ConfigFiles, activities: MutableMap, groups: MutableMap>, resolvers: MutableMap>, shortcuts: MutableList, ) { - val fragments = mutableMapOf() + val templates = loadTemplates(files.list(Settings["bots.templates"])) + loadActivities(activities, templates, files.list(Settings["bots.definitions"])) + loadSetups(resolvers, templates, files.list(Settings["bots.setups"])) + loadShortcuts(shortcuts, templates, files.list(Settings["bots.shortcuts"])) +} + +private fun loadActivities(activities: MutableMap, templates: Map, paths: List) { timedLoad("bot activity") { - val reqClones = mutableMapOf() - val resClones = mutableMapOf() + val fragments = mutableListOf() for (path in paths) { Config.fileReader(path) { while (nextSection()) { val id = section() - var capacity = 0 var template: String? = null - var type = "activity" - var weight = 0 - val actions: MutableList = mutableListOf() - val requirements: MutableList = mutableListOf() - val resolvables: MutableList = mutableListOf() - val produces: MutableList = mutableListOf() - var fields: Map = emptyMap() + var fields: Map? = null + var capacity = 1 + val requires = mutableListOf>>() + val setup = mutableListOf>>() + val actions = mutableListOf>() + val produces = mutableListOf>>() while (nextPair()) { when (val key = key()) { - "requires" -> requirements(requirements) - "setup" -> requirements(resolvables) - "actions" -> actions(actions) + "template" -> template = string() + "requires" -> requirements(requires) + "setup" -> requirements(setup) + "actions" -> while (nextElement()) { + actions.add(map()) + } "produces" -> requirements(produces) "capacity" -> capacity = int() - "type" -> type = string() - "template" -> template = string() - "weight" -> weight = int() - "fields" -> fields = fields() + "fields" -> fields = map() else -> throw IllegalArgumentException("Unexpected key: '$key' ${exception()}") } } - val clone = requirements.filterIsInstance().firstOrNull() - if (clone != null) { - reqClones[id] = clone.id - } - val resolveClone = resolvables.filterIsInstance().firstOrNull() - if (resolveClone != null) { - resClones[id] = resolveClone.id + if (template != null) { + requireNotNull(fields) + fragments.add(Fragment(id, template, fields, capacity, requires, setup, actions, produces)) + } else { + activities[id] = BotActivity(id, capacity, Requirement.parse(requires, "$id ${exception()}")) } + } + } + } + for (fragment in fragments) { + val template = templates[fragment.template] ?: error("Unable to find template '${fragment.template}' for ${fragment.id}.") + activities[fragment.id] = fragment.activity(template) + } + for (activity in activities.values) { + println(activity) + } + activities.size + } +} + +private fun loadSetups(resolvers: MutableMap>, templates: Map, paths: List) { + timedLoad("bot setup") { + val fragments = mutableListOf() + for (path in paths) { + Config.fileReader(path) { + while (nextSection()) { + val id = section() + var template: String? = null + var fields: Map? = null + var weight = 1 + val requires = mutableListOf>>() + val setup = mutableListOf>>() + val actions = mutableListOf>() + val produces = mutableListOf>>() + while (nextPair()) { + when (val key = key()) { + "template" -> template = string() + "requires" -> requirements(requires) + "setup" -> requirements(setup) + "actions" -> while (nextElement()) { + actions.add(map()) + } + "produces" -> requirements(produces) + "weight" -> weight = int() + "fields" -> fields = map() + else -> throw IllegalArgumentException("Unexpected key: '$key' ${exception()}") + } + } if (template != null) { - fragments[id] = BehaviourFragment(id, type, capacity, weight, template, requirements, resolvables, actions = actions, fields = fields, produces = produces.toSet()) - } else if (type == "resolver") { - val resolver = Resolver(id, weight, requirements, resolvables, actions = actions, produces = produces.toSet()) - for (fact in produces) { - for (key in fact.keys()) { + requireNotNull(fields) + fragments.add(Fragment(id, template, fields, weight, requires, setup, actions, produces)) + } else { + val debug = "$id ${exception()}" + val products = Requirement.parse(produces, debug) + val resolver = Resolver(id, weight, Requirement.parse(requires, debug), Requirement.parse(setup, debug), produces = products.toSet()) + for (product in products) { + for (key in product.fact.keys()) { resolvers.getOrPut(key) { mutableListOf() }.add(resolver) } } - } else if (type == "shortcut") { - require(resolvables.isEmpty()) { "Shortcuts cannot have setup requirements" } - shortcuts.add(NavigationShortcut(id, weight, requirements, actions = actions, produces = produces.toSet())) - } else { - activities[id] = BotActivity(id, capacity, requirements, resolvables, actions = actions, produces = produces.toSet()) } } } } - // Resolve cloning first - for (activity in activities.values + fragments.values) { - for (index in activity.actions.indices.reversed()) { - val action = activity.actions[index] - if (action is BotAction.Clone) { - val list = activities[action.id]?.actions ?: throw IllegalArgumentException("Unable to find activity to clone '${action.id}'.") - val actions = activity.actions as MutableList - actions.removeAt(index) - actions.addAll(index, list) - } - } - for ((id, cloneId) in reqClones) { - val activity = activities[id] ?: continue - val clone = activities[cloneId] ?: continue - val requirements = activity.requires as MutableList - requirements.removeIf { it is Condition.Clone && it.id == cloneId } - requirements.addAll(clone.requires) - requirements.sortBy { it.priority() } - } - for ((id, cloneId) in resClones) { - val activity = activities[id] ?: continue - val clone = activities[cloneId] ?: continue - val resolvables = activity.resolve as MutableList - resolvables.removeIf { it is Condition.Clone && it.id == cloneId } - resolvables.addAll(clone.resolve) - resolvables.sortBy { it.priority() } - } - } - // Fragments are partially filled behaviours with template + fields - // This code resolves those fields into actual values taken from the template. - val templates = mutableSetOf() - for ((id, fragment) in fragments) { - val template = activities[fragment.template] ?: throw IllegalArgumentException("Unable to find template '${fragment.template}' for activity '$id'.") - templates.add(fragment.template) - - val requirements = mutableListOf() - requirements.addAll(fragment.requires) - fragment.resolveRequirements(requirements, template.requires) - requirements.sortBy { it.priority() } - val resolvables = mutableListOf() - resolvables.addAll(fragment.resolve) - fragment.resolveRequirements(resolvables, template.resolve) - resolvables.sortBy { it.priority() } - - val products = mutableListOf() - products.addAll(fragment.produces) - fragment.resolveRequirements(products, template.produces.toList()) - products.sortBy { it.priority() } + for (fragment in fragments) { + val template = templates[fragment.template] ?: error("Unable to find template '${fragment.template}' for ${fragment.id}.") + resolvers.getOrPut(fragment.id) { mutableListOf() }.add(fragment.resolver(template)) + } + resolvers.size + } +} - val actions = mutableListOf() - actions.addAll(fragment.actions) - fragment.resolveActions(template, actions) - when (fragment.type) { - "resolver" -> { - val resolver = Resolver(id, fragment.weight, requirements, resolvables, actions, products.toSet()) - for (fact in products) { - for (key in fact.keys()) { - resolvers.getOrPut(key) { mutableListOf() }.add(resolver) +private fun loadShortcuts(shortcuts: MutableList, templates: Map, paths: List) { + timedLoad("bot shortcut") { + val fragments = mutableListOf() + for (path in paths) { + Config.fileReader(path) { + while (nextSection()) { + val id = section() + var template: String? = null + var fields: Map? = null + var weight = 1 + val requires = mutableListOf>>() + val setup = mutableListOf>>() + val actions = mutableListOf>() + val produces = mutableListOf>>() + while (nextPair()) { + when (val key = key()) { + "template" -> template = string() + "requires" -> requirements(requires) + "setup" -> requirements(setup) + "actions" -> while (nextElement()) { + actions.add(map()) + } + "produces" -> requirements(produces) + "weight" -> weight = int() + "fields" -> fields = map() + else -> throw IllegalArgumentException("Unexpected key: '$key' ${exception()}") } } + if (template != null) { + requireNotNull(fields) + fragments.add(Fragment(id, template, fields, weight, requires, setup, actions, produces)) + } else { + shortcuts.add(NavigationShortcut(id, weight, Requirement.parse(requires, id), Requirement.parse(setup, id))) + } } - "shortcut" -> shortcuts.add(NavigationShortcut(id, fragment.weight, requirements, actions = actions)) - else -> activities[id] = BotActivity(id, template.capacity, requirements, resolvables, actions) } } - // Templates aren't selectable activities - for (template in templates) { - activities.remove(template) - } - // Group activities by requirement types - for (activity in activities.values) { - for (fact in activity.requires) { - for (key in fact.groups()) { - groups.getOrPut(key) { mutableListOf() }.add(activity.id) - } - } - println(activity) + for (fragment in fragments) { + val template = templates[fragment.template] ?: error("Unable to find template '${fragment.template}' for ${fragment.id}.") + shortcuts.add(fragment.shortcut(template)) } - activities.size + shortcuts.size } } -private fun ConfigReader.fields(): Map { - val map = mutableMapOf() - while (nextEntry()) { - val key = key() - val value = value() - map[key] = value +private fun loadTemplates(paths: List): Map { + val templates = mutableMapOf() + timedLoad("bot template") { + for (path in paths) { + Config.fileReader(path) { + while (nextSection()) { + val id = section() + val requires = mutableListOf>>() + val setup = mutableListOf>>() + val actions = mutableListOf>() + val produces = mutableListOf>>() + while (nextPair()) { + when (val key = key()) { + "requires" -> requirements(requires) + "setup" -> requirements(setup) + "actions" -> while (nextElement()) { + actions.add(map()) + } + "produces" -> requirements(produces) + else -> throw IllegalArgumentException("Unexpected key: '$key' ${exception()}") + } + } + templates[id] = Template(requires, setup, actions, produces) + } + } + } + templates.size } - return map + return templates } -fun ConfigReader.requirements(list: MutableList, parentReferences: MutableMap? = null, exact: Boolean = false) { +private fun ConfigReader.requirements(requires: MutableList>>) { while (nextElement()) { - list.add(requirement(parentReferences = parentReferences, exact = exact)) + while (nextEntry()) { + val key = key() + val value = map() + requires.add(key to value) + } } - list.sortBy { it.priority() } } -private fun ConfigReader.requirement(parentReferences: MutableMap? = null, exact: Boolean = false): Condition { - var type = "" - var id = "" - var value: Any? = null - var default: Any? = null - var min: Int? = null - var max: Int? = null - val references = mutableMapOf() - while (nextEntry()) { - when (val key = key()) { - "skill", "carries", "equips", "interface", "owns", "clock", "variable", "clone", "location", "queue" -> { - type = key - id = string() - if (id.contains('$')) { - references[key] = id - } - } - "amount", "min" -> when (val value = value()) { - is Int -> min = value - is String if value.contains('$') -> references[key] = value - else -> throw IllegalArgumentException("Invalid '$key' value: $value ${exception()}") - } - "max" -> when (val value = value()) { - is Int -> max = value - is String if value.contains('$') -> references[key] = value - else -> throw IllegalArgumentException("Invalid '$key' value: $value ${exception()}") - } - "combat_level", "inventory_space" -> { - type = key - when (val value = value()) { - is Int -> min = value - is String if value.contains('$') -> references[key] = value - else -> throw IllegalArgumentException("Invalid '$key' value: $value ${exception()}") - } +private data class Fragment( + val id: String, + val template: String, + val fields: Map, + val int: Int, + val requires: List>>, + val setup: List>>, + val actions: List>, + val produces: List>>, +) { + fun activity(template: Template) = BotActivity( + id = id, + capacity = int, + requires = resolveRequirements(template.requires, requires), + setup = resolveRequirements(template.setup, setup), + produces = resolveRequirements(template.produces, produces, requirePredicates = false).toSet(), + ) + + private fun resolveRequirements(templated: List>>, original: List>>, requirePredicates: Boolean = true): List> { + val combinedList = mutableListOf>>() + for ((type, map) in templated) { + val combinedMap = mutableMapOf() + for ((key, value) in original) { + combinedMap[key] = value } - "radius" -> { - type = "tile" - when (val value = value()) { - is Int -> min = value - is String if value.contains('$') -> references[key] = value - else -> throw IllegalArgumentException("Invalid '$key' value: $value ${exception()}") + for ((key, value) in map) { + if (value !is String || !value.contains('$')) { + combinedMap[key] = value + continue } + val ref = value.reference() + val name = ref.trim('$', '{', '}') + val replacement = fields[name] ?: error("No field found for behaviour=$id type=${type} key=$key ref=$ref") + combinedMap[key] = if (replacement is String) value.replace(ref, replacement) else replacement } - "level" -> { - type = key - when (val v = value()) { - is Int -> value = v - is String if v.contains('$') -> references[key] = v - else -> throw IllegalArgumentException("Invalid '$key' value: $v ${exception()}") - } + if (combinedMap.isNotEmpty()) { + combinedList.add(type to combinedMap) } - "value" -> value = value() - "default" -> default = value() } - } - var requirement = getRequirement(type, id, min, max, value, default, exact) - if (requirement == null) { - if (type == "holds") { - throw IllegalArgumentException("Unknown requirement type 'holds'; did you mean 'carries' or 'equips'? ${exception()}.") + if (combinedList.isEmpty()) { + return emptyList() } - throw IllegalArgumentException("Unknown requirement type: $type ${exception()}") - } - if (references.isNotEmpty()) { - requirement = Condition.Reference(type, id, value, default, min, max, references) - parentReferences?.putAll(references) + return Requirement.parse(combinedList, "$id template $template", requirePredicates) } - return requirement -} -private fun getRequirement(type: String, id: String, min: Int?, max: Int?, value: Any?, default: Any?, exact: Boolean): Condition? = when (type) { - "skill" -> Condition.range(Fact.SkillLevel.of(id), min, max) - "carries" -> Condition.split(id, min, max, Wildcard.Item) { Fact.InventoryCount(it) } - "owns" -> Condition.split(id, min, max, Wildcard.Item) { Fact.ItemCount(it) } - "banked" -> Condition.split(id, min, max, Wildcard.Item) { Fact.BankCount(it) } - "equips" -> Condition.split(id, min, max, Wildcard.Item) { Fact.EquipCount(it) } - "clock" -> Condition.split(id, min, max, Wildcard.Variables) { Fact.ClockRemaining(it) } - "timer" -> Condition.Equals(Fact.HasTimer(id), value as? Boolean ?: true) - "queue" -> Condition.Equals(Fact.HasQueue(id), value as? Boolean ?: true) - "interface" -> Condition.Equals(Fact.InterfaceOpen(id), value as? Boolean ?: true) - "variable" -> { - if (min != null || max != null) { - Condition.range(Fact.IntVariable(id, default as Int), min, max) - } else { - when (value) { - is Int -> Condition.Equals(Fact.IntVariable(id, default as Int), value) - is String -> Condition.Equals(Fact.StringVariable(id, default as? String), value) - is Double -> Condition.Equals(Fact.DoubleVariable(id, default as? Double), value) - is Boolean -> Condition.Equals(Fact.BoolVariable(id, default as? Boolean), value) - else -> null - } + private fun String.reference(): String { + if (startsWith('$')) { + return this } + val index = indexOf($$"${") + if (index == -1) { + return "\$${substringAfter('$')}" + } + val end = indexOf('}', index) + 1 + return substring(index, end) } - "clone" -> Condition.Clone(id) - "inventory_space" -> if (exact && min != null) Condition.Equals(Fact.InventorySpace, min) else Condition.range(Fact.InventorySpace, min, max) - "location" -> Condition.Area(Fact.PlayerTile, id) - "level" -> Condition.Equals(Fact.PlayerLevel, value as Int) - "combat_level" -> Condition.AtLeast(Fact.CombatLevel, min ?: 1) - else -> null + + fun resolver(template: Template) = Resolver( + id = id, + weight = int, + requires = resolveRequirements(template.requires, requires), + setup = resolveRequirements(template.setup, setup), + produces = resolveRequirements(template.produces, produces, requirePredicates = false).toSet(), + ) + + fun shortcut(template: Template) = NavigationShortcut( + id = id, + weight = int, + requires = resolveRequirements(template.requires, requires), + setup = resolveRequirements(template.setup, setup), + produces = resolveRequirements(template.produces, produces, requirePredicates = false).toSet(), + ) } -fun ConfigReader.actions(list: MutableList) { - while (nextElement()) { - var type = "" - var id = "" - var option = "" - var int = 0 - var ticks = 0 - var radius = 10 - var delay = 0 - var heal = 0 - var loot = 0 - var x = 0 - var y = 0 - var success: Condition? = null - val references = mutableMapOf() - val wait = mutableListOf() - while (nextEntry()) { - when (val key = key()) { - "go_to", "go_to_nearest", "enter_string", "interface", "npc", "object", "clone", "item", "continue" -> { - type = key - id = string() - if (id.contains('$')) { - references[key] = id - } - } - "x" -> { - if (type == "") { - type = "tile" - } - val value = value() - if (value is String && value.contains('$')) { - references[key] = value - } else { - x = value as Int - } - } - "y" -> { - if (type == "") { - type = "tile" - } - val value = value() - if (value is String && value.contains('$')) { - references[key] = value - } else { - y = value as Int - } - } - "target", "id" -> { - id = string() - if (id.contains('$')) { - references[key] = id - } - } - "option", "on" -> { - option = string() - if (option.contains('$')) { - references[key] = option - } - } - "on_object" -> { - type = "${type}_on_object" - option = string() - if (option.contains('$')) { - references[key] = option - } - } - "restart" -> { - require(boolean()) { "Can't have restart = false ${exception()}" } - type = key - } - "success" -> success = requirement(references, exact = true) - "wait_if" -> requirements(wait, references, exact = true) - "wait" -> { - type = key - when (val value = value()) { - is Int -> ticks = value - is String if value.contains('$') -> references[key] = value - else -> throw IllegalArgumentException("Invalid '$key' value: $value ${exception()}") - } - } - "radius" -> when (val value = value()) { - is Int -> radius = value - is String if value.contains('$') -> references[key] = value - else -> throw IllegalArgumentException("Invalid '$key' value: $value ${exception()}") - } - "heal_percent" -> when (val value = value()) { - is Int -> heal = value - is String if value.contains('$') -> references[key] = value - else -> throw IllegalArgumentException("Invalid '$key' value: $value ${exception()}") - } - "loot_over" -> when (val value = value()) { - is Int -> loot = value - is String if value.contains('$') -> references[key] = value - else -> throw IllegalArgumentException("Invalid '$key' value: $value ${exception()}") - } - "delay" -> when (val value = value()) { - is Int -> delay = value - is String if value.contains('$') -> references[key] = value - else -> throw IllegalArgumentException("Invalid '$key' value: $value ${exception()}") - } - "enter_int" -> { - type = key - when (val value = value()) { - is Int -> int = value - is String if value.contains('$') -> references[key] = value - else -> throw IllegalArgumentException("Invalid '$key' value: $value ${exception()}") - } - } - else -> throw IllegalArgumentException("Unknown action key: $key ${exception()}") - } - } - var action = when (type) { - "go_to" -> BotAction.GoTo(id) - "go_to_nearest" -> BotAction.GoToNearest(id) - "enter_string" -> BotAction.StringEntry(id) - "enter_int" -> BotAction.IntEntry(int) - "wait" -> BotAction.Wait(ticks) - "restart" -> BotAction.Restart(wait, success ?: throw IllegalArgumentException("Restart must have success condition.")) - "npc" -> if (option == "Attack") { - BotAction.FightNpc(id = id, delay = delay, success = success, healPercentage = heal, lootOverValue = loot, radius = radius) - } else { - BotAction.InteractNpc(id = id, option = option, delay = delay, success = success, radius = radius) - } - "tile" -> BotAction.WalkTo(x = x, y = y) - "object" -> BotAction.InteractObject(id = id, option = option, delay = delay, success = success, radius = radius) - "interface" -> BotAction.InterfaceOption(id = id, option = option, success = success) - "continue" -> BotAction.DialogueContinue(id = id, option = option, success = success) - "item" -> BotAction.ItemOnItem(item = id, on = option, success = success) - "item_on_object" -> BotAction.ItemOnObject(item = id, id = option, success = success) - "clone" -> BotAction.Clone(id) - else -> throw IllegalArgumentException("Unknown action type: $type ${exception()}") - } - if (references.isNotEmpty()) { - action = BotAction.Reference(action, references) - } - list.add(action) - } -} \ No newline at end of file +private data class Template( + val requires: List>>, + val setup: List>>, + val actions: List>, + val produces: List>>, +) diff --git a/game/src/main/kotlin/content/bot/action/NavigationShortcut.kt b/game/src/main/kotlin/content/bot/action/NavigationShortcut.kt index abad9917eb..1193ee0547 100644 --- a/game/src/main/kotlin/content/bot/action/NavigationShortcut.kt +++ b/game/src/main/kotlin/content/bot/action/NavigationShortcut.kt @@ -1,12 +1,12 @@ package content.bot.action -import content.bot.fact.Condition +import content.bot.fact.Requirement data class NavigationShortcut( override val id: String, val weight: Int, - override val requires: List = emptyList(), - override val resolve: List = emptyList(), + override val requires: List> = emptyList(), + override val setup: List> = emptyList(), override val actions: List = emptyList(), - override val produces: Set = emptySet(), + override val produces: Set> = emptySet(), ) : Behaviour \ No newline at end of file diff --git a/game/src/main/kotlin/content/bot/action/Reason.kt b/game/src/main/kotlin/content/bot/action/Reason.kt index 01f7988aab..04caba4855 100644 --- a/game/src/main/kotlin/content/bot/action/Reason.kt +++ b/game/src/main/kotlin/content/bot/action/Reason.kt @@ -1,7 +1,5 @@ package content.bot.action -import content.bot.fact.Condition - interface Reason { data class Invalid(val message: String) : HardReason object Cancelled : HardReason @@ -9,7 +7,7 @@ interface Reason { object Timeout : HardReason object Stuck : SoftReason object NoTarget : SoftReason - data class Requirement(val fact: Condition) : HardReason + data class Requirement(val fact: content.bot.fact.Requirement<*>) : HardReason } interface SoftReason : Reason interface HardReason : Reason diff --git a/game/src/main/kotlin/content/bot/action/Resolver.kt b/game/src/main/kotlin/content/bot/action/Resolver.kt index 251b883aac..8aea05c4d1 100644 --- a/game/src/main/kotlin/content/bot/action/Resolver.kt +++ b/game/src/main/kotlin/content/bot/action/Resolver.kt @@ -1,6 +1,6 @@ package content.bot.action -import content.bot.fact.Condition +import content.bot.fact.Requirement /** * An activity that can be performed to resolve a requirement @@ -10,8 +10,8 @@ import content.bot.fact.Condition data class Resolver( override val id: String, val weight: Int, - override val requires: List = emptyList(), - override val resolve: List = emptyList(), + override val requires: List> = emptyList(), + override val setup: List> = emptyList(), override val actions: List = emptyList(), - override val produces: Set = emptySet(), + override val produces: Set> = emptySet(), ) : Behaviour \ No newline at end of file diff --git a/game/src/main/kotlin/content/bot/fact/Fact.kt b/game/src/main/kotlin/content/bot/fact/Fact.kt index 4297a494a4..cdd7ccedb0 100644 --- a/game/src/main/kotlin/content/bot/fact/Fact.kt +++ b/game/src/main/kotlin/content/bot/fact/Fact.kt @@ -146,6 +146,7 @@ sealed class Fact(val priority: Int) { override fun getValue(player: Player) = player.levels.get(skill!!) companion object { + fun of(skill: String): SkillLevel = when (skill.lowercase()) { "attack" -> AttackLevel "defence" -> DefenceLevel diff --git a/game/src/main/kotlin/content/bot/fact/FactParser.kt b/game/src/main/kotlin/content/bot/fact/FactParser.kt new file mode 100644 index 0000000000..de646165b7 --- /dev/null +++ b/game/src/main/kotlin/content/bot/fact/FactParser.kt @@ -0,0 +1,153 @@ +package content.bot.fact + +import world.gregs.voidps.type.Tile + +sealed class FactParser { + open val required: Set = emptySet() + abstract fun parse(map: Map): Fact + abstract fun predicate(map: Map): Predicate? + + fun requirement(map: Map) = Requirement(parse(map), predicate(map)) + + fun check(map: Map): String? { + for (key in required) { + if (!map.containsKey(key)) { + return "missing key '$key' in map ${map}" + } + } + return null + } + + object InventorySpace : FactParser() { + override fun parse(map: Map) = Fact.InventorySpace + override fun predicate(map: Map) = Predicate.parseInt(map) + } + + object InventoryCount : FactParser() { + override val required = setOf("id") + override fun parse(map: Map) = Fact.InventoryCount(map["id"] as String) + override fun predicate(map: Map): Predicate? { + if (!map.containsKey("min") && !map.containsKey("equals")) { + map as MutableMap + map["min"] = 1 + } + return Predicate.parseInt(map) + } + } + + object ItemCount : FactParser() { + override val required = setOf("id") + override fun parse(map: Map) = Fact.ItemCount(map["id"] as String) + override fun predicate(map: Map): Predicate? { + if (!map.containsKey("min") && !map.containsKey("equals")) { + map as MutableMap + map["min"] = 1 + } + return Predicate.parseInt(map) + } + } + + object BankCount : FactParser() { + override val required = setOf("id") + override fun parse(map: Map) = Fact.BankCount(map["id"] as String) + override fun predicate(map: Map) = Predicate.parseInt(map) + } + + object EquipCount : FactParser() { + override val required = setOf("id") + override fun parse(map: Map) = Fact.EquipCount(map["id"] as String) + override fun predicate(map: Map): Predicate? { + if (!map.containsKey("min") && !map.containsKey("equals")) { + map as MutableMap + map["min"] = 1 + } + return Predicate.parseInt(map) + } + } + + object Variable : FactParser() { + override val required = setOf("id", "default") + override fun parse(map: Map): Fact { + val id = map["id"] as String + return when (val default = map["default"]) { + is Int -> Fact.IntVariable(id, default) + is String -> Fact.StringVariable(id, default) + is Double -> Fact.DoubleVariable(id, default) + is Boolean -> Fact.BoolVariable(id, default) + else -> error("Invalid default value $default") + } as Fact + } + override fun predicate(map: Map): Predicate { + return when (val default = map["default"]) { + is Int -> Predicate.parseInt(map) + is String -> Predicate.parseString(map) + is Double -> Predicate.parseDouble(map) + is Boolean -> Predicate.parseBool(map) + else -> error("Invalid default value $default") + } as Predicate + } + } + + object Clock : FactParser() { + override val required = setOf("id") + override fun parse(map: Map): Fact { + return Fact.ClockRemaining(map["id"] as String, map["seconds"] as? Boolean ?: false) + } + override fun predicate(map: Map) = Predicate.parseInt(map) + } + + object Timer : FactParser() { + override val required = setOf("id") + override fun parse(map: Map): Fact { + return Fact.HasTimer(map["id"] as String) + } + override fun predicate(map: Map) = Predicate.parseBool(map) + } + + object Queue : FactParser() { + override val required = setOf("id") + override fun parse(map: Map) = Fact.HasQueue(map["id"] as String) + override fun predicate(map: Map) = Predicate.parseBool(map) + } + + object Interface : FactParser() { + override val required = setOf("id") + override fun parse(map: Map) = Fact.InterfaceOpen(map["id"] as String) + override fun predicate(map: Map) = Predicate.parseBool(map) + } + + object PlayerTile : FactParser() { + override fun parse(map: Map) = Fact.PlayerTile + override fun predicate(map: Map) = Predicate.parseTile(map) + } + + object CombatLevel : FactParser() { + override fun parse(map: Map) = Fact.CombatLevel + override fun predicate(map: Map) = Predicate.parseInt(map) + } + + object Skill : FactParser() { + override val required = setOf("id") + override fun parse(map: Map) = Fact.SkillLevel.of(map["id"] as String) + override fun predicate(map: Map) = Predicate.parseInt(map) + } + + companion object { + val parsers = mapOf( + "inventory_space" to InventorySpace, + "carries" to InventoryCount, + "owns" to ItemCount, + "banked" to BankCount, + "equips" to EquipCount, + "variable" to Variable, + "clock" to Clock, + "has_timer" to Timer, + "interface_open" to Interface, + "has_queue" to Queue, + "tile" to PlayerTile, + "area" to PlayerTile, + "combat_level" to CombatLevel, + "skill" to Skill, + ) + } +} diff --git a/game/src/main/kotlin/content/bot/fact/Matcher.kt b/game/src/main/kotlin/content/bot/fact/Matcher.kt new file mode 100644 index 0000000000..d4fce3e5c1 --- /dev/null +++ b/game/src/main/kotlin/content/bot/fact/Matcher.kt @@ -0,0 +1,8 @@ +package content.bot.fact + +interface Matcher, O: Outcome> { + /** + * Would [outcome] satisfy [predicate] + */ + fun matches(predicate: P, outcome: O): Boolean +} \ No newline at end of file diff --git a/game/src/main/kotlin/content/bot/fact/Outcome.kt b/game/src/main/kotlin/content/bot/fact/Outcome.kt new file mode 100644 index 0000000000..99d442dd41 --- /dev/null +++ b/game/src/main/kotlin/content/bot/fact/Outcome.kt @@ -0,0 +1,7 @@ +package content.bot.fact + +/** + * + */ +interface Outcome { +} \ No newline at end of file diff --git a/game/src/main/kotlin/content/bot/fact/Predicate.kt b/game/src/main/kotlin/content/bot/fact/Predicate.kt new file mode 100644 index 0000000000..ab60850ec1 --- /dev/null +++ b/game/src/main/kotlin/content/bot/fact/Predicate.kt @@ -0,0 +1,99 @@ +package content.bot.fact + +import world.gregs.voidps.engine.data.definition.Areas +import world.gregs.voidps.type.Tile + +sealed interface Predicate { + fun test(value: T): Boolean + + data class IntRange(val min: Int? = null, val max: Int? = null) : Predicate { + override fun test(value: Int): Boolean { + if (min != null && value < min) return false + if (max != null && value > max) return false + return true + } + } + + data class IntEquals(val value: Int) : Predicate { + override fun test(value: Int) = value == this.value + } + + data class DoubleRange(val min: Double? = null, val max: Double? = null) : Predicate { + override fun test(value: Double): Boolean { + if (min != null && value < min) return false + if (max != null && value > max) return false + return true + } + } + + data class DoubleEquals(val value: Double) : Predicate { + override fun test(value: Double) = value == this.value + } + + data class InArea(val name: String) : Predicate { + override fun test(value: Tile) = value in Areas[name] + } + + data class BooleanEquals(val value: Boolean) : Predicate { + override fun test(value: Boolean) = value == this.value + } + + data class StringEquals(val value: String) : Predicate { + override fun test(value: String) = value == this.value + } + + data class TileEquals(val x: Int?, val y: Int?, val level: Int?) : Predicate { + override fun test(value: Tile): Boolean { + if (x != null && value.x != x) return false + if (y != null && value.y != y) return false + if (level != null && value.level != level) return false + return true + } + } + + companion object { + fun parseInt(map: Map): Predicate? = when { + map.containsKey("min") || map.containsKey("max") -> IntRange(map["min"] as? Int, map["max"] as? Int) + map.containsKey("equals") -> { + when (val value = map["equals"]) { + is Int -> IntEquals(value) + else -> error("Unsupported equals type: '${value?.let { it::class.simpleName }}'") + } + } + else -> null + } + + fun parseDouble(map: Map): Predicate? = when { + map.containsKey("min") || map.containsKey("max") -> DoubleRange(map["min"] as? Double, map["max"] as? Double) + map.containsKey("equals") -> { + when (val value = map["equals"]) { + is Double -> DoubleEquals(value) + else -> error("Unsupported equals type: '${value?.let { it::class.simpleName }}'") + } + } + else -> null + } + + fun parseBool(map: Map): Predicate? { + val equals = map["equals"] ?: return null + if (equals !is Boolean) { + error("Unsupported equals type: '${equals.let { it::class.simpleName }}'") + } + return BooleanEquals(equals) + } + + fun parseString(map: Map): Predicate? { + val equals = map["equals"] ?: return null + if (equals !is String) { + error("Unsupported equals type: '${equals.let { it::class.simpleName }}'") + } + return StringEquals(equals) + } + + fun parseTile(map: Map): Predicate? = when { + map.containsKey("id") -> InArea(map["id"] as String) + map.containsKey("x") || map.containsKey("y") || map.containsKey("level") -> TileEquals(map["x"] as? Int, map["y"] as? Int, map["level"] as? Int) + else -> null + } + } +} \ No newline at end of file diff --git a/game/src/main/kotlin/content/bot/fact/PredicateParser.kt b/game/src/main/kotlin/content/bot/fact/PredicateParser.kt new file mode 100644 index 0000000000..923eed8621 --- /dev/null +++ b/game/src/main/kotlin/content/bot/fact/PredicateParser.kt @@ -0,0 +1,57 @@ +package content.bot.fact + +import world.gregs.voidps.type.Tile + +sealed class PredicateParser { + open val required: Set = emptySet() + open val optional: Set = emptySet() + abstract fun parse(map: Map) : Predicate? + + object IntegerParser : PredicateParser() { + override val optional = setOf("min", "max", "equals") + + override fun parse(map: Map) : Predicate? { + if (map.containsKey("min") || map.containsKey("max")) { + return Predicate.IntRange(map["min"] as? Int, map["max"] as? Int) + } else if (map.containsKey("equals")) { + return Predicate.IntEquals(map["equals"] as Int) + } + return null + } + } + + object BooleanParser : PredicateParser() { + override val required = setOf("equals") + + override fun parse(map: Map) : Predicate { + return Predicate.BooleanEquals(map["equals"] as Boolean) + } + } + + object TileParser : PredicateParser() { + override val optional = setOf("x", "y", "level") + + override fun parse(map: Map) : Predicate { + return Predicate.TileEquals(map["x"] as? Int, map["y"] as? Int, map["level"] as? Int) + } + } + + companion object { + val parsers = mapOf( + "inventory_space" to IntegerParser, + "inventory" to IntegerParser, + "carries" to IntegerParser, + "banked" to IntegerParser, + "equips" to IntegerParser, + "variable" to IntegerParser, + "clock" to IntegerParser, + "interface_open" to BooleanParser, + "has_timer" to BooleanParser, + "has_queue" to BooleanParser, + "tile" to TileParser, + "combat" to IntegerParser, + "skill" to IntegerParser, + ) + } +} + diff --git a/game/src/main/kotlin/content/bot/fact/Produces.kt b/game/src/main/kotlin/content/bot/fact/Produces.kt new file mode 100644 index 0000000000..fc10a777c3 --- /dev/null +++ b/game/src/main/kotlin/content/bot/fact/Produces.kt @@ -0,0 +1,3 @@ +package content.bot.fact + +data class Produces(val fact: Fact<*>, val outcome: Outcome? = null) diff --git a/game/src/main/kotlin/content/bot/fact/Reference.kt b/game/src/main/kotlin/content/bot/fact/Reference.kt new file mode 100644 index 0000000000..1325beaa2f --- /dev/null +++ b/game/src/main/kotlin/content/bot/fact/Reference.kt @@ -0,0 +1,9 @@ +package content.bot.fact + +sealed interface Value { + fun resolve(context: Map): T +} + +data class Literal(val value: T) + +data class Ref(val key: String) \ No newline at end of file diff --git a/game/src/main/kotlin/content/bot/fact/Requirement.kt b/game/src/main/kotlin/content/bot/fact/Requirement.kt new file mode 100644 index 0000000000..dcb7f33181 --- /dev/null +++ b/game/src/main/kotlin/content/bot/fact/Requirement.kt @@ -0,0 +1,70 @@ +package content.bot.fact + +import com.github.michaelbull.logging.InlineLogger +import world.gregs.voidps.engine.entity.character.player.Player + +data class Requirement(val fact: Fact, val predicate: Predicate?) { + + fun check(player: Player): Boolean { + return predicate?.test(fact.getValue(player)) ?: false + } + + companion object { + private val logger = InlineLogger() + + fun parse(list: List>>, name: String, requirePredicates: Boolean = true): List> { + val requirements = mutableListOf>() + + for ((type, map) in list) { + val parser = FactParser.parsers[type] ?: error("No fact parser for '$type' in ${name}.") + val error = parser.check(map) + if (error != null) { + error("Fact '$type' $error in ${name}.") + } + val requirement = parser.requirement(map) + if (requirePredicates && requirement.predicate == null) { + error("No predicates found for requirement $type map $map in ${name}.") + } + requirements.add(requirement) + } + return requirements + } + } +} + +//data class Parsed( +// val id: String, +// val template: String? = null, +// val requires: MutableMap, +// val resolves: MutableMap, +//) { +// fun resolve(fields: Map>) { +// for (index in requires.indices) { +// val map = requires[index] +// val template = templates[index] +// val fields = fields[index] +// for ((key, value) in map) { +// if (value !is String || !value.contains("$")) { +// continue +// } +// val ref = value.reference() +// val name = ref.trim('$', '{', '}') +// val replacement = fields[name] ?: error("No field found for type=${types[index]} key=$key ref=$ref") +// map[key] = if (replacement is String) value.replace(ref, replacement) else replacement +// } +// } +// } +// +// private fun String.reference(): String { +// if (startsWith('$')) { +// return this +// } +// val index = indexOf($$"${") +// if (index == -1) { +// return "\$${substringAfter('$')}" +// } +// val end = indexOf('}', index) + 1 +// return substring(index, end) +// } +//} + diff --git a/game/src/main/kotlin/content/bot/interact/path/Graph.kt b/game/src/main/kotlin/content/bot/interact/path/Graph.kt index 5424d7fed8..5cfaef3d16 100644 --- a/game/src/main/kotlin/content/bot/interact/path/Graph.kt +++ b/game/src/main/kotlin/content/bot/interact/path/Graph.kt @@ -2,10 +2,9 @@ package content.bot.interact.path import content.bot.action.BotAction import content.bot.action.NavigationShortcut -import content.bot.action.actions -import content.bot.action.requirements import content.bot.bot import content.bot.fact.Condition +import content.bot.fact.Requirement import content.bot.isBot import world.gregs.config.Config import world.gregs.voidps.engine.data.definition.Areas @@ -18,7 +17,7 @@ import java.util.PriorityQueue class Graph( val endNodes: IntArray = intArrayOf(), val edgeWeights: IntArray = intArrayOf(), - val edgeConditions: Array?> = emptyArray(), + val edgeConditions: Array>?> = emptyArray(), val actions: Array?> = emptyArray(), val adjacentEdges: Array = emptyArray(), val tiles: IntArray = intArrayOf(), @@ -28,7 +27,7 @@ class Graph( fun actions(edge: Int): List? = actions[edge] - fun conditions(edge: Int): List? = edgeConditions[edge] + fun conditions(edge: Int): List>? = edgeConditions[edge] fun tile(edge: Int): Tile { val nodeIndex = endNodes[edge] @@ -146,7 +145,7 @@ class Graph( // Edges val endNodes = mutableListOf() val weights = mutableListOf() - val conditions = mutableListOf?>() + val conditions = mutableListOf>?>() val actions = mutableListOf?>() val edges = mutableMapOf>() var edgeCount = 0 @@ -177,7 +176,7 @@ class Graph( addEdge(end, start, weight, actions) } - fun addEdge(from: Tile, to: Tile, weight: Int, actions: List, conditions: List?) { + fun addEdge(from: Tile, to: Tile, weight: Int, actions: List, conditions: List>?) { val start = add(from) val end = add(to) addEdge(start, end, weight, actions, conditions) @@ -190,7 +189,7 @@ class Graph( return tiles.indexOf(tile) } - fun addEdge(start: Int, end: Int, weight: Int, actions: List? = null, conditions: List? = null): Int { + fun addEdge(start: Int, end: Int, weight: Int, actions: List? = null, conditions: List>? = null): Int { val edgeIndex = edgeCount++ nodes.add(start) nodes.add(end) @@ -244,7 +243,7 @@ class Graph( var toLevel = 0 var cost = 0 val actions: MutableList = mutableListOf() - val requirements: MutableList = mutableListOf() + val requirements = mutableListOf>>() while (nextEntry()) { when (val key = key()) { "from_x" -> fromX = int() @@ -254,8 +253,19 @@ class Graph( "to_y" -> toY = int() "to_level" -> toLevel = int() "cost" -> cost = int() - "actions" -> actions(actions) - "conditions" -> requirements(requirements) + "actions" -> while (nextElement()) { + while (nextEntry()) { + val key = key() + val value = value() + } + } + "conditions" -> while (nextElement()) { + while (nextEntry()) { + val key = key() + val value = map() + requirements.add(key to value) + } + } else -> throw IllegalArgumentException("Unexpected key: '$key' ${exception()}") } } @@ -266,7 +276,7 @@ class Graph( builder.addEdge(Tile(toX, toY, toLevel), Tile(fromX, fromY, fromLevel), cost, listOf(BotAction.WalkTo(fromX, fromY)), null) } requirements.isEmpty() -> builder.addEdge(Tile(fromX, fromY, fromLevel), Tile(toX, toY, toLevel), cost, actions, null) - else -> builder.addEdge(Tile(fromX, fromY, fromLevel), Tile(toX, toY, toLevel), cost, actions, requirements) + else -> builder.addEdge(Tile(fromX, fromY, fromLevel), Tile(toX, toY, toLevel), cost, actions, Requirement.parse(requirements, exception())) } } } diff --git a/game/src/main/resources/game.properties b/game/src/main/resources/game.properties index eb00c09d2d..52ce62e6ca 100644 --- a/game/src/main/resources/game.properties +++ b/game/src/main/resources/game.properties @@ -256,6 +256,15 @@ bots.namePrefix="" # File ending for bot definitions bots.definitions=bots.toml +# File ending for bot definition templates +bots.templates=templates.toml + +# File ending for bot definition templates +bots.setups=setups.toml + +# File ending for bot definition templates +bots.shortcuts=shortcuts.toml + # File ending for bot navigation graph definitions bots.nav.definitions=nav-edges.toml From 88ea7b6cba24379e657f9fb035b238e46afe0d25 Mon Sep 17 00:00:00 2001 From: GregHib Date: Sat, 7 Feb 2026 17:31:56 +0000 Subject: [PATCH 060/101] Requirement action requirements --- .../misthalin/lumbridge/lumbridge.bots.toml | 1 + data/bot/bank.setups.toml | 4 +- data/bot/combat.templates.toml | 4 +- data/bot/cooking.templates.toml | 10 +- data/bot/fletching.templates.toml | 14 +- data/bot/lumbridge.nav-edges.toml | 16 +- data/bot/mining.templates.toml | 96 ++++----- data/bot/prayer.bots.toml | 2 +- data/bot/shop.templates.toml | 8 +- data/bot/thieving.templates.toml | 2 +- data/bot/woodcutting.templates.toml | 8 +- .../src/main/kotlin/content/bot/BotManager.kt | 6 +- .../content/bot/action/BehaviourFragment.kt | 82 -------- .../kotlin/content/bot/action/BotAction.kt | 19 +- .../kotlin/content/bot/action/BotActivity.kt | 199 ++++++++++++++++-- .../kotlin/content/bot/fact/FactParser.kt | 10 +- .../main/kotlin/content/bot/fact/Predicate.kt | 10 +- .../content/bot/fact/PredicateParser.kt | 12 +- .../kotlin/content/bot/interact/path/Graph.kt | 8 +- 19 files changed, 292 insertions(+), 219 deletions(-) diff --git a/data/area/misthalin/lumbridge/lumbridge.bots.toml b/data/area/misthalin/lumbridge/lumbridge.bots.toml index 09912a7e62..5adec101c4 100644 --- a/data/area/misthalin/lumbridge/lumbridge.bots.toml +++ b/data/area/misthalin/lumbridge/lumbridge.bots.toml @@ -1,4 +1,5 @@ [kill_freds_chickens_attack] +template = "kill_chickens" capacity = 2 fields = { skill = "attack", location = "lumbridge_chicken_pen", style = "style1" } diff --git a/data/bot/bank.setups.toml b/data/bot/bank.setups.toml index 1ab7cdb659..4b9932694f 100644 --- a/data/bot/bank.setups.toml +++ b/data/bot/bank.setups.toml @@ -1,8 +1,8 @@ [deposit_carried_items] actions = [ { go_to_nearest = "bank" }, - { option = "Use-quickly", object = "bank_booth*", success = { interface = "bank" } }, - { option = "Deposit carried items", interface = "bank:carried", success = { inventory_space = 28 } }, + { option = "Use-quickly", object = "bank_booth*", success = { interface_open = { id = "bank" } } }, + { option = "Deposit carried items", interface = "bank:carried", success = { inventory_space = { min = 28 } } }, ] produces = [ { inventory_space = { min = 28 } } diff --git a/data/bot/combat.templates.toml b/data/bot/combat.templates.toml index f7ceaa6dc5..640b478b44 100644 --- a/data/bot/combat.templates.toml +++ b/data/bot/combat.templates.toml @@ -9,7 +9,7 @@ setup = [ ] actions = [ { option = "Select", interface = "combat_styles:$style" }, - { option = "Attack", npc = "chicken*", delay = 5, success = { inventory_space = 0 } }, + { option = "Attack", npc = "chicken*", delay = 5, success = { inventory_space = { max = 0 } } }, ] produces = [ { carries = { id = "feather" } }, @@ -30,7 +30,7 @@ setup = [ ] actions = [ { option = "Select", interface = "combat_styles:style1" }, - { option = "Attack", npc = "cow*", delay = 5, success = { inventory_space = 0 } }, + { option = "Attack", npc = "cow*", delay = 5, success = { inventory_space = { max = 0 } } }, ] produces = [ { carries = { id = "bones" } }, diff --git a/data/bot/cooking.templates.toml b/data/bot/cooking.templates.toml index 06e6b98f80..d908d9c2fa 100644 --- a/data/bot/cooking.templates.toml +++ b/data/bot/cooking.templates.toml @@ -8,10 +8,10 @@ setup = [ { area = { id = "$location" } }, ] actions = [ - { item = "$raw", on_object = "$obj", success = { interface = "dialogue_skill_creation" } }, + { item = "$raw", on_object = "$obj", success = { interface_open = { id = "dialogue_skill_creation" } } }, { option = "All", interface = "skill_creation_amount:all" }, { continue = "dialogue_skill_creation:choice1" }, - { restart = true, wait_if = [{ queue = "cooking" }], success = { carries = "$raw", max = 0 } } + { restart = true, wait_if = [{ has_queue = { id = "cooking" } }], success = { carries = { id = "$raw", max = 0 } } } ] produces = [ { skill = { id = "cooking" } }, @@ -29,11 +29,11 @@ setup = [ { area = { id = "$location" } }, ] actions = [ - { item = "raw_beef", on_object = "$obj", success = { interface = "dialogue_multi2" } }, - { option = "Continue", continue = "dialogue_multi2:line2", success = { interface = "dialogue_skill_creation" } }, + { item = "raw_beef", on_object = "$obj", success = { interface_open = { id = "dialogue_multi2" } } }, + { option = "Continue", continue = "dialogue_multi2:line2", success = { interface_open = { id = "dialogue_skill_creation" } } }, { option = "All", interface = "skill_creation_amount:all" }, { continue = "dialogue_skill_creation:choice1" }, - { restart = true, wait_if = [{ queue = "cooking" }], success = { carries = "raw_beef", max = 0 } } + { restart = true, wait_if = [{ has_queue = { id = "cooking" } }], success = { carries = { id = "raw_beef", max = 0 } } } ] produces = [ { skill = { id = "cooking" } }, diff --git a/data/bot/fletching.templates.toml b/data/bot/fletching.templates.toml index ea00c8b467..aad701ec93 100644 --- a/data/bot/fletching.templates.toml +++ b/data/bot/fletching.templates.toml @@ -8,10 +8,10 @@ setup = [ { carries = { id = "$logs", min = 27 } } ] actions = [ - { item = "knife", on = "$logs", success = { interface = "dialogue_skill_creation" } }, + { item = "knife", on = "$logs", success = { interface_open = { id = "dialogue_skill_creation" } } }, { option = "All", interface = "skill_creation_amount:all" }, { continue = "dialogue_skill_creation:choice1" }, - { restart = true, wait_if = [{ queue = "fletching" }], success = { inventory_space = 26 } } + { restart = true, wait_if = [{ has_queue = { id = "fletching" } }], success = { inventory_space = { min = 26 } } } ] produces = [ { carries = { id = "arrow_shafts" } }, @@ -30,8 +30,8 @@ setup = [ actions = [ { item = "knife", on = "$logs" }, { option = "All", interface = "skill_creation_amount:all" }, - { continue = "dialogue_skill_creation:choice2", success = { queue = "fletching" } }, - { restart = true, success = { inventory_space = 26 } } + { continue = "dialogue_skill_creation:choice2", success = { has_queue = { id = "fletching" } } }, + { restart = true, success = { inventory_space = { min = 26 } } } ] produces = [ { skill = { id = "fletching" } } @@ -47,10 +47,10 @@ setup = [ { carries = { id = "$logs", min = 27 } } ] actions = [ - { item = "knife", on = "$logs", success = { queue = "fletching_make_dialog" } }, + { item = "knife", on = "$logs", success = { has_queue = { id = "fletching_make_dialog" } } }, { option = "All", interface = "skill_creation_amount:all" }, - { continue = "dialogue_skill_creation:choice3", success = { queue = "fletching" } }, - { restart = true, success = { inventory_space = 26 } } + { continue = "dialogue_skill_creation:choice3", success = { has_queue = { id = "fletching" } } }, + { restart = true, success = { inventory_space = { min = 26 } } } ] produces = [ { skill = { id = "fletching" } } diff --git a/data/bot/lumbridge.nav-edges.toml b/data/bot/lumbridge.nav-edges.toml index f88a020382..05128b8fc1 100644 --- a/data/bot/lumbridge.nav-edges.toml +++ b/data/bot/lumbridge.nav-edges.toml @@ -12,25 +12,25 @@ edges = [ { from_x = 3244, from_y = 3190, to_x = 3253, to_y = 3200 }, # lumbridge_graveyard_exit_to_behind_graveyard { from_x = 3250, from_y = 3212, to_x = 3253, to_y = 3200 }, # lumbridge_behind_church_to_behind_graveyard { from_x = 3258, from_y = 3206, to_x = 3253, to_y = 3200 }, # lumbridge_church_fishing_spot_to_behind_graveyard - { from_x = 3205, from_y = 3209, from_level = 2, to_x = 3205, to_y = 3209, to_level = 1, cost = 1, actions = [{ option = "Climb-down", object = "lumbridge_staircase_top", x = 3204, y = 3207, success = { level = 1 } }] }, # lumbridge_castle_2nd_floor_south_stairs_to_1st_floor + { from_x = 3205, from_y = 3209, from_level = 2, to_x = 3205, to_y = 3209, to_level = 1, cost = 1, actions = [{ option = "Climb-down", object = "lumbridge_staircase_top", x = 3204, y = 3207, success = { tile = { level = 1 } } }] }, # lumbridge_castle_2nd_floor_south_stairs_to_1st_floor { from_x = 3208, from_y = 3219, from_level = 2, to_x = 3205, to_y = 3209, to_level = 2 }, # lumbridge_castle_2nd_floor_bank_to_south_stairs - { from_x = 3205, from_y = 3209, to_x = 3205, to_y = 3209, to_level = 1, cost = 1, actions = [{ option = "Climb-up", object = "lumbridge_staircase", x = 3204, y = 3207, success = { level = 1 } }] }, # lumbridge_castle_ground_floor_south_stairs_to_1st_floor + { from_x = 3205, from_y = 3209, to_x = 3205, to_y = 3209, to_level = 1, cost = 1, actions = [{ option = "Climb-up", object = "lumbridge_staircase", x = 3204, y = 3207, success = { tile = { level = 1 } } }] }, # lumbridge_castle_ground_floor_south_stairs_to_1st_floor { from_x = 3205, from_y = 3209, to_x = 3208, to_y = 3210 }, # lumbridge_castle_ground_floor_south_stairs_to_kitchen_corridor { from_x = 3208, from_y = 3210, to_x = 3215, to_y = 3216 }, # lumbridge_castle_kitchen_corridor_to_castle_south_entrance { from_x = 3208, from_y = 3210, to_x = 3211, to_y = 3214 }, # lumbridge_castle_kitchen_corridor_to_kitchen { from_x = 3215, from_y = 3216, to_x = 3222, to_y = 3218 }, # lumbridge_castle_south_entrance_to_courtyard_south - { from_x = 3205, from_y = 3209, from_level = 1, to_x = 3205, to_y = 3209, cost = 1, actions = [{ option = "Climb-down", object = "lumbridge_castle_staircase_south_middle", x = 3204, y = 3207, success = { level = 0 } }] }, # lumbridge_castle_1st_floor_south_stairs_to_ground_floor - { from_x = 3205, from_y = 3209, from_level = 1, to_x = 3205, to_y = 3209, to_level = 2, cost = 1, actions = [{ option = "Climb-up", object = "lumbridge_castle_staircase_south_middle", x = 3204, y = 3207, success = { level = 2 } }] }, # lumbridge_castle_1st_floor_south_stairs_to_2nd_floor + { from_x = 3205, from_y = 3209, from_level = 1, to_x = 3205, to_y = 3209, cost = 1, actions = [{ option = "Climb-down", object = "lumbridge_castle_staircase_south_middle", x = 3204, y = 3207, success = { tile = { level = 0 } } }] }, # lumbridge_castle_1st_floor_south_stairs_to_ground_floor + { from_x = 3205, from_y = 3209, from_level = 1, to_x = 3205, to_y = 3209, to_level = 2, cost = 1, actions = [{ option = "Climb-up", object = "lumbridge_castle_staircase_south_middle", x = 3204, y = 3207, success = { tile = { level = 2 } } }] }, # lumbridge_castle_1st_floor_south_stairs_to_2nd_floor { from_x = 3209, from_y = 3205, to_x = 3199, to_y = 3218 }, # lumbridge_castle_grounds_south_to_tower_west { from_x = 3199, from_y = 3218, to_x = 3184, to_y = 3225 }, # lumbridge_castle_tower_west_to_yew_trees { from_x = 3199, from_y = 3218, to_x = 3193, to_y = 3236 }, # lumbridge_castle_tower_west_to_tree_patch { from_x = 3184, from_y = 3225, to_x = 3168, to_y = 3221 }, # lumbridge_castle_yew_trees_to_yew_trees_west { from_x = 3184, from_y = 3225, to_x = 3193, to_y = 3236 }, # lumbridge_castle_yew_trees_to_tree_patch { from_x = 3226, from_y = 3214, to_x = 3227, to_y = 3214, cost = 3, actions = [{ option = "Open", object = "door_627_closed", x = 3226, y = 3214 }] }, # lumbridge_south_tower_to_ground_floor - { from_x = 3227, from_y = 3214, to_x = 3229, to_y = 3214, to_level = 1, cost = 1, actions = [{ option = "Climb-up", object = "36768", x = 3229, y = 3213, success = { level = 1 } }] }, # lumbridge_south_tower_ground_floor_to_1st_floor - { from_x = 3229, from_y = 3214, from_level = 1, to_x = 3229, to_y = 3214, to_level = 2, cost = 1, actions = [{ option = "Climb-up", object = "36769", x = 3229, y = 3213, success = { level = 2 } }] }, # lumbridge_south_tower_1st_floor_to_2nd_floor - { from_x = 3229, from_y = 3214, from_level = 1, to_x = 3229, to_y = 3214, cost = 1, actions = [{ option = "Climb-down", object = "36769", x = 3229, y = 3213, success = { level = 0 } }] }, # lumbridge_south_tower_1st_floor_to_ground_floor - { from_x = 3229, from_y = 3214, from_level = 2, to_x = 3229, to_y = 3214, to_level = 1, cost = 1, actions = [{ option = "Climb-down", object = "36770", x = 3229, y = 3213, success = { level = 1 } }] }, # lumbridge_south_tower_2nd_floor_to_1st_floor + { from_x = 3227, from_y = 3214, to_x = 3229, to_y = 3214, to_level = 1, cost = 1, actions = [{ option = "Climb-up", object = "36768", x = 3229, y = 3213, success = { tile = { level = 1 } } }] }, # lumbridge_south_tower_ground_floor_to_1st_floor + { from_x = 3229, from_y = 3214, from_level = 1, to_x = 3229, to_y = 3214, to_level = 2, cost = 1, actions = [{ option = "Climb-up", object = "36769", x = 3229, y = 3213, success = { tile = { level = 2 } } }] }, # lumbridge_south_tower_1st_floor_to_2nd_floor + { from_x = 3229, from_y = 3214, from_level = 1, to_x = 3229, to_y = 3214, cost = 1, actions = [{ option = "Climb-down", object = "36769", x = 3229, y = 3213, success = { tile = { level = 0 } } }] }, # lumbridge_south_tower_1st_floor_to_ground_floor + { from_x = 3229, from_y = 3214, from_level = 2, to_x = 3229, to_y = 3214, to_level = 1, cost = 1, actions = [{ option = "Climb-down", object = "36770", x = 3229, y = 3213, success = { tile = { level = 1 } } }] }, # lumbridge_south_tower_2nd_floor_to_1st_floor { from_x = 3236, from_y = 3219, to_x = 3236, to_y = 3225 }, # lumbridge_gate_north_to_bridge_west { from_x = 3236, from_y = 3225, to_x = 3230, to_y = 3232 }, # lumbridge_bridge_west_to_unstable_house { from_x = 3236, from_y = 3225, to_x = 3253, to_y = 3225 }, # lumbridge_bridge_west_to_bridge_east diff --git a/data/bot/mining.templates.toml b/data/bot/mining.templates.toml index caa965981a..736d6506d9 100644 --- a/data/bot/mining.templates.toml +++ b/data/bot/mining.templates.toml @@ -1,135 +1,135 @@ [copper_ore_template] requires = [ - { skill = { id = "mining", min = 1, max = 15 }}, + { skill = { id = "mining", min = 1, max = 15 } }, ] setup = [ - { carries = { id = "steel_pickaxe,iron_pickaxe,bronze_pickaxe" }}, - { area = { id = "$location" }}, - { inventory_space = { min = 27 }}, + { carries = { id = "steel_pickaxe,iron_pickaxe,bronze_pickaxe" } }, + { area = { id = "$location" } }, + { inventory_space = { min = 27 } }, ] actions = [ - { option = "Mine", object = "copper_rocks*", delay = 5, success = { inventory_space = 0 } }, + { option = "Mine", object = "copper_rocks*", delay = 5, success = { inventory_space = { max = 0 } } }, ] produces = [ - { carries = { id = "copper_ore" }}, + { carries = { id = "copper_ore" } }, { skill = { id = "mining" } } ] [tin_ore_template] requires = [ - { skill = { id = "mining", min = 1, max = 15 }}, + { skill = { id = "mining", min = 1, max = 15 } }, ] setup = [ - { carries = { id = "steel_pickaxe,iron_pickaxe,bronze_pickaxe" }}, - { area = { id = "$location" }}, - { inventory_space = { min = 27 }}, + { carries = { id = "steel_pickaxe,iron_pickaxe,bronze_pickaxe" } }, + { area = { id = "$location" } }, + { inventory_space = { min = 27 } }, ] actions = [ - { option = "Mine", object = "tin_rocks*", delay = 5, success = { inventory_space = 0 } }, + { option = "Mine", object = "tin_rocks*", delay = 5, success = { inventory_space = { max = 0 } } }, ] produces = [ - { carries = { id = "tin_ore" }}, + { carries = { id = "tin_ore" } }, { skill = { id = "mining" } } ] [clay_template] requires = [ - { skill = { id = "mining", min = 1, max = 15 }}, + { skill = { id = "mining", min = 1, max = 15 } }, ] setup = [ - { carries = { id = "steel_pickaxe,iron_pickaxe,bronze_pickaxe" }}, - { area = { id = "$location" }}, - { inventory_space = { min = 27 }}, + { carries = { id = "steel_pickaxe,iron_pickaxe,bronze_pickaxe" } }, + { area = { id = "$location" } }, + { inventory_space = { min = 27 } }, ] actions = [ - { option = "Mine", object = "clay_rocks*", delay = 5, success = { inventory_space = 0 } }, + { option = "Mine", object = "clay_rocks*", delay = 5, success = { inventory_space = { max = 0 } } }, ] produces = [ - { carries = { id = "clay" }}, + { carries = { id = "clay" } }, { skill = { id = "mining" } } ] [iron_ore_template] requires = [ - { skill = { id = "mining", min = 15, max = 40 }}, + { skill = { id = "mining", min = 15, max = 40 } }, ] setup = [ - { carries = { id = "adamant_pickaxe,mithril_pickaxe,steel_pickaxe" }}, - { area = { id = "$location" }}, - { inventory_space = { min = 27 }}, + { carries = { id = "adamant_pickaxe,mithril_pickaxe,steel_pickaxe" } }, + { area = { id = "$location" } }, + { inventory_space = { min = 27 } }, ] actions = [ - { option = "Mine", object = "iron_rocks*", delay = 5, success = { inventory_space = 0 } }, + { option = "Mine", object = "iron_rocks*", delay = 5, success = { inventory_space = { max = 0 } } }, ] produces = [ - { carries = { id = "iron_ore" }}, + { carries = { id = "iron_ore" } }, { skill = { id = "mining" } } ] [silver_ore_template] requires = [ - { skill = { id = "mining", min = 20, max = 40 }}, + { skill = { id = "mining", min = 20, max = 40 } }, ] setup = [ - { carries = { id = "adamant_pickaxe,mithril_pickaxe,steel_pickaxe" }}, - { area = { id = "$location" }}, - { inventory_space = { min = 27 }}, + { carries = { id = "adamant_pickaxe,mithril_pickaxe,steel_pickaxe" } }, + { area = { id = "$location" } }, + { inventory_space = { min = 27 } }, ] actions = [ - { option = "Mine", object = "silver_rocks*", delay = 5, success = { inventory_space = 0 } }, + { option = "Mine", object = "silver_rocks*", delay = 5, success = { inventory_space = { max = 0 } } }, ] produces = [ - { carries = { id = "silver_ore" }}, + { carries = { id = "silver_ore" } }, { skill = { id = "mining" } } ] [coal_template] requires = [ - { skill = { id = "mining", min = 30, max = 55 }}, + { skill = { id = "mining", min = 30, max = 55 } }, ] setup = [ - { carries = { id = "rune_pickaxe,adamant_pickaxe,mithril_pickaxe" }}, - { area = { id = "$location" }}, - { inventory_space = { min = 27 }}, + { carries = { id = "rune_pickaxe,adamant_pickaxe,mithril_pickaxe" } }, + { area = { id = "$location" } }, + { inventory_space = { min = 27 } }, ] actions = [ - { option = "Mine", object = "coal_rocks*", delay = 10, success = { inventory_space = 0 } }, + { option = "Mine", object = "coal_rocks*", delay = 10, success = { inventory_space = { max = 0 } } }, ] produces = [ - { carries = { id = "coal" }}, + { carries = { id = "coal" } }, { skill = { id = "mining" } } ] [gold_ore_template] requires = [ - { skill = { id = "mining", min = 40, max = 60 }}, + { skill = { id = "mining", min = 40, max = 60 } }, ] setup = [ - { carries = { id = "rune_pickaxe,adamant_pickaxe,mithril_pickaxe" }}, - { area = { id = "$location" }}, - { inventory_space = { min = 27 }}, + { carries = { id = "rune_pickaxe,adamant_pickaxe,mithril_pickaxe" } }, + { area = { id = "$location" } }, + { inventory_space = { min = 27 } }, ] actions = [ - { option = "Mine", object = "gold_rocks*", delay = 15, success = { inventory_space = 0 } }, + { option = "Mine", object = "gold_rocks*", delay = 15, success = { inventory_space = { max = 0 } } }, ] produces = [ - { carries = { id = "gold_ore" }}, + { carries = { id = "gold_ore" } }, { skill = { id = "mining" } } ] [mithril_ore_template] requires = [ - { skill = { id = "mining", min = 55, max = 70 }}, + { skill = { id = "mining", min = 55, max = 70 } }, ] setup = [ - { carries = { id = "dragon_pickaxe,rune_pickaxe,adamant_pickaxe" }}, - { area = { id = "$location" }}, - { inventory_space = { min = 27 }}, + { carries = { id = "dragon_pickaxe,rune_pickaxe,adamant_pickaxe" } }, + { area = { id = "$location" } }, + { inventory_space = { min = 27 } }, ] actions = [ - { option = "Mine", object = "mithril_rocks*", delay = 15, success = { inventory_space = 0 } }, + { option = "Mine", object = "mithril_rocks*", delay = 15, success = { inventory_space = { max = 0 } } }, ] produces = [ - { carries = { id = "mithril_ore" }}, + { carries = { id = "mithril_ore" } }, { skill = { id = "mining" } } ] diff --git a/data/bot/prayer.bots.toml b/data/bot/prayer.bots.toml index 82afb9c84c..651b23684e 100644 --- a/data/bot/prayer.bots.toml +++ b/data/bot/prayer.bots.toml @@ -8,7 +8,7 @@ setup = [ ] actions = [ { option = "Bury", interface = "inventory:inventory:bones" }, - { restart = true, wait_if = [{ variable = "bone_delay", min = 0, default = -1 }], success = { inventory_space = 28 } } + { restart = true, wait_if = [{ variable = { id = "bone_delay", min = 0, default = -1 } }], success = { inventory_space = { min = 28 } } } ] produces = [ { skill = { id = "prayer" } } diff --git a/data/bot/shop.templates.toml b/data/bot/shop.templates.toml index d61eb82776..e6b740b3e0 100644 --- a/data/bot/shop.templates.toml +++ b/data/bot/shop.templates.toml @@ -5,8 +5,8 @@ setup = [ { inventory_space = { min = 1 } }, ] actions = [ - { option = "Trade", npc = "$shopkeeper", success = { interface = "shop" } }, - { option = "Buy-1", interface = "shop:stock:$item", success = { carries = "$item" } }, + { option = "Trade", npc = "$shopkeeper", success = { interface_open = { id = "shop" } } }, + { option = "Buy-1", interface = "shop:stock:$item", success = { carries = { id = "$item" } } }, ] produces = [ { carries = { id = "$item" } } @@ -18,8 +18,8 @@ setup = [ { inventory_space = { min = 1 } }, ] actions = [ - { option = "Trade", npc = "$shopkeeper", success = { interface = "shop" } }, - { option = "Take-1", interface = "shop:sample:$item", success = { carries = "$item" } }, + { option = "Trade", npc = "$shopkeeper", success = { interface_open = { id = "shop" } } }, + { option = "Take-1", interface = "shop:sample:$item", success = { carries = { id = "$item" } } }, ] produces = [ { carries = { id = "$item" } } diff --git a/data/bot/thieving.templates.toml b/data/bot/thieving.templates.toml index 7fc2d9e0ad..3e5726be06 100644 --- a/data/bot/thieving.templates.toml +++ b/data/bot/thieving.templates.toml @@ -9,7 +9,7 @@ setup = [ ] actions = [ { option = "Pickpocket", npc = "$npc" }, - { restart = true, wait_if = [{ variable = "delay", min = 0, default = -1 }, { clock = "stunned", min = 0 }], success = { skill = "constitution", max = 20 } } + { restart = true, wait_if = [{ variable = { id = "delay", min = 0, default = -1 } }, { clock = { id = "stunned", min = 0 } }], success = { skill = { id = "constitution", max = 20 } } } ] produces = [ { carries = { id = "coins" } }, diff --git a/data/bot/woodcutting.templates.toml b/data/bot/woodcutting.templates.toml index 5bc57e194a..6560b21349 100644 --- a/data/bot/woodcutting.templates.toml +++ b/data/bot/woodcutting.templates.toml @@ -8,7 +8,7 @@ setup = [ { inventory_space = { min = 27 } }, ] actions = [ - { option = "Chop down", object = "tree*", delay = 5, success = { inventory_space = 0 } }, + { option = "Chop down", object = "tree*", delay = 5, success = { inventory_space = { max = 0 } } }, ] produces = [ { carries = { id = "logs" } }, @@ -25,7 +25,7 @@ setup = [ { inventory_space = { min = 27 } }, ] actions = [ - { option = "Chop down", object = "oak*", delay = 5, success = { inventory_space = 0 } }, + { option = "Chop down", object = "oak*", delay = 5, success = { inventory_space = { max = 0 } } }, ] produces = [ { carries = { id = "oak_logs" } }, @@ -42,7 +42,7 @@ setup = [ { inventory_space = { min = 27 } }, ] actions = [ - { option = "Chop down", object = "willow*", delay = 5, success = { inventory_space = 0 } }, + { option = "Chop down", object = "willow*", delay = 5, success = { inventory_space = { max = 0 } } }, ] produces = [ { carries = { id = "willow_logs" } }, @@ -59,7 +59,7 @@ setup = [ { inventory_space = { min = 27 } }, ] actions = [ - { option = "Chop down", object = "yew*", delay = 5, success = { inventory_space = 0 } }, + { option = "Chop down", object = "yew*", delay = 5, success = { inventory_space = { max = 0 } } }, ] produces = [ { carries = { id = "yew_logs" } }, diff --git a/game/src/main/kotlin/content/bot/BotManager.kt b/game/src/main/kotlin/content/bot/BotManager.kt index 46064faa3c..9b387fdf00 100644 --- a/game/src/main/kotlin/content/bot/BotManager.kt +++ b/game/src/main/kotlin/content/bot/BotManager.kt @@ -237,7 +237,7 @@ class BotManager( ), actions = listOf( BotAction.GoToNearest("bank"), - BotAction.InteractObject("Use-quickly", "bank_booth*", success = Condition.Equals(Fact.InterfaceOpen("bank"), true)), + BotAction.InteractObject("Use-quickly", "bank_booth*", success = Requirement(Fact.InterfaceOpen("bank"), Predicate.BooleanTrue)), BotAction.InterfaceOption("Withdraw-${predicate.min}", "bank:inventory:${requirement.fact.id}"), BotAction.CloseInterface, ) @@ -252,7 +252,7 @@ class BotManager( ), actions = listOf( BotAction.GoToNearest("bank"), - BotAction.InteractObject("Use-quickly", "bank_booth*", success = Condition.Equals(Fact.InterfaceOpen("bank"), true)), + BotAction.InteractObject("Use-quickly", "bank_booth*", success = Requirement(Fact.InterfaceOpen("bank"), Predicate.BooleanTrue)), BotAction.InterfaceOption("Withdraw-X", "bank:inventory:${requirement.fact.id}"), BotAction.IntEntry(predicate.min), BotAction.CloseInterface, @@ -277,7 +277,7 @@ class BotManager( setup = listOf(Requirement(Fact.InventorySpace, Predicate.IntRange(predicate.min))), actions = listOf( BotAction.GoToNearest("bank"), - BotAction.InteractObject("Use-quickly", "bank_booth*", success = Condition.Equals(Fact.InterfaceOpen("bank"), true)), + BotAction.InteractObject("Use-quickly", "bank_booth*", success = Requirement(Fact.InterfaceOpen("bank"), Predicate.BooleanTrue)), BotAction.InterfaceOption("Withdraw-X", "bank:inventory:${requirement.fact.id}"), BotAction.IntEntry(predicate.min!!), BotAction.CloseInterface, diff --git a/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt b/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt index 2647d2ce17..fb30c4019b 100644 --- a/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt +++ b/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt @@ -16,88 +16,6 @@ data class BehaviourFragment( val fields: Map = emptyMap(), ) : Behaviour { fun resolveActions(template: BotActivity, actions: MutableList) { - for (action in template.actions) { - val resolved = when (action) { - is BotAction.Reference -> when (val copy = action.action) { - is BotAction.GoTo -> BotAction.GoTo(resolve(action.references["go_to"], copy.target)) - is BotAction.GoToNearest -> BotAction.GoToNearest(resolve(action.references["go_to_nearest"], copy.tag)) - is BotAction.InterfaceOption -> BotAction.InterfaceOption( - option = resolve(action.references["option"], copy.option), - id = resolve(action.references["interface"], copy.id), - success = resolveReference(copy.success), - ) - is BotAction.DialogueContinue -> BotAction.DialogueContinue( - option = resolve(action.references["option"], copy.option), - id = resolve(action.references["continue"], copy.id), - success = resolveReference(copy.success), - ) - is BotAction.ItemOnItem -> BotAction.ItemOnItem( - item = resolve(action.references["item"], copy.item), - on = resolve(action.references["on"], copy.on), - success = resolveReference(copy.success), - ) - is BotAction.ItemOnObject -> BotAction.ItemOnObject( - item = resolve(action.references["item"], copy.item), - id = resolve(action.references["on_object"], copy.id), - success = resolveReference(copy.success), - ) - is BotAction.InteractNpc -> { - val option = resolve(action.references["option"], copy.option) - if (option == "Attack") { - BotAction.FightNpc( - id = resolve(action.references["npc"], copy.id), - success = resolveReference(copy.success), - delay = resolve(action.references["radius"], copy.delay), - radius = resolve(action.references["radius"], copy.radius), - ) - } else { - BotAction.InteractNpc( - option = option, - id = resolve(action.references["npc"], copy.id), - success = resolveReference(copy.success), - delay = resolve(action.references["delay"], copy.delay), - radius = resolve(action.references["radius"], copy.radius), - ) - } - } - is BotAction.FightNpc -> BotAction.FightNpc( - id = resolve(action.references["npc"], copy.id), - delay = resolve(action.references["delay"], copy.delay), - success = resolveReference(copy.success), - healPercentage = resolve(action.references["heal_percent"], copy.healPercentage), - lootOverValue = resolve(action.references["loot_over"], copy.lootOverValue), - radius = resolve(action.references["radius"], copy.radius), - ) - is BotAction.InteractObject -> BotAction.InteractObject( - option = resolve(action.references["option"], copy.option), - id = resolve(action.references["object"], copy.id), - success = resolveReference(copy.success), - delay = resolve(action.references["delay"], copy.delay), - radius = resolve(action.references["radius"], copy.radius), - ) - is BotAction.WalkTo -> BotAction.WalkTo( - x = resolve(action.references["x"], copy.x), - y = resolve(action.references["y"], copy.y), - ) - is BotAction.StringEntry -> BotAction.StringEntry( - value = resolve(action.references["value"], copy.value), - ) - is BotAction.IntEntry -> BotAction.IntEntry( - value = resolve(action.references["value"], copy.value), - ) - is BotAction.Restart -> BotAction.Restart( - copy.wait.map { resolveReference(it) ?: throw IllegalArgumentException("Restart wait must have success condition.") }, - resolveReference(copy.success) ?: throw IllegalArgumentException("Restart action must have success condition.") - ) - is BotAction.CloseInterface -> BotAction.CloseInterface - is BotAction.Wait -> BotAction.Wait(resolve(action.references["wait"], copy.ticks)) - is BotAction.Clone, is BotAction.Reference -> throw IllegalArgumentException("Invalid reference action type: ${action.action::class.simpleName}.") - } - is BotAction.Clone -> throw IllegalArgumentException("Unresolved clone action in template ${id}.") - else -> action - } - actions.add(resolved) - } } fun resolveRequirements(requirements: MutableList, facts: List) { diff --git a/game/src/main/kotlin/content/bot/action/BotAction.kt b/game/src/main/kotlin/content/bot/action/BotAction.kt index 0604b113e1..c38c0eb30f 100644 --- a/game/src/main/kotlin/content/bot/action/BotAction.kt +++ b/game/src/main/kotlin/content/bot/action/BotAction.kt @@ -3,6 +3,7 @@ package content.bot.action import content.bot.Bot import content.bot.BotManager import content.bot.fact.Condition +import content.bot.fact.Requirement import content.bot.interact.path.Graph import content.entity.combat.attackers import content.entity.combat.dead @@ -136,7 +137,7 @@ sealed interface BotAction { val option: String, val id: String, val delay: Int = 0, - val success: Condition? = null, + val success: Requirement<*>? = null, val radius: Int = 10, ) : BotAction { @@ -188,7 +189,7 @@ sealed interface BotAction { data class FightNpc( val id: String, val delay: Int = 0, - val success: Condition? = null, + val success: Requirement<*>? = null, val radius: Int = 10, val healPercentage: Int = 20, val lootOverValue: Int = 0, @@ -278,7 +279,7 @@ sealed interface BotAction { val option: String, val id: String, val delay: Int = 0, - val success: Condition? = null, + val success: Requirement<*>? = null, val radius: Int = 10, ) : BotAction { @@ -326,7 +327,7 @@ sealed interface BotAction { } } - data class ItemOnItem(val item: String, val on: String, val success: Condition? = null) : BotAction { + data class ItemOnItem(val item: String, val on: String, val success: Requirement<*>? = null) : BotAction { override fun update(bot: Bot, frame: BehaviourFrame): BehaviourState? { if (success != null && success.check(bot.player)) { return BehaviourState.Success @@ -352,7 +353,7 @@ sealed interface BotAction { } } - data class ItemOnObject(val item: String, val id: String, val success: Condition? = null) : BotAction { + data class ItemOnObject(val item: String, val id: String, val success: Requirement<*>? = null) : BotAction { override fun update(bot: Bot, frame: BehaviourFrame): BehaviourState { if (success != null && success.check(bot.player)) { return BehaviourState.Success @@ -391,7 +392,7 @@ sealed interface BotAction { } } - data class InterfaceOption(val option: String, val id: String, val success: Condition? = null) : BotAction { + data class InterfaceOption(val option: String, val id: String, val success: Requirement<*>? = null) : BotAction { override fun update(bot: Bot, frame: BehaviourFrame): BehaviourState? { if (success != null && success.check(bot.player)) { return BehaviourState.Success @@ -439,7 +440,7 @@ sealed interface BotAction { } } - data class DialogueContinue(val option: String, val id: String, val success: Condition? = null) : BotAction { + data class DialogueContinue(val option: String, val id: String, val success: Requirement<*>? = null) : BotAction { override fun update(bot: Bot, frame: BehaviourFrame): BehaviourState { if (success != null && success.check(bot.player)) { return BehaviourState.Success @@ -496,8 +497,8 @@ sealed interface BotAction { * Restarts the current action when [check] doesn't hold true (or bot has no mode) and success state isn't matched. */ data class Restart( - val wait: List, - val success: Condition, + val wait: List>, + val success: Requirement<*>, ) : BotAction { override fun update(bot: Bot, frame: BehaviourFrame): BehaviourState { if (success.check(bot.player)) { diff --git a/game/src/main/kotlin/content/bot/action/BotActivity.kt b/game/src/main/kotlin/content/bot/action/BotActivity.kt index 44be06353d..041c92d205 100644 --- a/game/src/main/kotlin/content/bot/action/BotActivity.kt +++ b/game/src/main/kotlin/content/bot/action/BotActivity.kt @@ -1,5 +1,6 @@ package content.bot.action +import content.bot.fact.Condition import content.bot.fact.Requirement import world.gregs.config.Config import world.gregs.config.ConfigReader @@ -30,6 +31,14 @@ fun loadActivities( ) { val templates = loadTemplates(files.list(Settings["bots.templates"])) loadActivities(activities, templates, files.list(Settings["bots.definitions"])) + // Group activities by requirement types + for (activity in activities.values) { + for (req in activity.requires) { + for (key in req.fact.groups()) { + groups.getOrPut(key) { mutableListOf() }.add(activity.id) + } + } + } loadSetups(resolvers, templates, files.list(Settings["bots.setups"])) loadShortcuts(shortcuts, templates, files.list(Settings["bots.shortcuts"])) } @@ -46,16 +55,14 @@ private fun loadActivities(activities: MutableMap, template var capacity = 1 val requires = mutableListOf>>() val setup = mutableListOf>>() - val actions = mutableListOf>() + val actions = mutableListOf() val produces = mutableListOf>>() while (nextPair()) { when (val key = key()) { "template" -> template = string() "requires" -> requirements(requires) "setup" -> requirements(setup) - "actions" -> while (nextElement()) { - actions.add(map()) - } + "actions" -> actions(actions) "produces" -> requirements(produces) "capacity" -> capacity = int() "fields" -> fields = map() @@ -66,7 +73,8 @@ private fun loadActivities(activities: MutableMap, template requireNotNull(fields) fragments.add(Fragment(id, template, fields, capacity, requires, setup, actions, produces)) } else { - activities[id] = BotActivity(id, capacity, Requirement.parse(requires, "$id ${exception()}")) + val debug = "$id ${exception()}" + activities[id] = BotActivity(id, capacity, Requirement.parse(requires, debug), Requirement.parse(setup, debug), actions, Requirement.parse(produces, debug, requirePredicates = false).toSet()) } } } @@ -95,16 +103,14 @@ private fun loadSetups(resolvers: MutableMap>, tem var weight = 1 val requires = mutableListOf>>() val setup = mutableListOf>>() - val actions = mutableListOf>() + val actions = mutableListOf() val produces = mutableListOf>>() while (nextPair()) { when (val key = key()) { "template" -> template = string() "requires" -> requirements(requires) "setup" -> requirements(setup) - "actions" -> while (nextElement()) { - actions.add(map()) - } + "actions" -> actions(actions) "produces" -> requirements(produces) "weight" -> weight = int() "fields" -> fields = map() @@ -117,7 +123,7 @@ private fun loadSetups(resolvers: MutableMap>, tem } else { val debug = "$id ${exception()}" val products = Requirement.parse(produces, debug) - val resolver = Resolver(id, weight, Requirement.parse(requires, debug), Requirement.parse(setup, debug), produces = products.toSet()) + val resolver = Resolver(id, weight, Requirement.parse(requires, debug), Requirement.parse(setup, debug), actions, products.toSet()) for (product in products) { for (key in product.fact.keys()) { resolvers.getOrPut(key) { mutableListOf() }.add(resolver) @@ -148,27 +154,28 @@ private fun loadShortcuts(shortcuts: MutableList, templates: var weight = 1 val requires = mutableListOf>>() val setup = mutableListOf>>() - val actions = mutableListOf>() + val actions = mutableListOf() val produces = mutableListOf>>() while (nextPair()) { when (val key = key()) { "template" -> template = string() "requires" -> requirements(requires) "setup" -> requirements(setup) - "actions" -> while (nextElement()) { - actions.add(map()) - } + "actions" -> actions(actions) "produces" -> requirements(produces) "weight" -> weight = int() "fields" -> fields = map() else -> throw IllegalArgumentException("Unexpected key: '$key' ${exception()}") } } - if (template != null) { - requireNotNull(fields) + if (fields != null && template == null) { + error("Found fields but no template for $id in ${exception()}") + } else if (template != null) { + requireNotNull(fields) { "No fields found for $id ${exception()}"} fragments.add(Fragment(id, template, fields, weight, requires, setup, actions, produces)) } else { - shortcuts.add(NavigationShortcut(id, weight, Requirement.parse(requires, id), Requirement.parse(setup, id))) + val debug = "$id ${exception()}" + shortcuts.add(NavigationShortcut(id, weight, Requirement.parse(requires, debug), Requirement.parse(setup, debug), actions, Requirement.parse(produces, debug, requirePredicates = false).toSet())) } } } @@ -190,15 +197,13 @@ private fun loadTemplates(paths: List): Map { val id = section() val requires = mutableListOf>>() val setup = mutableListOf>>() - val actions = mutableListOf>() + val actions = mutableListOf() val produces = mutableListOf>>() while (nextPair()) { when (val key = key()) { "requires" -> requirements(requires) "setup" -> requirements(setup) - "actions" -> while (nextElement()) { - actions.add(map()) - } + "actions" -> actions(actions) "produces" -> requirements(produces) else -> throw IllegalArgumentException("Unexpected key: '$key' ${exception()}") } @@ -229,7 +234,7 @@ private data class Fragment( val int: Int, val requires: List>>, val setup: List>>, - val actions: List>, + val actions: List, // Can fragments even have actions? val produces: List>>, ) { fun activity(template: Template) = BotActivity( @@ -237,6 +242,7 @@ private data class Fragment( capacity = int, requires = resolveRequirements(template.requires, requires), setup = resolveRequirements(template.setup, setup), + actions = template.actions, produces = resolveRequirements(template.produces, produces, requirePredicates = false).toSet(), ) @@ -284,6 +290,7 @@ private data class Fragment( weight = int, requires = resolveRequirements(template.requires, requires), setup = resolveRequirements(template.setup, setup), + actions = template.actions, produces = resolveRequirements(template.produces, produces, requirePredicates = false).toSet(), ) @@ -292,6 +299,7 @@ private data class Fragment( weight = int, requires = resolveRequirements(template.requires, requires), setup = resolveRequirements(template.setup, setup), + actions = template.actions, produces = resolveRequirements(template.produces, produces, requirePredicates = false).toSet(), ) } @@ -299,6 +307,151 @@ private data class Fragment( private data class Template( val requires: List>>, val setup: List>>, - val actions: List>, + val actions: List, val produces: List>>, ) + +fun ConfigReader.actions(list: MutableList) { + while (nextElement()) { + var type = "" + var id = "" + var option = "" + var int = 0 + var ticks = 0 + var radius = 10 + var delay = 0 + var heal = 0 + var loot = 0 + var x = 0 + var y = 0 + val references = mutableMapOf() + val wait = mutableListOf>>() + val success = mutableListOf>>() + while (nextEntry()) { + when (val key = key()) { + "go_to", "go_to_nearest", "enter_string", "interface", "npc", "object", "clone", "item", "continue" -> { + type = key + id = string() + if (id.contains('$')) { + references[key] = id + } + } + "x" -> { + if (type == "") { + type = "tile" + } + val value = value() + if (value is String && value.contains('$')) { + references[key] = value + } else { + x = value as Int + } + } + "y" -> { + if (type == "") { + type = "tile" + } + val value = value() + if (value is String && value.contains('$')) { + references[key] = value + } else { + y = value as Int + } + } + "target", "id" -> { + id = string() + if (id.contains('$')) { + references[key] = id + } + } + "option", "on" -> { + option = string() + if (option.contains('$')) { + references[key] = option + } + } + "on_object" -> { + type = "${type}_on_object" + option = string() + if (option.contains('$')) { + references[key] = option + } + } + "restart" -> { + require(boolean()) { "Can't have restart = false ${exception()}" } + type = key + } + "success" -> while (nextEntry()) { + success.add(key() to map()) + } + "wait_if" -> while (nextElement()) { + while (nextEntry()) { + wait.add(key() to map()) + } + } + "wait" -> { + type = key + when (val value = value()) { + is Int -> ticks = value + is String if value.contains('$') -> references[key] = value + else -> throw IllegalArgumentException("Invalid '$key' value: $value ${exception()}") + } + } + "radius" -> when (val value = value()) { + is Int -> radius = value + is String if value.contains('$') -> references[key] = value + else -> throw IllegalArgumentException("Invalid '$key' value: $value ${exception()}") + } + "heal_percent" -> when (val value = value()) { + is Int -> heal = value + is String if value.contains('$') -> references[key] = value + else -> throw IllegalArgumentException("Invalid '$key' value: $value ${exception()}") + } + "loot_over" -> when (val value = value()) { + is Int -> loot = value + is String if value.contains('$') -> references[key] = value + else -> throw IllegalArgumentException("Invalid '$key' value: $value ${exception()}") + } + "delay" -> when (val value = value()) { + is Int -> delay = value + is String if value.contains('$') -> references[key] = value + else -> throw IllegalArgumentException("Invalid '$key' value: $value ${exception()}") + } + "enter_int" -> { + type = key + when (val value = value()) { + is Int -> int = value + is String if value.contains('$') -> references[key] = value + else -> throw IllegalArgumentException("Invalid '$key' value: $value ${exception()}") + } + } + else -> throw IllegalArgumentException("Unknown action key: $key ${exception()}") + } + } + var action = when (type) { + "go_to" -> BotAction.GoTo(id) + "go_to_nearest" -> BotAction.GoToNearest(id) + "enter_string" -> BotAction.StringEntry(id) + "enter_int" -> BotAction.IntEntry(int) + "wait" -> BotAction.Wait(ticks) + "restart" -> BotAction.Restart(Requirement.parse(wait, id), Requirement.parse(success, id).singleOrNull() ?: throw IllegalArgumentException("Restart must have success condition.")) + "npc" -> if (option == "Attack") { + BotAction.FightNpc(id = id, delay = delay, success = Requirement.parse(success, id).singleOrNull(), healPercentage = heal, lootOverValue = loot, radius = radius) + } else { + BotAction.InteractNpc(id = id, option = option, delay = delay, success = Requirement.parse(success, id).singleOrNull(), radius = radius) + } + "tile" -> BotAction.WalkTo(x = x, y = y) + "object" -> BotAction.InteractObject(id = id, option = option, delay = delay, success = Requirement.parse(success, id).singleOrNull(), radius = radius) + "interface" -> BotAction.InterfaceOption(id = id, option = option, success = Requirement.parse(success, id).singleOrNull()) + "continue" -> BotAction.DialogueContinue(id = id, option = option, success = Requirement.parse(success, id).singleOrNull()) + "item" -> BotAction.ItemOnItem(item = id, on = option, success = Requirement.parse(success, id).singleOrNull()) + "item_on_object" -> BotAction.ItemOnObject(item = id, id = option, success = Requirement.parse(success, id).singleOrNull()) + "clone" -> BotAction.Clone(id) + else -> throw IllegalArgumentException("Unknown action type: $type ${exception()}") + } + if (references.isNotEmpty()) { + action = BotAction.Reference(action, references) + } + list.add(action) + } +} diff --git a/game/src/main/kotlin/content/bot/fact/FactParser.kt b/game/src/main/kotlin/content/bot/fact/FactParser.kt index de646165b7..c5077ccd56 100644 --- a/game/src/main/kotlin/content/bot/fact/FactParser.kt +++ b/game/src/main/kotlin/content/bot/fact/FactParser.kt @@ -98,22 +98,20 @@ sealed class FactParser { object Timer : FactParser() { override val required = setOf("id") - override fun parse(map: Map): Fact { - return Fact.HasTimer(map["id"] as String) - } - override fun predicate(map: Map) = Predicate.parseBool(map) + override fun parse(map: Map) = Fact.HasTimer(map["id"] as String) + override fun predicate(map: Map) = Predicate.BooleanTrue } object Queue : FactParser() { override val required = setOf("id") override fun parse(map: Map) = Fact.HasQueue(map["id"] as String) - override fun predicate(map: Map) = Predicate.parseBool(map) + override fun predicate(map: Map) = Predicate.BooleanTrue } object Interface : FactParser() { override val required = setOf("id") override fun parse(map: Map) = Fact.InterfaceOpen(map["id"] as String) - override fun predicate(map: Map) = Predicate.parseBool(map) + override fun predicate(map: Map) = Predicate.BooleanTrue } object PlayerTile : FactParser() { diff --git a/game/src/main/kotlin/content/bot/fact/Predicate.kt b/game/src/main/kotlin/content/bot/fact/Predicate.kt index ab60850ec1..69e2747649 100644 --- a/game/src/main/kotlin/content/bot/fact/Predicate.kt +++ b/game/src/main/kotlin/content/bot/fact/Predicate.kt @@ -34,8 +34,12 @@ sealed interface Predicate { override fun test(value: Tile) = value in Areas[name] } - data class BooleanEquals(val value: Boolean) : Predicate { - override fun test(value: Boolean) = value == this.value + object BooleanTrue : Predicate { + override fun test(value: Boolean) = value + } + + object BooleanFalse : Predicate { + override fun test(value: Boolean) = !value } data class StringEquals(val value: String) : Predicate { @@ -79,7 +83,7 @@ sealed interface Predicate { if (equals !is Boolean) { error("Unsupported equals type: '${equals.let { it::class.simpleName }}'") } - return BooleanEquals(equals) + return if (equals) BooleanTrue else BooleanFalse } fun parseString(map: Map): Predicate? { diff --git a/game/src/main/kotlin/content/bot/fact/PredicateParser.kt b/game/src/main/kotlin/content/bot/fact/PredicateParser.kt index 923eed8621..382b0561a3 100644 --- a/game/src/main/kotlin/content/bot/fact/PredicateParser.kt +++ b/game/src/main/kotlin/content/bot/fact/PredicateParser.kt @@ -5,12 +5,12 @@ import world.gregs.voidps.type.Tile sealed class PredicateParser { open val required: Set = emptySet() open val optional: Set = emptySet() - abstract fun parse(map: Map) : Predicate? + abstract fun parse(map: Map): Predicate? object IntegerParser : PredicateParser() { override val optional = setOf("min", "max", "equals") - override fun parse(map: Map) : Predicate? { + override fun parse(map: Map): Predicate? { if (map.containsKey("min") || map.containsKey("max")) { return Predicate.IntRange(map["min"] as? Int, map["max"] as? Int) } else if (map.containsKey("equals")) { @@ -23,15 +23,17 @@ sealed class PredicateParser { object BooleanParser : PredicateParser() { override val required = setOf("equals") - override fun parse(map: Map) : Predicate { - return Predicate.BooleanEquals(map["equals"] as Boolean) + override fun parse(map: Map) = if (map["equals"] as Boolean) { + Predicate.BooleanTrue + } else { + Predicate.BooleanFalse } } object TileParser : PredicateParser() { override val optional = setOf("x", "y", "level") - override fun parse(map: Map) : Predicate { + override fun parse(map: Map): Predicate { return Predicate.TileEquals(map["x"] as? Int, map["y"] as? Int, map["level"] as? Int) } } diff --git a/game/src/main/kotlin/content/bot/interact/path/Graph.kt b/game/src/main/kotlin/content/bot/interact/path/Graph.kt index 5cfaef3d16..a4692da850 100644 --- a/game/src/main/kotlin/content/bot/interact/path/Graph.kt +++ b/game/src/main/kotlin/content/bot/interact/path/Graph.kt @@ -2,6 +2,7 @@ package content.bot.interact.path import content.bot.action.BotAction import content.bot.action.NavigationShortcut +import content.bot.action.actions import content.bot.bot import content.bot.fact.Condition import content.bot.fact.Requirement @@ -253,12 +254,7 @@ class Graph( "to_y" -> toY = int() "to_level" -> toLevel = int() "cost" -> cost = int() - "actions" -> while (nextElement()) { - while (nextEntry()) { - val key = key() - val value = value() - } - } + "actions" -> actions(actions) "conditions" -> while (nextElement()) { while (nextEntry()) { val key = key() From 252671919177da9aee313601c59fe3e5cd9c13b7 Mon Sep 17 00:00:00 2001 From: GregHib Date: Sat, 7 Feb 2026 17:42:50 +0000 Subject: [PATCH 061/101] Remove Condition --- .../src/main/kotlin/content/bot/BotManager.kt | 1 - .../kotlin/content/bot/action/Behaviour.kt | 1 - .../content/bot/action/BehaviourFragment.kt | 167 ---------- .../kotlin/content/bot/action/BotAction.kt | 2 - .../kotlin/content/bot/action/BotActivity.kt | 1 - .../main/kotlin/content/bot/fact/Condition.kt | 129 -------- game/src/main/kotlin/content/bot/fact/Fact.kt | 2 +- .../main/kotlin/content/bot/fact/Predicate.kt | 6 +- .../kotlin/content/bot/fact/Requirement.kt | 42 +-- .../kotlin/content/bot/interact/path/Graph.kt | 8 +- .../test/kotlin/content/bot/BotManagerTest.kt | 42 +-- .../bot/action/BehaviourFragmentTest.kt | 310 ------------------ .../content/bot/interact/path/GraphTest.kt | 11 +- 13 files changed, 38 insertions(+), 684 deletions(-) delete mode 100644 game/src/main/kotlin/content/bot/action/BehaviourFragment.kt delete mode 100644 game/src/main/kotlin/content/bot/fact/Condition.kt delete mode 100644 game/src/test/kotlin/content/bot/action/BehaviourFragmentTest.kt diff --git a/game/src/main/kotlin/content/bot/BotManager.kt b/game/src/main/kotlin/content/bot/BotManager.kt index 9b387fdf00..a34e032cca 100644 --- a/game/src/main/kotlin/content/bot/BotManager.kt +++ b/game/src/main/kotlin/content/bot/BotManager.kt @@ -2,7 +2,6 @@ package content.bot import com.github.michaelbull.logging.InlineLogger import content.bot.action.* -import content.bot.fact.Condition import content.bot.fact.Fact import content.bot.fact.Predicate import content.bot.fact.Requirement diff --git a/game/src/main/kotlin/content/bot/action/Behaviour.kt b/game/src/main/kotlin/content/bot/action/Behaviour.kt index 84ffbd40e8..e6ac2b76e9 100644 --- a/game/src/main/kotlin/content/bot/action/Behaviour.kt +++ b/game/src/main/kotlin/content/bot/action/Behaviour.kt @@ -1,6 +1,5 @@ package content.bot.action -import content.bot.fact.Condition import content.bot.fact.Requirement interface Behaviour { diff --git a/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt b/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt deleted file mode 100644 index fb30c4019b..0000000000 --- a/game/src/main/kotlin/content/bot/action/BehaviourFragment.kt +++ /dev/null @@ -1,167 +0,0 @@ -package content.bot.action - -import content.bot.fact.* -import world.gregs.voidps.engine.event.Wildcard - -data class BehaviourFragment( - override val id: String, - val type: String, - val capacity: Int, - val weight: Int, - var template: String, - override val requires: List> = emptyList(), - override val setup: List> = emptyList(), - override val actions: List = emptyList(), - override val produces: Set> = emptySet(), - val fields: Map = emptyMap(), -) : Behaviour { - fun resolveActions(template: BotActivity, actions: MutableList) { - } - - fun resolveRequirements(requirements: MutableList, facts: List) { - for (req in facts) { - val resolved = resolveReference(req) ?: continue - requirements.add(resolved) - } - } - - private fun BehaviourFragment.resolveReference(req: Condition?): Condition? = when (req) { - is Condition.Reference -> { - val references = req.references - val min = resolve(references["min"], req.min) - val max = resolve(references["max"], req.max) - when (req.type) { - "skill" -> { - val id = resolve(references[req.type], req.id) - Condition.range(Fact.SkillLevel.of(id), min, max) - } - "carries" -> { - val id = resolve(references[req.type], req.id) - val min = resolve(references["amount"], req.min) - Condition.split(id, min, max, Wildcard.Item) { Fact.InventoryCount(it) } - } - "equips" -> { - val id = resolve(references[req.type], req.id) - val min = resolve(references["amount"], req.min) - Condition.split(id, min, max, Wildcard.Item) { Fact.EquipCount(it) } - } - "owns" -> { - val id = resolve(references[req.type], req.id) - val min = resolve(references["amount"], req.min) - Condition.split(id, min, max, Wildcard.Item) { Fact.ItemCount(it) } - } - "banked" -> { - val id = resolve(references[req.type], req.id) - val min = resolve(references["amount"], req.min) - Condition.split(id, min, max, Wildcard.Item) { Fact.BankCount(it) } - } - "clock" -> { - val id = resolve(references[req.type], req.id) - Condition.split(id, min, max, Wildcard.Variables) { Fact.ClockRemaining(it) } - } - "timer" -> { - val id = resolve(references[req.type], req.id) - val value = resolve(references["value"], req.value as? Boolean) - Condition.Equals(Fact.HasTimer(id), value as? Boolean ?: true) - } - "queue" -> { - val id = resolve(references[req.type], req.id) - val value = resolve(references["value"], req.value as? Boolean) - Condition.Equals(Fact.HasQueue(id), value as? Boolean ?: true) - } - "interface" -> { - val id = resolve(references[req.type], req.id) - val value = resolve(references["value"], req.value as? Boolean) - Condition.Equals(Fact.InterfaceOpen(id), value as? Boolean ?: true) - } - "variable" -> { - val id = resolve(references[req.type], req.id) - val default = resolve(references["default"], req.default) - if (min != null || max != null) { - Condition.range(Fact.IntVariable(id, default as Int), min, max) - } else { - when (val value = resolve(references["value"], req.value)) { - is Int -> Condition.Equals(Fact.IntVariable(id, default as Int), value) - is String -> Condition.Equals(Fact.StringVariable(id, default as? String), value) - is Double -> Condition.Equals(Fact.DoubleVariable(id, default as? Double), value) - is Boolean -> Condition.Equals(Fact.BoolVariable(id, default as? Boolean), value) - else -> null - } - } - } - "inventory_space" -> { - val min = resolve(references["inventory_space"], req.min) - Condition.range(Fact.InventorySpace, min, null) - } - "location" -> { - val id = resolve(references["location"], req.id) - Condition.Area(Fact.PlayerTile, id) - } - "combat_level" -> { - Condition.AtLeast(Fact.CombatLevel, resolve(references["combat_level"], req.min) ?: 1) - } - else -> null - } - } - is Condition.Clone -> throw IllegalArgumentException("Unresolved clone requirement in template ${id}.") - else -> req - } - - private fun String.key(): String { - if (startsWith('$')) { - return substring(1) - } - val index = indexOf($$"${") - if (index == -1) { - return substringAfter('$') - } - val end = indexOf('}', index + 2) - return substring(index + 2, end) - } - - private fun String.reference(): String { - if (startsWith('$')) { - return this - } - val index = indexOf($$"${") - if (index == -1) { - return "\$${substringAfter('$')}" - } - val end = indexOf('}', index) + 1 - return substring(index, end) - } - - private fun resolve(reference: String?, default: Int): Int { - return if (reference != null) { - fields[reference.key()] as? Int ?: throw IllegalArgumentException("Unable to find field '$reference' in ${id}.") - } else { - default - } - } - - private fun resolve(reference: String?, default: Int?): Int? { - return if (reference != null) { - fields[reference.key()] as? Int ?: throw IllegalArgumentException("Unable to find field '$reference' in ${id}.") - } else { - default - } - } - - private fun resolve(reference: String?, default: String): String { - return if (reference != null) { - val key = reference.key() - val value = fields[key] as? String ?: throw IllegalArgumentException("Unable to find field '$reference' in ${id}.") - reference.replace(reference.reference(), value) - } else { - default - } - } - - private fun resolve(reference: String?, default: Any?): Any? { - return if (reference != null) { - fields[reference.key()] ?: throw IllegalArgumentException("Unable to find field '$reference' in ${id}.") - } else { - default - } - } -} \ No newline at end of file diff --git a/game/src/main/kotlin/content/bot/action/BotAction.kt b/game/src/main/kotlin/content/bot/action/BotAction.kt index c38c0eb30f..d37796a14c 100644 --- a/game/src/main/kotlin/content/bot/action/BotAction.kt +++ b/game/src/main/kotlin/content/bot/action/BotAction.kt @@ -2,7 +2,6 @@ package content.bot.action import content.bot.Bot import content.bot.BotManager -import content.bot.fact.Condition import content.bot.fact.Requirement import content.bot.interact.path.Graph import content.entity.combat.attackers @@ -10,7 +9,6 @@ import content.entity.combat.dead import world.gregs.voidps.engine.GameLoop import world.gregs.voidps.engine.client.instruction.InstructionHandlers import world.gregs.voidps.engine.client.instruction.InterfaceHandler -import world.gregs.voidps.engine.client.instruction.handle.ObjectOptionHandler import world.gregs.voidps.engine.client.ui.menu import world.gregs.voidps.engine.data.definition.Areas import world.gregs.voidps.engine.data.definition.InterfaceDefinitions diff --git a/game/src/main/kotlin/content/bot/action/BotActivity.kt b/game/src/main/kotlin/content/bot/action/BotActivity.kt index 041c92d205..cb0b874e76 100644 --- a/game/src/main/kotlin/content/bot/action/BotActivity.kt +++ b/game/src/main/kotlin/content/bot/action/BotActivity.kt @@ -1,6 +1,5 @@ package content.bot.action -import content.bot.fact.Condition import content.bot.fact.Requirement import world.gregs.config.Config import world.gregs.config.ConfigReader diff --git a/game/src/main/kotlin/content/bot/fact/Condition.kt b/game/src/main/kotlin/content/bot/fact/Condition.kt deleted file mode 100644 index f247576a57..0000000000 --- a/game/src/main/kotlin/content/bot/fact/Condition.kt +++ /dev/null @@ -1,129 +0,0 @@ -package content.bot.fact - -import world.gregs.voidps.engine.data.definition.Areas -import world.gregs.voidps.engine.entity.character.player.Player -import world.gregs.voidps.engine.event.Wildcard -import world.gregs.voidps.engine.event.Wildcards -import world.gregs.voidps.type.Tile -import kotlin.collections.map - -sealed interface Condition { - fun check(player: Player): Boolean - fun keys(): Set - fun groups(): Set - fun priority(): Int - - data class Equals(val fact: Fact, val value: T) : Condition { - override fun check(player: Player) = fact.getValue(player) == value - override fun priority() = fact.priority - override fun keys() = fact.keys() - override fun groups() = fact.groups() - } - - data class AtLeast(val fact: Fact, val min: Int) : Condition { - override fun check(player: Player) = fact.getValue(player) >= min - override fun priority() = fact.priority - override fun keys() = fact.keys() - override fun groups() = fact.groups() - } - - data class AtMost(val fact: Fact, val max: Int) : Condition { - override fun check(player: Player) = fact.getValue(player) <= max - override fun priority() = fact.priority - override fun keys() = fact.keys() - override fun groups() = fact.groups() - } - - data class Range(val fact: Fact, val min: Int, val max: Int) : Condition { - override fun check(player: Player) = fact.getValue(player) in min..max - override fun priority() = fact.priority - override fun keys() = fact.keys() - override fun groups() = fact.groups() - } - - data class Within(val fact: Fact, val tile: Tile, val radius: Int) : Condition { - override fun check(player: Player) = fact.getValue(player).within(tile, radius) - override fun priority() = fact.priority - override fun keys() = fact.keys() - override fun groups() = fact.groups() - } - - data class Area(val fact: Fact, val area: String) : Condition { // TODO make fact always PlayerTile? - override fun check(player: Player) = fact.getValue(player) in Areas[area] - override fun priority() = fact.priority - override fun keys() = setOf("enter:$area") - override fun groups() = setOf("area") - } - - data class OneOf(val fact: Fact, val values: Set) : Condition { - override fun check(player: Player) = fact.getValue(player) in values - override fun priority() = fact.priority - override fun keys() = fact.keys() - override fun groups() = fact.groups() - } - - data class Not(val inner: Condition) : Condition { - override fun check(player: Player) = !inner.check(player) - override fun priority() = inner.priority() - override fun keys() = inner.keys() - override fun groups() = inner.groups() - } - - data class All(val conditions: List) : Condition { - override fun check(player: Player) = conditions.all { it.check(player) } - override fun priority() = conditions.first().priority() - override fun keys() = conditions.flatMap { it.keys() }.toSet() - override fun groups() = conditions.flatMap { it.groups() }.toSet() - } - - data class Any(val conditions: List) : Condition { - override fun check(player: Player) = conditions.any { it.check(player) } - override fun priority() = conditions.first().priority() - override fun keys() = conditions.flatMap { it.keys() }.toSet() - override fun groups() = conditions.flatMap { it.groups() }.toSet() - } - - data class Reference( - val type: String = "", - val id: String = "", - val value: kotlin.Any? = null, - val default: kotlin.Any? = null, - val min: Int? = null, - val max: Int? = null, - val references: Map = emptyMap(), - ) : Condition { - override fun check(player: Player) = false - override fun priority() = -1 - override fun keys() = emptySet() - override fun groups() = emptySet() - } - - class Clone(val id: String) : Condition { - override fun check(player: Player) = false - override fun priority() = -1 - override fun keys() = emptySet() - override fun groups() = emptySet() - } - - companion object { - fun split(id: String, min: Int?, max: Int?, wildcard: Wildcard, fact: (String) -> Fact): Condition = when { - id.contains(",") -> Any(id.split(",").flatMap { individual -> - if (individual.any { char -> char == '*' || char == '#' }) { - Wildcards.get(individual, wildcard).map { resolved -> range(fact(resolved), min, max) } - } else { - listOf(range(fact(individual), min, max)) - } - }) - id.any { char -> char == '*' || char == '#' } -> Any(Wildcards.get(id, wildcard).map { resolved -> range(fact(resolved), min, max) }) - else -> range(fact(id), min, max) - } - - fun range(fact: Fact, min: Int?, max: Int?) = when { - min != null && max != null -> Range(fact, min, max) - min != null -> AtLeast(fact, min) - max != null -> AtMost(fact, max) - else -> AtLeast(fact, 1) - } - } - -} \ No newline at end of file diff --git a/game/src/main/kotlin/content/bot/fact/Fact.kt b/game/src/main/kotlin/content/bot/fact/Fact.kt index cdd7ccedb0..ecad018f2c 100644 --- a/game/src/main/kotlin/content/bot/fact/Fact.kt +++ b/game/src/main/kotlin/content/bot/fact/Fact.kt @@ -12,7 +12,7 @@ import world.gregs.voidps.engine.timer.epochSeconds import world.gregs.voidps.type.Tile /** - * A bots state which can be a [Condition] for, or a product of performing a [content.bot.action.Behaviour] + * A bots state which can be a [Requirement] for, or a product of performing a [content.bot.action.Behaviour] * @param priority Ensure bots aren't walking to locations before getting items etc... lower values are prioritised first. */ sealed class Fact(val priority: Int) { diff --git a/game/src/main/kotlin/content/bot/fact/Predicate.kt b/game/src/main/kotlin/content/bot/fact/Predicate.kt index 69e2747649..50643cfe16 100644 --- a/game/src/main/kotlin/content/bot/fact/Predicate.kt +++ b/game/src/main/kotlin/content/bot/fact/Predicate.kt @@ -46,7 +46,7 @@ sealed interface Predicate { override fun test(value: String) = value == this.value } - data class TileEquals(val x: Int?, val y: Int?, val level: Int?) : Predicate { + data class TileEquals(val x: Int? = null, val y: Int? = null, val level: Int? = null) : Predicate { override fun test(value: Tile): Boolean { if (x != null && value.x != x) return false if (y != null && value.y != y) return false @@ -55,6 +55,10 @@ sealed interface Predicate { } } + data class Within(val x: Int, val y: Int, val level: Int, val radius: Int) : Predicate { + override fun test(value: Tile) = value.within(x, y, level, radius) + } + companion object { fun parseInt(map: Map): Predicate? = when { map.containsKey("min") || map.containsKey("max") -> IntRange(map["min"] as? Int, map["max"] as? Int) diff --git a/game/src/main/kotlin/content/bot/fact/Requirement.kt b/game/src/main/kotlin/content/bot/fact/Requirement.kt index dcb7f33181..4054947d89 100644 --- a/game/src/main/kotlin/content/bot/fact/Requirement.kt +++ b/game/src/main/kotlin/content/bot/fact/Requirement.kt @@ -1,17 +1,14 @@ package content.bot.fact -import com.github.michaelbull.logging.InlineLogger import world.gregs.voidps.engine.entity.character.player.Player -data class Requirement(val fact: Fact, val predicate: Predicate?) { +data class Requirement(val fact: Fact, val predicate: Predicate? = null) { fun check(player: Player): Boolean { return predicate?.test(fact.getValue(player)) ?: false } companion object { - private val logger = InlineLogger() - fun parse(list: List>>, name: String, requirePredicates: Boolean = true): List> { val requirements = mutableListOf>() @@ -31,40 +28,3 @@ data class Requirement(val fact: Fact, val predicate: Predicate?) { } } } - -//data class Parsed( -// val id: String, -// val template: String? = null, -// val requires: MutableMap, -// val resolves: MutableMap, -//) { -// fun resolve(fields: Map>) { -// for (index in requires.indices) { -// val map = requires[index] -// val template = templates[index] -// val fields = fields[index] -// for ((key, value) in map) { -// if (value !is String || !value.contains("$")) { -// continue -// } -// val ref = value.reference() -// val name = ref.trim('$', '{', '}') -// val replacement = fields[name] ?: error("No field found for type=${types[index]} key=$key ref=$ref") -// map[key] = if (replacement is String) value.replace(ref, replacement) else replacement -// } -// } -// } -// -// private fun String.reference(): String { -// if (startsWith('$')) { -// return this -// } -// val index = indexOf($$"${") -// if (index == -1) { -// return "\$${substringAfter('$')}" -// } -// val end = indexOf('}', index) + 1 -// return substring(index, end) -// } -//} - diff --git a/game/src/main/kotlin/content/bot/interact/path/Graph.kt b/game/src/main/kotlin/content/bot/interact/path/Graph.kt index a4692da850..bb5db07850 100644 --- a/game/src/main/kotlin/content/bot/interact/path/Graph.kt +++ b/game/src/main/kotlin/content/bot/interact/path/Graph.kt @@ -4,7 +4,7 @@ import content.bot.action.BotAction import content.bot.action.NavigationShortcut import content.bot.action.actions import content.bot.bot -import content.bot.fact.Condition +import content.bot.fact.Predicate import content.bot.fact.Requirement import content.bot.isBot import world.gregs.config.Config @@ -159,11 +159,11 @@ class Graph( } fun add(shortcut: NavigationShortcut): Int { - val first = shortcut.produces.filterIsInstance().firstOrNull() ?: throw IllegalArgumentException("Shortcut requires location product ${shortcut.id}") - val area = Areas[first.area] + val first = shortcut.produces.map { it.predicate }.filterIsInstance().firstOrNull() ?: throw IllegalArgumentException("Shortcut requires location product ${shortcut.id}") + val area = Areas[first.name] val end = tiles.indexOfFirst { it in area } if (end == -1) { - throw IllegalArgumentException("Unable to find nav graph tile in shortcut area '${first.area}'.") + throw IllegalArgumentException("Unable to find nav graph tile in shortcut area '${first.name}'.") } val index = addEdge(0, end, shortcut.weight, shortcut.actions, shortcut.requires) shortcuts[index] = shortcut diff --git a/game/src/test/kotlin/content/bot/BotManagerTest.kt b/game/src/test/kotlin/content/bot/BotManagerTest.kt index bcf28b0d91..9c03bd0cab 100644 --- a/game/src/test/kotlin/content/bot/BotManagerTest.kt +++ b/game/src/test/kotlin/content/bot/BotManagerTest.kt @@ -1,12 +1,12 @@ package content.bot import content.bot.action.* -import content.bot.fact.Condition import content.bot.fact.Fact +import content.bot.fact.Predicate +import content.bot.fact.Requirement import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.Test import world.gregs.voidps.engine.entity.character.player.Player -import world.gregs.voidps.type.Tile class BotManagerTest { @@ -158,7 +158,7 @@ class BotManagerTest { manager.tick(bot) manager.tick(bot) - bot.frame().fail(Reason.Requirement(Condition.Clone(""))) + bot.frame().fail(Reason.Requirement(Requirement(Fact.PlayerTile))) manager.tick(bot) manager.tick(bot) @@ -171,7 +171,7 @@ class BotManagerTest { val activity = testActivity( id = "test", requires = listOf( - Condition.Range(Fact.AttackLevel, 99, 99) + Requirement(Fact.AttackLevel, Predicate.IntRange(99, 99)) ), plan = listOf(BotAction.Wait(4)) ) @@ -190,7 +190,7 @@ class BotManagerTest { @Test fun `Resolvable requirement queues resolver before activity starts`() { - val condition = Condition.Within(Fact.PlayerTile, Tile(100, 100, 2), 2) + val condition = Requirement(Fact.PlayerTile, Predicate.Within(100, 100, 2, 2)) val resolver = Resolver( id = "go_to_area", weight = 1, @@ -204,7 +204,7 @@ class BotManagerTest { ) val manager = BotManager( mutableMapOf(activity.id to activity), - mutableMapOf(condition.keys().first() to mutableListOf(resolver)) + mutableMapOf(condition.fact.keys().first() to mutableListOf(resolver)) ) val bot = testBot(activity) @@ -217,7 +217,7 @@ class BotManagerTest { @Test fun `Lowest weight resolver is selected`() { - val condition = Condition.Within(Fact.PlayerTile, Tile(100, 100, 2), 2) + val condition = Requirement(Fact.PlayerTile, Predicate.Within(100, 100, 2, 2)) val bad = Resolver("bad", weight = 10, actions = listOf(BotAction.Clone(""))) val good = Resolver("good", weight = 1, actions = listOf(BotAction.Clone(""))) @@ -230,7 +230,7 @@ class BotManagerTest { val manager = BotManager( mutableMapOf(activity.id to activity), - mutableMapOf(condition.keys().first() to mutableListOf(bad, good)) + mutableMapOf(condition.fact.keys().first() to mutableListOf(bad, good)) ) val bot = testBot(activity) @@ -242,7 +242,7 @@ class BotManagerTest { @Test fun `Blocked resolver is not reselected`() { - val condition = Condition.Within(Fact.PlayerTile, Tile(100, 100, 2), 2) + val condition = Requirement(Fact.PlayerTile, Predicate.Within(100, 100, 2, 2)) val resolver = Resolver(id = "get_key", weight = 1, actions = listOf(BotAction.Clone(""))) val activity = testActivity( id = "open_door", @@ -251,7 +251,7 @@ class BotManagerTest { ) val manager = BotManager( mutableMapOf(activity.id to activity), - mutableMapOf(condition.keys().first() to mutableListOf(resolver)) + mutableMapOf(condition.fact.keys().first() to mutableListOf(resolver)) ) val bot = testBot(activity) @@ -268,7 +268,7 @@ class BotManagerTest { @Test fun `Hard failure in resolver stops bot`() { - val condition = Condition.Within(Fact.PlayerTile, Tile(100, 100, 2), 2) + val condition = Requirement(Fact.PlayerTile, Predicate.Within(100, 100, 2, 2)) val resolver = Resolver( id = "walk", weight = 1, @@ -281,7 +281,7 @@ class BotManagerTest { ) val manager = BotManager( mutableMapOf(activity.id to activity), - mutableMapOf(condition.keys().first() to mutableListOf(resolver)) + mutableMapOf(condition.fact.keys().first() to mutableListOf(resolver)) ) val bot = testBot(activity) @@ -296,7 +296,7 @@ class BotManagerTest { @Test fun `Soft failure in resolver only pops resolver`() { - val condition = Condition.Within(Fact.PlayerTile, Tile(100, 100, 2), 2) + val condition = Requirement(Fact.PlayerTile, Predicate.Within(100, 100, 2, 2)) val resolver = Resolver( id = "test", weight = 1, @@ -309,7 +309,7 @@ class BotManagerTest { ) val manager = BotManager( mutableMapOf(activity.id to activity), - mutableMapOf(condition.keys().first() to mutableListOf(resolver)) + mutableMapOf(condition.fact.keys().first() to mutableListOf(resolver)) ) val bot = testBot(activity) @@ -325,12 +325,12 @@ class BotManagerTest { @Test fun `Resolver with unmet mandatory requirements is skipped`() { - val condition = Condition.Within(Fact.PlayerTile, Tile(100, 100, 2), 2) + val condition = Requirement(Fact.PlayerTile, Predicate.Within(100, 100, 2, 2)) val resolver = Resolver( id = "mine_gem", weight = 1, actions = listOf(BotAction.Clone("")), - requires = listOf(Condition.Range(Fact.MiningLevel, 99, 99)) + requires = listOf(Requirement(Fact.MiningLevel, Predicate.IntRange(99, 99))) ) val activity = testActivity( id = "craft", @@ -339,7 +339,7 @@ class BotManagerTest { ) val manager = BotManager( mutableMapOf(activity.id to activity), - mutableMapOf(condition.keys().first() to mutableListOf(resolver)) + mutableMapOf(condition.fact.keys().first() to mutableListOf(resolver)) ) val bot = testBot(activity) @@ -353,7 +353,7 @@ class BotManagerTest { @Test fun `Activity are occupied while resolver is running`() { - val condition = Condition.Within(Fact.PlayerTile, Tile(100, 100, 2), 2) + val condition = Requirement(Fact.PlayerTile, Predicate.Within(100, 100, 2, 2)) val resolver = Resolver( id = "get_tool", weight = 1, @@ -366,7 +366,7 @@ class BotManagerTest { ) val manager = BotManager( mutableMapOf(activity.id to activity), - mutableMapOf(condition.keys().first() to mutableListOf(resolver)) + mutableMapOf(condition.fact.keys().first() to mutableListOf(resolver)) ) val bot = testBot(activity) @@ -378,8 +378,8 @@ class BotManagerTest { fun testActivity( id: String, - requires: List = emptyList(), - resolves: List = emptyList(), + requires: List> = emptyList(), + resolves: List> = emptyList(), plan: List, ) = BotActivity(id, 1, requires, resolves, plan) } \ No newline at end of file diff --git a/game/src/test/kotlin/content/bot/action/BehaviourFragmentTest.kt b/game/src/test/kotlin/content/bot/action/BehaviourFragmentTest.kt deleted file mode 100644 index c7b9397100..0000000000 --- a/game/src/test/kotlin/content/bot/action/BehaviourFragmentTest.kt +++ /dev/null @@ -1,310 +0,0 @@ -package content.bot.action - -import content.bot.fact.* -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.DynamicTest.dynamicTest -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.TestFactory -import org.junit.jupiter.api.assertThrows - -class BehaviourFragmentTest { - - private fun fragment(fields: Map = emptyMap()) = - BehaviourFragment( - id = "test", - capacity = 1, - template = "tpl", - type = "activity", - fields = fields, - weight = 1, - ) - - /* - Actions - */ - - @Test - fun `Nested clone action throws`() { - val fragment = fragment() - val template = BotActivity( - id = "a", - capacity = 1, - actions = listOf(BotAction.Clone("x")) - ) - assertThrows { - fragment.resolveActions(template, mutableListOf()) - } - } - - @Test - fun `Nested reference action throws`() { - val fragment = fragment() - val template = BotActivity( - id = "a", - capacity = 1, - actions = listOf( - BotAction.Reference( - BotAction.Reference( - BotAction.GoTo("x"), - emptyMap() - ), - emptyMap() - ) - ) - ) - assertThrows { - fragment.resolveActions(template, mutableListOf()) - } - } - - @Test - fun `Wrong type in action field throws`() { - val fragment = fragment(mapOf("dest" to 123)) - val template = BotActivity( - id = "a", - capacity = 1, - actions = listOf( - BotAction.Reference( - BotAction.GoTo("x"), - references = mapOf("go_to" to "dest") - ) - ) - ) - - assertThrows { - fragment.resolveActions(template, mutableListOf()) - } - } - - @Test - fun `No references uses default value`() { - val fragment = fragment() - val template = BotActivity( - id = "a", - capacity = 1, - actions = listOf( - BotAction.Reference( - BotAction.Wait(10), - references = emptyMap() - ) - ) - ) - - val actions = mutableListOf() - fragment.resolveActions(template, actions) - - assertEquals(BotAction.Wait(10), actions.single()) - } - - @Test - fun `Non-reference action is added`() { - val fragment = fragment() - val template = BotActivity( - id = "a", - capacity = 1, - actions = listOf( - BotAction.Wait(10) - ) - ) - - val actions = mutableListOf() - fragment.resolveActions(template, actions) - - assertEquals(BotAction.Wait(10), actions.single()) - } - - @TestFactory - fun `Resolve action references`() = listOf( - Triple(BotAction.GoTo("default"), mapOf("go_to" to "lumbridge"), BotAction.GoTo("lumbridge")), - Triple(BotAction.GoToNearest("default"), mapOf("go_to_nearest" to "lumbridge"), BotAction.GoToNearest("lumbridge")), - Triple(BotAction.InterfaceOption(option = "click", id = "something"), mapOf("option" to "Open", "interface" to "bank"), BotAction.InterfaceOption(option = "Open", id = "bank")), - Triple(BotAction.DialogueContinue(option = "click", id = "something"), mapOf("option" to "Option", "continue" to "now"), BotAction.DialogueContinue(option = "Option", id = "now")), - Triple(BotAction.ItemOnItem(item = "default", on = "on"), mapOf("item" to "item", "on" to "another"), BotAction.ItemOnItem(item = "item", on = "another")), - Triple( - BotAction.InteractNpc( - option = "talk", - id = "default", - delay = 2, - radius = 10 - ), - mapOf("option" to "Talk-to", "npc" to "bob", "delay" to 5, "radius" to 5), - BotAction.InteractNpc( - option = "Talk-to", - id = "bob", - delay = 5, - radius = 5 - ) - ), - Triple( - BotAction.InteractObject( - option = "interact", - id = "default", - delay = 2, - radius = 10 - ), - mapOf("option" to "Open", "object" to "door", "delay" to 5, "radius" to 5), - BotAction.InteractObject( - option = "Open", - id = "door", - delay = 5, - radius = 5 - ) - ), - Triple(BotAction.Wait(4), mapOf("wait" to 5), BotAction.Wait(5)), - ).map { (default, values, expected) -> - dynamicTest("Resolve ${default::class.simpleName} references") { - val fields = values.mapKeys { "ref_${it.key}" } - val fragment = fragment(fields) - val references = values.map { it.key to "\$ref_${it.key}" }.toMap() - val template = BotActivity( - id = "a", - capacity = 1, - actions = listOf( - BotAction.Reference( - default, - references = references - ) - ) - ) - val actions = mutableListOf() - fragment.resolveActions(template, actions) - assertEquals(expected, actions.single()) - } - } - - @Test - fun `Resolve partial references`() { - val fragment = fragment(mapOf("type" to "fun")) - val template = BotActivity( - id = "a", - capacity = 1, - requires = listOf( - Condition.Reference( - "location", "default", references = mapOf( - "location" to $$"some_${type}_area" - ) - ) - ) - ) - val actions = mutableListOf() - fragment.resolveRequirements(actions, template.requires) - assertEquals(Condition.Area(Fact.PlayerTile, "some_fun_area"), actions.single()) - } - - @Test - fun `Resolve ending reference`() { - val fragment = fragment(mapOf("area_type" to "fun")) - val template = BotActivity( - id = "a", - capacity = 1, - requires = listOf( - Condition.Reference("location", "default", - references = mapOf( - "location" to $$"some_$area_type" - ) - ) - ) - ) - val actions = mutableListOf() - fragment.resolveRequirements(actions, template.requires) - assertEquals(Condition.Area(Fact.PlayerTile, "some_fun"), actions.single()) - } - - /* - Requirements - */ - - @TestFactory - fun `Resolve requirement references`() = listOf( - Triple(Condition.Reference("skill", "defence", min = 1, max = 120), mapOf("skill" to "attack", "min" to 5, "max" to 99), Condition.Range(Fact.AttackLevel, 5, 99)), - Triple(Condition.Reference("variable", "default", value = 1), mapOf("variable" to "test", "value" to true), Condition.Equals(Fact.BoolVariable("test", null), true)), - Triple(Condition.Reference("equips", "default", min = 1), mapOf("equips" to "item", "amount" to 10), Condition.AtLeast(Fact.EquipCount("item"), 10)), - Triple(Condition.Reference("carries", "default", min = 1), mapOf("carries" to "item", "amount" to 10), Condition.AtLeast(Fact.InventoryCount("item"), 10)), - Triple(Condition.Reference("inventory_space", min = 1), mapOf("inventory_space" to 10), Condition.AtLeast(Fact.InventorySpace, 10)), - Triple(Condition.Reference("location", "default"), mapOf("location" to "area"), Condition.Area(Fact.PlayerTile, "area")), - Triple(Condition.Reference("timer", "default"), mapOf("timer" to "tick"), Condition.Equals(Fact.HasTimer("tick"), true)), - Triple(Condition.Reference("interface", "default"), mapOf("interface" to "id"), Condition.Equals(Fact.InterfaceOpen("id"), true)), - Triple(Condition.Reference("clock", "default", min = 1), mapOf("clock" to "tock", "min" to 5), Condition.AtLeast(Fact.ClockRemaining("tock"), 5)), - ).map { (reference, values, expected) -> - dynamicTest("Resolve ${reference::class.simpleName} references") { - val fields = values.mapKeys { "ref_${it.key}" } - val fragment = fragment(fields) - val references = values.map { it.key to "\$ref_${it.key}" }.toMap() - val template = BotActivity( - id = "a", - capacity = 1, - requires = listOf( - reference.copy(references = references) - ) - ) - val actions = mutableListOf() - fragment.resolveRequirements(actions, template.requires) - assertEquals(expected, actions.single()) - } - } - - @Test - fun `Missing requirement field throws`() { - val fragment = fragment() - val template = BotActivity( - id = "a", - capacity = 1, - requires = listOf( - Condition.Reference("skill", "attack", references = mapOf("skill" to "missing")) - ) - ) - assertThrows { - fragment.resolveRequirements(mutableListOf(), template.requires) - } - } - - @Test - fun `No requirement reference uses default value`() { - val fragment = fragment() - val template = BotActivity( - id = "a", - capacity = 1, - requires = listOf( - Condition.Reference("skill", "attack", references = emptyMap()) - ) - ) - - val actions = mutableListOf() - fragment.resolveRequirements(actions, template.requires) - - assertEquals(Condition.Equals(Fact.AttackLevel, 1), actions.single()) - } - - @Test - fun `Non-reference requirement is added`() { - val fragment = fragment() - val template = BotActivity( - id = "a", - capacity = 1, - requires = listOf( - Condition.Reference("skill", "attack") - ) - ) - - val actions = mutableListOf() - fragment.resolveRequirements(actions, template.requires) - - assertEquals(Condition.Equals(Fact.AttackLevel, 1), actions.single()) - } - - @Test - fun `Any clone requirement throws`() { - val fragment = fragment() - val template = BotActivity( - id = "a", - capacity = 1, - requires = listOf( - Condition.Clone("x") - ) - ) - assertThrows { - fragment.resolveRequirements(mutableListOf(), template.requires) - } - } - -} \ No newline at end of file diff --git a/game/src/test/kotlin/content/bot/interact/path/GraphTest.kt b/game/src/test/kotlin/content/bot/interact/path/GraphTest.kt index 4f48a6b9c7..594e5df832 100644 --- a/game/src/test/kotlin/content/bot/interact/path/GraphTest.kt +++ b/game/src/test/kotlin/content/bot/interact/path/GraphTest.kt @@ -1,8 +1,9 @@ package content.bot.interact.path import content.bot.action.NavigationShortcut -import content.bot.fact.Condition import content.bot.fact.Fact +import content.bot.fact.Predicate +import content.bot.fact.Requirement import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Test import world.gregs.voidps.engine.data.definition.AreaDefinition @@ -137,7 +138,7 @@ class GraphTest { builder.addEdge(b, c, 3) val cd = builder.addEdge(c, d, 6) val ec = builder.addEdge(e, c, 2) - builder.addEdge(e, d, 7, conditions = listOf(Condition.Equals(Fact.PlayerTile, Tile(100)))) + builder.addEdge(e, d, 7, conditions = listOf(Requirement(Fact.PlayerTile, Predicate.TileEquals(100)))) // builder.print() val output = mutableListOf() @@ -256,7 +257,7 @@ class GraphTest { to = b, weight = 1, actions = emptyList(), - conditions = listOf(Condition.Equals(Fact.PlayerTile, Tile(100))) + conditions = listOf(Requirement(Fact.PlayerTile, Predicate.TileEquals(100))) ) val graph = builder.build() @@ -300,8 +301,8 @@ class GraphTest { val shortcut = NavigationShortcut( id = "teleport", weight = 1, - requires = listOf(Condition.Equals(Fact.PlayerTile, Tile(50, 50))), - produces = setOf(Condition.Area(Fact.PlayerTile, "town")) + requires = listOf(Requirement(Fact.PlayerTile, Predicate.TileEquals(50, 50))), + produces = setOf(Requirement(Fact.PlayerTile, Predicate.InArea("town"))) ) val builder = Graph.Builder() From 6114c2c965b09cab75f6cb01411784a8c990afcd Mon Sep 17 00:00:00 2001 From: GregHib Date: Sat, 7 Feb 2026 20:38:33 +0000 Subject: [PATCH 062/101] Add deficit concept and evaluator to produce them --- .../misthalin/lumbridge/lumbridge.bots.toml | 4 +- data/bot/combat.templates.toml | 12 +- data/bot/cooking.templates.toml | 12 +- data/bot/fletching.templates.toml | 9 +- data/bot/lumbridge.nav-edges.toml | 8 +- data/bot/mining.templates.toml | 32 ++-- data/bot/prayer.bots.toml | 2 +- data/bot/shop.templates.toml | 10 +- data/bot/thieving.templates.toml | 2 +- data/bot/woodcutting.templates.toml | 16 +- .../src/main/kotlin/content/bot/BotManager.kt | 140 ++++++------------ .../kotlin/content/bot/action/BotActivity.kt | 113 ++++++++------ .../main/kotlin/content/bot/fact/Deficit.kt | 52 +++++++ game/src/main/kotlin/content/bot/fact/Fact.kt | 8 + .../kotlin/content/bot/fact/FactParser.kt | 34 +++-- .../main/kotlin/content/bot/fact/Matcher.kt | 8 - .../main/kotlin/content/bot/fact/Outcome.kt | 7 - .../main/kotlin/content/bot/fact/Predicate.kt | 129 +++++++++++++--- .../main/kotlin/content/bot/fact/Produces.kt | 3 - .../kotlin/content/bot/fact/Requirement.kt | 23 +-- .../content/bot/fact/RequirementEvaluator.kt | 59 ++++++++ .../kotlin/content/bot/interact/path/Graph.kt | 13 +- 22 files changed, 432 insertions(+), 264 deletions(-) create mode 100644 game/src/main/kotlin/content/bot/fact/Deficit.kt delete mode 100644 game/src/main/kotlin/content/bot/fact/Matcher.kt delete mode 100644 game/src/main/kotlin/content/bot/fact/Outcome.kt delete mode 100644 game/src/main/kotlin/content/bot/fact/Produces.kt create mode 100644 game/src/main/kotlin/content/bot/fact/RequirementEvaluator.kt diff --git a/data/area/misthalin/lumbridge/lumbridge.bots.toml b/data/area/misthalin/lumbridge/lumbridge.bots.toml index 5adec101c4..032359923e 100644 --- a/data/area/misthalin/lumbridge/lumbridge.bots.toml +++ b/data/area/misthalin/lumbridge/lumbridge.bots.toml @@ -69,7 +69,7 @@ requires = [ { skill = { id = "fletching", min = 5 } } ] produces = [ - { carries = { id = "shortbow_u" } } + { carries = [{ id = "shortbow_u" }] } ] [lumbridge_fletch_longbows] @@ -79,7 +79,7 @@ requires = [ { skill = { id = "fletching", min = 10 } } ] produces = [ - { carries = { id = "longbow_u" } } + { carries = [{ id = "longbow_u" }] } ] # Woodcutting diff --git a/data/bot/combat.templates.toml b/data/bot/combat.templates.toml index 640b478b44..1bcd681e7d 100644 --- a/data/bot/combat.templates.toml +++ b/data/bot/combat.templates.toml @@ -12,9 +12,9 @@ actions = [ { option = "Attack", npc = "chicken*", delay = 5, success = { inventory_space = { max = 0 } } }, ] produces = [ - { carries = { id = "feather" } }, - { carries = { id = "bones" } }, - { carries = { id = "raw_chicken" } }, + { carries = [{ id = "feather" }] }, + { carries = [{ id = "bones" }] }, + { carries = [{ id = "raw_chicken" }] }, { skill = { id = "$skill" } } ] @@ -33,8 +33,8 @@ actions = [ { option = "Attack", npc = "cow*", delay = 5, success = { inventory_space = { max = 0 } } }, ] produces = [ - { carries = { id = "bones" } }, - { carries = { id = "cowhide" } }, - { carries = { id = "raw_beef" } }, + { carries = [{ id = "bones" }] }, + { carries = [{ id = "cowhide" }] }, + { carries = [{ id = "raw_beef" }] }, { skill = { id = "$skill" } } ] \ No newline at end of file diff --git a/data/bot/cooking.templates.toml b/data/bot/cooking.templates.toml index d908d9c2fa..19d970796c 100644 --- a/data/bot/cooking.templates.toml +++ b/data/bot/cooking.templates.toml @@ -4,18 +4,18 @@ requires = [ { skill = { id = "cooking", min = "$level" } }, ] setup = [ - { carries = { id = "$raw", amount = 28 } }, + { carries = [{ id = "$raw", amount = 28 }] }, { area = { id = "$location" } }, ] actions = [ { item = "$raw", on_object = "$obj", success = { interface_open = { id = "dialogue_skill_creation" } } }, { option = "All", interface = "skill_creation_amount:all" }, { continue = "dialogue_skill_creation:choice1" }, - { restart = true, wait_if = [{ has_queue = { id = "cooking" } }], success = { carries = { id = "$raw", max = 0 } } } + { restart = true, wait_if = [{ has_queue = { id = "cooking" } }], success = { carries = [{ id = "$raw", max = 0 }] } } ] produces = [ { skill = { id = "cooking" } }, - { carries = { id = "$cooked" } } + { carries = [{ id = "$cooked" }] } ] # Beef has a different popup so can't use normal template @@ -25,7 +25,7 @@ requires = [ { skill = { id = "cooking", min = 1 } }, ] setup = [ - { carries = { id = "raw_beef", amount = 28 } }, + { carries = [{ id = "raw_beef", amount = 28 }] }, { area = { id = "$location" } }, ] actions = [ @@ -33,9 +33,9 @@ actions = [ { option = "Continue", continue = "dialogue_multi2:line2", success = { interface_open = { id = "dialogue_skill_creation" } } }, { option = "All", interface = "skill_creation_amount:all" }, { continue = "dialogue_skill_creation:choice1" }, - { restart = true, wait_if = [{ has_queue = { id = "cooking" } }], success = { carries = { id = "raw_beef", max = 0 } } } + { restart = true, wait_if = [{ has_queue = { id = "cooking" } }], success = { carries = [{ id = "raw_beef", max = 0 }] } } ] produces = [ { skill = { id = "cooking" } }, - { carries = { id = "beef" } } + { carries = [{ id = "beef" }] } ] \ No newline at end of file diff --git a/data/bot/fletching.templates.toml b/data/bot/fletching.templates.toml index aad701ec93..8335ba251d 100644 --- a/data/bot/fletching.templates.toml +++ b/data/bot/fletching.templates.toml @@ -4,8 +4,7 @@ requires = [ ] setup = [ { area = { id = "$location" } }, - { carries = { id = "knife" } }, - { carries = { id = "$logs", min = 27 } } + { carries = [{ id = "knife" }, { id = "$logs", min = 27 }] }, ] actions = [ { item = "knife", on = "$logs", success = { interface_open = { id = "dialogue_skill_creation" } } }, @@ -24,8 +23,7 @@ requires = [ ] setup = [ { area = { id = "$location" } }, - { carries = { id = "knife" } }, - { carries = { id = "$logs", min = 27 } } + { carries = [{ id = "knife" }, { id = "$logs", min = 27 }] }, ] actions = [ { item = "knife", on = "$logs" }, @@ -43,8 +41,7 @@ requires = [ ] setup = [ { area = { id = "$location" } }, - { carries = { id = "knife" } }, - { carries = { id = "$logs", min = 27 } } + { carries = [{ id = "knife" }, { id = "$logs", min = 27 }] }, ] actions = [ { item = "knife", on = "$logs", success = { has_queue = { id = "fletching_make_dialog" } } }, diff --git a/data/bot/lumbridge.nav-edges.toml b/data/bot/lumbridge.nav-edges.toml index 05128b8fc1..9a5f6c071c 100644 --- a/data/bot/lumbridge.nav-edges.toml +++ b/data/bot/lumbridge.nav-edges.toml @@ -229,10 +229,10 @@ edges = [ { from_x = 3278, from_y = 3228, to_x = 3268, to_y = 3228 }, # al_kharid_crossroad_to_tollgate_north { from_x = 3278, from_y = 3228, to_x = 3268, to_y = 3227 }, # al_kharid_crossroad_to_tollgate { from_x = 3278, from_y = 3228, to_x = 3280, to_y = 3216 }, # al_kharid_crossroad_to_glider - { from_x = 3267, from_y = 3228, to_x = 3268, to_y = 3228, cost = 1, actions = [{ option = "Pay-toll(10gp)", object = "toll_gate_al_kharid_north", x = 3268, y = 3228 }], conditions = [{ carries = { id = "coins", amount = 10 } }] }, # lumbridge_tollgate_north_to_al_kharid_tollgate - { from_x = 3268, from_y = 3228, to_x = 3267, to_y = 3228, cost = 1, actions = [{ option = "Pay-toll(10gp)", object = "toll_gate_al_kharid_north", x = 3268, y = 3228 }], conditions = [{ carries = { id = "coins", amount = 10 } }] }, # al_kharid_tollgate_north_to_lumbridge_tollgate - { from_x = 3267, from_y = 3227, to_x = 3268, to_y = 3227, cost = 1, actions = [{ option = "Pay-toll(10gp)", object = "toll_gate_al_kharid_south", x = 3268, y = 3227 }], conditions = [{ carries = { id = "coins", amount = 10 } }] }, # lumbridge_tollgate_to_al_kharid - { from_x = 3268, from_y = 3227, to_x = 3267, to_y = 3227, cost = 1, actions = [{ option = "Pay-toll(10gp)", object = "toll_gate_al_kharid_south", x = 3268, y = 3227 }], conditions = [{ carries = { id = "coins", amount = 10 } }] }, # al_kharid_tollgate_to_lumbridge + { from_x = 3267, from_y = 3228, to_x = 3268, to_y = 3228, cost = 1, actions = [{ option = "Pay-toll(10gp)", object = "toll_gate_al_kharid_north", x = 3268, y = 3228 }], conditions = [{ carries = [{ id = "coins", amount = 10 }] }] }, # lumbridge_tollgate_north_to_al_kharid_tollgate + { from_x = 3268, from_y = 3228, to_x = 3267, to_y = 3228, cost = 1, actions = [{ option = "Pay-toll(10gp)", object = "toll_gate_al_kharid_north", x = 3268, y = 3228 }], conditions = [{ carries = [{ id = "coins", amount = 10 }] }] }, # al_kharid_tollgate_north_to_lumbridge_tollgate + { from_x = 3267, from_y = 3227, to_x = 3268, to_y = 3227, cost = 1, actions = [{ option = "Pay-toll(10gp)", object = "toll_gate_al_kharid_south", x = 3268, y = 3227 }], conditions = [{ carries = [{ id = "coins", amount = 10 }] }] }, # lumbridge_tollgate_to_al_kharid + { from_x = 3268, from_y = 3227, to_x = 3267, to_y = 3227, cost = 1, actions = [{ option = "Pay-toll(10gp)", object = "toll_gate_al_kharid_south", x = 3268, y = 3227 }], conditions = [{ carries = [{ id = "coins", amount = 10 }] }] }, # al_kharid_tollgate_to_lumbridge { from_x = 3268, from_y = 3227, to_x = 3280, to_y = 3216 }, # al_kharid_tollgate_to_glider { from_x = 3268, from_y = 3228, to_x = 3280, to_y = 3216 }, # al_kharid_tollgate_north_to_glider { from_x = 3280, from_y = 3216, to_x = 3292, to_y = 3215 }, # al_kharid_glider_to_musician diff --git a/data/bot/mining.templates.toml b/data/bot/mining.templates.toml index 736d6506d9..2dbf0c8b98 100644 --- a/data/bot/mining.templates.toml +++ b/data/bot/mining.templates.toml @@ -3,7 +3,7 @@ requires = [ { skill = { id = "mining", min = 1, max = 15 } }, ] setup = [ - { carries = { id = "steel_pickaxe,iron_pickaxe,bronze_pickaxe" } }, + { carries = [{ id = "steel_pickaxe,iron_pickaxe,bronze_pickaxe", usable = true }] }, { area = { id = "$location" } }, { inventory_space = { min = 27 } }, ] @@ -11,7 +11,7 @@ actions = [ { option = "Mine", object = "copper_rocks*", delay = 5, success = { inventory_space = { max = 0 } } }, ] produces = [ - { carries = { id = "copper_ore" } }, + { carries = [{ id = "copper_ore" }] }, { skill = { id = "mining" } } ] @@ -20,7 +20,7 @@ requires = [ { skill = { id = "mining", min = 1, max = 15 } }, ] setup = [ - { carries = { id = "steel_pickaxe,iron_pickaxe,bronze_pickaxe" } }, + { carries = [{ id = "steel_pickaxe,iron_pickaxe,bronze_pickaxe", usable = true }] }, { area = { id = "$location" } }, { inventory_space = { min = 27 } }, ] @@ -28,7 +28,7 @@ actions = [ { option = "Mine", object = "tin_rocks*", delay = 5, success = { inventory_space = { max = 0 } } }, ] produces = [ - { carries = { id = "tin_ore" } }, + { carries = [{ id = "tin_ore" }] }, { skill = { id = "mining" } } ] @@ -37,7 +37,7 @@ requires = [ { skill = { id = "mining", min = 1, max = 15 } }, ] setup = [ - { carries = { id = "steel_pickaxe,iron_pickaxe,bronze_pickaxe" } }, + { carries = [{ id = "steel_pickaxe,iron_pickaxe,bronze_pickaxe", usable = true }] }, { area = { id = "$location" } }, { inventory_space = { min = 27 } }, ] @@ -45,7 +45,7 @@ actions = [ { option = "Mine", object = "clay_rocks*", delay = 5, success = { inventory_space = { max = 0 } } }, ] produces = [ - { carries = { id = "clay" } }, + { carries = [{ id = "clay" }] }, { skill = { id = "mining" } } ] @@ -54,7 +54,7 @@ requires = [ { skill = { id = "mining", min = 15, max = 40 } }, ] setup = [ - { carries = { id = "adamant_pickaxe,mithril_pickaxe,steel_pickaxe" } }, + { carries = [{ id = "adamant_pickaxe,mithril_pickaxe,steel_pickaxe", usable = true }] }, { area = { id = "$location" } }, { inventory_space = { min = 27 } }, ] @@ -62,7 +62,7 @@ actions = [ { option = "Mine", object = "iron_rocks*", delay = 5, success = { inventory_space = { max = 0 } } }, ] produces = [ - { carries = { id = "iron_ore" } }, + { carries = [{ id = "iron_ore" }] }, { skill = { id = "mining" } } ] @@ -71,7 +71,7 @@ requires = [ { skill = { id = "mining", min = 20, max = 40 } }, ] setup = [ - { carries = { id = "adamant_pickaxe,mithril_pickaxe,steel_pickaxe" } }, + { carries = [{ id = "adamant_pickaxe,mithril_pickaxe,steel_pickaxe", usable = true }] }, { area = { id = "$location" } }, { inventory_space = { min = 27 } }, ] @@ -79,7 +79,7 @@ actions = [ { option = "Mine", object = "silver_rocks*", delay = 5, success = { inventory_space = { max = 0 } } }, ] produces = [ - { carries = { id = "silver_ore" } }, + { carries = [{ id = "silver_ore" }] }, { skill = { id = "mining" } } ] @@ -88,7 +88,7 @@ requires = [ { skill = { id = "mining", min = 30, max = 55 } }, ] setup = [ - { carries = { id = "rune_pickaxe,adamant_pickaxe,mithril_pickaxe" } }, + { carries = [{ id = "rune_pickaxe,adamant_pickaxe,mithril_pickaxe", usable = true }] }, { area = { id = "$location" } }, { inventory_space = { min = 27 } }, ] @@ -96,7 +96,7 @@ actions = [ { option = "Mine", object = "coal_rocks*", delay = 10, success = { inventory_space = { max = 0 } } }, ] produces = [ - { carries = { id = "coal" } }, + { carries = [{ id = "coal" }] }, { skill = { id = "mining" } } ] @@ -105,7 +105,7 @@ requires = [ { skill = { id = "mining", min = 40, max = 60 } }, ] setup = [ - { carries = { id = "rune_pickaxe,adamant_pickaxe,mithril_pickaxe" } }, + { carries = [{ id = "rune_pickaxe,adamant_pickaxe,mithril_pickaxe", usable = true }] }, { area = { id = "$location" } }, { inventory_space = { min = 27 } }, ] @@ -113,7 +113,7 @@ actions = [ { option = "Mine", object = "gold_rocks*", delay = 15, success = { inventory_space = { max = 0 } } }, ] produces = [ - { carries = { id = "gold_ore" } }, + { carries = [{ id = "gold_ore" }] }, { skill = { id = "mining" } } ] @@ -122,7 +122,7 @@ requires = [ { skill = { id = "mining", min = 55, max = 70 } }, ] setup = [ - { carries = { id = "dragon_pickaxe,rune_pickaxe,adamant_pickaxe" } }, + { carries = [{ id = "dragon_pickaxe,rune_pickaxe,adamant_pickaxe", usable = true }] }, { area = { id = "$location" } }, { inventory_space = { min = 27 } }, ] @@ -130,6 +130,6 @@ actions = [ { option = "Mine", object = "mithril_rocks*", delay = 15, success = { inventory_space = { max = 0 } } }, ] produces = [ - { carries = { id = "mithril_ore" } }, + { carries = [{ id = "mithril_ore" }] }, { skill = { id = "mining" } } ] diff --git a/data/bot/prayer.bots.toml b/data/bot/prayer.bots.toml index 651b23684e..aa7be7423a 100644 --- a/data/bot/prayer.bots.toml +++ b/data/bot/prayer.bots.toml @@ -4,7 +4,7 @@ requires = [ { owns = { id = "bones", min = 28 } }, ] setup = [ - { carries = { id = "bones", min = 28 } }, + { carries = [{ id = "bones", min = 28 }] }, ] actions = [ { option = "Bury", interface = "inventory:inventory:bones" }, diff --git a/data/bot/shop.templates.toml b/data/bot/shop.templates.toml index e6b740b3e0..b7740973c3 100644 --- a/data/bot/shop.templates.toml +++ b/data/bot/shop.templates.toml @@ -1,15 +1,15 @@ [buy_from_shop] setup = [ - { carries = { id = "coins", min = "$cost" } }, + { carries = [{ id = "coins", min = "$cost" }] }, { area = { id = "$shop_location" } }, { inventory_space = { min = 1 } }, ] actions = [ { option = "Trade", npc = "$shopkeeper", success = { interface_open = { id = "shop" } } }, - { option = "Buy-1", interface = "shop:stock:$item", success = { carries = { id = "$item" } } }, + { option = "Buy-1", interface = "shop:stock:$item", success = { carries = [{ id = "$item" }] } }, ] produces = [ - { carries = { id = "$item" } } + { carries = [{ id = "$item" }] } ] [take_shop_sample] @@ -19,8 +19,8 @@ setup = [ ] actions = [ { option = "Trade", npc = "$shopkeeper", success = { interface_open = { id = "shop" } } }, - { option = "Take-1", interface = "shop:sample:$item", success = { carries = { id = "$item" } } }, + { option = "Take-1", interface = "shop:sample:$item", success = { carries = [{ id = "$item" }] } }, ] produces = [ - { carries = { id = "$item" } } + { carries = [{ id = "$item" }] } ] \ No newline at end of file diff --git a/data/bot/thieving.templates.toml b/data/bot/thieving.templates.toml index 3e5726be06..712bfdfd7b 100644 --- a/data/bot/thieving.templates.toml +++ b/data/bot/thieving.templates.toml @@ -12,6 +12,6 @@ actions = [ { restart = true, wait_if = [{ variable = { id = "delay", min = 0, default = -1 } }, { clock = { id = "stunned", min = 0 } }], success = { skill = { id = "constitution", max = 20 } } } ] produces = [ - { carries = { id = "coins" } }, + { carries = [{ id = "coins" }] }, { skill = { id = "thieving" } } ] \ No newline at end of file diff --git a/data/bot/woodcutting.templates.toml b/data/bot/woodcutting.templates.toml index 6560b21349..85b72df23f 100644 --- a/data/bot/woodcutting.templates.toml +++ b/data/bot/woodcutting.templates.toml @@ -3,7 +3,7 @@ requires = [ { skill = { id = "woodcutting", min = 1, max = 15 } }, ] setup = [ - { carries = { id = "steel_hatchet,iron_hatchet,bronze_hatchet" } }, + { carries = [{ id = "steel_hatchet,iron_hatchet,bronze_hatchet" }] }, { area = { id = "$location" } }, { inventory_space = { min = 27 } }, ] @@ -11,7 +11,7 @@ actions = [ { option = "Chop down", object = "tree*", delay = 5, success = { inventory_space = { max = 0 } } }, ] produces = [ - { carries = { id = "logs" } }, + { carries = [{ id = "logs" }] }, { skill = { id = "woodcutting" } } ] @@ -20,7 +20,7 @@ requires = [ { skill = { id = "woodcutting", min = 15, max = 30 } }, ] setup = [ - { carries = { id = "mithril_hatchet,steel_hatchet" } }, + { carries = [{ id = "mithril_hatchet,steel_hatchet" }] }, { area = { id = "$location" } }, { inventory_space = { min = 27 } }, ] @@ -28,7 +28,7 @@ actions = [ { option = "Chop down", object = "oak*", delay = 5, success = { inventory_space = { max = 0 } } }, ] produces = [ - { carries = { id = "oak_logs" } }, + { carries = [{ id = "oak_logs" }] }, { skill = { id = "woodcutting" } } ] @@ -37,7 +37,7 @@ requires = [ { skill = { id = "woodcutting", min = 30, max = 60 } }, ] setup = [ - { carries = { id = "rune_hatchet,adamant_hatchet,mithril_hatchet,steel_hatchet" } }, + { carries = [{ id = "rune_hatchet,adamant_hatchet,mithril_hatchet,steel_hatchet" }] }, { area = { id = "$location" } }, { inventory_space = { min = 27 } }, ] @@ -45,7 +45,7 @@ actions = [ { option = "Chop down", object = "willow*", delay = 5, success = { inventory_space = { max = 0 } } }, ] produces = [ - { carries = { id = "willow_logs" } }, + { carries = [{ id = "willow_logs" }] }, { skill = { id = "woodcutting" } } ] @@ -54,7 +54,7 @@ requires = [ { skill = { id = "woodcutting", min = 60, max = 75 } }, ] setup = [ - { carries = { id = "dragon_hatchet,rune_hatchet,adamant_hatchet" } }, + { carries = [{ id = "dragon_hatchet,rune_hatchet,adamant_hatchet" }] }, { area = { id = "$location" } }, { inventory_space = { min = 27 } }, ] @@ -62,6 +62,6 @@ actions = [ { option = "Chop down", object = "yew*", delay = 5, success = { inventory_space = { max = 0 } } }, ] produces = [ - { carries = { id = "yew_logs" } }, + { carries = [{ id = "yew_logs" }] }, { skill = { id = "woodcutting" } } ] diff --git a/game/src/main/kotlin/content/bot/BotManager.kt b/game/src/main/kotlin/content/bot/BotManager.kt index a34e032cca..e440d82cc7 100644 --- a/game/src/main/kotlin/content/bot/BotManager.kt +++ b/game/src/main/kotlin/content/bot/BotManager.kt @@ -2,6 +2,7 @@ package content.bot import com.github.michaelbull.logging.InlineLogger import content.bot.action.* +import content.bot.fact.Deficit import content.bot.fact.Fact import content.bot.fact.Predicate import content.bot.fact.Requirement @@ -119,25 +120,8 @@ class BotManager( val activity = activities[it] activity != null && hasRequirements(bot, activity) }.randomOrNull(random) // TODO weight by distance? - if (id == null) { - if (bot.player["debug", false]) { - logger.info { "Failed to find activity for bot ${bot.player.accountName}." } - for (id in bot.available) { - val activity = activities[id] ?: continue - if (!slots.hasFree(activity)) { - logger.trace { "Activity: $id - No available slots." } - } else if (bot.blocked.contains(activity.id)) { - logger.trace { "Activity: $id - Blocked." } - } else { - for (requirement in activity.requires) { - if (!requirement.check(bot.player)) { - logger.trace { "Activity: $id - Failed requirement: $requirement" } - break - } - } - } - } - } + if (id == null && bot.player["debug", false]) { + debugActivities(bot) } activities[id] ?: idle } @@ -178,19 +162,7 @@ class BotManager( .minByOrNull { it.weight } if (resolver == null) { if (bot.player["debug", false]) { - logger.info { "No resolver found for for ${behaviour.id} keys: ${requirement.fact.keys()} requirement: ${requirement}." } - for (resolver in resolvers) { - if (frame.blocked.contains(resolver.id)) { - logger.debug { "Resolver: ${resolver.id} - Blocked by frame behaviour: ${frame.behaviour.id}." } - break - } - for (requirement in resolver.requires) { - if (!requirement.check(bot.player)) { - logger.debug { "Resolver: ${resolver.id} - Failed requirement: $requirement." } - break - } - } - } + debugResolvers(behaviour, requirement, resolvers, frame, bot) } frame.fail(Reason.Requirement(requirement)) // No way to resolve return @@ -215,78 +187,15 @@ class BotManager( private fun availableResolvers(bot: Bot, condition: Requirement<*>): MutableList { val options = mutableListOf() - addDefaultResolvers(bot, options, condition) + for (deficit in condition.deficits(bot.player)) { + options.add(deficit.resolve(bot.player) ?: continue) + } for (key in condition.fact.keys()) { options.addAll(resolvers[key] ?: continue) } return options } - private fun addDefaultResolvers(bot: Bot, resolvers: MutableList, requirement: Requirement<*>) { - val predicate = requirement.predicate - if (predicate is Predicate.InArea) { - resolvers.add(Resolver("go_to_${predicate.name}", -1, actions = listOf(BotAction.GoTo(predicate.name)), produces = setOf(requirement))) - } else if (predicate is Predicate.IntRange && requirement.fact is Fact.InventoryCount && bot.player.bank.contains(requirement.fact.id, predicate.min!!)) { - if (predicate.min == 1 || predicate.min == 5 || predicate.min == 10) { - resolvers.add( - Resolver( - "withdraw_${requirement.fact.id}", weight = 20, - setup = listOf( - Requirement(Fact.InventorySpace, Predicate.IntRange(predicate.min)) - ), - actions = listOf( - BotAction.GoToNearest("bank"), - BotAction.InteractObject("Use-quickly", "bank_booth*", success = Requirement(Fact.InterfaceOpen("bank"), Predicate.BooleanTrue)), - BotAction.InterfaceOption("Withdraw-${predicate.min}", "bank:inventory:${requirement.fact.id}"), - BotAction.CloseInterface, - ) - ) - ) - } else { - resolvers.add( - Resolver( - "withdraw_${requirement.fact.id}", weight = 20, - setup = listOf( - Requirement(Fact.InventorySpace, Predicate.IntRange(predicate.min)) - ), - actions = listOf( - BotAction.GoToNearest("bank"), - BotAction.InteractObject("Use-quickly", "bank_booth*", success = Requirement(Fact.InterfaceOpen("bank"), Predicate.BooleanTrue)), - BotAction.InterfaceOption("Withdraw-X", "bank:inventory:${requirement.fact.id}"), - BotAction.IntEntry(predicate.min), - BotAction.CloseInterface, - ) - ) - ) - } - } else if (predicate is Predicate.IntRange && requirement.fact is Fact.EquipCount) { - resolvers.add( - Resolver( - "equip_${requirement.fact.id}", weight = 0, - setup = listOf( - Requirement(Fact.InventoryCount(requirement.fact.id), Predicate.IntRange(predicate.min)) - ), - actions = listOf(BotAction.InterfaceOption("Equip", "inventory:inventory:${requirement.fact.id}")) - ) - ) - resolvers.add( - Resolver( - "withdraw_and_equip_${requirement.fact.id}", weight = 0, - requires = listOf(Requirement(Fact.BankCount(requirement.fact.id), Predicate.IntRange(predicate.min))), - setup = listOf(Requirement(Fact.InventorySpace, Predicate.IntRange(predicate.min))), - actions = listOf( - BotAction.GoToNearest("bank"), - BotAction.InteractObject("Use-quickly", "bank_booth*", success = Requirement(Fact.InterfaceOpen("bank"), Predicate.BooleanTrue)), - BotAction.InterfaceOption("Withdraw-X", "bank:inventory:${requirement.fact.id}"), - BotAction.IntEntry(predicate.min!!), - BotAction.CloseInterface, - BotAction.InterfaceOption("Equip", "inventory:inventory:${requirement.fact.id}") - ) - ) - ) - } - } - private fun execute(bot: Bot) { val frame = bot.frame() val behaviour = frame.behaviour @@ -352,4 +261,39 @@ class BotManager( bot.reset() } + private fun debugResolvers(behaviour: Behaviour, requirement: Requirement<*>, resolvers: MutableList, frame: BehaviourFrame, bot: Bot) { + logger.info { "No resolver found for ${behaviour.id} keys: ${requirement.fact.keys()} requirement: ${requirement}." } + for (resolver in resolvers) { + if (frame.blocked.contains(resolver.id)) { + logger.debug { "Resolver: ${resolver.id} - Blocked by frame behaviour: ${frame.behaviour.id}." } + break + } + for (requirement in resolver.requires) { + if (!requirement.check(bot.player)) { + logger.debug { "Resolver: ${resolver.id} - Failed requirement: $requirement." } + break + } + } + } + } + + private fun debugActivities(bot: Bot) { + logger.info { "Failed to find activity for bot ${bot.player.accountName}." } + for (id in bot.available) { + val activity = activities[id] ?: continue + if (!slots.hasFree(activity)) { + logger.trace { "Activity: $id - No available slots." } + } else if (bot.blocked.contains(activity.id)) { + logger.trace { "Activity: $id - Blocked." } + } else { + for (requirement in activity.requires) { + if (!requirement.check(bot.player)) { + logger.trace { "Activity: $id - Failed requirement: $requirement" } + break + } + } + } + } + } + } \ No newline at end of file diff --git a/game/src/main/kotlin/content/bot/action/BotActivity.kt b/game/src/main/kotlin/content/bot/action/BotActivity.kt index cb0b874e76..e0edc6ef44 100644 --- a/game/src/main/kotlin/content/bot/action/BotActivity.kt +++ b/game/src/main/kotlin/content/bot/action/BotActivity.kt @@ -1,6 +1,7 @@ package content.bot.action import content.bot.fact.Requirement +import it.unimi.dsi.fastutil.objects.ObjectArrayList import world.gregs.config.Config import world.gregs.config.ConfigReader import world.gregs.voidps.engine.data.ConfigFiles @@ -52,10 +53,10 @@ private fun loadActivities(activities: MutableMap, template var template: String? = null var fields: Map? = null var capacity = 1 - val requires = mutableListOf>>() - val setup = mutableListOf>>() + val requires = mutableListOf>>>() + val setup = mutableListOf>>>() val actions = mutableListOf() - val produces = mutableListOf>>() + val produces = mutableListOf>>>() while (nextPair()) { when (val key = key()) { "template" -> template = string() @@ -100,10 +101,10 @@ private fun loadSetups(resolvers: MutableMap>, tem var template: String? = null var fields: Map? = null var weight = 1 - val requires = mutableListOf>>() - val setup = mutableListOf>>() + val requires = mutableListOf>>>() + val setup = mutableListOf>>>() val actions = mutableListOf() - val produces = mutableListOf>>() + val produces = mutableListOf>>>() while (nextPair()) { when (val key = key()) { "template" -> template = string() @@ -151,10 +152,10 @@ private fun loadShortcuts(shortcuts: MutableList, templates: var template: String? = null var fields: Map? = null var weight = 1 - val requires = mutableListOf>>() - val setup = mutableListOf>>() + val requires = mutableListOf>>>() + val setup = mutableListOf>>>() val actions = mutableListOf() - val produces = mutableListOf>>() + val produces = mutableListOf>>>() while (nextPair()) { when (val key = key()) { "template" -> template = string() @@ -170,7 +171,7 @@ private fun loadShortcuts(shortcuts: MutableList, templates: if (fields != null && template == null) { error("Found fields but no template for $id in ${exception()}") } else if (template != null) { - requireNotNull(fields) { "No fields found for $id ${exception()}"} + requireNotNull(fields) { "No fields found for $id ${exception()}" } fragments.add(Fragment(id, template, fields, weight, requires, setup, actions, produces)) } else { val debug = "$id ${exception()}" @@ -194,10 +195,10 @@ private fun loadTemplates(paths: List): Map { Config.fileReader(path) { while (nextSection()) { val id = section() - val requires = mutableListOf>>() - val setup = mutableListOf>>() + val requires = mutableListOf>>>() + val setup = mutableListOf>>>() val actions = mutableListOf() - val produces = mutableListOf>>() + val produces = mutableListOf>>>() while (nextPair()) { when (val key = key()) { "requires" -> requirements(requires) @@ -216,12 +217,19 @@ private fun loadTemplates(paths: List): Map { return templates } -private fun ConfigReader.requirements(requires: MutableList>>) { +private fun ConfigReader.requirements(requires: MutableList>>>) { while (nextElement()) { while (nextEntry()) { val key = key() - val value = map() - requires.add(key to value) + if (peek == '[') { + val list = ObjectArrayList>() + while (nextElement()) { + list.add(map()) + } + requires.add(key to list) + } else { + requires.add(key to listOf(map())) + } } } } @@ -231,10 +239,10 @@ private data class Fragment( val template: String, val fields: Map, val int: Int, - val requires: List>>, - val setup: List>>, + val requires: List>>>, + val setup: List>>>, val actions: List, // Can fragments even have actions? - val produces: List>>, + val produces: List>>>, ) { fun activity(template: Template) = BotActivity( id = id, @@ -245,25 +253,22 @@ private data class Fragment( produces = resolveRequirements(template.produces, produces, requirePredicates = false).toSet(), ) - private fun resolveRequirements(templated: List>>, original: List>>, requirePredicates: Boolean = true): List> { - val combinedList = mutableListOf>>() - for ((type, map) in templated) { - val combinedMap = mutableMapOf() - for ((key, value) in original) { - combinedMap[key] = value - } - for ((key, value) in map) { - if (value !is String || !value.contains('$')) { - combinedMap[key] = value - continue - } - val ref = value.reference() - val name = ref.trim('$', '{', '}') - val replacement = fields[name] ?: error("No field found for behaviour=$id type=${type} key=$key ref=$ref") - combinedMap[key] = if (replacement is String) value.replace(ref, replacement) else replacement + private fun resolveRequirements(templated: List>>>, original: List>>>, requirePredicates: Boolean = true): List> { + val combinedList = mutableListOf>>>() + combinedList.addAll(original) + for ((type, list) in templated) { + val resolved = list.map { map -> + map.mapValues { (key, value) -> + if (value is String && value.contains('$')) { + val ref = value.reference() + val name = ref.trim('$', '{', '}') + val replacement = fields[name] ?: error("No field found for behaviour=$id type=${type} key=$key ref=$ref") + if (replacement is String) value.replace(ref, replacement) else replacement + } else value + }.toMap() } - if (combinedMap.isNotEmpty()) { - combinedList.add(type to combinedMap) + if (resolved.isNotEmpty()) { + combinedList.add(type to resolved) } } if (combinedList.isEmpty()) { @@ -304,10 +309,10 @@ private data class Fragment( } private data class Template( - val requires: List>>, - val setup: List>>, + val requires: List>>>, + val setup: List>>>, val actions: List, - val produces: List>>, + val produces: List>>>, ) fun ConfigReader.actions(list: MutableList) { @@ -324,8 +329,8 @@ fun ConfigReader.actions(list: MutableList) { var x = 0 var y = 0 val references = mutableMapOf() - val wait = mutableListOf>>() - val success = mutableListOf>>() + val wait = mutableListOf>>>() + val success = mutableListOf>>>() while (nextEntry()) { when (val key = key()) { "go_to", "go_to_nearest", "enter_string", "interface", "npc", "object", "clone", "item", "continue" -> { @@ -381,11 +386,29 @@ fun ConfigReader.actions(list: MutableList) { type = key } "success" -> while (nextEntry()) { - success.add(key() to map()) + val key = key() + if (peek == '[') { + val list = mutableListOf>() + while (nextElement()) { + list.add(map()) + } + success.add(key to list) + } else { + success.add(key to listOf(map())) + } } "wait_if" -> while (nextElement()) { while (nextEntry()) { - wait.add(key() to map()) + val key = key() + if (peek == '[') { + val list = mutableListOf>() + while (nextElement()) { + list.add(map()) + } + wait.add(key to list) + } else { + wait.add(key to listOf(map())) + } } } "wait" -> { @@ -433,7 +456,7 @@ fun ConfigReader.actions(list: MutableList) { "enter_string" -> BotAction.StringEntry(id) "enter_int" -> BotAction.IntEntry(int) "wait" -> BotAction.Wait(ticks) - "restart" -> BotAction.Restart(Requirement.parse(wait, id), Requirement.parse(success, id).singleOrNull() ?: throw IllegalArgumentException("Restart must have success condition.")) + "restart" -> BotAction.Restart(Requirement.parse(wait, id), Requirement.parse(success, id).singleOrNull() ?: throw IllegalArgumentException("Restart must have success condition. $id ${exception()}")) "npc" -> if (option == "Attack") { BotAction.FightNpc(id = id, delay = delay, success = Requirement.parse(success, id).singleOrNull(), healPercentage = heal, lootOverValue = loot, radius = radius) } else { diff --git a/game/src/main/kotlin/content/bot/fact/Deficit.kt b/game/src/main/kotlin/content/bot/fact/Deficit.kt new file mode 100644 index 0000000000..58783f9b04 --- /dev/null +++ b/game/src/main/kotlin/content/bot/fact/Deficit.kt @@ -0,0 +1,52 @@ +package content.bot.fact + +import content.bot.action.BotAction +import content.bot.action.Resolver +import content.entity.player.bank.bank +import world.gregs.voidps.engine.entity.character.player.Player +import world.gregs.voidps.engine.entity.item.Item + +sealed interface Deficit { + fun resolve(player: Player): Resolver? + + data class NotInArea(val area: String) : Deficit { + override fun resolve(player: Player): Resolver { + return Resolver("go_to_${area}", -1, actions = listOf(BotAction.GoTo(area))) + } + } + + data class MissingItem(val filter: Predicate, val needed: Int) : Deficit { + override fun resolve(player: Player): Resolver? { + var spaceNeeded = 0 + val actions = mutableListOf( + BotAction.GoToNearest("bank"), + BotAction.InteractObject("Use-quickly", "bank_booth*", success = Requirement(Fact.InterfaceOpen("bank"), Predicate.BooleanTrue)) + ) + val uniqueName = StringBuilder() + for (item in player.bank.items) { + if (item.isEmpty() || !filter.test(player, item)) { + continue + } + spaceNeeded += needed + uniqueName.append("_${item.id}") + if (needed == 1 || needed == 5 || needed == 10) { + actions.add(BotAction.InterfaceOption("Withdraw-${needed}", "bank:inventory:${item.id}")) + } else { + BotAction.InterfaceOption("Withdraw-X", "bank:inventory:${item.id}") + BotAction.IntEntry(needed) + } + } + if (spaceNeeded > 0) { + actions.add(BotAction.CloseInterface) + return Resolver( + "withdraw_$uniqueName", weight = 20, + setup = listOf( + Requirement(Fact.InventorySpace, Predicate.IntRange(spaceNeeded)) + ), + actions = actions + ) + } + return null + } + } +} \ No newline at end of file diff --git a/game/src/main/kotlin/content/bot/fact/Fact.kt b/game/src/main/kotlin/content/bot/fact/Fact.kt index ecad018f2c..f6cc180afd 100644 --- a/game/src/main/kotlin/content/bot/fact/Fact.kt +++ b/game/src/main/kotlin/content/bot/fact/Fact.kt @@ -6,6 +6,9 @@ import world.gregs.voidps.engine.client.variable.remaining import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.entity.character.player.combatLevel import world.gregs.voidps.engine.entity.character.player.skill.Skill +import world.gregs.voidps.engine.entity.character.player.skill.level.Level.hasRequirements +import world.gregs.voidps.engine.entity.character.player.skill.level.Level.hasRequirementsToUse +import world.gregs.voidps.engine.entity.item.Item import world.gregs.voidps.engine.inv.equipment import world.gregs.voidps.engine.inv.inventory import world.gregs.voidps.engine.timer.epochSeconds @@ -39,6 +42,11 @@ sealed class Fact(val priority: Int) { override fun getValue(player: Player) = player.inventory.count(id) } + object InventoryItems : Fact>(100) { + override fun keys() = setOf("inv:inventory") + override fun getValue(player: Player) = player.inventory.items + } + data class ItemCount(val id: String) : Fact(100) { override fun keys() = setOf("inventory:$id", "bank:$id", "worn_equipment:$id") override fun groups() = setOf("inv:inventory", "inv:bank", "inv:worn_equipment") diff --git a/game/src/main/kotlin/content/bot/fact/FactParser.kt b/game/src/main/kotlin/content/bot/fact/FactParser.kt index c5077ccd56..9b86ad19de 100644 --- a/game/src/main/kotlin/content/bot/fact/FactParser.kt +++ b/game/src/main/kotlin/content/bot/fact/FactParser.kt @@ -1,5 +1,6 @@ package content.bot.fact +import world.gregs.voidps.engine.entity.item.Item import world.gregs.voidps.type.Tile sealed class FactParser { @@ -7,12 +8,18 @@ sealed class FactParser { abstract fun parse(map: Map): Fact abstract fun predicate(map: Map): Predicate? - fun requirement(map: Map) = Requirement(parse(map), predicate(map)) + open fun requirement(list: List>): Requirement { + val map = list.singleOrNull() + if (map != null) { + return Requirement(parse(map), predicate(map)) + } + throw IllegalStateException("No list requirement implemented for ${this::class.simpleName} fact type") + } fun check(map: Map): String? { for (key in required) { if (!map.containsKey(key)) { - return "missing key '$key' in map ${map}" + return "missing key '$key' in map $map" } } return null @@ -23,15 +30,11 @@ sealed class FactParser { override fun predicate(map: Map) = Predicate.parseInt(map) } - object InventoryCount : FactParser() { - override val required = setOf("id") - override fun parse(map: Map) = Fact.InventoryCount(map["id"] as String) - override fun predicate(map: Map): Predicate? { - if (!map.containsKey("min") && !map.containsKey("equals")) { - map as MutableMap - map["min"] = 1 - } - return Predicate.parseInt(map) + object InventoryItems : FactParser>() { + override fun parse(map: Map) = Fact.InventoryItems + override fun predicate(map: Map) = null + override fun requirement(list: List>): Requirement> { + return Requirement(Fact.InventoryItems, Predicate.parseItems(list)) } } @@ -58,8 +61,9 @@ sealed class FactParser { override fun parse(map: Map) = Fact.EquipCount(map["id"] as String) override fun predicate(map: Map): Predicate? { if (!map.containsKey("min") && !map.containsKey("equals")) { - map as MutableMap - map["min"] = 1 + val mutable = map.toMutableMap() + mutable["min"] = 1 + return Predicate.parseInt(mutable) } return Predicate.parseInt(map) } @@ -77,6 +81,7 @@ sealed class FactParser { else -> error("Invalid default value $default") } as Fact } + override fun predicate(map: Map): Predicate { return when (val default = map["default"]) { is Int -> Predicate.parseInt(map) @@ -93,6 +98,7 @@ sealed class FactParser { override fun parse(map: Map): Fact { return Fact.ClockRemaining(map["id"] as String, map["seconds"] as? Boolean ?: false) } + override fun predicate(map: Map) = Predicate.parseInt(map) } @@ -133,7 +139,7 @@ sealed class FactParser { companion object { val parsers = mapOf( "inventory_space" to InventorySpace, - "carries" to InventoryCount, + "carries" to InventoryItems, "owns" to ItemCount, "banked" to BankCount, "equips" to EquipCount, diff --git a/game/src/main/kotlin/content/bot/fact/Matcher.kt b/game/src/main/kotlin/content/bot/fact/Matcher.kt deleted file mode 100644 index d4fce3e5c1..0000000000 --- a/game/src/main/kotlin/content/bot/fact/Matcher.kt +++ /dev/null @@ -1,8 +0,0 @@ -package content.bot.fact - -interface Matcher, O: Outcome> { - /** - * Would [outcome] satisfy [predicate] - */ - fun matches(predicate: P, outcome: O): Boolean -} \ No newline at end of file diff --git a/game/src/main/kotlin/content/bot/fact/Outcome.kt b/game/src/main/kotlin/content/bot/fact/Outcome.kt deleted file mode 100644 index 99d442dd41..0000000000 --- a/game/src/main/kotlin/content/bot/fact/Outcome.kt +++ /dev/null @@ -1,7 +0,0 @@ -package content.bot.fact - -/** - * - */ -interface Outcome { -} \ No newline at end of file diff --git a/game/src/main/kotlin/content/bot/fact/Predicate.kt b/game/src/main/kotlin/content/bot/fact/Predicate.kt index 50643cfe16..6ba8b37e9b 100644 --- a/game/src/main/kotlin/content/bot/fact/Predicate.kt +++ b/game/src/main/kotlin/content/bot/fact/Predicate.kt @@ -1,53 +1,64 @@ package content.bot.fact import world.gregs.voidps.engine.data.definition.Areas +import world.gregs.voidps.engine.entity.character.player.Player +import world.gregs.voidps.engine.entity.character.player.skill.level.Level.hasRequirements +import world.gregs.voidps.engine.entity.character.player.skill.level.Level.hasRequirementsToUse +import world.gregs.voidps.engine.entity.item.Item +import world.gregs.voidps.engine.event.Wildcard +import world.gregs.voidps.engine.event.Wildcards import world.gregs.voidps.type.Tile -sealed interface Predicate { - fun test(value: T): Boolean +sealed class Predicate { + abstract fun test(player: Player, value: T): Boolean + open val children: Set> = emptySet() + open val evaluator: RequirementEvaluator? = null - data class IntRange(val min: Int? = null, val max: Int? = null) : Predicate { - override fun test(value: Int): Boolean { + data class IntRange(val min: Int? = null, val max: Int? = null) : Predicate() { + override val evaluator = RequirementEvaluator.IntEvaluator + override fun test(player: Player, value: Int): Boolean { if (min != null && value < min) return false if (max != null && value > max) return false return true } } - data class IntEquals(val value: Int) : Predicate { - override fun test(value: Int) = value == this.value + data class IntEquals(val value: Int) : Predicate() { + override val evaluator = RequirementEvaluator.IntEvaluator + override fun test(player: Player, value: Int) = value == this.value } - data class DoubleRange(val min: Double? = null, val max: Double? = null) : Predicate { - override fun test(value: Double): Boolean { + data class DoubleRange(val min: Double? = null, val max: Double? = null) : Predicate() { + override fun test(player: Player, value: Double): Boolean { if (min != null && value < min) return false if (max != null && value > max) return false return true } } - data class DoubleEquals(val value: Double) : Predicate { - override fun test(value: Double) = value == this.value + data class DoubleEquals(val value: Double) : Predicate() { + override fun test(player: Player, value: Double) = value == this.value } - data class InArea(val name: String) : Predicate { - override fun test(value: Tile) = value in Areas[name] + data class InArea(val name: String) : Predicate() { + override fun test(player: Player, value: Tile) = value in Areas[name] } - object BooleanTrue : Predicate { - override fun test(value: Boolean) = value + object BooleanTrue : Predicate() { + override fun test(player: Player, value: Boolean) = value } - object BooleanFalse : Predicate { - override fun test(value: Boolean) = !value + object BooleanFalse : Predicate() { + override fun test(player: Player, value: Boolean) = !value } - data class StringEquals(val value: String) : Predicate { - override fun test(value: String) = value == this.value + data class StringEquals(val value: String) : Predicate() { + override fun test(player: Player, value: String) = value == this.value } - data class TileEquals(val x: Int? = null, val y: Int? = null, val level: Int? = null) : Predicate { - override fun test(value: Tile): Boolean { + data class TileEquals(val x: Int? = null, val y: Int? = null, val level: Int? = null) : Predicate() { + override val evaluator = RequirementEvaluator.TileEval + override fun test(player: Player, value: Tile): Boolean { if (x != null && value.x != x) return false if (y != null && value.y != y) return false if (level != null && value.level != level) return false @@ -55,8 +66,51 @@ sealed interface Predicate { } } - data class Within(val x: Int, val y: Int, val level: Int, val radius: Int) : Predicate { - override fun test(value: Tile) = value.within(x, y, level, radius) + data class Within(val x: Int, val y: Int, val level: Int, val radius: Int) : Predicate() { + override fun test(player: Player, value: Tile) = value.within(x, y, level, radius) + } + + data class InventoryItems(val entries: List) : Predicate>() { + data class Entry( + val filter: Predicate, + val count: Predicate, + ) + override val evaluator = RequirementEvaluator.InventoryEval + override val children = entries.map { it.count }.toSet() + entries.map { it.filter }.toSet() + + override fun test(player: Player, value: Array): Boolean { + for (entry in entries) { + val count = value.count { entry.filter.test(player, it) } + if (!entry.count.test(player, count)) { + return false + } + } + return true + } + } + + data class AnyItem(private val ids: Set) : Predicate() { + override fun test(player: Player, value: Item) = value.id in ids + } + + object EquipableItem : Predicate() { + override fun test(player: Player, value: Item) = player.hasRequirements(value) + } + + object UsableItem : Predicate() { + override fun test(player: Player, value: Item) = player.hasRequirementsToUse(value) + } + + data class EqualsItem(private val id: String) : Predicate() { + override fun test(player: Player, value: Item) = value.id == id + } + + data class AllOf(override val children: Set>) : Predicate() { + override fun test(player: Player, value: T) = children.all { it.test(player, value) } + } + + data class AnyOf(override val children: Set>) : Predicate() { + override fun test(player: Player, value: T) = children.any { it.test(player, value) } } companion object { @@ -103,5 +157,36 @@ sealed interface Predicate { map.containsKey("x") || map.containsKey("y") || map.containsKey("level") -> TileEquals(map["x"] as? Int, map["y"] as? Int, map["level"] as? Int) else -> null } + + fun parseItems(items: List>): Predicate> { + val entries = mutableListOf() + for (item in items) { + require(item.containsKey("id")) { "Item must have field 'id' in map $item" } + val id = item["id"] as String + var filter = if (id.contains(",")) { + val ids = id.split(",") + AnyItem(ids.flatMap { id -> + if (id.any { char -> char == '*' || char == '#' }) { + Wildcards.get(id, Wildcard.Item) + } else { + setOf(id) + } + }.toSet()) + } else if (id.any { it == '*' || it == '#' }) { + AnyItem(Wildcards.get(id, Wildcard.Item)) + } else { + EqualsItem(id) + } + if (item.containsKey("usable") && item["usable"] as Boolean) { + // TODO lookup values from custom configs e.g. firemaking.level + filter = AllOf(setOf(filter, UsableItem)) + } else if (item.containsKey("equipable") && item["equipable"] as Boolean) { + filter = AllOf(setOf(filter, EquipableItem)) + } + val counter = parseInt(item) ?: IntRange(min = 1) + entries.add(InventoryItems.Entry(filter, counter)) + } + return InventoryItems(entries) + } } } \ No newline at end of file diff --git a/game/src/main/kotlin/content/bot/fact/Produces.kt b/game/src/main/kotlin/content/bot/fact/Produces.kt deleted file mode 100644 index fc10a777c3..0000000000 --- a/game/src/main/kotlin/content/bot/fact/Produces.kt +++ /dev/null @@ -1,3 +0,0 @@ -package content.bot.fact - -data class Produces(val fact: Fact<*>, val outcome: Outcome? = null) diff --git a/game/src/main/kotlin/content/bot/fact/Requirement.kt b/game/src/main/kotlin/content/bot/fact/Requirement.kt index 4054947d89..ef708041fc 100644 --- a/game/src/main/kotlin/content/bot/fact/Requirement.kt +++ b/game/src/main/kotlin/content/bot/fact/Requirement.kt @@ -5,22 +5,27 @@ import world.gregs.voidps.engine.entity.character.player.Player data class Requirement(val fact: Fact, val predicate: Predicate? = null) { fun check(player: Player): Boolean { - return predicate?.test(fact.getValue(player)) ?: false + return predicate?.test(player, fact.getValue(player)) ?: false + } + + fun deficits(player: Player): List { + return predicate?.evaluator?.evaluate(player, fact, predicate) ?: emptyList() } companion object { - fun parse(list: List>>, name: String, requirePredicates: Boolean = true): List> { + fun parse(list: List>>>, name: String, requirePredicates: Boolean = true): List> { val requirements = mutableListOf>() - - for ((type, map) in list) { + for ((type, value) in list) { val parser = FactParser.parsers[type] ?: error("No fact parser for '$type' in ${name}.") - val error = parser.check(map) - if (error != null) { - error("Fact '$type' $error in ${name}.") + for (map in value) { + val error = parser.check(map) + if (error != null) { + error("Fact '$type' $error in ${name}.") + } } - val requirement = parser.requirement(map) + val requirement = parser.requirement(value) if (requirePredicates && requirement.predicate == null) { - error("No predicates found for requirement $type map $map in ${name}.") + error("No predicates found for requirement $type map $value in ${name}.") } requirements.add(requirement) } diff --git a/game/src/main/kotlin/content/bot/fact/RequirementEvaluator.kt b/game/src/main/kotlin/content/bot/fact/RequirementEvaluator.kt new file mode 100644 index 0000000000..d0e2b8ecbd --- /dev/null +++ b/game/src/main/kotlin/content/bot/fact/RequirementEvaluator.kt @@ -0,0 +1,59 @@ +package content.bot.fact + +import content.bot.fact.Predicate.IntEquals +import world.gregs.voidps.engine.entity.character.player.Player +import world.gregs.voidps.engine.entity.item.Item +import world.gregs.voidps.type.Tile + +sealed class RequirementEvaluator { + abstract fun evaluate(player: Player, fact: Fact, predicate: Predicate): List + + object TileEval : RequirementEvaluator() { + override fun evaluate(player: Player, fact: Fact, predicate: Predicate): List { + if (fact is Fact.PlayerTile && predicate is Predicate.InArea) { + val value = fact.getValue(player) + if (!predicate.test(player, value)) { + return listOf(Deficit.NotInArea(predicate.name)) + } + } + return emptyList() + } + } + + object IntEvaluator : RequirementEvaluator() { + override fun evaluate(player: Player, fact: Fact, predicate: Predicate): List { + return when (fact) { + is Fact.EquipCount if predicate is Predicate.IntRange -> listOf(Deficit.MissingItem(Predicate.EqualsItem(fact.id), predicate.min!!)) + is Fact.InventoryCount if predicate is Predicate.IntRange -> listOf(Deficit.MissingItem(Predicate.EqualsItem(fact.id), predicate.min!!)) + is Fact.EquipCount if predicate is IntEquals -> listOf(Deficit.MissingItem(Predicate.EqualsItem(fact.id), predicate.value)) + is Fact.InventoryCount if predicate is IntEquals -> listOf(Deficit.MissingItem(Predicate.EqualsItem(fact.id), predicate.value)) + else -> emptyList() + } + } + } + + object InventoryEval : RequirementEvaluator>() { + override fun evaluate(player: Player, fact: Fact>, predicate: Predicate>): List { + if (predicate is Predicate.InventoryItems) { + val deficits = mutableListOf() + val value = fact.getValue(player) + for (entry in predicate.entries) { + val have = value.count { entry.filter.test(player, it) } + if (entry.count.test(player, have)) { + continue + } + val needed = when (entry.count) { + is Predicate.IntRange -> entry.count.min!! - have + is IntEquals -> entry.count.value - have + else -> continue + } + if (needed > 0) { + deficits += Deficit.MissingItem(entry.filter, needed) + } + } + return deficits + } + return emptyList() + } + } +} \ No newline at end of file diff --git a/game/src/main/kotlin/content/bot/interact/path/Graph.kt b/game/src/main/kotlin/content/bot/interact/path/Graph.kt index bb5db07850..d5210427db 100644 --- a/game/src/main/kotlin/content/bot/interact/path/Graph.kt +++ b/game/src/main/kotlin/content/bot/interact/path/Graph.kt @@ -244,7 +244,7 @@ class Graph( var toLevel = 0 var cost = 0 val actions: MutableList = mutableListOf() - val requirements = mutableListOf>>() + val requirements = mutableListOf>>>() while (nextEntry()) { when (val key = key()) { "from_x" -> fromX = int() @@ -258,8 +258,15 @@ class Graph( "conditions" -> while (nextElement()) { while (nextEntry()) { val key = key() - val value = map() - requirements.add(key to value) + if (peek == '[') { + val list = mutableListOf>() + while (nextElement()) { + list.add(map()) + } + requirements.add(key to list) + } else { + requirements.add(key to listOf(map())) + } } } else -> throw IllegalArgumentException("Unexpected key: '$key' ${exception()}") From fec240c5cfd670d8c35d883a4b8a69f603a45dfd Mon Sep 17 00:00:00 2001 From: GregHib Date: Sat, 7 Feb 2026 21:35:29 +0000 Subject: [PATCH 063/101] Fix shortcuts --- ...shortcuts.toml => teleport.shortcuts.toml} | 24 +++++++++---------- .../kotlin/content/bot/action/BotAction.kt | 3 +-- .../kotlin/content/bot/action/BotActivity.kt | 3 --- .../kotlin/content/bot/fact/FactParser.kt | 4 ++-- 4 files changed, 14 insertions(+), 20 deletions(-) rename data/bot/{teleport.bot-shortcuts.toml => teleport.shortcuts.toml} (68%) diff --git a/data/bot/teleport.bot-shortcuts.toml b/data/bot/teleport.shortcuts.toml similarity index 68% rename from data/bot/teleport.bot-shortcuts.toml rename to data/bot/teleport.shortcuts.toml index bdcfe8299b..9703807069 100644 --- a/data/bot/teleport.bot-shortcuts.toml +++ b/data/bot/teleport.shortcuts.toml @@ -28,26 +28,24 @@ [teleport_varrock] weight = 125 requires = [ - { variable = "spellbook_config", value = 0, default = 0 }, - { carries = "fire_rune", amount = 1 }, - { carries = "air_rune", amount = 3 }, - { carries = "law_rune", amount = 1 }, + { variable = { id = "spellbook_config", equals = 0, default = 0 } }, + { carries = [{ id = "fire_rune", min = 1 }, { id = "air_rune", min = 3 }, { id = "law_rune", min = 1 }] }, ] actions = [ { option = "Cast", interface = "modern_spellbook:varrock_teleport" }, { wait = 5 }, ] produces = [ - { location = "varrock_teleport" } + { area = { id = "varrock_teleport" } } ] [teleport_varrock_via_bank] weight = 150 requires = [ - { variable = "spellbook_config", value = 0, default = 0 }, - { owns = "fire_rune", amount = 1 }, - { owns = "air_rune", amount = 3 }, - { owns = "law_rune", amount = 1 }, + { variable = { id = "spellbook_config", equals = 0, default = 0 }}, + { owns = { id = "fire_rune", min = 1 }}, + { owns = { id = "air_rune", min = 3 }}, + { owns = { id = "law_rune", min = 1 }}, ] actions = [ { go_to_nearest = "bank" }, @@ -61,18 +59,18 @@ actions = [ # { wait = { location = "varrock_teleport" } }, ] produces = [ - { location = "varrock_teleport" } + { area = { id = "varrock_teleport" } } ] [teleport_lumbridge] weight = 5 requires = [ - { variable = "spellbook_config", value = 0, default = 0 }, - { variable = "lumbridge_cooldown", value = "normal" }, # clock/timer teleport_delay + { variable = { id = "spellbook_config", equals = 0, default = 0 } }, + { clock = { id = "home_teleport_timeout", max = 0 } }, ] actions = [ { option = "Cast", interface = "modern_spellbook:lumbridge_home_teleport" }, ] produces = [ - { location = "lumbridge_teleport" } + { area = { id = "lumbridge_teleport" } } ] diff --git a/game/src/main/kotlin/content/bot/action/BotAction.kt b/game/src/main/kotlin/content/bot/action/BotAction.kt index d37796a14c..a5b43ef77a 100644 --- a/game/src/main/kotlin/content/bot/action/BotAction.kt +++ b/game/src/main/kotlin/content/bot/action/BotAction.kt @@ -543,8 +543,7 @@ sealed interface BotAction { * bot spawning in other locations * tidy up old bot code * move tags into edges not areas - * - * TODO need a better way of managing inventory. Removing unneeded items in favour of required ones vs emptying full inventory every time etc... + * item tags? * * Idea: Reactions? * A separate queue that runs "reactions" e.g. diff --git a/game/src/main/kotlin/content/bot/action/BotActivity.kt b/game/src/main/kotlin/content/bot/action/BotActivity.kt index e0edc6ef44..b51ced7b00 100644 --- a/game/src/main/kotlin/content/bot/action/BotActivity.kt +++ b/game/src/main/kotlin/content/bot/action/BotActivity.kt @@ -84,9 +84,6 @@ private fun loadActivities(activities: MutableMap, template val template = templates[fragment.template] ?: error("Unable to find template '${fragment.template}' for ${fragment.id}.") activities[fragment.id] = fragment.activity(template) } - for (activity in activities.values) { - println(activity) - } activities.size } } diff --git a/game/src/main/kotlin/content/bot/fact/FactParser.kt b/game/src/main/kotlin/content/bot/fact/FactParser.kt index 9b86ad19de..929b2af25e 100644 --- a/game/src/main/kotlin/content/bot/fact/FactParser.kt +++ b/game/src/main/kotlin/content/bot/fact/FactParser.kt @@ -82,14 +82,14 @@ sealed class FactParser { } as Fact } - override fun predicate(map: Map): Predicate { + override fun predicate(map: Map): Predicate? { return when (val default = map["default"]) { is Int -> Predicate.parseInt(map) is String -> Predicate.parseString(map) is Double -> Predicate.parseDouble(map) is Boolean -> Predicate.parseBool(map) else -> error("Invalid default value $default") - } as Predicate + } as? Predicate } } From 098dd74a1ff2197748db97d215ca36c1f943f531 Mon Sep 17 00:00:00 2001 From: GregHib Date: Sat, 7 Feb 2026 23:36:48 +0000 Subject: [PATCH 064/101] Split nav edges into locations --- data/bot/al_kharid.nav-edges.toml | 30 +++++++++ data/bot/draynor.nav-edges.toml | 20 ++++++ data/bot/lumbridge.nav-edges.toml | 108 ------------------------------ data/bot/varrock.nav-edges.toml | 64 ++++++++++++++++++ 4 files changed, 114 insertions(+), 108 deletions(-) create mode 100644 data/bot/al_kharid.nav-edges.toml create mode 100644 data/bot/draynor.nav-edges.toml create mode 100644 data/bot/varrock.nav-edges.toml diff --git a/data/bot/al_kharid.nav-edges.toml b/data/bot/al_kharid.nav-edges.toml new file mode 100644 index 0000000000..c66ba35190 --- /dev/null +++ b/data/bot/al_kharid.nav-edges.toml @@ -0,0 +1,30 @@ +edges = [ + { from_x = 3283, from_y = 3331, to_x = 3284, to_y = 3313 }, # al_kharid_mine_north_entrance_to_west_path + { from_x = 3284, from_y = 3313, to_x = 3287, to_y = 3294 }, # al_kharid_mine_west_path_to_south + { from_x = 3287, from_y = 3294, to_x = 3298, to_y = 3280 }, # al_kharid_mine_west_path_to_entrance + { from_x = 3298, from_y = 3280, to_x = 3299, to_y = 3294 }, + { from_x = 3299, from_y = 3294, to_x = 3300, to_y = 3311 }, + { from_x = 3298, from_y = 3280, to_x = 3299, to_y = 3263 }, # al_kharid_mine_entrance_to_mine_south + { from_x = 3299, from_y = 3263, to_x = 3294, to_y = 3242 }, # al_kharid_mine_south_to_north_path + { from_x = 3294, from_y = 3242, to_x = 3278, to_y = 3228 }, # al_kharid_north_path_to_crossroad + { from_x = 3278, from_y = 3228, to_x = 3268, to_y = 3228 }, # al_kharid_crossroad_to_tollgate_north + { from_x = 3278, from_y = 3228, to_x = 3268, to_y = 3227 }, # al_kharid_crossroad_to_tollgate + { from_x = 3278, from_y = 3228, to_x = 3280, to_y = 3216 }, # al_kharid_crossroad_to_glider + { from_x = 3267, from_y = 3228, to_x = 3268, to_y = 3228, cost = 1, actions = [{ option = "Pay-toll(10gp)", object = "toll_gate_al_kharid_north", x = 3268, y = 3228 }], conditions = [{ carries = [{ id = "coins", amount = 10 }] }] }, # lumbridge_tollgate_north_to_al_kharid_tollgate + { from_x = 3268, from_y = 3228, to_x = 3267, to_y = 3228, cost = 1, actions = [{ option = "Pay-toll(10gp)", object = "toll_gate_al_kharid_north", x = 3268, y = 3228 }], conditions = [{ carries = [{ id = "coins", amount = 10 }] }] }, # al_kharid_tollgate_north_to_lumbridge_tollgate + { from_x = 3267, from_y = 3227, to_x = 3268, to_y = 3227, cost = 1, actions = [{ option = "Pay-toll(10gp)", object = "toll_gate_al_kharid_south", x = 3268, y = 3227 }], conditions = [{ carries = [{ id = "coins", amount = 10 }] }] }, # lumbridge_tollgate_to_al_kharid + { from_x = 3268, from_y = 3227, to_x = 3267, to_y = 3227, cost = 1, actions = [{ option = "Pay-toll(10gp)", object = "toll_gate_al_kharid_south", x = 3268, y = 3227 }], conditions = [{ carries = [{ id = "coins", amount = 10 }] }] }, # al_kharid_tollgate_to_lumbridge + { from_x = 3268, from_y = 3227, to_x = 3280, to_y = 3216 }, # al_kharid_tollgate_to_glider + { from_x = 3268, from_y = 3228, to_x = 3280, to_y = 3216 }, # al_kharid_tollgate_north_to_glider + { from_x = 3280, from_y = 3216, to_x = 3292, to_y = 3215 }, # al_kharid_glider_to_musician + { from_x = 3292, from_y = 3215, to_x = 3300, to_y = 3198 }, # al_kharid_musician_to_silk_path + { from_x = 3300, from_y = 3198, to_x = 3288, to_y = 3189 }, # al_kharid_silk_path_to_scimitar_shop + { from_x = 3280, from_y = 3216, to_x = 3280, to_y = 3200 }, # al_kharid_glider_to_west_shortcut + { from_x = 3280, from_y = 3200, to_x = 3288, to_y = 3189 }, # al_kharid_west_shortcut_to_scimitar_shop + { from_x = 3288, from_y = 3189, to_x = 3282, to_y = 3185 }, # al_kharid_scimitar_shop_to_furnace_entrance + { from_x = 3280, from_y = 3200, to_x = 3282, to_y = 3185 }, # al_kharid_west_shortcut_to_furnace_to_entrance + { from_x = 3282, from_y = 3185, to_x = 3278, to_y = 3177 }, # al_kharid_furnace_entrance_to_bank_crossroads + { from_x = 3278, from_y = 3177, to_x = 3276, to_y = 3168 }, # al_kharid_bank_crossroad_to_entrance + { from_x = 3278, from_y = 3177, to_x = 3274, to_y = 3180 }, # al_kharid_bank_crossroad_to_kebab_shop + { from_x = 3276, from_y = 3168, to_x = 3270, to_y = 3167 }, # al_kharid_bank_entrance_to_bank +] \ No newline at end of file diff --git a/data/bot/draynor.nav-edges.toml b/data/bot/draynor.nav-edges.toml new file mode 100644 index 0000000000..bcd589ca1f --- /dev/null +++ b/data/bot/draynor.nav-edges.toml @@ -0,0 +1,20 @@ +edges = [ + { from_x = 3138, from_y = 3227, to_x = 3119, to_y = 3228 }, # draynor_east_to_jail_path_south + { from_x = 3119, from_y = 3228, to_x = 3105, to_y = 3238 }, # draynor_path_south_to_west + { from_x = 3105, from_y = 3238, to_x = 3104, to_y = 3248 }, # draynor_path_west_to_bank_crossroad + { from_x = 3104, from_y = 3248, to_x = 3093, to_y = 3245 }, # draynor_bank_crossroad_to_bank + { from_x = 3093, from_y = 3245, to_x = 3079, to_y = 3249 }, # draynor_bank_to_stalls + { from_x = 3093, from_y = 3245, to_x = 3099, to_y = 3246 }, # draynor_bank_to_trees + { from_x = 3104, from_y = 3248, to_x = 3099, to_y = 3246 }, # draynor_bank_crossroad_to_trees + { from_x = 3079, from_y = 3249, to_x = 3071, to_y = 3266 }, # draynor_stalls_to_pigsty + { from_x = 3079, from_y = 3249, to_x = 3086, to_y = 3237 }, # draynor_stall_to_willow_trees + { from_x = 3079, from_y = 3249, to_x = 3072, to_y = 3250 }, # draynor_stalls_to_west_trees + { from_x = 3079, from_y = 3249, to_x = 3079, to_y = 3265 }, # draynor_stalls_to_north_trees + { from_x = 3093, from_y = 3245, to_x = 3086, to_y = 3237 }, # draynor_bank_to_willow_trees + { from_x = 3086, from_y = 3237, to_x = 3097, to_y = 3235 }, # draynor_willow_trees_to_south + { from_x = 3086, from_y = 3237, to_x = 3086, to_y = 3231 }, # draynor_willow_trees_to_fishing_spot + { from_x = 3086, from_y = 3231, to_x = 3097, to_y = 3235 }, # draynor_fishing_spot_to_south + { from_x = 3105, from_y = 3238, to_x = 3097, to_y = 3235 }, # draynor_jail_path_west_to_south + { from_x = 3138, from_y = 3227, to_x = 3153, to_y = 3216 }, # draynor_east_path_to_swamp_north_wall + { from_x = 3268, from_y = 3331, to_x = 3283, to_y = 3331 }, # varrock_al_kharid_crossroad_to_north_entrance +] \ No newline at end of file diff --git a/data/bot/lumbridge.nav-edges.toml b/data/bot/lumbridge.nav-edges.toml index 9a5f6c071c..e2df401e21 100644 --- a/data/bot/lumbridge.nav-edges.toml +++ b/data/bot/lumbridge.nav-edges.toml @@ -138,112 +138,4 @@ edges = [ { from_x = 3260, from_y = 3228, to_x = 3267, to_y = 3227 }, # lumbridge_east_crossroad_to_tollgate { from_x = 3260, from_y = 3228, to_x = 3267, to_y = 3228 }, # lumbridge_east_crossroad_to_tollgate_north { from_x = 3260, from_y = 3228, to_x = 3263, to_y = 3222 }, # lumbridge_east_crossroad_to_trees_east - { from_x = 3213, from_y = 3428, to_x = 3223, to_y = 3429 }, # varrock_teleport_to_centre_east - { from_x = 3213, from_y = 3428, to_x = 3200, to_y = 3429 }, # varrock_teleport_to_centre_west - { from_x = 3213, from_y = 3428, to_x = 3211, to_y = 3420 }, # varrock_teleport_to_centre_south - { from_x = 3200, from_y = 3429, to_x = 3186, to_y = 3430 }, # varrock_centre_west_to_west_bank_south_entrance - { from_x = 3186, from_y = 3430, to_x = 3186, to_y = 3435 }, # varrock_west_bank_south_entrance_to_bank_south - { from_x = 3186, from_y = 3430, to_x = 3171, to_y = 3429 }, # varrock_west_bank_south_entrance_to_romeo_crossroad - { from_x = 3171, from_y = 3429, to_x = 3162, to_y = 3420 }, # varrock_west_romeo_crossroads_to_oak_tress - { from_x = 3162, from_y = 3420, to_x = 3150, to_y = 3416 }, # varrock_west_oak_trees_to_cat_house - { from_x = 3150, from_y = 3416, to_x = 3135, to_y = 3416 }, # varrock_west_cat_house_to_barbarian_path - { from_x = 3135, from_y = 3416, to_x = 3128, to_y = 3407 }, # varrock_west_barbarian_path_to_air_altar_ruins - { from_x = 3128, from_y = 3407, to_x = 2841, to_y = 4830, cost = 3, actions = [{ option = "null", object = "air_altar_ruins", x = 3126, y = 3404 }] }, # varrock_west_air_altar_ruins_to_air_altar - { from_x = 2841, from_y = 4830, to_x = 2841, to_y = 4829 }, # varrock_west_air_altar_to_exit - { from_x = 2841, from_y = 4829, to_x = 3128, to_y = 3407, cost = 3, actions = [{ option = "Enter", object = "air_altar_portal", x = 2841, y = 4828 }] }, # varrock_west_air_altar_exit_to_ruins - { from_x = 3304, from_y = 3474, to_x = 2655, to_y = 4831, cost = 3, actions = [{ option = "Enter", object = "earth_altar_ruins", x = 3305, y = 3473 }] }, # varrock_east_earth_altar_ruins_to_altar - { from_x = 2655, from_y = 4831, to_x = 2655, to_y = 4830 }, # varrock_east_earth_altar_to_exit - { from_x = 2655, from_y = 4830, to_x = 3304, to_y = 3474, cost = 3, actions = [{ option = "Enter", object = "earth_altar_portal", x = 2655, y = 4829 }] }, # varrock_east_earth_altar_exit_to_ruins - { from_x = 3254, from_y = 3422, to_x = 3265, to_y = 3428 }, # varrock_east_bank_to_dirt_crossroad - { from_x = 3265, from_y = 3428, to_x = 3280, to_y = 3428 }, # varrock_east_dirt_crossroad_to_east_crossroad - { from_x = 3280, from_y = 3428, to_x = 3287, to_y = 3414 }, # varrock_east_crossroad_to_east_path_south - { from_x = 3287, from_y = 3414, to_x = 3291, to_y = 3399 }, # varrock_east_path_to_south - { from_x = 3291, from_y = 3399, to_x = 3293, to_y = 3384 }, # varrock_east_path_south_to_east_border - { from_x = 3280, from_y = 3428, to_x = 3286, to_y = 3442 }, # varrock_east_crossroad_to_path_north - { from_x = 3286, from_y = 3442, to_x = 3287, to_y = 3457 }, # varrock_east_path_to_north - { from_x = 3287, from_y = 3457, to_x = 3296, to_y = 3462 }, # varrock_east_path_north_to_sawmill_crossroad - { from_x = 3296, from_y = 3462, to_x = 3304, to_y = 3474 }, # varrock_east_sawmill_crossroad_to_eath_altar_ruins - { from_x = 3265, from_y = 3428, to_x = 3246, to_y = 3429 }, # varrock_east_dirt_crossroad_to_bank_crossroad - { from_x = 3246, from_y = 3429, to_x = 3254, to_y = 3422 }, # varrock_east_bank_crossroad_to_east_bank - { from_x = 3230, from_y = 3430, to_x = 3246, to_y = 3429 }, # varrock_east_armour_shop_to_bank_crossroad - { from_x = 3230, from_y = 3430, to_x = 3223, to_y = 3429 }, # varrock_east_armour_shop_to_centre_east - { from_x = 3238, from_y = 3295, to_x = 3238, to_y = 3304 }, # lumbridge_chicken_entrance_to_varrock_south_split - { from_x = 3238, from_y = 3304, to_x = 3251, to_y = 3319 }, # varrock_south_split_to_al_kharid_path_west - { from_x = 3238, from_y = 3304, to_x = 3238, to_y = 3320 }, # varrock_south_split_to_al_kharid_path_west - { from_x = 3238, from_y = 3320, to_x = 3240, to_y = 3336 }, # varrock_south_split_to_al_kharid_path_west - { from_x = 3251, from_y = 3319, to_x = 3268, to_y = 3331 }, # varrock_al_kharid_path_west_to_crossroads - { from_x = 3283, from_y = 3331, to_x = 3295, to_y = 3334 }, # al_kharid_north_entrance_to_varrock_north_west - { from_x = 3295, from_y = 3334, to_x = 3304, to_y = 3335 }, # varrock_al_kharid_north_west_to_crossroad - { from_x = 3295, from_y = 3334, to_x = 3299, to_y = 3346 }, # varrock_al_kharid_north_west_to_south_east_path - { from_x = 3304, from_y = 3335, to_x = 3299, to_y = 3346 }, # varrock_al_kharid_north_west_crossroad_to_south_east_path - { from_x = 3299, from_y = 3346, to_x = 3298, to_y = 3359 }, # varrock_south_east_path_to_east_mine_path_south - { from_x = 3298, from_y = 3359, to_x = 3295, to_y = 3372 }, # varrock_south_east_mine_south_to_path - { from_x = 3295, from_y = 3372, to_x = 3286, to_y = 3371 }, # varrock_south_east_mine_path_to_east - { from_x = 3286, from_y = 3371, to_x = 3293, to_y = 3384 }, # varrock_south_east_mine_to_border - { from_x = 3295, from_y = 3372, to_x = 3293, to_y = 3384 }, # varrock_south_east_mine_path_to_border - { from_x = 3211, from_y = 3420, to_x = 3210, to_y = 3407 }, # varrock_centre_south_to_dirt_crossroads - { from_x = 3210, from_y = 3407, to_x = 3211, to_y = 3395 }, # varrock_south_dirt_crossroads_to_blue_moon_inn - { from_x = 3210, from_y = 3407, to_x = 3209, to_y = 3399 }, - { from_x = 3210, from_y = 3407, to_x = 3209, to_y = 3399 }, - { from_x = 3209, from_y = 3399, to_x = 3207, to_y = 3399, cost = 1, actions = [{ option = "Open", object = "door_444_closed", x = 3209, y = 3399 }, { x = 3207, y = 3399 }] }, - { from_x = 3211, from_y = 3395, to_x = 3209, to_y = 3399 }, # varrock_sword_shop - { from_x = 3211, from_y = 3395, to_x = 3211, to_y = 3381 }, # varrock_blue_moon_inn_to_south_border - { from_x = 3211, from_y = 3381, to_x = 3214, to_y = 3367 }, # varrock_south_border_to_dark_mages - { from_x = 3214, from_y = 3367, to_x = 3226, to_y = 3352 }, # varrock_south_dark_mages_to_south - { from_x = 3214, from_y = 3367, to_x = 3206, to_y = 3376 }, - { from_x = 3211, from_y = 3381, to_x = 3206, to_y = 3376 }, - { from_x = 3206, from_y = 3376, to_x = 3197, to_y = 3373 }, - { from_x = 3197, from_y = 3373, to_x = 3191, to_y = 3367 }, - { from_x = 3197, from_y = 3373, to_x = 3184, to_y = 3370 }, - { from_x = 3191, from_y = 3367, to_x = 3184, to_y = 3370 }, - { from_x = 3226, from_y = 3352, to_x = 3228, to_y = 3337 }, # varrock_dark_mages_to_south_fields_path - { from_x = 3228, from_y = 3337, to_x = 3240, to_y = 3336 }, # varrock_south_fields_path_to_stile - { from_x = 3240, from_y = 3336, to_x = 3254, to_y = 3333 }, # varrock_south_stile_to_path - { from_x = 3254, from_y = 3333, to_x = 3268, to_y = 3331 }, # varrock_south_stile_to_al_kharid_crossroad - { from_x = 3138, from_y = 3227, to_x = 3119, to_y = 3228 }, # draynor_east_to_jail_path_south - { from_x = 3119, from_y = 3228, to_x = 3105, to_y = 3238 }, # draynor_path_south_to_west - { from_x = 3105, from_y = 3238, to_x = 3104, to_y = 3248 }, # draynor_path_west_to_bank_crossroad - { from_x = 3104, from_y = 3248, to_x = 3093, to_y = 3245 }, # draynor_bank_crossroad_to_bank - { from_x = 3093, from_y = 3245, to_x = 3079, to_y = 3249 }, # draynor_bank_to_stalls - { from_x = 3093, from_y = 3245, to_x = 3099, to_y = 3246 }, # draynor_bank_to_trees - { from_x = 3104, from_y = 3248, to_x = 3099, to_y = 3246 }, # draynor_bank_crossroad_to_trees - { from_x = 3079, from_y = 3249, to_x = 3071, to_y = 3266 }, # draynor_stalls_to_pigsty - { from_x = 3079, from_y = 3249, to_x = 3086, to_y = 3237 }, # draynor_stall_to_willow_trees - { from_x = 3079, from_y = 3249, to_x = 3072, to_y = 3250 }, # draynor_stalls_to_west_trees - { from_x = 3079, from_y = 3249, to_x = 3079, to_y = 3265 }, # draynor_stalls_to_north_trees - { from_x = 3093, from_y = 3245, to_x = 3086, to_y = 3237 }, # draynor_bank_to_willow_trees - { from_x = 3086, from_y = 3237, to_x = 3097, to_y = 3235 }, # draynor_willow_trees_to_south - { from_x = 3086, from_y = 3237, to_x = 3086, to_y = 3231 }, # draynor_willow_trees_to_fishing_spot - { from_x = 3086, from_y = 3231, to_x = 3097, to_y = 3235 }, # draynor_fishing_spot_to_south - { from_x = 3105, from_y = 3238, to_x = 3097, to_y = 3235 }, # draynor_jail_path_west_to_south - { from_x = 3138, from_y = 3227, to_x = 3153, to_y = 3216 }, # draynor_east_path_to_swamp_north_wall - { from_x = 3268, from_y = 3331, to_x = 3283, to_y = 3331 }, # varrock_al_kharid_crossroad_to_north_entrance - { from_x = 3283, from_y = 3331, to_x = 3284, to_y = 3313 }, # al_kharid_mine_north_entrance_to_west_path - { from_x = 3284, from_y = 3313, to_x = 3287, to_y = 3294 }, # al_kharid_mine_west_path_to_south - { from_x = 3287, from_y = 3294, to_x = 3298, to_y = 3280 }, # al_kharid_mine_west_path_to_entrance - { from_x = 3298, from_y = 3280, to_x = 3299, to_y = 3294 }, - { from_x = 3299, from_y = 3294, to_x = 3300, to_y = 3311 }, - { from_x = 3298, from_y = 3280, to_x = 3299, to_y = 3263 }, # al_kharid_mine_entrance_to_mine_south - { from_x = 3299, from_y = 3263, to_x = 3294, to_y = 3242 }, # al_kharid_mine_south_to_north_path - { from_x = 3294, from_y = 3242, to_x = 3278, to_y = 3228 }, # al_kharid_north_path_to_crossroad - { from_x = 3278, from_y = 3228, to_x = 3268, to_y = 3228 }, # al_kharid_crossroad_to_tollgate_north - { from_x = 3278, from_y = 3228, to_x = 3268, to_y = 3227 }, # al_kharid_crossroad_to_tollgate - { from_x = 3278, from_y = 3228, to_x = 3280, to_y = 3216 }, # al_kharid_crossroad_to_glider - { from_x = 3267, from_y = 3228, to_x = 3268, to_y = 3228, cost = 1, actions = [{ option = "Pay-toll(10gp)", object = "toll_gate_al_kharid_north", x = 3268, y = 3228 }], conditions = [{ carries = [{ id = "coins", amount = 10 }] }] }, # lumbridge_tollgate_north_to_al_kharid_tollgate - { from_x = 3268, from_y = 3228, to_x = 3267, to_y = 3228, cost = 1, actions = [{ option = "Pay-toll(10gp)", object = "toll_gate_al_kharid_north", x = 3268, y = 3228 }], conditions = [{ carries = [{ id = "coins", amount = 10 }] }] }, # al_kharid_tollgate_north_to_lumbridge_tollgate - { from_x = 3267, from_y = 3227, to_x = 3268, to_y = 3227, cost = 1, actions = [{ option = "Pay-toll(10gp)", object = "toll_gate_al_kharid_south", x = 3268, y = 3227 }], conditions = [{ carries = [{ id = "coins", amount = 10 }] }] }, # lumbridge_tollgate_to_al_kharid - { from_x = 3268, from_y = 3227, to_x = 3267, to_y = 3227, cost = 1, actions = [{ option = "Pay-toll(10gp)", object = "toll_gate_al_kharid_south", x = 3268, y = 3227 }], conditions = [{ carries = [{ id = "coins", amount = 10 }] }] }, # al_kharid_tollgate_to_lumbridge - { from_x = 3268, from_y = 3227, to_x = 3280, to_y = 3216 }, # al_kharid_tollgate_to_glider - { from_x = 3268, from_y = 3228, to_x = 3280, to_y = 3216 }, # al_kharid_tollgate_north_to_glider - { from_x = 3280, from_y = 3216, to_x = 3292, to_y = 3215 }, # al_kharid_glider_to_musician - { from_x = 3292, from_y = 3215, to_x = 3300, to_y = 3198 }, # al_kharid_musician_to_silk_path - { from_x = 3300, from_y = 3198, to_x = 3288, to_y = 3189 }, # al_kharid_silk_path_to_scimitar_shop - { from_x = 3280, from_y = 3216, to_x = 3280, to_y = 3200 }, # al_kharid_glider_to_west_shortcut - { from_x = 3280, from_y = 3200, to_x = 3288, to_y = 3189 }, # al_kharid_west_shortcut_to_scimitar_shop - { from_x = 3288, from_y = 3189, to_x = 3282, to_y = 3185 }, # al_kharid_scimitar_shop_to_furnace_entrance - { from_x = 3280, from_y = 3200, to_x = 3282, to_y = 3185 }, # al_kharid_west_shortcut_to_furnace_to_entrance - { from_x = 3282, from_y = 3185, to_x = 3278, to_y = 3177 }, # al_kharid_furnace_entrance_to_bank_crossroads - { from_x = 3278, from_y = 3177, to_x = 3276, to_y = 3168 }, # al_kharid_bank_crossroad_to_entrance - { from_x = 3278, from_y = 3177, to_x = 3274, to_y = 3180 }, # al_kharid_bank_crossroad_to_kebab_shop - { from_x = 3276, from_y = 3168, to_x = 3270, to_y = 3167 }, # al_kharid_bank_entrance_to_bank ] diff --git a/data/bot/varrock.nav-edges.toml b/data/bot/varrock.nav-edges.toml new file mode 100644 index 0000000000..98761b7b36 --- /dev/null +++ b/data/bot/varrock.nav-edges.toml @@ -0,0 +1,64 @@ +edges = [ + { from_x = 3213, from_y = 3428, to_x = 3223, to_y = 3429 }, # varrock_teleport_to_centre_east + { from_x = 3213, from_y = 3428, to_x = 3200, to_y = 3429 }, # varrock_teleport_to_centre_west + { from_x = 3213, from_y = 3428, to_x = 3211, to_y = 3420 }, # varrock_teleport_to_centre_south + { from_x = 3200, from_y = 3429, to_x = 3186, to_y = 3430 }, # varrock_centre_west_to_west_bank_south_entrance + { from_x = 3186, from_y = 3430, to_x = 3186, to_y = 3435 }, # varrock_west_bank_south_entrance_to_bank_south + { from_x = 3186, from_y = 3430, to_x = 3171, to_y = 3429 }, # varrock_west_bank_south_entrance_to_romeo_crossroad + { from_x = 3171, from_y = 3429, to_x = 3162, to_y = 3420 }, # varrock_west_romeo_crossroads_to_oak_tress + { from_x = 3162, from_y = 3420, to_x = 3150, to_y = 3416 }, # varrock_west_oak_trees_to_cat_house + { from_x = 3150, from_y = 3416, to_x = 3135, to_y = 3416 }, # varrock_west_cat_house_to_barbarian_path + { from_x = 3135, from_y = 3416, to_x = 3128, to_y = 3407 }, # varrock_west_barbarian_path_to_air_altar_ruins + { from_x = 3128, from_y = 3407, to_x = 2841, to_y = 4830, cost = 3, actions = [{ option = "null", object = "air_altar_ruins", x = 3126, y = 3404 }] }, # varrock_west_air_altar_ruins_to_air_altar + { from_x = 2841, from_y = 4830, to_x = 2841, to_y = 4829 }, # varrock_west_air_altar_to_exit + { from_x = 2841, from_y = 4829, to_x = 3128, to_y = 3407, cost = 3, actions = [{ option = "Enter", object = "air_altar_portal", x = 2841, y = 4828 }] }, # varrock_west_air_altar_exit_to_ruins + { from_x = 3304, from_y = 3474, to_x = 2655, to_y = 4831, cost = 3, actions = [{ option = "Enter", object = "earth_altar_ruins", x = 3305, y = 3473 }] }, # varrock_east_earth_altar_ruins_to_altar + { from_x = 2655, from_y = 4831, to_x = 2655, to_y = 4830 }, # varrock_east_earth_altar_to_exit + { from_x = 2655, from_y = 4830, to_x = 3304, to_y = 3474, cost = 3, actions = [{ option = "Enter", object = "earth_altar_portal", x = 2655, y = 4829 }] }, # varrock_east_earth_altar_exit_to_ruins + { from_x = 3254, from_y = 3422, to_x = 3265, to_y = 3428 }, # varrock_east_bank_to_dirt_crossroad + { from_x = 3265, from_y = 3428, to_x = 3280, to_y = 3428 }, # varrock_east_dirt_crossroad_to_east_crossroad + { from_x = 3280, from_y = 3428, to_x = 3287, to_y = 3414 }, # varrock_east_crossroad_to_east_path_south + { from_x = 3287, from_y = 3414, to_x = 3291, to_y = 3399 }, # varrock_east_path_to_south + { from_x = 3291, from_y = 3399, to_x = 3293, to_y = 3384 }, # varrock_east_path_south_to_east_border + { from_x = 3280, from_y = 3428, to_x = 3286, to_y = 3442 }, # varrock_east_crossroad_to_path_north + { from_x = 3286, from_y = 3442, to_x = 3287, to_y = 3457 }, # varrock_east_path_to_north + { from_x = 3287, from_y = 3457, to_x = 3296, to_y = 3462 }, # varrock_east_path_north_to_sawmill_crossroad + { from_x = 3296, from_y = 3462, to_x = 3304, to_y = 3474 }, # varrock_east_sawmill_crossroad_to_eath_altar_ruins + { from_x = 3265, from_y = 3428, to_x = 3246, to_y = 3429 }, # varrock_east_dirt_crossroad_to_bank_crossroad + { from_x = 3246, from_y = 3429, to_x = 3254, to_y = 3422 }, # varrock_east_bank_crossroad_to_east_bank + { from_x = 3230, from_y = 3430, to_x = 3246, to_y = 3429 }, # varrock_east_armour_shop_to_bank_crossroad + { from_x = 3230, from_y = 3430, to_x = 3223, to_y = 3429 }, # varrock_east_armour_shop_to_centre_east + { from_x = 3238, from_y = 3295, to_x = 3238, to_y = 3304 }, # lumbridge_chicken_entrance_to_varrock_south_split + { from_x = 3238, from_y = 3304, to_x = 3251, to_y = 3319 }, # varrock_south_split_to_al_kharid_path_west + { from_x = 3238, from_y = 3304, to_x = 3238, to_y = 3320 }, # varrock_south_split_to_al_kharid_path_west + { from_x = 3238, from_y = 3320, to_x = 3240, to_y = 3336 }, # varrock_south_split_to_al_kharid_path_west + { from_x = 3251, from_y = 3319, to_x = 3268, to_y = 3331 }, # varrock_al_kharid_path_west_to_crossroads + { from_x = 3283, from_y = 3331, to_x = 3295, to_y = 3334 }, # al_kharid_north_entrance_to_varrock_north_west + { from_x = 3295, from_y = 3334, to_x = 3304, to_y = 3335 }, # varrock_al_kharid_north_west_to_crossroad + { from_x = 3295, from_y = 3334, to_x = 3299, to_y = 3346 }, # varrock_al_kharid_north_west_to_south_east_path + { from_x = 3304, from_y = 3335, to_x = 3299, to_y = 3346 }, # varrock_al_kharid_north_west_crossroad_to_south_east_path + { from_x = 3299, from_y = 3346, to_x = 3298, to_y = 3359 }, # varrock_south_east_path_to_east_mine_path_south + { from_x = 3298, from_y = 3359, to_x = 3295, to_y = 3372 }, # varrock_south_east_mine_south_to_path + { from_x = 3295, from_y = 3372, to_x = 3286, to_y = 3371 }, # varrock_south_east_mine_path_to_east + { from_x = 3286, from_y = 3371, to_x = 3293, to_y = 3384 }, # varrock_south_east_mine_to_border + { from_x = 3295, from_y = 3372, to_x = 3293, to_y = 3384 }, # varrock_south_east_mine_path_to_border + { from_x = 3211, from_y = 3420, to_x = 3210, to_y = 3407 }, # varrock_centre_south_to_dirt_crossroads + { from_x = 3210, from_y = 3407, to_x = 3211, to_y = 3395 }, # varrock_south_dirt_crossroads_to_blue_moon_inn + { from_x = 3210, from_y = 3407, to_x = 3209, to_y = 3399 }, + { from_x = 3210, from_y = 3407, to_x = 3209, to_y = 3399 }, + { from_x = 3209, from_y = 3399, to_x = 3207, to_y = 3399, cost = 1, actions = [{ option = "Open", object = "door_444_closed", x = 3209, y = 3399 }, { x = 3207, y = 3399 }] }, + { from_x = 3211, from_y = 3395, to_x = 3209, to_y = 3399 }, # varrock_sword_shop + { from_x = 3211, from_y = 3395, to_x = 3211, to_y = 3381 }, # varrock_blue_moon_inn_to_south_border + { from_x = 3211, from_y = 3381, to_x = 3214, to_y = 3367 }, # varrock_south_border_to_dark_mages + { from_x = 3214, from_y = 3367, to_x = 3226, to_y = 3352 }, # varrock_south_dark_mages_to_south + { from_x = 3214, from_y = 3367, to_x = 3206, to_y = 3376 }, + { from_x = 3211, from_y = 3381, to_x = 3206, to_y = 3376 }, + { from_x = 3206, from_y = 3376, to_x = 3197, to_y = 3373 }, + { from_x = 3197, from_y = 3373, to_x = 3191, to_y = 3367 }, + { from_x = 3197, from_y = 3373, to_x = 3184, to_y = 3370 }, + { from_x = 3191, from_y = 3367, to_x = 3184, to_y = 3370 }, + { from_x = 3226, from_y = 3352, to_x = 3228, to_y = 3337 }, # varrock_dark_mages_to_south_fields_path + { from_x = 3228, from_y = 3337, to_x = 3240, to_y = 3336 }, # varrock_south_fields_path_to_stile + { from_x = 3240, from_y = 3336, to_x = 3254, to_y = 3333 }, # varrock_south_stile_to_path + { from_x = 3254, from_y = 3333, to_x = 3268, to_y = 3331 }, # varrock_south_stile_to_al_kharid_crossroad +] \ No newline at end of file From 634aa3a6c731e88c6b3edfffe5a34d13c1742c0a Mon Sep 17 00:00:00 2001 From: GregHib Date: Sat, 7 Feb 2026 23:38:57 +0000 Subject: [PATCH 065/101] Introduce ItemView for multi inventory lookups with no copies --- .../main/kotlin/content/bot/fact/Deficit.kt | 84 +++++++++++++++++-- game/src/main/kotlin/content/bot/fact/Fact.kt | 31 +++---- .../kotlin/content/bot/fact/FactParser.kt | 73 +++++++++------- .../main/kotlin/content/bot/fact/Predicate.kt | 57 +++++++------ .../content/bot/fact/RequirementEvaluator.kt | 24 ++---- 5 files changed, 166 insertions(+), 103 deletions(-) diff --git a/game/src/main/kotlin/content/bot/fact/Deficit.kt b/game/src/main/kotlin/content/bot/fact/Deficit.kt index 58783f9b04..4c63bc7ec9 100644 --- a/game/src/main/kotlin/content/bot/fact/Deficit.kt +++ b/game/src/main/kotlin/content/bot/fact/Deficit.kt @@ -5,6 +5,7 @@ import content.bot.action.Resolver import content.entity.player.bank.bank import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.entity.item.Item +import world.gregs.voidps.engine.inv.inventory sealed interface Deficit { fun resolve(player: Player): Resolver? @@ -15,7 +16,66 @@ sealed interface Deficit { } } - data class MissingItem(val filter: Predicate, val needed: Int) : Deficit { + data class MissingEquipment(val entries: List>) : Deficit { + override fun resolve(player: Player): Resolver? { + var spaceNeeded = 0 + val entries = entries.toMutableList() + val actions = mutableListOf() + val uniqueName = StringBuilder() + for (item in player.inventory.items) { + if (item.isEmpty()) { + continue + } + val iterator = entries.iterator() + while (iterator.hasNext()) { + val entry = iterator.next() + if (!entry.test(player, item)) { + continue + } + iterator.remove() + uniqueName.append("_${item.id}") + actions.add(BotAction.InterfaceOption("Wield", "bank:inventory:${item.id}")) + } + } + if (entries.isNotEmpty()) { + actions.add(BotAction.GoToNearest("bank")) + actions.add(BotAction.InteractObject("Use-quickly", "bank_booth*", success = Requirement(Fact.InterfaceOpen("bank"), Predicate.BooleanTrue))) + for (item in player.bank.items) { + if (item.isEmpty()) { + continue + } + val iterator = entries.iterator() + while (iterator.hasNext()) { + val entry = iterator.next() + if (!entry.test(player, item)) { + continue + } + iterator.remove() + spaceNeeded += 1 + uniqueName.append("_${item.id}") + actions.add(BotAction.InterfaceOption("Withdraw-1", "bank:inventory:${item.id}")) + } + } + if (spaceNeeded > 0) { + actions.add(BotAction.CloseInterface) + } + } + if (actions.isNotEmpty()) { + return Resolver( + "withdraw_$uniqueName", weight = 20, + setup = listOf( + Requirement(Fact.InventorySpace, Predicate.IntRange(spaceNeeded)) + ), + actions = actions + ) + } + return null + } + } + + data class MissingInventory(val entries: List) : Deficit { + data class Entry(val filter: Predicate, val needed: Int) + override fun resolve(player: Player): Resolver? { var spaceNeeded = 0 val actions = mutableListOf( @@ -24,16 +84,22 @@ sealed interface Deficit { ) val uniqueName = StringBuilder() for (item in player.bank.items) { - if (item.isEmpty() || !filter.test(player, item)) { + if (item.isEmpty()) { continue } - spaceNeeded += needed - uniqueName.append("_${item.id}") - if (needed == 1 || needed == 5 || needed == 10) { - actions.add(BotAction.InterfaceOption("Withdraw-${needed}", "bank:inventory:${item.id}")) - } else { - BotAction.InterfaceOption("Withdraw-X", "bank:inventory:${item.id}") - BotAction.IntEntry(needed) + for (entry in entries) { + if (!entry.filter.test(player, item)) { + continue + } + val needed = entry.needed + spaceNeeded += needed + uniqueName.append("_${item.id}") + if (needed == 1 || needed == 5 || needed == 10) { + actions.add(BotAction.InterfaceOption("Withdraw-${needed}", "bank:inventory:${item.id}")) + } else { + BotAction.InterfaceOption("Withdraw-X", "bank:inventory:${item.id}") + BotAction.IntEntry(needed) + } } } if (spaceNeeded > 0) { diff --git a/game/src/main/kotlin/content/bot/fact/Fact.kt b/game/src/main/kotlin/content/bot/fact/Fact.kt index f6cc180afd..45683a77d5 100644 --- a/game/src/main/kotlin/content/bot/fact/Fact.kt +++ b/game/src/main/kotlin/content/bot/fact/Fact.kt @@ -36,33 +36,24 @@ sealed class Fact(val priority: Int) { override fun getValue(player: Player) = player.inventory.spaces } - data class InventoryCount(val id: String) : Fact(100) { - override fun keys() = setOf("inv:$id") - override fun groups() = setOf("inv:inventory") - override fun getValue(player: Player) = player.inventory.count(id) - } - - object InventoryItems : Fact>(100) { + object InventoryItems : Fact(100) { override fun keys() = setOf("inv:inventory") - override fun getValue(player: Player) = player.inventory.items + override fun getValue(player: Player) = ItemView(player.inventory) } - data class ItemCount(val id: String) : Fact(100) { - override fun keys() = setOf("inventory:$id", "bank:$id", "worn_equipment:$id") - override fun groups() = setOf("inv:inventory", "inv:bank", "inv:worn_equipment") - override fun getValue(player: Player) = player.inventory.count(id) + player.bank.count(id) + player.equipment.count(id) + object EquipmentItems : Fact(100) { // TODO equipment need lower priority so items are equipped before inventory setup + override fun keys() = setOf("inv:worn_equipment") + override fun getValue(player: Player) = ItemView(player.equipment) } - data class BankCount(val id: String) : Fact(100) { - override fun keys() = setOf("bank:$id") - override fun groups() = setOf("inv:bank") - override fun getValue(player: Player) = player.bank.count(id) + object BankItems : Fact(100) { + override fun keys() = setOf("inv:bank") + override fun getValue(player: Player) = ItemView(player.bank) } - data class EquipCount(val id: String) : Fact(100) { - override fun keys() = setOf("worn_equipment:$id") - override fun groups() = setOf("inv:worn_equipment") - override fun getValue(player: Player) = player.equipment.count(id) + object AllItems : Fact(100) { + override fun keys() = setOf("inv:inventory", "inv:bank", "inv:worn_equipment") + override fun getValue(player: Player) = ItemView(player.inventory, player.bank, player.equipment) } data class IntVariable(val id: String, val default: Int) : Fact(1) { diff --git a/game/src/main/kotlin/content/bot/fact/FactParser.kt b/game/src/main/kotlin/content/bot/fact/FactParser.kt index 929b2af25e..811f528743 100644 --- a/game/src/main/kotlin/content/bot/fact/FactParser.kt +++ b/game/src/main/kotlin/content/bot/fact/FactParser.kt @@ -1,6 +1,7 @@ package content.bot.fact import world.gregs.voidps.engine.entity.item.Item +import world.gregs.voidps.engine.inv.Inventory import world.gregs.voidps.type.Tile sealed class FactParser { @@ -30,42 +31,48 @@ sealed class FactParser { override fun predicate(map: Map) = Predicate.parseInt(map) } - object InventoryItems : FactParser>() { + object InventoryItems : FactParser() { override fun parse(map: Map) = Fact.InventoryItems override fun predicate(map: Map) = null - override fun requirement(list: List>): Requirement> { - return Requirement(Fact.InventoryItems, Predicate.parseItems(list)) + override fun requirement(list: List>): Requirement { + val predicate = Predicate.parseItems(list) + + // TODO how to handle in a more efficient way? + if (predicate.entries.size == 1) { + val entry = predicate.entries.single() + if(entry.count is Predicate.IntRange) { + + } else if(entry.count is Predicate.IntEquals) { + + } + + entry.filter + } + return Requirement(Fact.InventoryItems, predicate) } } - object ItemCount : FactParser() { - override val required = setOf("id") - override fun parse(map: Map) = Fact.ItemCount(map["id"] as String) - override fun predicate(map: Map): Predicate? { - if (!map.containsKey("min") && !map.containsKey("equals")) { - map as MutableMap - map["min"] = 1 - } - return Predicate.parseInt(map) + object EquipmentItems : FactParser() { + override fun parse(map: Map) = Fact.EquipmentItems + override fun predicate(map: Map) = null + override fun requirement(list: List>): Requirement { + return Requirement(Fact.EquipmentItems, Predicate.parseItems(list)) } } - object BankCount : FactParser() { - override val required = setOf("id") - override fun parse(map: Map) = Fact.BankCount(map["id"] as String) - override fun predicate(map: Map) = Predicate.parseInt(map) + object BankedItems : FactParser() { + override fun parse(map: Map) = Fact.BankItems + override fun predicate(map: Map) = null + override fun requirement(list: List>): Requirement { + return Requirement(Fact.BankItems, Predicate.parseItems(list)) + } } - object EquipCount : FactParser() { - override val required = setOf("id") - override fun parse(map: Map) = Fact.EquipCount(map["id"] as String) - override fun predicate(map: Map): Predicate? { - if (!map.containsKey("min") && !map.containsKey("equals")) { - val mutable = map.toMutableMap() - mutable["min"] = 1 - return Predicate.parseInt(mutable) - } - return Predicate.parseInt(map) + object AllItems : FactParser() { + override fun parse(map: Map) = Fact.AllItems + override fun predicate(map: Map) = null + override fun requirement(list: List>): Requirement { + return Requirement(Fact.AllItems, Predicate.parseItems(list)) } } @@ -140,9 +147,9 @@ sealed class FactParser { val parsers = mapOf( "inventory_space" to InventorySpace, "carries" to InventoryItems, - "owns" to ItemCount, - "banked" to BankCount, - "equips" to EquipCount, + "owns" to AllItems, + "banked" to BankedItems, + "equips" to EquipmentItems, "variable" to Variable, "clock" to Clock, "has_timer" to Timer, @@ -155,3 +162,11 @@ sealed class FactParser { ) } } + +class ItemView(vararg val inventories: Inventory) { + fun contains(item: String) = inventories.any { it.contains(item) } + fun contains(item: String, amount: Int) = inventories.any { it.contains(item, amount) } + fun count(item: String) = inventories.sumOf { it.count(item) } + fun count(block: (Item) -> Boolean) = inventories.sumOf { it.items.count(block) } + fun size() = inventories.sumOf { it.size } +} diff --git a/game/src/main/kotlin/content/bot/fact/Predicate.kt b/game/src/main/kotlin/content/bot/fact/Predicate.kt index 6ba8b37e9b..76130d0688 100644 --- a/game/src/main/kotlin/content/bot/fact/Predicate.kt +++ b/game/src/main/kotlin/content/bot/fact/Predicate.kt @@ -15,7 +15,6 @@ sealed class Predicate { open val evaluator: RequirementEvaluator? = null data class IntRange(val min: Int? = null, val max: Int? = null) : Predicate() { - override val evaluator = RequirementEvaluator.IntEvaluator override fun test(player: Player, value: Int): Boolean { if (min != null && value < min) return false if (max != null && value > max) return false @@ -24,7 +23,6 @@ sealed class Predicate { } data class IntEquals(val value: Int) : Predicate() { - override val evaluator = RequirementEvaluator.IntEvaluator override fun test(player: Player, value: Int) = value == this.value } @@ -70,7 +68,7 @@ sealed class Predicate { override fun test(player: Player, value: Tile) = value.within(x, y, level, radius) } - data class InventoryItems(val entries: List) : Predicate>() { + data class InventoryItems(val entries: List) : Predicate() { data class Entry( val filter: Predicate, val count: Predicate, @@ -78,7 +76,7 @@ sealed class Predicate { override val evaluator = RequirementEvaluator.InventoryEval override val children = entries.map { it.count }.toSet() + entries.map { it.filter }.toSet() - override fun test(player: Player, value: Array): Boolean { + override fun test(player: Player, value: ItemView): Boolean { for (entry in entries) { val count = value.count { entry.filter.test(player, it) } if (!entry.count.test(player, count)) { @@ -158,35 +156,40 @@ sealed class Predicate { else -> null } - fun parseItems(items: List>): Predicate> { + fun parseItems(items: List>): InventoryItems { val entries = mutableListOf() for (item in items) { - require(item.containsKey("id")) { "Item must have field 'id' in map $item" } - val id = item["id"] as String - var filter = if (id.contains(",")) { - val ids = id.split(",") - AnyItem(ids.flatMap { id -> - if (id.any { char -> char == '*' || char == '#' }) { - Wildcards.get(id, Wildcard.Item) - } else { - setOf(id) - } - }.toSet()) - } else if (id.any { it == '*' || it == '#' }) { - AnyItem(Wildcards.get(id, Wildcard.Item)) - } else { - EqualsItem(id) - } - if (item.containsKey("usable") && item["usable"] as Boolean) { - // TODO lookup values from custom configs e.g. firemaking.level - filter = AllOf(setOf(filter, UsableItem)) - } else if (item.containsKey("equipable") && item["equipable"] as Boolean) { - filter = AllOf(setOf(filter, EquipableItem)) - } + val filter = itemFilter(item) val counter = parseInt(item) ?: IntRange(min = 1) entries.add(InventoryItems.Entry(filter, counter)) } return InventoryItems(entries) } + + private fun itemFilter(item: Map): Predicate { + require(item.containsKey("id")) { "Item must have field 'id' in map $item" } + val id = item["id"] as String + var filter = if (id.contains(",")) { + val ids = id.split(",") + AnyItem(ids.flatMap { id -> + if (id.any { char -> char == '*' || char == '#' }) { + Wildcards.get(id, Wildcard.Item) + } else { + setOf(id) + } + }.toSet()) + } else if (id.any { it == '*' || it == '#' }) { + AnyItem(Wildcards.get(id, Wildcard.Item)) + } else { + EqualsItem(id) + } + if (item.containsKey("usable") && item["usable"] as Boolean) { + // TODO lookup values from custom configs e.g. firemaking.level + filter = AllOf(setOf(filter, UsableItem)) + } else if (item.containsKey("equipable") && item["equipable"] as Boolean) { + filter = AllOf(setOf(filter, EquipableItem)) + } + return filter + } } } \ No newline at end of file diff --git a/game/src/main/kotlin/content/bot/fact/RequirementEvaluator.kt b/game/src/main/kotlin/content/bot/fact/RequirementEvaluator.kt index d0e2b8ecbd..b3545f4bbb 100644 --- a/game/src/main/kotlin/content/bot/fact/RequirementEvaluator.kt +++ b/game/src/main/kotlin/content/bot/fact/RequirementEvaluator.kt @@ -1,8 +1,8 @@ package content.bot.fact +import content.bot.fact.Deficit.MissingInventory import content.bot.fact.Predicate.IntEquals import world.gregs.voidps.engine.entity.character.player.Player -import world.gregs.voidps.engine.entity.item.Item import world.gregs.voidps.type.Tile sealed class RequirementEvaluator { @@ -20,22 +20,10 @@ sealed class RequirementEvaluator { } } - object IntEvaluator : RequirementEvaluator() { - override fun evaluate(player: Player, fact: Fact, predicate: Predicate): List { - return when (fact) { - is Fact.EquipCount if predicate is Predicate.IntRange -> listOf(Deficit.MissingItem(Predicate.EqualsItem(fact.id), predicate.min!!)) - is Fact.InventoryCount if predicate is Predicate.IntRange -> listOf(Deficit.MissingItem(Predicate.EqualsItem(fact.id), predicate.min!!)) - is Fact.EquipCount if predicate is IntEquals -> listOf(Deficit.MissingItem(Predicate.EqualsItem(fact.id), predicate.value)) - is Fact.InventoryCount if predicate is IntEquals -> listOf(Deficit.MissingItem(Predicate.EqualsItem(fact.id), predicate.value)) - else -> emptyList() - } - } - } - - object InventoryEval : RequirementEvaluator>() { - override fun evaluate(player: Player, fact: Fact>, predicate: Predicate>): List { + object InventoryEval : RequirementEvaluator() { + override fun evaluate(player: Player, fact: Fact, predicate: Predicate): List { if (predicate is Predicate.InventoryItems) { - val deficits = mutableListOf() + val entries = mutableListOf() val value = fact.getValue(player) for (entry in predicate.entries) { val have = value.count { entry.filter.test(player, it) } @@ -48,10 +36,10 @@ sealed class RequirementEvaluator { else -> continue } if (needed > 0) { - deficits += Deficit.MissingItem(entry.filter, needed) + entries += MissingInventory.Entry(entry.filter, needed) } } - return deficits + return listOf(MissingInventory(entries)) } return emptyList() } From da45610ab55c7366c5d6a2b7ac101ce9527ac701 Mon Sep 17 00:00:00 2001 From: GregHib Date: Sun, 8 Feb 2026 10:57:57 +0000 Subject: [PATCH 066/101] Tidy up --- .../src/main/kotlin/content/bot/BotManager.kt | 6 +- .../kotlin/content/bot/action/Behaviour.kt | 418 +++++++++++++++- .../kotlin/content/bot/action/BotAction.kt | 1 - .../kotlin/content/bot/action/BotActivity.kt | 453 ------------------ .../main/kotlin/content/bot/fact/Deficit.kt | 57 ++- game/src/main/kotlin/content/bot/fact/Fact.kt | 2 +- .../kotlin/content/bot/fact/FactParser.kt | 25 +- .../main/kotlin/content/bot/fact/ItemView.kt | 12 + .../main/kotlin/content/bot/fact/Predicate.kt | 10 +- .../content/bot/fact/PredicateParser.kt | 59 --- .../main/kotlin/content/bot/fact/Reference.kt | 9 - .../content/bot/fact/RequirementEvaluator.kt | 44 +- 12 files changed, 498 insertions(+), 598 deletions(-) create mode 100644 game/src/main/kotlin/content/bot/fact/ItemView.kt delete mode 100644 game/src/main/kotlin/content/bot/fact/PredicateParser.kt delete mode 100644 game/src/main/kotlin/content/bot/fact/Reference.kt diff --git a/game/src/main/kotlin/content/bot/BotManager.kt b/game/src/main/kotlin/content/bot/BotManager.kt index e440d82cc7..bbb18b84e9 100644 --- a/game/src/main/kotlin/content/bot/BotManager.kt +++ b/game/src/main/kotlin/content/bot/BotManager.kt @@ -2,13 +2,9 @@ package content.bot import com.github.michaelbull.logging.InlineLogger import content.bot.action.* -import content.bot.fact.Deficit -import content.bot.fact.Fact -import content.bot.fact.Predicate import content.bot.fact.Requirement import content.bot.interact.path.Graph import content.bot.interact.path.Graph.Companion.loadGraph -import content.entity.player.bank.bank import world.gregs.voidps.engine.data.ConfigFiles import world.gregs.voidps.engine.data.Settings import world.gregs.voidps.engine.event.AuditLog @@ -74,7 +70,7 @@ class BotManager( fun load(files: ConfigFiles): BotManager { val shortcuts = mutableListOf() - loadActivities(files, activities, groups, resolvers, shortcuts) + loadBehaviours(files, activities, groups, resolvers, shortcuts) graph = loadGraph(files.list(Settings["bots.nav.definitions"]), shortcuts) return this } diff --git a/game/src/main/kotlin/content/bot/action/Behaviour.kt b/game/src/main/kotlin/content/bot/action/Behaviour.kt index e6ac2b76e9..5e5d5c35a6 100644 --- a/game/src/main/kotlin/content/bot/action/Behaviour.kt +++ b/game/src/main/kotlin/content/bot/action/Behaviour.kt @@ -1,6 +1,12 @@ package content.bot.action import content.bot.fact.Requirement +import it.unimi.dsi.fastutil.objects.ObjectArrayList +import world.gregs.config.Config +import world.gregs.config.ConfigReader +import world.gregs.voidps.engine.data.ConfigFiles +import world.gregs.voidps.engine.data.Settings +import world.gregs.voidps.engine.timedLoad interface Behaviour { val id: String @@ -8,4 +14,414 @@ interface Behaviour { val setup: List> val actions: List val produces: Set> -} \ No newline at end of file +} + +fun loadBehaviours( + files: ConfigFiles, + activities: MutableMap, + groups: MutableMap>, + resolvers: MutableMap>, + shortcuts: MutableList, +) { + val templates = loadTemplates(files.list(Settings["bots.templates"])) + loadActivities(activities, templates, files.list(Settings["bots.definitions"])) + // Group activities by requirement types + for (activity in activities.values) { + for (req in activity.requires) { + for (key in req.fact.groups()) { + groups.getOrPut(key) { mutableListOf() }.add(activity.id) + } + } + } + loadSetups(resolvers, templates, files.list(Settings["bots.setups"])) + loadShortcuts(shortcuts, templates, files.list(Settings["bots.shortcuts"])) +} + +private fun loadActivities(activities: MutableMap, templates: Map, paths: List) { + timedLoad("bot activity") { + val fragments = mutableListOf() + load(paths) { id, template, fields, capacity, requires, setup, actions, produces -> + if (template != null) { + requireNotNull(fields) + fragments.add(Fragment(id, template, fields, capacity, requires, setup, actions, produces)) + } else { + val debug = "$id ${exception()}" + activities[id] = BotActivity(id, capacity, Requirement.parse(requires, debug), Requirement.parse(setup, debug), actions, Requirement.parse(produces, debug, requirePredicates = false).toSet()) + } + } + for (fragment in fragments) { + val template = templates[fragment.template] ?: error("Unable to find template '${fragment.template}' for ${fragment.id}.") + activities[fragment.id] = fragment.activity(template) + } + activities.size + } +} + +private fun loadSetups(resolvers: MutableMap>, templates: Map, paths: List) { + timedLoad("bot setup") { + val fragments = mutableListOf() + load(paths) { id, template, fields, weight, requires, setup, actions, produces -> + if (template != null) { + requireNotNull(fields) + fragments.add(Fragment(id, template, fields, weight, requires, setup, actions, produces)) + } else { + val debug = "$id ${exception()}" + val products = Requirement.parse(produces, debug) + val resolver = Resolver(id, weight, Requirement.parse(requires, debug), Requirement.parse(setup, debug), actions, products.toSet()) + for (product in products) { + for (key in product.fact.keys()) { + resolvers.getOrPut(key) { mutableListOf() }.add(resolver) + } + } + } + } + for (fragment in fragments) { + val template = templates[fragment.template] ?: error("Unable to find template '${fragment.template}' for ${fragment.id}.") + resolvers.getOrPut(fragment.id) { mutableListOf() }.add(fragment.resolver(template)) + } + resolvers.size + } +} + +private fun loadShortcuts(shortcuts: MutableList, templates: Map, paths: List) { + timedLoad("bot shortcut") { + val fragments = mutableListOf() + load(paths) { id, template, fields, weight, requires, setup, actions, produces -> + if (template != null) { + requireNotNull(fields) { "No fields found for $id ${exception()}" } + fragments.add(Fragment(id, template, fields, weight, requires, setup, actions, produces)) + } else { + val debug = "$id ${exception()}" + shortcuts.add(NavigationShortcut(id, weight, Requirement.parse(requires, debug), Requirement.parse(setup, debug), actions, Requirement.parse(produces, debug, requirePredicates = false).toSet())) + } + } + for (fragment in fragments) { + val template = templates[fragment.template] ?: error("Unable to find template '${fragment.template}' for ${fragment.id}.") + shortcuts.add(fragment.shortcut(template)) + } + shortcuts.size + } +} + +private fun load(paths: List, block: ConfigReader.(String, String?, Map?, Int, List>>>, List>>>, List, List>>>) -> Unit) { + for (path in paths) { + Config.fileReader(path) { + while (nextSection()) { + val id = section() + var template: String? = null + var fields: Map? = null + var value = 1 + val requires = mutableListOf>>>() + val setup = mutableListOf>>>() + val actions = mutableListOf() + val produces = mutableListOf>>>() + while (nextPair()) { + when (val key = key()) { + "template" -> template = string() + "requires" -> requirements(requires) + "setup" -> requirements(setup) + "actions" -> actions(actions) + "produces" -> requirements(produces) + "weight", "capacity" -> value = int() + "fields" -> fields = map() + else -> throw IllegalArgumentException("Unexpected key: '$key' ${exception()}") + } + } + if (fields != null && template == null) { + error("Found fields but no template for $id in ${exception()}") + } + block.invoke(this, id, template, fields, value, requires, setup, actions, produces) + } + } + } +} + +private fun loadTemplates(paths: List): Map { + val templates = mutableMapOf() + timedLoad("bot template") { + for (path in paths) { + Config.fileReader(path) { + while (nextSection()) { + val id = section() + val requires = mutableListOf>>>() + val setup = mutableListOf>>>() + val actions = mutableListOf() + val produces = mutableListOf>>>() + while (nextPair()) { + when (val key = key()) { + "requires" -> requirements(requires) + "setup" -> requirements(setup) + "actions" -> actions(actions) + "produces" -> requirements(produces) + else -> throw IllegalArgumentException("Unexpected key: '$key' ${exception()}") + } + } + templates[id] = Template(requires, setup, actions, produces) + } + } + } + templates.size + } + return templates +} + +private fun ConfigReader.requirements(requires: MutableList>>>) { + while (nextElement()) { + while (nextEntry()) { + val key = key() + if (peek == '[') { + val list = ObjectArrayList>() + while (nextElement()) { + list.add(map()) + } + requires.add(key to list) + } else { + requires.add(key to listOf(map())) + } + } + } +} + +private data class Fragment( + val id: String, + val template: String, + val fields: Map, + val int: Int, + val requires: List>>>, + val setup: List>>>, + val actions: List, // Can fragments even have actions? + val produces: List>>>, +) { + fun activity(template: Template) = BotActivity( + id = id, + capacity = int, + requires = resolveRequirements(template.requires, requires), + setup = resolveRequirements(template.setup, setup), + actions = template.actions, + produces = resolveRequirements(template.produces, produces, requirePredicates = false).toSet(), + ) + + private fun resolveRequirements(templated: List>>>, original: List>>>, requirePredicates: Boolean = true): List> { + val combinedList = mutableListOf>>>() + combinedList.addAll(original) + for ((type, list) in templated) { + val resolved = list.map { map -> + map.mapValues { (key, value) -> + if (value is String && value.contains('$')) { + val ref = value.reference() + val name = ref.trim('$', '{', '}') + val replacement = fields[name] ?: error("No field found for behaviour=$id type=${type} key=$key ref=$ref") + if (replacement is String) value.replace(ref, replacement) else replacement + } else value + }.toMap() + } + if (resolved.isNotEmpty()) { + combinedList.add(type to resolved) + } + } + if (combinedList.isEmpty()) { + return emptyList() + } + return Requirement.parse(combinedList, "$id template $template", requirePredicates) + } + + private fun String.reference(): String { + if (startsWith('$')) { + return this + } + val index = indexOf($$"${") + if (index == -1) { + return "\$${substringAfter('$')}" + } + val end = indexOf('}', index) + 1 + return substring(index, end) + } + + fun resolver(template: Template) = Resolver( + id = id, + weight = int, + requires = resolveRequirements(template.requires, requires), + setup = resolveRequirements(template.setup, setup), + actions = template.actions, + produces = resolveRequirements(template.produces, produces, requirePredicates = false).toSet(), + ) + + fun shortcut(template: Template) = NavigationShortcut( + id = id, + weight = int, + requires = resolveRequirements(template.requires, requires), + setup = resolveRequirements(template.setup, setup), + actions = template.actions, + produces = resolveRequirements(template.produces, produces, requirePredicates = false).toSet(), + ) +} + +private data class Template( + val requires: List>>>, + val setup: List>>>, + val actions: List, + val produces: List>>>, +) + +fun ConfigReader.actions(list: MutableList) { + while (nextElement()) { + var type = "" + var id = "" + var option = "" + var int = 0 + var ticks = 0 + var radius = 10 + var delay = 0 + var heal = 0 + var loot = 0 + var x = 0 + var y = 0 + val references = mutableMapOf() + val wait = mutableListOf>>>() + val success = mutableListOf>>>() + while (nextEntry()) { + when (val key = key()) { + "go_to", "go_to_nearest", "enter_string", "interface", "npc", "object", "clone", "item", "continue" -> { + type = key + id = string() + if (id.contains('$')) { + references[key] = id + } + } + "x" -> { + if (type == "") { + type = "tile" + } + val value = value() + if (value is String && value.contains('$')) { + references[key] = value + } else { + x = value as Int + } + } + "y" -> { + if (type == "") { + type = "tile" + } + val value = value() + if (value is String && value.contains('$')) { + references[key] = value + } else { + y = value as Int + } + } + "target", "id" -> { + id = string() + if (id.contains('$')) { + references[key] = id + } + } + "option", "on" -> { + option = string() + if (option.contains('$')) { + references[key] = option + } + } + "on_object" -> { + type = "${type}_on_object" + option = string() + if (option.contains('$')) { + references[key] = option + } + } + "restart" -> { + require(boolean()) { "Can't have restart = false ${exception()}" } + type = key + } + "success" -> while (nextEntry()) { + val key = key() + if (peek == '[') { + val list = mutableListOf>() + while (nextElement()) { + list.add(map()) + } + success.add(key to list) + } else { + success.add(key to listOf(map())) + } + } + "wait_if" -> while (nextElement()) { + while (nextEntry()) { + val key = key() + if (peek == '[') { + val list = mutableListOf>() + while (nextElement()) { + list.add(map()) + } + wait.add(key to list) + } else { + wait.add(key to listOf(map())) + } + } + } + "wait" -> { + type = key + when (val value = value()) { + is Int -> ticks = value + is String if value.contains('$') -> references[key] = value + else -> throw IllegalArgumentException("Invalid '$key' value: $value ${exception()}") + } + } + "radius" -> when (val value = value()) { + is Int -> radius = value + is String if value.contains('$') -> references[key] = value + else -> throw IllegalArgumentException("Invalid '$key' value: $value ${exception()}") + } + "heal_percent" -> when (val value = value()) { + is Int -> heal = value + is String if value.contains('$') -> references[key] = value + else -> throw IllegalArgumentException("Invalid '$key' value: $value ${exception()}") + } + "loot_over" -> when (val value = value()) { + is Int -> loot = value + is String if value.contains('$') -> references[key] = value + else -> throw IllegalArgumentException("Invalid '$key' value: $value ${exception()}") + } + "delay" -> when (val value = value()) { + is Int -> delay = value + is String if value.contains('$') -> references[key] = value + else -> throw IllegalArgumentException("Invalid '$key' value: $value ${exception()}") + } + "enter_int" -> { + type = key + when (val value = value()) { + is Int -> int = value + is String if value.contains('$') -> references[key] = value + else -> throw IllegalArgumentException("Invalid '$key' value: $value ${exception()}") + } + } + else -> throw IllegalArgumentException("Unknown action key: $key ${exception()}") + } + } + var action = when (type) { + "go_to" -> BotAction.GoTo(id) + "go_to_nearest" -> BotAction.GoToNearest(id) + "enter_string" -> BotAction.StringEntry(id) + "enter_int" -> BotAction.IntEntry(int) + "wait" -> BotAction.Wait(ticks) + "restart" -> BotAction.Restart(Requirement.parse(wait, id), Requirement.parse(success, id).singleOrNull() ?: throw IllegalArgumentException("Restart must have success condition. $id ${exception()}")) + "npc" -> if (option == "Attack") { + BotAction.FightNpc(id = id, delay = delay, success = Requirement.parse(success, id).singleOrNull(), healPercentage = heal, lootOverValue = loot, radius = radius) + } else { + BotAction.InteractNpc(id = id, option = option, delay = delay, success = Requirement.parse(success, id).singleOrNull(), radius = radius) + } + "tile" -> BotAction.WalkTo(x = x, y = y) + "object" -> BotAction.InteractObject(id = id, option = option, delay = delay, success = Requirement.parse(success, id).singleOrNull(), radius = radius) + "interface" -> BotAction.InterfaceOption(id = id, option = option, success = Requirement.parse(success, id).singleOrNull()) + "continue" -> BotAction.DialogueContinue(id = id, option = option, success = Requirement.parse(success, id).singleOrNull()) + "item" -> BotAction.ItemOnItem(item = id, on = option, success = Requirement.parse(success, id).singleOrNull()) + "item_on_object" -> BotAction.ItemOnObject(item = id, id = option, success = Requirement.parse(success, id).singleOrNull()) + "clone" -> BotAction.Clone(id) + else -> throw IllegalArgumentException("Unknown action type: $type ${exception()}") + } + if (references.isNotEmpty()) { + action = BotAction.Reference(action, references) + } + list.add(action) + } +} diff --git a/game/src/main/kotlin/content/bot/action/BotAction.kt b/game/src/main/kotlin/content/bot/action/BotAction.kt index a5b43ef77a..45ef671759 100644 --- a/game/src/main/kotlin/content/bot/action/BotAction.kt +++ b/game/src/main/kotlin/content/bot/action/BotAction.kt @@ -534,7 +534,6 @@ sealed interface BotAction { /** * TODO - * Split edges into separate files * combat training dummy bots * firemaking bot * rune mysteries quest bot diff --git a/game/src/main/kotlin/content/bot/action/BotActivity.kt b/game/src/main/kotlin/content/bot/action/BotActivity.kt index b51ced7b00..7e18ebd3bf 100644 --- a/game/src/main/kotlin/content/bot/action/BotActivity.kt +++ b/game/src/main/kotlin/content/bot/action/BotActivity.kt @@ -21,456 +21,3 @@ data class BotActivity( override val actions: List = emptyList(), override val produces: Set> = emptySet(), ) : Behaviour - -fun loadActivities( - files: ConfigFiles, - activities: MutableMap, - groups: MutableMap>, - resolvers: MutableMap>, - shortcuts: MutableList, -) { - val templates = loadTemplates(files.list(Settings["bots.templates"])) - loadActivities(activities, templates, files.list(Settings["bots.definitions"])) - // Group activities by requirement types - for (activity in activities.values) { - for (req in activity.requires) { - for (key in req.fact.groups()) { - groups.getOrPut(key) { mutableListOf() }.add(activity.id) - } - } - } - loadSetups(resolvers, templates, files.list(Settings["bots.setups"])) - loadShortcuts(shortcuts, templates, files.list(Settings["bots.shortcuts"])) -} - -private fun loadActivities(activities: MutableMap, templates: Map, paths: List) { - timedLoad("bot activity") { - val fragments = mutableListOf() - for (path in paths) { - Config.fileReader(path) { - while (nextSection()) { - val id = section() - var template: String? = null - var fields: Map? = null - var capacity = 1 - val requires = mutableListOf>>>() - val setup = mutableListOf>>>() - val actions = mutableListOf() - val produces = mutableListOf>>>() - while (nextPair()) { - when (val key = key()) { - "template" -> template = string() - "requires" -> requirements(requires) - "setup" -> requirements(setup) - "actions" -> actions(actions) - "produces" -> requirements(produces) - "capacity" -> capacity = int() - "fields" -> fields = map() - else -> throw IllegalArgumentException("Unexpected key: '$key' ${exception()}") - } - } - if (template != null) { - requireNotNull(fields) - fragments.add(Fragment(id, template, fields, capacity, requires, setup, actions, produces)) - } else { - val debug = "$id ${exception()}" - activities[id] = BotActivity(id, capacity, Requirement.parse(requires, debug), Requirement.parse(setup, debug), actions, Requirement.parse(produces, debug, requirePredicates = false).toSet()) - } - } - } - } - - for (fragment in fragments) { - val template = templates[fragment.template] ?: error("Unable to find template '${fragment.template}' for ${fragment.id}.") - activities[fragment.id] = fragment.activity(template) - } - activities.size - } -} - -private fun loadSetups(resolvers: MutableMap>, templates: Map, paths: List) { - timedLoad("bot setup") { - val fragments = mutableListOf() - for (path in paths) { - Config.fileReader(path) { - while (nextSection()) { - val id = section() - var template: String? = null - var fields: Map? = null - var weight = 1 - val requires = mutableListOf>>>() - val setup = mutableListOf>>>() - val actions = mutableListOf() - val produces = mutableListOf>>>() - while (nextPair()) { - when (val key = key()) { - "template" -> template = string() - "requires" -> requirements(requires) - "setup" -> requirements(setup) - "actions" -> actions(actions) - "produces" -> requirements(produces) - "weight" -> weight = int() - "fields" -> fields = map() - else -> throw IllegalArgumentException("Unexpected key: '$key' ${exception()}") - } - } - if (template != null) { - requireNotNull(fields) - fragments.add(Fragment(id, template, fields, weight, requires, setup, actions, produces)) - } else { - val debug = "$id ${exception()}" - val products = Requirement.parse(produces, debug) - val resolver = Resolver(id, weight, Requirement.parse(requires, debug), Requirement.parse(setup, debug), actions, products.toSet()) - for (product in products) { - for (key in product.fact.keys()) { - resolvers.getOrPut(key) { mutableListOf() }.add(resolver) - } - } - } - } - } - } - - for (fragment in fragments) { - val template = templates[fragment.template] ?: error("Unable to find template '${fragment.template}' for ${fragment.id}.") - resolvers.getOrPut(fragment.id) { mutableListOf() }.add(fragment.resolver(template)) - } - resolvers.size - } -} - -private fun loadShortcuts(shortcuts: MutableList, templates: Map, paths: List) { - timedLoad("bot shortcut") { - val fragments = mutableListOf() - for (path in paths) { - Config.fileReader(path) { - while (nextSection()) { - val id = section() - var template: String? = null - var fields: Map? = null - var weight = 1 - val requires = mutableListOf>>>() - val setup = mutableListOf>>>() - val actions = mutableListOf() - val produces = mutableListOf>>>() - while (nextPair()) { - when (val key = key()) { - "template" -> template = string() - "requires" -> requirements(requires) - "setup" -> requirements(setup) - "actions" -> actions(actions) - "produces" -> requirements(produces) - "weight" -> weight = int() - "fields" -> fields = map() - else -> throw IllegalArgumentException("Unexpected key: '$key' ${exception()}") - } - } - if (fields != null && template == null) { - error("Found fields but no template for $id in ${exception()}") - } else if (template != null) { - requireNotNull(fields) { "No fields found for $id ${exception()}" } - fragments.add(Fragment(id, template, fields, weight, requires, setup, actions, produces)) - } else { - val debug = "$id ${exception()}" - shortcuts.add(NavigationShortcut(id, weight, Requirement.parse(requires, debug), Requirement.parse(setup, debug), actions, Requirement.parse(produces, debug, requirePredicates = false).toSet())) - } - } - } - } - for (fragment in fragments) { - val template = templates[fragment.template] ?: error("Unable to find template '${fragment.template}' for ${fragment.id}.") - shortcuts.add(fragment.shortcut(template)) - } - shortcuts.size - } -} - -private fun loadTemplates(paths: List): Map { - val templates = mutableMapOf() - timedLoad("bot template") { - for (path in paths) { - Config.fileReader(path) { - while (nextSection()) { - val id = section() - val requires = mutableListOf>>>() - val setup = mutableListOf>>>() - val actions = mutableListOf() - val produces = mutableListOf>>>() - while (nextPair()) { - when (val key = key()) { - "requires" -> requirements(requires) - "setup" -> requirements(setup) - "actions" -> actions(actions) - "produces" -> requirements(produces) - else -> throw IllegalArgumentException("Unexpected key: '$key' ${exception()}") - } - } - templates[id] = Template(requires, setup, actions, produces) - } - } - } - templates.size - } - return templates -} - -private fun ConfigReader.requirements(requires: MutableList>>>) { - while (nextElement()) { - while (nextEntry()) { - val key = key() - if (peek == '[') { - val list = ObjectArrayList>() - while (nextElement()) { - list.add(map()) - } - requires.add(key to list) - } else { - requires.add(key to listOf(map())) - } - } - } -} - -private data class Fragment( - val id: String, - val template: String, - val fields: Map, - val int: Int, - val requires: List>>>, - val setup: List>>>, - val actions: List, // Can fragments even have actions? - val produces: List>>>, -) { - fun activity(template: Template) = BotActivity( - id = id, - capacity = int, - requires = resolveRequirements(template.requires, requires), - setup = resolveRequirements(template.setup, setup), - actions = template.actions, - produces = resolveRequirements(template.produces, produces, requirePredicates = false).toSet(), - ) - - private fun resolveRequirements(templated: List>>>, original: List>>>, requirePredicates: Boolean = true): List> { - val combinedList = mutableListOf>>>() - combinedList.addAll(original) - for ((type, list) in templated) { - val resolved = list.map { map -> - map.mapValues { (key, value) -> - if (value is String && value.contains('$')) { - val ref = value.reference() - val name = ref.trim('$', '{', '}') - val replacement = fields[name] ?: error("No field found for behaviour=$id type=${type} key=$key ref=$ref") - if (replacement is String) value.replace(ref, replacement) else replacement - } else value - }.toMap() - } - if (resolved.isNotEmpty()) { - combinedList.add(type to resolved) - } - } - if (combinedList.isEmpty()) { - return emptyList() - } - return Requirement.parse(combinedList, "$id template $template", requirePredicates) - } - - private fun String.reference(): String { - if (startsWith('$')) { - return this - } - val index = indexOf($$"${") - if (index == -1) { - return "\$${substringAfter('$')}" - } - val end = indexOf('}', index) + 1 - return substring(index, end) - } - - fun resolver(template: Template) = Resolver( - id = id, - weight = int, - requires = resolveRequirements(template.requires, requires), - setup = resolveRequirements(template.setup, setup), - actions = template.actions, - produces = resolveRequirements(template.produces, produces, requirePredicates = false).toSet(), - ) - - fun shortcut(template: Template) = NavigationShortcut( - id = id, - weight = int, - requires = resolveRequirements(template.requires, requires), - setup = resolveRequirements(template.setup, setup), - actions = template.actions, - produces = resolveRequirements(template.produces, produces, requirePredicates = false).toSet(), - ) -} - -private data class Template( - val requires: List>>>, - val setup: List>>>, - val actions: List, - val produces: List>>>, -) - -fun ConfigReader.actions(list: MutableList) { - while (nextElement()) { - var type = "" - var id = "" - var option = "" - var int = 0 - var ticks = 0 - var radius = 10 - var delay = 0 - var heal = 0 - var loot = 0 - var x = 0 - var y = 0 - val references = mutableMapOf() - val wait = mutableListOf>>>() - val success = mutableListOf>>>() - while (nextEntry()) { - when (val key = key()) { - "go_to", "go_to_nearest", "enter_string", "interface", "npc", "object", "clone", "item", "continue" -> { - type = key - id = string() - if (id.contains('$')) { - references[key] = id - } - } - "x" -> { - if (type == "") { - type = "tile" - } - val value = value() - if (value is String && value.contains('$')) { - references[key] = value - } else { - x = value as Int - } - } - "y" -> { - if (type == "") { - type = "tile" - } - val value = value() - if (value is String && value.contains('$')) { - references[key] = value - } else { - y = value as Int - } - } - "target", "id" -> { - id = string() - if (id.contains('$')) { - references[key] = id - } - } - "option", "on" -> { - option = string() - if (option.contains('$')) { - references[key] = option - } - } - "on_object" -> { - type = "${type}_on_object" - option = string() - if (option.contains('$')) { - references[key] = option - } - } - "restart" -> { - require(boolean()) { "Can't have restart = false ${exception()}" } - type = key - } - "success" -> while (nextEntry()) { - val key = key() - if (peek == '[') { - val list = mutableListOf>() - while (nextElement()) { - list.add(map()) - } - success.add(key to list) - } else { - success.add(key to listOf(map())) - } - } - "wait_if" -> while (nextElement()) { - while (nextEntry()) { - val key = key() - if (peek == '[') { - val list = mutableListOf>() - while (nextElement()) { - list.add(map()) - } - wait.add(key to list) - } else { - wait.add(key to listOf(map())) - } - } - } - "wait" -> { - type = key - when (val value = value()) { - is Int -> ticks = value - is String if value.contains('$') -> references[key] = value - else -> throw IllegalArgumentException("Invalid '$key' value: $value ${exception()}") - } - } - "radius" -> when (val value = value()) { - is Int -> radius = value - is String if value.contains('$') -> references[key] = value - else -> throw IllegalArgumentException("Invalid '$key' value: $value ${exception()}") - } - "heal_percent" -> when (val value = value()) { - is Int -> heal = value - is String if value.contains('$') -> references[key] = value - else -> throw IllegalArgumentException("Invalid '$key' value: $value ${exception()}") - } - "loot_over" -> when (val value = value()) { - is Int -> loot = value - is String if value.contains('$') -> references[key] = value - else -> throw IllegalArgumentException("Invalid '$key' value: $value ${exception()}") - } - "delay" -> when (val value = value()) { - is Int -> delay = value - is String if value.contains('$') -> references[key] = value - else -> throw IllegalArgumentException("Invalid '$key' value: $value ${exception()}") - } - "enter_int" -> { - type = key - when (val value = value()) { - is Int -> int = value - is String if value.contains('$') -> references[key] = value - else -> throw IllegalArgumentException("Invalid '$key' value: $value ${exception()}") - } - } - else -> throw IllegalArgumentException("Unknown action key: $key ${exception()}") - } - } - var action = when (type) { - "go_to" -> BotAction.GoTo(id) - "go_to_nearest" -> BotAction.GoToNearest(id) - "enter_string" -> BotAction.StringEntry(id) - "enter_int" -> BotAction.IntEntry(int) - "wait" -> BotAction.Wait(ticks) - "restart" -> BotAction.Restart(Requirement.parse(wait, id), Requirement.parse(success, id).singleOrNull() ?: throw IllegalArgumentException("Restart must have success condition. $id ${exception()}")) - "npc" -> if (option == "Attack") { - BotAction.FightNpc(id = id, delay = delay, success = Requirement.parse(success, id).singleOrNull(), healPercentage = heal, lootOverValue = loot, radius = radius) - } else { - BotAction.InteractNpc(id = id, option = option, delay = delay, success = Requirement.parse(success, id).singleOrNull(), radius = radius) - } - "tile" -> BotAction.WalkTo(x = x, y = y) - "object" -> BotAction.InteractObject(id = id, option = option, delay = delay, success = Requirement.parse(success, id).singleOrNull(), radius = radius) - "interface" -> BotAction.InterfaceOption(id = id, option = option, success = Requirement.parse(success, id).singleOrNull()) - "continue" -> BotAction.DialogueContinue(id = id, option = option, success = Requirement.parse(success, id).singleOrNull()) - "item" -> BotAction.ItemOnItem(item = id, on = option, success = Requirement.parse(success, id).singleOrNull()) - "item_on_object" -> BotAction.ItemOnObject(item = id, id = option, success = Requirement.parse(success, id).singleOrNull()) - "clone" -> BotAction.Clone(id) - else -> throw IllegalArgumentException("Unknown action type: $type ${exception()}") - } - if (references.isNotEmpty()) { - action = BotAction.Reference(action, references) - } - list.add(action) - } -} diff --git a/game/src/main/kotlin/content/bot/fact/Deficit.kt b/game/src/main/kotlin/content/bot/fact/Deficit.kt index 4c63bc7ec9..3a0fe5d715 100644 --- a/game/src/main/kotlin/content/bot/fact/Deficit.kt +++ b/game/src/main/kotlin/content/bot/fact/Deficit.kt @@ -7,6 +7,9 @@ import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.entity.item.Item import world.gregs.voidps.engine.inv.inventory +/** + * Missing setup [Requirement]'s which can be produce dynamic [Resolver]'s + */ sealed interface Deficit { fun resolve(player: Player): Resolver? @@ -18,7 +21,6 @@ sealed interface Deficit { data class MissingEquipment(val entries: List>) : Deficit { override fun resolve(player: Player): Resolver? { - var spaceNeeded = 0 val entries = entries.toMutableList() val actions = mutableListOf() val uniqueName = StringBuilder() @@ -37,29 +39,7 @@ sealed interface Deficit { actions.add(BotAction.InterfaceOption("Wield", "bank:inventory:${item.id}")) } } - if (entries.isNotEmpty()) { - actions.add(BotAction.GoToNearest("bank")) - actions.add(BotAction.InteractObject("Use-quickly", "bank_booth*", success = Requirement(Fact.InterfaceOpen("bank"), Predicate.BooleanTrue))) - for (item in player.bank.items) { - if (item.isEmpty()) { - continue - } - val iterator = entries.iterator() - while (iterator.hasNext()) { - val entry = iterator.next() - if (!entry.test(player, item)) { - continue - } - iterator.remove() - spaceNeeded += 1 - uniqueName.append("_${item.id}") - actions.add(BotAction.InterfaceOption("Withdraw-1", "bank:inventory:${item.id}")) - } - } - if (spaceNeeded > 0) { - actions.add(BotAction.CloseInterface) - } - } + val spaceNeeded = withdraw(actions, player, entries, uniqueName) if (actions.isNotEmpty()) { return Resolver( "withdraw_$uniqueName", weight = 20, @@ -71,6 +51,35 @@ sealed interface Deficit { } return null } + + private fun withdraw(actions: MutableList, player: Player, entries: MutableList>, uniqueName: StringBuilder): Int { + if (entries.isEmpty()) { + return 0 + } + var spaceNeeded = 0 + actions.add(BotAction.GoToNearest("bank")) + actions.add(BotAction.InteractObject("Use-quickly", "bank_booth*", success = Requirement(Fact.InterfaceOpen("bank"), Predicate.BooleanTrue))) + for (item in player.bank.items) { + if (item.isEmpty()) { + continue + } + val iterator = entries.iterator() + while (iterator.hasNext()) { + val entry = iterator.next() + if (!entry.test(player, item)) { + continue + } + iterator.remove() + spaceNeeded++ + uniqueName.append("_${item.id}") + actions.add(BotAction.InterfaceOption("Withdraw-1", "bank:inventory:${item.id}")) + } + } + if (spaceNeeded > 0) { + actions.add(BotAction.CloseInterface) + } + return spaceNeeded + } } data class MissingInventory(val entries: List) : Deficit { diff --git a/game/src/main/kotlin/content/bot/fact/Fact.kt b/game/src/main/kotlin/content/bot/fact/Fact.kt index 45683a77d5..7e1c2fc1be 100644 --- a/game/src/main/kotlin/content/bot/fact/Fact.kt +++ b/game/src/main/kotlin/content/bot/fact/Fact.kt @@ -41,7 +41,7 @@ sealed class Fact(val priority: Int) { override fun getValue(player: Player) = ItemView(player.inventory) } - object EquipmentItems : Fact(100) { // TODO equipment need lower priority so items are equipped before inventory setup + object EquipmentItems : Fact(90) { override fun keys() = setOf("inv:worn_equipment") override fun getValue(player: Player) = ItemView(player.equipment) } diff --git a/game/src/main/kotlin/content/bot/fact/FactParser.kt b/game/src/main/kotlin/content/bot/fact/FactParser.kt index 811f528743..9ad71ee0c3 100644 --- a/game/src/main/kotlin/content/bot/fact/FactParser.kt +++ b/game/src/main/kotlin/content/bot/fact/FactParser.kt @@ -1,7 +1,5 @@ package content.bot.fact -import world.gregs.voidps.engine.entity.item.Item -import world.gregs.voidps.engine.inv.Inventory import world.gregs.voidps.type.Tile sealed class FactParser { @@ -35,20 +33,7 @@ sealed class FactParser { override fun parse(map: Map) = Fact.InventoryItems override fun predicate(map: Map) = null override fun requirement(list: List>): Requirement { - val predicate = Predicate.parseItems(list) - - // TODO how to handle in a more efficient way? - if (predicate.entries.size == 1) { - val entry = predicate.entries.single() - if(entry.count is Predicate.IntRange) { - - } else if(entry.count is Predicate.IntEquals) { - - } - - entry.filter - } - return Requirement(Fact.InventoryItems, predicate) + return Requirement(Fact.InventoryItems, Predicate.parseItems(list)) } } @@ -162,11 +147,3 @@ sealed class FactParser { ) } } - -class ItemView(vararg val inventories: Inventory) { - fun contains(item: String) = inventories.any { it.contains(item) } - fun contains(item: String, amount: Int) = inventories.any { it.contains(item, amount) } - fun count(item: String) = inventories.sumOf { it.count(item) } - fun count(block: (Item) -> Boolean) = inventories.sumOf { it.items.count(block) } - fun size() = inventories.sumOf { it.size } -} diff --git a/game/src/main/kotlin/content/bot/fact/ItemView.kt b/game/src/main/kotlin/content/bot/fact/ItemView.kt new file mode 100644 index 0000000000..c1c727bc26 --- /dev/null +++ b/game/src/main/kotlin/content/bot/fact/ItemView.kt @@ -0,0 +1,12 @@ +package content.bot.fact + +import world.gregs.voidps.engine.entity.item.Item +import world.gregs.voidps.engine.inv.Inventory + +class ItemView(vararg val inventories: Inventory) { + fun contains(item: String) = inventories.any { it.contains(item) } + fun contains(item: String, amount: Int) = inventories.any { it.contains(item, amount) } + fun count(item: String) = inventories.sumOf { it.count(item) } + fun count(block: (Item) -> Boolean) = inventories.sumOf { it.items.count(block) } + fun size() = inventories.sumOf { it.size } +} diff --git a/game/src/main/kotlin/content/bot/fact/Predicate.kt b/game/src/main/kotlin/content/bot/fact/Predicate.kt index 76130d0688..8e4c8f60b4 100644 --- a/game/src/main/kotlin/content/bot/fact/Predicate.kt +++ b/game/src/main/kotlin/content/bot/fact/Predicate.kt @@ -71,15 +71,15 @@ sealed class Predicate { data class InventoryItems(val entries: List) : Predicate() { data class Entry( val filter: Predicate, - val count: Predicate, + val amount: Predicate, ) override val evaluator = RequirementEvaluator.InventoryEval - override val children = entries.map { it.count }.toSet() + entries.map { it.filter }.toSet() + override val children = entries.map { it.amount }.toSet() + entries.map { it.filter }.toSet() override fun test(player: Player, value: ItemView): Boolean { - for (entry in entries) { - val count = value.count { entry.filter.test(player, it) } - if (!entry.count.test(player, count)) { + for ((filter, amount) in entries) { + val count = value.count { item -> filter.test(player, item) } + if (!amount.test(player, count)) { return false } } diff --git a/game/src/main/kotlin/content/bot/fact/PredicateParser.kt b/game/src/main/kotlin/content/bot/fact/PredicateParser.kt deleted file mode 100644 index 382b0561a3..0000000000 --- a/game/src/main/kotlin/content/bot/fact/PredicateParser.kt +++ /dev/null @@ -1,59 +0,0 @@ -package content.bot.fact - -import world.gregs.voidps.type.Tile - -sealed class PredicateParser { - open val required: Set = emptySet() - open val optional: Set = emptySet() - abstract fun parse(map: Map): Predicate? - - object IntegerParser : PredicateParser() { - override val optional = setOf("min", "max", "equals") - - override fun parse(map: Map): Predicate? { - if (map.containsKey("min") || map.containsKey("max")) { - return Predicate.IntRange(map["min"] as? Int, map["max"] as? Int) - } else if (map.containsKey("equals")) { - return Predicate.IntEquals(map["equals"] as Int) - } - return null - } - } - - object BooleanParser : PredicateParser() { - override val required = setOf("equals") - - override fun parse(map: Map) = if (map["equals"] as Boolean) { - Predicate.BooleanTrue - } else { - Predicate.BooleanFalse - } - } - - object TileParser : PredicateParser() { - override val optional = setOf("x", "y", "level") - - override fun parse(map: Map): Predicate { - return Predicate.TileEquals(map["x"] as? Int, map["y"] as? Int, map["level"] as? Int) - } - } - - companion object { - val parsers = mapOf( - "inventory_space" to IntegerParser, - "inventory" to IntegerParser, - "carries" to IntegerParser, - "banked" to IntegerParser, - "equips" to IntegerParser, - "variable" to IntegerParser, - "clock" to IntegerParser, - "interface_open" to BooleanParser, - "has_timer" to BooleanParser, - "has_queue" to BooleanParser, - "tile" to TileParser, - "combat" to IntegerParser, - "skill" to IntegerParser, - ) - } -} - diff --git a/game/src/main/kotlin/content/bot/fact/Reference.kt b/game/src/main/kotlin/content/bot/fact/Reference.kt deleted file mode 100644 index 1325beaa2f..0000000000 --- a/game/src/main/kotlin/content/bot/fact/Reference.kt +++ /dev/null @@ -1,9 +0,0 @@ -package content.bot.fact - -sealed interface Value { - fun resolve(context: Map): T -} - -data class Literal(val value: T) - -data class Ref(val key: String) \ No newline at end of file diff --git a/game/src/main/kotlin/content/bot/fact/RequirementEvaluator.kt b/game/src/main/kotlin/content/bot/fact/RequirementEvaluator.kt index b3545f4bbb..4c41c8d265 100644 --- a/game/src/main/kotlin/content/bot/fact/RequirementEvaluator.kt +++ b/game/src/main/kotlin/content/bot/fact/RequirementEvaluator.kt @@ -3,8 +3,12 @@ package content.bot.fact import content.bot.fact.Deficit.MissingInventory import content.bot.fact.Predicate.IntEquals import world.gregs.voidps.engine.entity.character.player.Player +import world.gregs.voidps.engine.entity.item.Item import world.gregs.voidps.type.Tile +/** + * Evaluates [Requirement]'s to produce known [Deficit]'s + */ sealed class RequirementEvaluator { abstract fun evaluate(player: Player, fact: Fact, predicate: Predicate): List @@ -22,26 +26,34 @@ sealed class RequirementEvaluator { object InventoryEval : RequirementEvaluator() { override fun evaluate(player: Player, fact: Fact, predicate: Predicate): List { - if (predicate is Predicate.InventoryItems) { + if (fact is Fact.InventoryItems && predicate is Predicate.InventoryItems) { val entries = mutableListOf() - val value = fact.getValue(player) - for (entry in predicate.entries) { - val have = value.count { entry.filter.test(player, it) } - if (entry.count.test(player, have)) { - continue - } - val needed = when (entry.count) { - is Predicate.IntRange -> entry.count.min!! - have - is IntEquals -> entry.count.value - have - else -> continue - } - if (needed > 0) { - entries += MissingInventory.Entry(entry.filter, needed) - } - } + collect(player, fact, predicate) { filter, needed -> entries += MissingInventory.Entry(filter, needed) } return listOf(MissingInventory(entries)) + } else if (fact is Fact.EquipmentItems && predicate is Predicate.InventoryItems) { + val entries = mutableListOf>() + collect(player, fact, predicate) { filter, _ -> entries += filter } + return listOf(Deficit.MissingEquipment(entries)) } return emptyList() } + + private fun collect(player: Player, fact: Fact, predicate: Predicate.InventoryItems, block: (Predicate, Int) -> Unit) { + val value = fact.getValue(player) + for (entry in predicate.entries) { + val have = value.count { entry.filter.test(player, it) } + if (entry.amount.test(player, have)) { + continue + } + val needed = when (entry.amount) { + is Predicate.IntRange -> entry.amount.min!! - have + is IntEquals -> entry.amount.value - have + else -> continue + } + if (needed > 0) { + block.invoke(entry.filter, needed) + } + } + } } } \ No newline at end of file From a534ed9bbd5fc74f14af5531d33af05dfc085247 Mon Sep 17 00:00:00 2001 From: GregHib Date: Sun, 8 Feb 2026 12:06:23 +0000 Subject: [PATCH 067/101] Add ActionParser with new format --- data/bot/al_kharid.nav-edges.toml | 8 +- data/bot/bank.setups.toml | 6 +- data/bot/combat.templates.toml | 8 +- data/bot/cooking.templates.toml | 18 +- data/bot/fletching.templates.toml | 24 +- data/bot/lumbridge.nav-edges.toml | 42 +-- data/bot/mining.templates.toml | 16 +- data/bot/prayer.bots.toml | 4 +- data/bot/shop.templates.toml | 8 +- data/bot/teleport.shortcuts.toml | 22 +- data/bot/thieving.templates.toml | 7 +- data/bot/varrock.nav-edges.toml | 10 +- data/bot/walking.setups.toml | 2 +- data/bot/woodcutting.templates.toml | 8 +- .../kotlin/content/bot/action/ActionParser.kt | 190 ++++++++++++ .../kotlin/content/bot/action/Behaviour.kt | 271 ++++++------------ .../kotlin/content/bot/action/BotAction.kt | 1 - .../kotlin/content/bot/interact/path/Graph.kt | 23 +- 18 files changed, 375 insertions(+), 293 deletions(-) create mode 100644 game/src/main/kotlin/content/bot/action/ActionParser.kt diff --git a/data/bot/al_kharid.nav-edges.toml b/data/bot/al_kharid.nav-edges.toml index c66ba35190..f4c9bf251f 100644 --- a/data/bot/al_kharid.nav-edges.toml +++ b/data/bot/al_kharid.nav-edges.toml @@ -10,10 +10,10 @@ edges = [ { from_x = 3278, from_y = 3228, to_x = 3268, to_y = 3228 }, # al_kharid_crossroad_to_tollgate_north { from_x = 3278, from_y = 3228, to_x = 3268, to_y = 3227 }, # al_kharid_crossroad_to_tollgate { from_x = 3278, from_y = 3228, to_x = 3280, to_y = 3216 }, # al_kharid_crossroad_to_glider - { from_x = 3267, from_y = 3228, to_x = 3268, to_y = 3228, cost = 1, actions = [{ option = "Pay-toll(10gp)", object = "toll_gate_al_kharid_north", x = 3268, y = 3228 }], conditions = [{ carries = [{ id = "coins", amount = 10 }] }] }, # lumbridge_tollgate_north_to_al_kharid_tollgate - { from_x = 3268, from_y = 3228, to_x = 3267, to_y = 3228, cost = 1, actions = [{ option = "Pay-toll(10gp)", object = "toll_gate_al_kharid_north", x = 3268, y = 3228 }], conditions = [{ carries = [{ id = "coins", amount = 10 }] }] }, # al_kharid_tollgate_north_to_lumbridge_tollgate - { from_x = 3267, from_y = 3227, to_x = 3268, to_y = 3227, cost = 1, actions = [{ option = "Pay-toll(10gp)", object = "toll_gate_al_kharid_south", x = 3268, y = 3227 }], conditions = [{ carries = [{ id = "coins", amount = 10 }] }] }, # lumbridge_tollgate_to_al_kharid - { from_x = 3268, from_y = 3227, to_x = 3267, to_y = 3227, cost = 1, actions = [{ option = "Pay-toll(10gp)", object = "toll_gate_al_kharid_south", x = 3268, y = 3227 }], conditions = [{ carries = [{ id = "coins", amount = 10 }] }] }, # al_kharid_tollgate_to_lumbridge + { from_x = 3267, from_y = 3228, to_x = 3268, to_y = 3228, cost = 1, actions = [{ object = { option = "Pay-toll(10gp)", id = "toll_gate_al_kharid_north", x = 3268, y = 3228 } }], conditions = [{ carries = [{ id = "coins", amount = 10 }] }] }, # lumbridge_tollgate_north_to_al_kharid_tollgate + { from_x = 3268, from_y = 3228, to_x = 3267, to_y = 3228, cost = 1, actions = [{ object = { option = "Pay-toll(10gp)", id = "toll_gate_al_kharid_north", x = 3268, y = 3228 } }], conditions = [{ carries = [{ id = "coins", amount = 10 }] }] }, # al_kharid_tollgate_north_to_lumbridge_tollgate + { from_x = 3267, from_y = 3227, to_x = 3268, to_y = 3227, cost = 1, actions = [{ object = { option = "Pay-toll(10gp)", id = "toll_gate_al_kharid_south", x = 3268, y = 3227 } }], conditions = [{ carries = [{ id = "coins", amount = 10 }] }] }, # lumbridge_tollgate_to_al_kharid + { from_x = 3268, from_y = 3227, to_x = 3267, to_y = 3227, cost = 1, actions = [{ object = { option = "Pay-toll(10gp)", id = "toll_gate_al_kharid_south", x = 3268, y = 3227 } }], conditions = [{ carries = [{ id = "coins", amount = 10 }] }] }, # al_kharid_tollgate_to_lumbridge { from_x = 3268, from_y = 3227, to_x = 3280, to_y = 3216 }, # al_kharid_tollgate_to_glider { from_x = 3268, from_y = 3228, to_x = 3280, to_y = 3216 }, # al_kharid_tollgate_north_to_glider { from_x = 3280, from_y = 3216, to_x = 3292, to_y = 3215 }, # al_kharid_glider_to_musician diff --git a/data/bot/bank.setups.toml b/data/bot/bank.setups.toml index 4b9932694f..6d9c753a4b 100644 --- a/data/bot/bank.setups.toml +++ b/data/bot/bank.setups.toml @@ -1,8 +1,8 @@ [deposit_carried_items] actions = [ - { go_to_nearest = "bank" }, - { option = "Use-quickly", object = "bank_booth*", success = { interface_open = { id = "bank" } } }, - { option = "Deposit carried items", interface = "bank:carried", success = { inventory_space = { min = 28 } } }, + { go_to = { nearest = "bank" } }, + { object = { option = "Use-quickly", id = "bank_booth*", success = { interface_open = { id = "bank" } } } }, + { interface = { option = "Deposit carried items", id = "bank:carried", success = { inventory_space = { min = 28 } } } }, ] produces = [ { inventory_space = { min = 28 } } diff --git a/data/bot/combat.templates.toml b/data/bot/combat.templates.toml index 1bcd681e7d..0da866a4dd 100644 --- a/data/bot/combat.templates.toml +++ b/data/bot/combat.templates.toml @@ -8,8 +8,8 @@ setup = [ { inventory_space = { min = 27 } }, ] actions = [ - { option = "Select", interface = "combat_styles:$style" }, - { option = "Attack", npc = "chicken*", delay = 5, success = { inventory_space = { max = 0 } } }, + { interface = { option = "Select", id = "combat_styles:$style" } }, + { npc = { option = "Attack", id = "chicken*", delay = 5, success = { inventory_space = { max = 0 } } } }, ] produces = [ { carries = [{ id = "feather" }] }, @@ -29,8 +29,8 @@ setup = [ { inventory_space = { min = 27 } }, ] actions = [ - { option = "Select", interface = "combat_styles:style1" }, - { option = "Attack", npc = "cow*", delay = 5, success = { inventory_space = { max = 0 } } }, + { interface = { option = "Select", id = "combat_styles:style1" } }, + { npc = { option = "Attack", id = "cow*", delay = 5, success = { inventory_space = { max = 0 } } } }, ] produces = [ { carries = [{ id = "bones" }] }, diff --git a/data/bot/cooking.templates.toml b/data/bot/cooking.templates.toml index 19d970796c..ec11f647ec 100644 --- a/data/bot/cooking.templates.toml +++ b/data/bot/cooking.templates.toml @@ -8,10 +8,10 @@ setup = [ { area = { id = "$location" } }, ] actions = [ - { item = "$raw", on_object = "$obj", success = { interface_open = { id = "dialogue_skill_creation" } } }, - { option = "All", interface = "skill_creation_amount:all" }, - { continue = "dialogue_skill_creation:choice1" }, - { restart = true, wait_if = [{ has_queue = { id = "cooking" } }], success = { carries = [{ id = "$raw", max = 0 }] } } + { item_on_object = { id = "$raw", object = "$obj", success = { interface_open = { id = "dialogue_skill_creation" } } } }, + { interface = { option = "All", id = "skill_creation_amount:all" } }, + { continue = { id = "dialogue_skill_creation:choice1" } }, + { restart = { wait_if = [{ has_queue = { id = "cooking" } }], success = { carries = [{ id = "$raw", max = 0 }] } } } ] produces = [ { skill = { id = "cooking" } }, @@ -29,11 +29,11 @@ setup = [ { area = { id = "$location" } }, ] actions = [ - { item = "raw_beef", on_object = "$obj", success = { interface_open = { id = "dialogue_multi2" } } }, - { option = "Continue", continue = "dialogue_multi2:line2", success = { interface_open = { id = "dialogue_skill_creation" } } }, - { option = "All", interface = "skill_creation_amount:all" }, - { continue = "dialogue_skill_creation:choice1" }, - { restart = true, wait_if = [{ has_queue = { id = "cooking" } }], success = { carries = [{ id = "raw_beef", max = 0 }] } } + { item_on_object = { id = "raw_beef", object = "$obj", success = { interface_open = { id = "dialogue_multi2" } } } }, + { continue = { option = "Continue", id = "dialogue_multi2:line2", success = { interface_open = { id = "dialogue_skill_creation" } } } }, + { interface = { option = "All", id = "skill_creation_amount:all" } }, + { continue = { id = "dialogue_skill_creation:choice1" } }, + { restart = { wait_if = [{ has_queue = { id = "cooking" } }], success = { carries = [{ id = "raw_beef", max = 0 }] } } } ] produces = [ { skill = { id = "cooking" } }, diff --git a/data/bot/fletching.templates.toml b/data/bot/fletching.templates.toml index 8335ba251d..191f742e58 100644 --- a/data/bot/fletching.templates.toml +++ b/data/bot/fletching.templates.toml @@ -7,10 +7,10 @@ setup = [ { carries = [{ id = "knife" }, { id = "$logs", min = 27 }] }, ] actions = [ - { item = "knife", on = "$logs", success = { interface_open = { id = "dialogue_skill_creation" } } }, - { option = "All", interface = "skill_creation_amount:all" }, - { continue = "dialogue_skill_creation:choice1" }, - { restart = true, wait_if = [{ has_queue = { id = "fletching" } }], success = { inventory_space = { min = 26 } } } + { item_on_item = { id = "knife", on = "$logs", success = { interface_open = { id = "dialogue_skill_creation" } } } }, + { interface = { option = "All", id = "skill_creation_amount:all" } }, + { continue = { id = "dialogue_skill_creation:choice1" } }, + { restart = { wait_if = [{ has_queue = { id = "fletching" } }], success = { inventory_space = { min = 26 } } } } ] produces = [ { carries = { id = "arrow_shafts" } }, @@ -26,10 +26,10 @@ setup = [ { carries = [{ id = "knife" }, { id = "$logs", min = 27 }] }, ] actions = [ - { item = "knife", on = "$logs" }, - { option = "All", interface = "skill_creation_amount:all" }, - { continue = "dialogue_skill_creation:choice2", success = { has_queue = { id = "fletching" } } }, - { restart = true, success = { inventory_space = { min = 26 } } } + { item_on_item = { id = "knife", on = "$logs" } }, + { interface = { option = "All", id = "skill_creation_amount:all" } }, + { continue = { id = "dialogue_skill_creation:choice2", success = { has_queue = { id = "fletching" } } } }, + { restart = { success = { inventory_space = { min = 26 } } } } ] produces = [ { skill = { id = "fletching" } } @@ -44,10 +44,10 @@ setup = [ { carries = [{ id = "knife" }, { id = "$logs", min = 27 }] }, ] actions = [ - { item = "knife", on = "$logs", success = { has_queue = { id = "fletching_make_dialog" } } }, - { option = "All", interface = "skill_creation_amount:all" }, - { continue = "dialogue_skill_creation:choice3", success = { has_queue = { id = "fletching" } } }, - { restart = true, success = { inventory_space = { min = 26 } } } + { item_on_item = { id = "knife", on = "$logs", success = { has_queue = { id = "fletching_make_dialog" } } } }, + { interface = { option = "All", id = "skill_creation_amount:all" } }, + { continue = { id = "dialogue_skill_creation:choice3", success = { has_queue = { id = "fletching" } } } }, + { restart = { success = { inventory_space = { min = 26 } } } } ] produces = [ { skill = { id = "fletching" } } diff --git a/data/bot/lumbridge.nav-edges.toml b/data/bot/lumbridge.nav-edges.toml index e2df401e21..bdd3e5f974 100644 --- a/data/bot/lumbridge.nav-edges.toml +++ b/data/bot/lumbridge.nav-edges.toml @@ -4,33 +4,33 @@ edges = [ { from_x = 3236, from_y = 3205, to_x = 3243, to_y = 3209 }, # lumbridge_south_village_to_church { from_x = 3236, from_y = 3218, to_x = 3250, to_y = 3212 }, # lumbridge_gate_south_to_behind_church { from_x = 3250, from_y = 3212, to_x = 3258, to_y = 3206 }, # lumbridge_behind_church_to_church_fishing_spot - { from_x = 3236, from_y = 3205, to_x = 3231, to_y = 3203, cost = 5, actions = [{ option = "Open", object = "door_720_closed", x = 3234, y = 3203 }, { x = 3231, y = 3203 }] }, # lumbridge_south_village_to_bobs_axes - { from_x = 3231, from_y = 3203, to_x = 3236, to_y = 3205, cost = 5, actions = [{ option = "Open", object = "door_720_closed", x = 3234, y = 3203 }, { x = 3236, y = 3205 }] }, # lumbridge_bobs_axes_to_south_village - { from_x = 3236, from_y = 3205, to_x = 3231, to_y = 3197, cost = 5, actions = [{ option = "Open", object = "door_720_closed", x = 3235, y = 3199 }, { x = 3231, y = 3197 }] }, # lumbridge_south_village_to_south_range_house - { from_x = 3231, from_y = 3197, to_x = 3236, to_y = 3205, cost = 5, actions = [{ option = "Open", object = "door_720_closed", x = 3235, y = 3199 }, { x = 3236, y = 3205 }] }, # lumbridge_south_range_house_to_south_village + { from_x = 3236, from_y = 3205, to_x = 3231, to_y = 3203, cost = 5, actions = [{ object = { option = "Open", id = "door_720_closed", x = 3234, y = 3203 } }, { tile = { x = 3231, y = 3203 } }] }, # lumbridge_south_village_to_bobs_axes + { from_x = 3231, from_y = 3203, to_x = 3236, to_y = 3205, cost = 5, actions = [{ object = { option = "Open", id = "door_720_closed", x = 3234, y = 3203 } }, { tile = { x = 3236, y = 3205 } }] }, # lumbridge_bobs_axes_to_south_village + { from_x = 3236, from_y = 3205, to_x = 3231, to_y = 3197, cost = 5, actions = [{ object = { option = "Open", id = "door_720_closed", x = 3235, y = 3199 } }, { tile = { x = 3231, y = 3197 } }] }, # lumbridge_south_village_to_south_range_house + { from_x = 3231, from_y = 3197, to_x = 3236, to_y = 3205, cost = 5, actions = [{ object = { option = "Open", id = "door_720_closed", x = 3235, y = 3199 } }, { tile = { x = 3236, y = 3205 } }] }, # lumbridge_south_range_house_to_south_village { from_x = 3236, from_y = 3205, to_x = 3244, to_y = 3190 }, # lumbridge_south_village_to_graveyard_exit { from_x = 3244, from_y = 3190, to_x = 3253, to_y = 3200 }, # lumbridge_graveyard_exit_to_behind_graveyard { from_x = 3250, from_y = 3212, to_x = 3253, to_y = 3200 }, # lumbridge_behind_church_to_behind_graveyard { from_x = 3258, from_y = 3206, to_x = 3253, to_y = 3200 }, # lumbridge_church_fishing_spot_to_behind_graveyard - { from_x = 3205, from_y = 3209, from_level = 2, to_x = 3205, to_y = 3209, to_level = 1, cost = 1, actions = [{ option = "Climb-down", object = "lumbridge_staircase_top", x = 3204, y = 3207, success = { tile = { level = 1 } } }] }, # lumbridge_castle_2nd_floor_south_stairs_to_1st_floor + { from_x = 3205, from_y = 3209, from_level = 2, to_x = 3205, to_y = 3209, to_level = 1, cost = 1, actions = [{ object = { option = "Climb-down", id = "lumbridge_staircase_top", x = 3204, y = 3207, success = { tile = { level = 1 } } } }] }, # lumbridge_castle_2nd_floor_south_stairs_to_1st_floor { from_x = 3208, from_y = 3219, from_level = 2, to_x = 3205, to_y = 3209, to_level = 2 }, # lumbridge_castle_2nd_floor_bank_to_south_stairs - { from_x = 3205, from_y = 3209, to_x = 3205, to_y = 3209, to_level = 1, cost = 1, actions = [{ option = "Climb-up", object = "lumbridge_staircase", x = 3204, y = 3207, success = { tile = { level = 1 } } }] }, # lumbridge_castle_ground_floor_south_stairs_to_1st_floor + { from_x = 3205, from_y = 3209, to_x = 3205, to_y = 3209, to_level = 1, cost = 1, actions = [{ object = { option = "Climb-up", id = "lumbridge_staircase", x = 3204, y = 3207, success = { tile = { level = 1 } } } }] }, # lumbridge_castle_ground_floor_south_stairs_to_1st_floor { from_x = 3205, from_y = 3209, to_x = 3208, to_y = 3210 }, # lumbridge_castle_ground_floor_south_stairs_to_kitchen_corridor { from_x = 3208, from_y = 3210, to_x = 3215, to_y = 3216 }, # lumbridge_castle_kitchen_corridor_to_castle_south_entrance { from_x = 3208, from_y = 3210, to_x = 3211, to_y = 3214 }, # lumbridge_castle_kitchen_corridor_to_kitchen { from_x = 3215, from_y = 3216, to_x = 3222, to_y = 3218 }, # lumbridge_castle_south_entrance_to_courtyard_south - { from_x = 3205, from_y = 3209, from_level = 1, to_x = 3205, to_y = 3209, cost = 1, actions = [{ option = "Climb-down", object = "lumbridge_castle_staircase_south_middle", x = 3204, y = 3207, success = { tile = { level = 0 } } }] }, # lumbridge_castle_1st_floor_south_stairs_to_ground_floor - { from_x = 3205, from_y = 3209, from_level = 1, to_x = 3205, to_y = 3209, to_level = 2, cost = 1, actions = [{ option = "Climb-up", object = "lumbridge_castle_staircase_south_middle", x = 3204, y = 3207, success = { tile = { level = 2 } } }] }, # lumbridge_castle_1st_floor_south_stairs_to_2nd_floor + { from_x = 3205, from_y = 3209, from_level = 1, to_x = 3205, to_y = 3209, cost = 1, actions = [{ object = { option = "Climb-down", id = "lumbridge_castle_staircase_south_middle", x = 3204, y = 3207, success = { tile = { level = 0 } } } }] }, # lumbridge_castle_1st_floor_south_stairs_to_ground_floor + { from_x = 3205, from_y = 3209, from_level = 1, to_x = 3205, to_y = 3209, to_level = 2, cost = 1, actions = [{ object = { option = "Climb-up", id = "lumbridge_castle_staircase_south_middle", x = 3204, y = 3207, success = { tile = { level = 2 } } } }] }, # lumbridge_castle_1st_floor_south_stairs_to_2nd_floor { from_x = 3209, from_y = 3205, to_x = 3199, to_y = 3218 }, # lumbridge_castle_grounds_south_to_tower_west { from_x = 3199, from_y = 3218, to_x = 3184, to_y = 3225 }, # lumbridge_castle_tower_west_to_yew_trees { from_x = 3199, from_y = 3218, to_x = 3193, to_y = 3236 }, # lumbridge_castle_tower_west_to_tree_patch { from_x = 3184, from_y = 3225, to_x = 3168, to_y = 3221 }, # lumbridge_castle_yew_trees_to_yew_trees_west { from_x = 3184, from_y = 3225, to_x = 3193, to_y = 3236 }, # lumbridge_castle_yew_trees_to_tree_patch - { from_x = 3226, from_y = 3214, to_x = 3227, to_y = 3214, cost = 3, actions = [{ option = "Open", object = "door_627_closed", x = 3226, y = 3214 }] }, # lumbridge_south_tower_to_ground_floor - { from_x = 3227, from_y = 3214, to_x = 3229, to_y = 3214, to_level = 1, cost = 1, actions = [{ option = "Climb-up", object = "36768", x = 3229, y = 3213, success = { tile = { level = 1 } } }] }, # lumbridge_south_tower_ground_floor_to_1st_floor - { from_x = 3229, from_y = 3214, from_level = 1, to_x = 3229, to_y = 3214, to_level = 2, cost = 1, actions = [{ option = "Climb-up", object = "36769", x = 3229, y = 3213, success = { tile = { level = 2 } } }] }, # lumbridge_south_tower_1st_floor_to_2nd_floor - { from_x = 3229, from_y = 3214, from_level = 1, to_x = 3229, to_y = 3214, cost = 1, actions = [{ option = "Climb-down", object = "36769", x = 3229, y = 3213, success = { tile = { level = 0 } } }] }, # lumbridge_south_tower_1st_floor_to_ground_floor - { from_x = 3229, from_y = 3214, from_level = 2, to_x = 3229, to_y = 3214, to_level = 1, cost = 1, actions = [{ option = "Climb-down", object = "36770", x = 3229, y = 3213, success = { tile = { level = 1 } } }] }, # lumbridge_south_tower_2nd_floor_to_1st_floor + { from_x = 3226, from_y = 3214, to_x = 3227, to_y = 3214, cost = 3, actions = [{ object = { option = "Open", id = "door_627_closed", x = 3226, y = 3214 } }] }, # lumbridge_south_tower_to_ground_floor + { from_x = 3227, from_y = 3214, to_x = 3229, to_y = 3214, to_level = 1, cost = 1, actions = [{ object = { option = "Climb-up", id = "36768", x = 3229, y = 3213, success = { tile = { level = 1 } } } }] }, # lumbridge_south_tower_ground_floor_to_1st_floor + { from_x = 3229, from_y = 3214, from_level = 1, to_x = 3229, to_y = 3214, to_level = 2, cost = 1, actions = [{ object = { option = "Climb-up", id = "36769", x = 3229, y = 3213, success = { tile = { level = 2 } } } }] }, # lumbridge_south_tower_1st_floor_to_2nd_floor + { from_x = 3229, from_y = 3214, from_level = 1, to_x = 3229, to_y = 3214, cost = 1, actions = [{ object = { option = "Climb-down", id = "36769", x = 3229, y = 3213, success = { tile = { level = 0 } } } }] }, # lumbridge_south_tower_1st_floor_to_ground_floor + { from_x = 3229, from_y = 3214, from_level = 2, to_x = 3229, to_y = 3214, to_level = 1, cost = 1, actions = [{ object = { option = "Climb-down", id = "36770", x = 3229, y = 3213, success = { tile = { level = 1 } } } }] }, # lumbridge_south_tower_2nd_floor_to_1st_floor { from_x = 3236, from_y = 3219, to_x = 3236, to_y = 3225 }, # lumbridge_gate_north_to_bridge_west { from_x = 3236, from_y = 3225, to_x = 3230, to_y = 3232 }, # lumbridge_bridge_west_to_unstable_house { from_x = 3236, from_y = 3225, to_x = 3253, to_y = 3225 }, # lumbridge_bridge_west_to_bridge_east @@ -46,10 +46,10 @@ edges = [ { from_x = 3251, from_y = 3252, to_x = 3258, to_y = 3250 }, { from_x = 3250, from_y = 3266, to_x = 3240, to_y = 3280 }, # lumbridge_cow_entrance_to_cow_path { from_x = 3240, from_y = 3280, to_x = 3238, to_y = 3295 }, # lumbridge_cow_path_to_chicken_entrance - { from_x = 3238, from_y = 3295, to_x = 3235, to_y = 3295, cost = 0, actions = [{ option = "Open", object = "gate_235_closed", x = 3237, y = 3295 }] }, # lumbridge_chicken_entrance_to_chicken_pen - { from_x = 3235, from_y = 3295, to_x = 3238, to_y = 3295, cost = 0, actions = [{ option = "Open", object = "gate_237_closed", x = 3237, y = 3296 }] }, # lumbridge_chicken_pen_to_chicken_entrance - { from_x = 3250, from_y = 3266, to_x = 3255, to_y = 3266, cost = 0, actions = [{ option = "Open", object = "gate_241_closed", x = 3252, y = 3266 }] }, # lumbridge_cow_entrance_to_cow_field - { from_x = 3255, from_y = 3266, to_x = 3250, to_y = 3266, cost = 0, actions = [{ option = "Open", object = "gate_239_closed", x = 3252, y = 3267 }] }, # lumbridge_cow_field_to_cow_entrance + { from_x = 3238, from_y = 3295, to_x = 3235, to_y = 3295, cost = 0, actions = [{ object = { option = "Open", id = "gate_235_closed", x = 3237, y = 3295 } }] }, # lumbridge_chicken_entrance_to_chicken_pen + { from_x = 3235, from_y = 3295, to_x = 3238, to_y = 3295, cost = 0, actions = [{ object = { option = "Open", id = "gate_237_closed", x = 3237, y = 3296 } }] }, # lumbridge_chicken_pen_to_chicken_entrance + { from_x = 3250, from_y = 3266, to_x = 3255, to_y = 3266, cost = 0, actions = [{ object = { option = "Open", id = "gate_241_closed", x = 3252, y = 3266 } }] }, # lumbridge_cow_entrance_to_cow_field + { from_x = 3255, from_y = 3266, to_x = 3250, to_y = 3266, cost = 0, actions = [{ object = { option = "Open", id = "gate_239_closed", x = 3252, y = 3267 } }] }, # lumbridge_cow_field_to_cow_entrance { from_x = 3244, from_y = 3190, to_x = 3241, to_y = 3176 }, # lumbridge_graveyard_exit_to_swamp_path { from_x = 3241, from_y = 3176, to_x = 3239, to_y = 3160 }, # lumbridge_swamp_path_to_swamp_cross_roads { from_x = 3244, from_y = 3190, to_x = 3231, to_y = 3188 }, # lumbridge_graveyard_exit_to_rats_north_east @@ -83,8 +83,8 @@ edges = [ { from_x = 3236, from_y = 3218, to_x = 3236, to_y = 3219 }, # lumbridge_courtyard_gate_south_to_gate_north { from_x = 3222, from_y = 3218, to_x = 3209, to_y = 3205 }, # lumbridge_courtyard_south_to_grounds_south { from_x = 3230, from_y = 3232, to_x = 3222, to_y = 3241 }, # lumbridge_unstable_house_to_general_store_east - { from_x = 3222, from_y = 3241, to_x = 3217, to_y = 3241, cost = 6, actions = [{ option = "Open", object = "door_720_closed", x = 3219, y = 3241 }, { x = 3217, y = 3241 }] }, # lumbridge_general_store_east_to_general_store - { from_x = 3217, from_y = 3241, to_x = 3222, to_y = 3241, cost = 6, actions = [{ option = "Open", object = "door_720_closed", x = 3219, y = 3241 }, { x = 3222, y = 3241 }] }, # lumbridge_general_store_to_general_store_east + { from_x = 3222, from_y = 3241, to_x = 3217, to_y = 3241, cost = 6, actions = [{ object = { option = "Open", id = "door_720_closed", x = 3219, y = 3241 } }, { tile = { x = 3217, y = 3241 } }] }, # lumbridge_general_store_east_to_general_store + { from_x = 3217, from_y = 3241, to_x = 3222, to_y = 3241, cost = 6, actions = [{ object = { option = "Open", id = "door_720_closed", x = 3219, y = 3241 } }, { tile = { x = 3222, y = 3241 } }] }, # lumbridge_general_store_to_general_store_east { from_x = 3222, from_y = 3241, to_x = 3226, to_y = 3245 }, # lumbridge_general_store_east_to_trees_west { from_x = 3226, from_y = 3245, to_x = 3235, to_y = 3261 }, # lumbridge_west_village_trees_to_bridge_north { from_x = 3222, from_y = 3241, to_x = 3204, to_y = 3247 }, # lumbridge_general_store_east_to_task_building @@ -116,8 +116,8 @@ edges = [ { from_x = 3190, from_y = 3283, to_x = 3190, to_y = 3294 }, { from_x = 3190, from_y = 3294, to_x = 3186, to_y = 3307 }, { from_x = 3186, from_y = 3307, to_x = 3177, to_y = 3315 }, - { from_x = 3177, from_y = 3315, to_x = 3177, to_y = 3316, cost = 0, actions = [{ option = "Open", object = "gate_235_closed", x = 3177, y = 3316 }] }, - { from_x = 3177, from_y = 3316, to_x = 3177, to_y = 3315, cost = 0, actions = [{ option = "Open", object = "gate_235_closed", x = 3177, y = 3316 }] }, + { from_x = 3177, from_y = 3315, to_x = 3177, to_y = 3316, cost = 0, actions = [{ object = { option = "Open", id = "gate_235_closed", x = 3177, y = 3316 } }] }, + { from_x = 3177, from_y = 3316, to_x = 3177, to_y = 3315, cost = 0, actions = [{ object = { option = "Open", id = "gate_235_closed", x = 3177, y = 3316 } }] }, { from_x = 3177, from_y = 3316, to_x = 3179, to_y = 3326 }, { from_x = 3179, from_y = 3326, to_x = 3178, to_y = 3334 }, { from_x = 3178, from_y = 3334, to_x = 3177, to_y = 3345 }, diff --git a/data/bot/mining.templates.toml b/data/bot/mining.templates.toml index 2dbf0c8b98..147b9c9b49 100644 --- a/data/bot/mining.templates.toml +++ b/data/bot/mining.templates.toml @@ -8,7 +8,7 @@ setup = [ { inventory_space = { min = 27 } }, ] actions = [ - { option = "Mine", object = "copper_rocks*", delay = 5, success = { inventory_space = { max = 0 } } }, + { object = { option = "Mine", id = "copper_rocks*", delay = 5, success = { inventory_space = { max = 0 } } } }, ] produces = [ { carries = [{ id = "copper_ore" }] }, @@ -25,7 +25,7 @@ setup = [ { inventory_space = { min = 27 } }, ] actions = [ - { option = "Mine", object = "tin_rocks*", delay = 5, success = { inventory_space = { max = 0 } } }, + { object = { option = "Mine", id = "tin_rocks*", delay = 5, success = { inventory_space = { max = 0 } } } }, ] produces = [ { carries = [{ id = "tin_ore" }] }, @@ -42,7 +42,7 @@ setup = [ { inventory_space = { min = 27 } }, ] actions = [ - { option = "Mine", object = "clay_rocks*", delay = 5, success = { inventory_space = { max = 0 } } }, + { object = { option = "Mine", id = "clay_rocks*", delay = 5, success = { inventory_space = { max = 0 } } } }, ] produces = [ { carries = [{ id = "clay" }] }, @@ -59,7 +59,7 @@ setup = [ { inventory_space = { min = 27 } }, ] actions = [ - { option = "Mine", object = "iron_rocks*", delay = 5, success = { inventory_space = { max = 0 } } }, + { object = { option = "Mine", id = "iron_rocks*", delay = 5, success = { inventory_space = { max = 0 } } } }, ] produces = [ { carries = [{ id = "iron_ore" }] }, @@ -76,7 +76,7 @@ setup = [ { inventory_space = { min = 27 } }, ] actions = [ - { option = "Mine", object = "silver_rocks*", delay = 5, success = { inventory_space = { max = 0 } } }, + { object = { option = "Mine", id = "silver_rocks*", delay = 5, success = { inventory_space = { max = 0 } } } }, ] produces = [ { carries = [{ id = "silver_ore" }] }, @@ -93,7 +93,7 @@ setup = [ { inventory_space = { min = 27 } }, ] actions = [ - { option = "Mine", object = "coal_rocks*", delay = 10, success = { inventory_space = { max = 0 } } }, + { object = { option = "Mine", id = "coal_rocks*", delay = 10, success = { inventory_space = { max = 0 } } } }, ] produces = [ { carries = [{ id = "coal" }] }, @@ -110,7 +110,7 @@ setup = [ { inventory_space = { min = 27 } }, ] actions = [ - { option = "Mine", object = "gold_rocks*", delay = 15, success = { inventory_space = { max = 0 } } }, + { object = { option = "Mine", id = "gold_rocks*", delay = 15, success = { inventory_space = { max = 0 } } } }, ] produces = [ { carries = [{ id = "gold_ore" }] }, @@ -127,7 +127,7 @@ setup = [ { inventory_space = { min = 27 } }, ] actions = [ - { option = "Mine", object = "mithril_rocks*", delay = 15, success = { inventory_space = { max = 0 } } }, + { object = { option = "Mine", id = "mithril_rocks*", delay = 15, success = { inventory_space = { max = 0 } } } }, ] produces = [ { carries = [{ id = "mithril_ore" }] }, diff --git a/data/bot/prayer.bots.toml b/data/bot/prayer.bots.toml index aa7be7423a..afe06d0463 100644 --- a/data/bot/prayer.bots.toml +++ b/data/bot/prayer.bots.toml @@ -7,8 +7,8 @@ setup = [ { carries = [{ id = "bones", min = 28 }] }, ] actions = [ - { option = "Bury", interface = "inventory:inventory:bones" }, - { restart = true, wait_if = [{ variable = { id = "bone_delay", min = 0, default = -1 } }], success = { inventory_space = { min = 28 } } } + { interface = { option = "Bury", id = "inventory:inventory:bones" } }, + { restart = { wait_if = [{ variable = { id = "bone_delay", min = 0, default = -1 } }], success = { inventory_space = { min = 28 } } } } ] produces = [ { skill = { id = "prayer" } } diff --git a/data/bot/shop.templates.toml b/data/bot/shop.templates.toml index b7740973c3..0313e4a7c6 100644 --- a/data/bot/shop.templates.toml +++ b/data/bot/shop.templates.toml @@ -5,8 +5,8 @@ setup = [ { inventory_space = { min = 1 } }, ] actions = [ - { option = "Trade", npc = "$shopkeeper", success = { interface_open = { id = "shop" } } }, - { option = "Buy-1", interface = "shop:stock:$item", success = { carries = [{ id = "$item" }] } }, + { npc = { option = "Trade", id = "$shopkeeper", success = { interface_open = { id = "shop" } } } }, + { interface = { option = "Buy-1", id = "shop:stock:$item", success = { carries = [{ id = "$item" }] } } }, ] produces = [ { carries = [{ id = "$item" }] } @@ -18,8 +18,8 @@ setup = [ { inventory_space = { min = 1 } }, ] actions = [ - { option = "Trade", npc = "$shopkeeper", success = { interface_open = { id = "shop" } } }, - { option = "Take-1", interface = "shop:sample:$item", success = { carries = [{ id = "$item" }] } }, + { npc = { option = "Trade", id = "$shopkeeper", success = { interface_open = { id = "shop" } } } }, + { interface = { option = "Take-1", id = "shop:sample:$item", success = { carries = [{ id = "$item" }] } } }, ] produces = [ { carries = [{ id = "$item" }] } diff --git a/data/bot/teleport.shortcuts.toml b/data/bot/teleport.shortcuts.toml index 9703807069..262e354700 100644 --- a/data/bot/teleport.shortcuts.toml +++ b/data/bot/teleport.shortcuts.toml @@ -32,8 +32,8 @@ requires = [ { carries = [{ id = "fire_rune", min = 1 }, { id = "air_rune", min = 3 }, { id = "law_rune", min = 1 }] }, ] actions = [ - { option = "Cast", interface = "modern_spellbook:varrock_teleport" }, - { wait = 5 }, + { interface = { option = "Cast", id = "modern_spellbook:varrock_teleport" } }, + { wait = { ticks = 5 } }, ] produces = [ { area = { id = "varrock_teleport" } } @@ -48,14 +48,14 @@ requires = [ { owns = { id = "law_rune", min = 1 }}, ] actions = [ - { go_to_nearest = "bank" }, - { option = "Use-quickly", object = "bank_booth*" }, - { option = "Withdraw-1", interface = "bank:inventory:fire_rune" }, - { option = "Withdraw-X", interface = "bank:inventory:air_rune" }, - { enter_int = 3 }, - { option = "Withdraw-1", interface = "bank:inventory:law_rune" }, - { option = "Cast", interface = "modern_spellbook:varrock_teleport" }, - { wait = 5 }, + { go_to = { nearest = "bank" } }, + { object = { option = "Use-quickly", id = "bank_booth*" } }, + { interface = { option = "Withdraw-1", id = "bank:inventory:fire_rune" } }, + { interface = { option = "Withdraw-X", id = "bank:inventory:air_rune" } }, + { enter = { int = 3 } }, + { interface = { option = "Withdraw-1", id = "bank:inventory:law_rune" } }, + { interface = { option = "Cast", id = "modern_spellbook:varrock_teleport" } }, + { wait = { ticks = 5 } }, # { wait = { location = "varrock_teleport" } }, ] produces = [ @@ -69,7 +69,7 @@ requires = [ { clock = { id = "home_teleport_timeout", max = 0 } }, ] actions = [ - { option = "Cast", interface = "modern_spellbook:lumbridge_home_teleport" }, + { interface = { option = "Cast", id = "modern_spellbook:lumbridge_home_teleport" } }, ] produces = [ { area = { id = "lumbridge_teleport" } } diff --git a/data/bot/thieving.templates.toml b/data/bot/thieving.templates.toml index 712bfdfd7b..ab17df3bf8 100644 --- a/data/bot/thieving.templates.toml +++ b/data/bot/thieving.templates.toml @@ -8,8 +8,11 @@ setup = [ { area = { id = "$location" } }, ] actions = [ - { option = "Pickpocket", npc = "$npc" }, - { restart = true, wait_if = [{ variable = { id = "delay", min = 0, default = -1 } }, { clock = { id = "stunned", min = 0 } }], success = { skill = { id = "constitution", max = 20 } } } + { npc = { option = "Pickpocket", id = "$npc" } }, + { restart = { wait_if = [ + { variable = { id = "delay", min = 0, default = -1 } }, + { clock = { id = "stunned", min = 0 } } + ], success = { skill = { id = "constitution", max = 20 } } } } ] produces = [ { carries = [{ id = "coins" }] }, diff --git a/data/bot/varrock.nav-edges.toml b/data/bot/varrock.nav-edges.toml index 98761b7b36..4438095300 100644 --- a/data/bot/varrock.nav-edges.toml +++ b/data/bot/varrock.nav-edges.toml @@ -9,12 +9,12 @@ edges = [ { from_x = 3162, from_y = 3420, to_x = 3150, to_y = 3416 }, # varrock_west_oak_trees_to_cat_house { from_x = 3150, from_y = 3416, to_x = 3135, to_y = 3416 }, # varrock_west_cat_house_to_barbarian_path { from_x = 3135, from_y = 3416, to_x = 3128, to_y = 3407 }, # varrock_west_barbarian_path_to_air_altar_ruins - { from_x = 3128, from_y = 3407, to_x = 2841, to_y = 4830, cost = 3, actions = [{ option = "null", object = "air_altar_ruins", x = 3126, y = 3404 }] }, # varrock_west_air_altar_ruins_to_air_altar + { from_x = 3128, from_y = 3407, to_x = 2841, to_y = 4830, cost = 3, actions = [{ object = { option = "null", id = "air_altar_ruins", x = 3126, y = 3404 } }] }, # varrock_west_air_altar_ruins_to_air_altar { from_x = 2841, from_y = 4830, to_x = 2841, to_y = 4829 }, # varrock_west_air_altar_to_exit - { from_x = 2841, from_y = 4829, to_x = 3128, to_y = 3407, cost = 3, actions = [{ option = "Enter", object = "air_altar_portal", x = 2841, y = 4828 }] }, # varrock_west_air_altar_exit_to_ruins - { from_x = 3304, from_y = 3474, to_x = 2655, to_y = 4831, cost = 3, actions = [{ option = "Enter", object = "earth_altar_ruins", x = 3305, y = 3473 }] }, # varrock_east_earth_altar_ruins_to_altar + { from_x = 2841, from_y = 4829, to_x = 3128, to_y = 3407, cost = 3, actions = [{ object = { option = "Enter", id = "air_altar_portal", x = 2841, y = 4828 } }] }, # varrock_west_air_altar_exit_to_ruins + { from_x = 3304, from_y = 3474, to_x = 2655, to_y = 4831, cost = 3, actions = [{ object = { option = "Enter", id = "earth_altar_ruins", x = 3305, y = 3473 } }] }, # varrock_east_earth_altar_ruins_to_altar { from_x = 2655, from_y = 4831, to_x = 2655, to_y = 4830 }, # varrock_east_earth_altar_to_exit - { from_x = 2655, from_y = 4830, to_x = 3304, to_y = 3474, cost = 3, actions = [{ option = "Enter", object = "earth_altar_portal", x = 2655, y = 4829 }] }, # varrock_east_earth_altar_exit_to_ruins + { from_x = 2655, from_y = 4830, to_x = 3304, to_y = 3474, cost = 3, actions = [{ object = { option = "Enter", id = "earth_altar_portal", x = 2655, y = 4829 } }] }, # varrock_east_earth_altar_exit_to_ruins { from_x = 3254, from_y = 3422, to_x = 3265, to_y = 3428 }, # varrock_east_bank_to_dirt_crossroad { from_x = 3265, from_y = 3428, to_x = 3280, to_y = 3428 }, # varrock_east_dirt_crossroad_to_east_crossroad { from_x = 3280, from_y = 3428, to_x = 3287, to_y = 3414 }, # varrock_east_crossroad_to_east_path_south @@ -46,7 +46,7 @@ edges = [ { from_x = 3210, from_y = 3407, to_x = 3211, to_y = 3395 }, # varrock_south_dirt_crossroads_to_blue_moon_inn { from_x = 3210, from_y = 3407, to_x = 3209, to_y = 3399 }, { from_x = 3210, from_y = 3407, to_x = 3209, to_y = 3399 }, - { from_x = 3209, from_y = 3399, to_x = 3207, to_y = 3399, cost = 1, actions = [{ option = "Open", object = "door_444_closed", x = 3209, y = 3399 }, { x = 3207, y = 3399 }] }, + { from_x = 3209, from_y = 3399, to_x = 3207, to_y = 3399, cost = 1, actions = [{ object = { option = "Open", id = "door_444_closed", x = 3209, y = 3399 } }, { tile = { x = 3207, y = 3399 } }] }, { from_x = 3211, from_y = 3395, to_x = 3209, to_y = 3399 }, # varrock_sword_shop { from_x = 3211, from_y = 3395, to_x = 3211, to_y = 3381 }, # varrock_blue_moon_inn_to_south_border { from_x = 3211, from_y = 3381, to_x = 3214, to_y = 3367 }, # varrock_south_border_to_dark_mages diff --git a/data/bot/walking.setups.toml b/data/bot/walking.setups.toml index 89c483da66..e1a12f746e 100644 --- a/data/bot/walking.setups.toml +++ b/data/bot/walking.setups.toml @@ -16,7 +16,7 @@ requires = [ { variable = { id = "movement", equals = "walk", default = "walk" } } ] actions = [ - { option = "Turn Run mode on", interface = "energy_orb:run_background" } + { interface = { option = "Turn Run mode on", id = "energy_orb:run_background" } } ] produces = [ { variable = { id = "movement", equals = "run", default = "walk" } } diff --git a/data/bot/woodcutting.templates.toml b/data/bot/woodcutting.templates.toml index 85b72df23f..742c1283a8 100644 --- a/data/bot/woodcutting.templates.toml +++ b/data/bot/woodcutting.templates.toml @@ -8,7 +8,7 @@ setup = [ { inventory_space = { min = 27 } }, ] actions = [ - { option = "Chop down", object = "tree*", delay = 5, success = { inventory_space = { max = 0 } } }, + { object = { option = "Chop down", id = "tree*", delay = 5, success = { inventory_space = { max = 0 } } } }, ] produces = [ { carries = [{ id = "logs" }] }, @@ -25,7 +25,7 @@ setup = [ { inventory_space = { min = 27 } }, ] actions = [ - { option = "Chop down", object = "oak*", delay = 5, success = { inventory_space = { max = 0 } } }, + { object = { option = "Chop down", id = "oak*", delay = 5, success = { inventory_space = { max = 0 } } } }, ] produces = [ { carries = [{ id = "oak_logs" }] }, @@ -42,7 +42,7 @@ setup = [ { inventory_space = { min = 27 } }, ] actions = [ - { option = "Chop down", object = "willow*", delay = 5, success = { inventory_space = { max = 0 } } }, + { object = { option = "Chop down", id = "willow*", delay = 5, success = { inventory_space = { max = 0 } } } }, ] produces = [ { carries = [{ id = "willow_logs" }] }, @@ -59,7 +59,7 @@ setup = [ { inventory_space = { min = 27 } }, ] actions = [ - { option = "Chop down", object = "yew*", delay = 5, success = { inventory_space = { max = 0 } } }, + { object = { option = "Chop down", id = "yew*", delay = 5, success = { inventory_space = { max = 0 } } } }, ] produces = [ { carries = [{ id = "yew_logs" }] }, diff --git a/game/src/main/kotlin/content/bot/action/ActionParser.kt b/game/src/main/kotlin/content/bot/action/ActionParser.kt new file mode 100644 index 0000000000..a0f7bc80af --- /dev/null +++ b/game/src/main/kotlin/content/bot/action/ActionParser.kt @@ -0,0 +1,190 @@ +package content.bot.action + +import content.bot.fact.Requirement + +sealed class ActionParser { + open val required = emptySet() + open val optional = emptySet() + + abstract fun parse(map: Map): BotAction + + fun check(map: Map): String? { + for (key in required) { + if (!map.containsKey(key)) { + return "missing key '$key' in map $map" + } + } + for (key in map.keys) { + if (!map.containsKey(key) && !optional.contains(key)) { + return "unexpected key '$key' in map $map" + } + } + return null + } + + object InteractNpcParser : ActionParser() { + override val required = setOf("id", "option") + override val optional = setOf("delay", "success", "radius", "heal_percent", "loot_over_value") + + override fun parse(map: Map): BotAction { + val option = map["option"] as String + val id = map["id"] as String + val delay = map["delay"] as? Int ?: 0 + val success = requirement(map, "success").singleOrNull() + val radius = map["radius"] as? Int ?: 10 + return if (option == "Attack") { + val healPercent = map["heal_percent"] as? Int ?: 20 + val lootOverValue = map["loot_over_value"] as? Int ?: 0 + BotAction.FightNpc(id, delay, success, radius, healPercent, lootOverValue) + } else { + BotAction.InteractNpc(option, id, delay, success, radius) + } + } + } + + object InterfaceParser : ActionParser() { + override val required = setOf("option", "id") + override val optional = setOf("success") + + override fun parse(map: Map): BotAction { + val option = map["option"] as String + val id = map["id"] as String + val success = requirement(map, "success").singleOrNull() + return BotAction.InterfaceOption(option, id, success) + } + } + + object DialogueParser : ActionParser() { + override val required = setOf("id") + override val optional = setOf("option", "success") + + override fun parse(map: Map): BotAction { + val option = map["option"] as? String ?: "" + val id = map["id"] as String + val success = requirement(map, "success").singleOrNull() + return BotAction.DialogueContinue(option, id, success) + } + } + + object ItemOnObjectParser : ActionParser() { + override val required = setOf("id", "object") + override val optional = setOf("success") + + override fun parse(map: Map): BotAction { + val id = map["id"] as String + val obj = map["object"] as String + val success = requirement(map, "success").singleOrNull() + return BotAction.ItemOnObject(id, obj, success) + } + } + + object ItemOnItemParser : ActionParser() { + override val required = setOf("id", "on") + override val optional = setOf("success") + + override fun parse(map: Map): BotAction { + val id = map["id"] as String + val item = map["on"] as String + val success = requirement(map, "success").singleOrNull() + return BotAction.ItemOnItem(id, item, success) + } + } + + object InteractObjectParser : ActionParser() { + override val required = setOf("option", "id") + override val optional = setOf("delay", "success", "radius") + + override fun parse(map: Map): BotAction { + val option = map["option"] as String + val id = map["id"] as String + val delay = map["delay"] as? Int ?: 0 + val success = requirement(map, "success").singleOrNull() + val radius = map["radius"] as? Int ?: 10 + return BotAction.InteractObject(option, id, delay, success, radius) + } + } + + object GoToParser : ActionParser() { + override val optional = setOf("area", "nearest") + + override fun parse(map: Map) = when { + map.containsKey("area") -> BotAction.GoTo(map["area"] as String) + map.containsKey("nearest") -> BotAction.GoTo(map["nearest"] as String) + else -> error("Expected field 'area' or 'nearest', but got '${map["area"]}'") + } + + } + + object WalkToParser : ActionParser() { + override val required = setOf("x", "y") + override val optional = setOf("radius") + override fun parse(map: Map) = BotAction.WalkTo(map["x"] as Int, map["y"] as Int, map["radius"] as? Int ?: 4) + } + + object WaitParser : ActionParser() { + override val required = setOf("ticks") + override fun parse(map: Map) = BotAction.Wait(map["ticks"] as Int) + } + + object EnterParser : ActionParser() { + override val optional = setOf("int", "string") + override fun parse(map: Map) = when { + map.containsKey("int") -> BotAction.IntEntry(map["int"] as Int) + map.containsKey("string") -> BotAction.StringEntry(map["string"] as String) + else -> error("Expected field 'int' or 'string', but got '${map["area"]}'") + } + } + + object RestartParser : ActionParser() { + override val required = setOf("success") + override val optional = setOf("wait_if") + + override fun parse(map: Map): BotAction { + val requirement = requirement(map, "success").single() + return BotAction.Restart(wait = requirement(map, "wait_if"), success = requirement) + } + } + + companion object { + @Suppress("UNCHECKED_CAST") + private fun requirement(map: Map, key: String): List> { + val parent = map[key] as? Map ?: return listOf() + val key = parent.keys.single() + val value = parent[key] ?: return listOf() + val list = when (value) { + is Map<*, *> -> listOf(value as Map) + is List<*> -> value as List> + else -> return listOf() + } + return Requirement.parse(listOf(key to list), "ActionParser.${key}") + } + + fun parse(list: List>>, name: String): List { + val actions = mutableListOf() + for ((type, map) in list) { + val parser = parsers[type] ?: error("No action parser for '$type' in ${name}.") + val error = parser.check(map) + if (error != null) { + error("Action '$type' $error in ${name}.") + } + val action = parser.parse(map) + actions.add(action) + } + return actions + } + + private val parsers = mapOf( + "npc" to InteractNpcParser, + "object" to InteractObjectParser, + "item_on_object" to ItemOnObjectParser, + "item_on_item" to ItemOnItemParser, + "go_to" to GoToParser, + "tile" to WalkToParser, + "wait" to WaitParser, + "restart" to RestartParser, + "interface" to InterfaceParser, + "continue" to DialogueParser, + "enter" to EnterParser, + ) + } +} \ No newline at end of file diff --git a/game/src/main/kotlin/content/bot/action/Behaviour.kt b/game/src/main/kotlin/content/bot/action/Behaviour.kt index 5e5d5c35a6..5bf0991081 100644 --- a/game/src/main/kotlin/content/bot/action/Behaviour.kt +++ b/game/src/main/kotlin/content/bot/action/Behaviour.kt @@ -46,7 +46,14 @@ private fun loadActivities(activities: MutableMap, template fragments.add(Fragment(id, template, fields, capacity, requires, setup, actions, produces)) } else { val debug = "$id ${exception()}" - activities[id] = BotActivity(id, capacity, Requirement.parse(requires, debug), Requirement.parse(setup, debug), actions, Requirement.parse(produces, debug, requirePredicates = false).toSet()) + activities[id] = BotActivity( + id = id, + capacity = capacity, + requires = Requirement.parse(requires, debug), + setup = Requirement.parse(setup, debug), + actions = ActionParser.parse(actions, debug), + produces = Requirement.parse(produces, debug, requirePredicates = false).toSet() + ) } } for (fragment in fragments) { @@ -67,7 +74,14 @@ private fun loadSetups(resolvers: MutableMap>, tem } else { val debug = "$id ${exception()}" val products = Requirement.parse(produces, debug) - val resolver = Resolver(id, weight, Requirement.parse(requires, debug), Requirement.parse(setup, debug), actions, products.toSet()) + val resolver = Resolver( + id = id, + weight = weight, + requires = Requirement.parse(requires, debug), + setup = Requirement.parse(setup, debug), + actions = ActionParser.parse(actions, debug), + produces = products.toSet() + ) for (product in products) { for (key in product.fact.keys()) { resolvers.getOrPut(key) { mutableListOf() }.add(resolver) @@ -92,7 +106,16 @@ private fun loadShortcuts(shortcuts: MutableList, templates: fragments.add(Fragment(id, template, fields, weight, requires, setup, actions, produces)) } else { val debug = "$id ${exception()}" - shortcuts.add(NavigationShortcut(id, weight, Requirement.parse(requires, debug), Requirement.parse(setup, debug), actions, Requirement.parse(produces, debug, requirePredicates = false).toSet())) + shortcuts.add( + NavigationShortcut( + id = id, + weight = weight, + requires = Requirement.parse(requires, debug), + setup = Requirement.parse(setup, debug), + actions = ActionParser.parse(actions, debug), + produces = Requirement.parse(produces, debug, requirePredicates = false).toSet() + ) + ) } } for (fragment in fragments) { @@ -103,7 +126,7 @@ private fun loadShortcuts(shortcuts: MutableList, templates: } } -private fun load(paths: List, block: ConfigReader.(String, String?, Map?, Int, List>>>, List>>>, List, List>>>) -> Unit) { +private fun load(paths: List, block: ConfigReader.(String, String?, Map?, Int, List>>>, List>>>, List>>, List>>>) -> Unit) { for (path in paths) { Config.fileReader(path) { while (nextSection()) { @@ -113,7 +136,7 @@ private fun load(paths: List, block: ConfigReader.(String, String?, Map< var value = 1 val requires = mutableListOf>>>() val setup = mutableListOf>>>() - val actions = mutableListOf() + val actions = mutableListOf>>() val produces = mutableListOf>>>() while (nextPair()) { when (val key = key()) { @@ -145,7 +168,7 @@ private fun loadTemplates(paths: List): Map { val id = section() val requires = mutableListOf>>>() val setup = mutableListOf>>>() - val actions = mutableListOf() + val actions = mutableListOf>>() val produces = mutableListOf>>>() while (nextPair()) { when (val key = key()) { @@ -165,7 +188,7 @@ private fun loadTemplates(paths: List): Map { return templates } -private fun ConfigReader.requirements(requires: MutableList>>>) { +internal fun ConfigReader.requirements(requires: MutableList>>>) { while (nextElement()) { while (nextEntry()) { val key = key() @@ -182,6 +205,16 @@ private fun ConfigReader.requirements(requires: MutableList>>) { + while (nextElement()) { + while (nextEntry()) { + val type = key() + val map = map() + list.add(type to map) + } + } +} + private data class Fragment( val id: String, val template: String, @@ -189,7 +222,7 @@ private data class Fragment( val int: Int, val requires: List>>>, val setup: List>>>, - val actions: List, // Can fragments even have actions? + val actions: List>>, val produces: List>>>, ) { fun activity(template: Template) = BotActivity( @@ -197,7 +230,7 @@ private data class Fragment( capacity = int, requires = resolveRequirements(template.requires, requires), setup = resolveRequirements(template.setup, setup), - actions = template.actions, + actions = resolveActions(template.actions, actions), produces = resolveRequirements(template.produces, produces, requirePredicates = false).toSet(), ) @@ -205,16 +238,7 @@ private data class Fragment( val combinedList = mutableListOf>>>() combinedList.addAll(original) for ((type, list) in templated) { - val resolved = list.map { map -> - map.mapValues { (key, value) -> - if (value is String && value.contains('$')) { - val ref = value.reference() - val name = ref.trim('$', '{', '}') - val replacement = fields[name] ?: error("No field found for behaviour=$id type=${type} key=$key ref=$ref") - if (replacement is String) value.replace(ref, replacement) else replacement - } else value - }.toMap() - } + val resolved = list.map { map -> resolve(map, type) } if (resolved.isNotEmpty()) { combinedList.add(type to resolved) } @@ -225,6 +249,46 @@ private data class Fragment( return Requirement.parse(combinedList, "$id template $template", requirePredicates) } + private fun resolveActions(templated: List>>, original: List>>, requirePredicates: Boolean = true): List { + val combinedList = mutableListOf>>() + combinedList.addAll(original) + for ((type, map) in templated) { + val resolved = resolve(map, type) + if (resolved.isNotEmpty()) { + combinedList.add(type to resolved) + } + } + if (combinedList.isEmpty()) { + return emptyList() + } + return ActionParser.parse(combinedList, "$id template $template") + } + + @Suppress("UNCHECKED_CAST") + private fun resolve(map: Map, type: String): Map = map.mapValues { (key, value) -> + if (value is String && value.contains('$')) { + val ref = value.reference() + val name = ref.trim('$', '{', '}') + val replacement = fields[name] ?: error("No field found for behaviour=$id type=${type} key=$key ref=$ref") + if (replacement is String) value.replace(ref, replacement) else replacement + } else if (value is Map<*, *>) { + resolve(value as Map, type) + } else if (value is List<*>) { + resolve(value as List, type) + } else { + value + } + }.toMap() + + @Suppress("UNCHECKED_CAST") + private fun resolve(value: List, type: String): List = value.map { element -> + when (element) { + is Map<*, *> -> resolve(element as Map, type) + is List<*> -> resolve(element as List, type) + else -> element + } + } + private fun String.reference(): String { if (startsWith('$')) { return this @@ -242,7 +306,7 @@ private data class Fragment( weight = int, requires = resolveRequirements(template.requires, requires), setup = resolveRequirements(template.setup, setup), - actions = template.actions, + actions = resolveActions(template.actions, actions), produces = resolveRequirements(template.produces, produces, requirePredicates = false).toSet(), ) @@ -251,7 +315,7 @@ private data class Fragment( weight = int, requires = resolveRequirements(template.requires, requires), setup = resolveRequirements(template.setup, setup), - actions = template.actions, + actions = resolveActions(template.actions, actions), produces = resolveRequirements(template.produces, produces, requirePredicates = false).toSet(), ) } @@ -259,169 +323,6 @@ private data class Fragment( private data class Template( val requires: List>>>, val setup: List>>>, - val actions: List, + val actions: List>>, val produces: List>>>, ) - -fun ConfigReader.actions(list: MutableList) { - while (nextElement()) { - var type = "" - var id = "" - var option = "" - var int = 0 - var ticks = 0 - var radius = 10 - var delay = 0 - var heal = 0 - var loot = 0 - var x = 0 - var y = 0 - val references = mutableMapOf() - val wait = mutableListOf>>>() - val success = mutableListOf>>>() - while (nextEntry()) { - when (val key = key()) { - "go_to", "go_to_nearest", "enter_string", "interface", "npc", "object", "clone", "item", "continue" -> { - type = key - id = string() - if (id.contains('$')) { - references[key] = id - } - } - "x" -> { - if (type == "") { - type = "tile" - } - val value = value() - if (value is String && value.contains('$')) { - references[key] = value - } else { - x = value as Int - } - } - "y" -> { - if (type == "") { - type = "tile" - } - val value = value() - if (value is String && value.contains('$')) { - references[key] = value - } else { - y = value as Int - } - } - "target", "id" -> { - id = string() - if (id.contains('$')) { - references[key] = id - } - } - "option", "on" -> { - option = string() - if (option.contains('$')) { - references[key] = option - } - } - "on_object" -> { - type = "${type}_on_object" - option = string() - if (option.contains('$')) { - references[key] = option - } - } - "restart" -> { - require(boolean()) { "Can't have restart = false ${exception()}" } - type = key - } - "success" -> while (nextEntry()) { - val key = key() - if (peek == '[') { - val list = mutableListOf>() - while (nextElement()) { - list.add(map()) - } - success.add(key to list) - } else { - success.add(key to listOf(map())) - } - } - "wait_if" -> while (nextElement()) { - while (nextEntry()) { - val key = key() - if (peek == '[') { - val list = mutableListOf>() - while (nextElement()) { - list.add(map()) - } - wait.add(key to list) - } else { - wait.add(key to listOf(map())) - } - } - } - "wait" -> { - type = key - when (val value = value()) { - is Int -> ticks = value - is String if value.contains('$') -> references[key] = value - else -> throw IllegalArgumentException("Invalid '$key' value: $value ${exception()}") - } - } - "radius" -> when (val value = value()) { - is Int -> radius = value - is String if value.contains('$') -> references[key] = value - else -> throw IllegalArgumentException("Invalid '$key' value: $value ${exception()}") - } - "heal_percent" -> when (val value = value()) { - is Int -> heal = value - is String if value.contains('$') -> references[key] = value - else -> throw IllegalArgumentException("Invalid '$key' value: $value ${exception()}") - } - "loot_over" -> when (val value = value()) { - is Int -> loot = value - is String if value.contains('$') -> references[key] = value - else -> throw IllegalArgumentException("Invalid '$key' value: $value ${exception()}") - } - "delay" -> when (val value = value()) { - is Int -> delay = value - is String if value.contains('$') -> references[key] = value - else -> throw IllegalArgumentException("Invalid '$key' value: $value ${exception()}") - } - "enter_int" -> { - type = key - when (val value = value()) { - is Int -> int = value - is String if value.contains('$') -> references[key] = value - else -> throw IllegalArgumentException("Invalid '$key' value: $value ${exception()}") - } - } - else -> throw IllegalArgumentException("Unknown action key: $key ${exception()}") - } - } - var action = when (type) { - "go_to" -> BotAction.GoTo(id) - "go_to_nearest" -> BotAction.GoToNearest(id) - "enter_string" -> BotAction.StringEntry(id) - "enter_int" -> BotAction.IntEntry(int) - "wait" -> BotAction.Wait(ticks) - "restart" -> BotAction.Restart(Requirement.parse(wait, id), Requirement.parse(success, id).singleOrNull() ?: throw IllegalArgumentException("Restart must have success condition. $id ${exception()}")) - "npc" -> if (option == "Attack") { - BotAction.FightNpc(id = id, delay = delay, success = Requirement.parse(success, id).singleOrNull(), healPercentage = heal, lootOverValue = loot, radius = radius) - } else { - BotAction.InteractNpc(id = id, option = option, delay = delay, success = Requirement.parse(success, id).singleOrNull(), radius = radius) - } - "tile" -> BotAction.WalkTo(x = x, y = y) - "object" -> BotAction.InteractObject(id = id, option = option, delay = delay, success = Requirement.parse(success, id).singleOrNull(), radius = radius) - "interface" -> BotAction.InterfaceOption(id = id, option = option, success = Requirement.parse(success, id).singleOrNull()) - "continue" -> BotAction.DialogueContinue(id = id, option = option, success = Requirement.parse(success, id).singleOrNull()) - "item" -> BotAction.ItemOnItem(item = id, on = option, success = Requirement.parse(success, id).singleOrNull()) - "item_on_object" -> BotAction.ItemOnObject(item = id, id = option, success = Requirement.parse(success, id).singleOrNull()) - "clone" -> BotAction.Clone(id) - else -> throw IllegalArgumentException("Unknown action type: $type ${exception()}") - } - if (references.isNotEmpty()) { - action = BotAction.Reference(action, references) - } - list.add(action) - } -} diff --git a/game/src/main/kotlin/content/bot/action/BotAction.kt b/game/src/main/kotlin/content/bot/action/BotAction.kt index 45ef671759..979771fba5 100644 --- a/game/src/main/kotlin/content/bot/action/BotAction.kt +++ b/game/src/main/kotlin/content/bot/action/BotAction.kt @@ -552,7 +552,6 @@ sealed interface BotAction { * * TODO behaviour loop detection * - TODO how to handle repeat actions e.g. repeat Chop-down trees until inv is full - These are actions Gathering, Skilling etc.. more resolvers like bank all, drop cheap items how to handle combat, one task or multiple? - One Fight action frames should have tick(): State methods diff --git a/game/src/main/kotlin/content/bot/interact/path/Graph.kt b/game/src/main/kotlin/content/bot/interact/path/Graph.kt index d5210427db..9822cd0cfc 100644 --- a/game/src/main/kotlin/content/bot/interact/path/Graph.kt +++ b/game/src/main/kotlin/content/bot/interact/path/Graph.kt @@ -1,8 +1,10 @@ package content.bot.interact.path +import content.bot.action.ActionParser import content.bot.action.BotAction import content.bot.action.NavigationShortcut import content.bot.action.actions +import content.bot.action.requirements import content.bot.bot import content.bot.fact.Predicate import content.bot.fact.Requirement @@ -243,7 +245,7 @@ class Graph( var toY = 0 var toLevel = 0 var cost = 0 - val actions: MutableList = mutableListOf() + val actions: MutableList>> = mutableListOf() val requirements = mutableListOf>>>() while (nextEntry()) { when (val key = key()) { @@ -255,20 +257,7 @@ class Graph( "to_level" -> toLevel = int() "cost" -> cost = int() "actions" -> actions(actions) - "conditions" -> while (nextElement()) { - while (nextEntry()) { - val key = key() - if (peek == '[') { - val list = mutableListOf>() - while (nextElement()) { - list.add(map()) - } - requirements.add(key to list) - } else { - requirements.add(key to listOf(map())) - } - } - } + "conditions" -> requirements(requirements) else -> throw IllegalArgumentException("Unexpected key: '$key' ${exception()}") } } @@ -278,8 +267,8 @@ class Graph( builder.addEdge(Tile(fromX, fromY, fromLevel), Tile(toX, toY, toLevel), cost, listOf(BotAction.WalkTo(toX, toY)), null) builder.addEdge(Tile(toX, toY, toLevel), Tile(fromX, fromY, fromLevel), cost, listOf(BotAction.WalkTo(fromX, fromY)), null) } - requirements.isEmpty() -> builder.addEdge(Tile(fromX, fromY, fromLevel), Tile(toX, toY, toLevel), cost, actions, null) - else -> builder.addEdge(Tile(fromX, fromY, fromLevel), Tile(toX, toY, toLevel), cost, actions, Requirement.parse(requirements, exception())) + requirements.isEmpty() -> builder.addEdge(Tile(fromX, fromY, fromLevel), Tile(toX, toY, toLevel), cost, ActionParser.parse(actions, exception()), null) + else -> builder.addEdge(Tile(fromX, fromY, fromLevel), Tile(toX, toY, toLevel), cost, ActionParser.parse(actions, exception()), Requirement.parse(requirements, exception())) } } } From 875128b7abba3a709dc23ff241ef53c70616d1fd Mon Sep 17 00:00:00 2001 From: GregHib Date: Sun, 8 Feb 2026 13:34:37 +0000 Subject: [PATCH 068/101] Add object fact and require object interact success predicates --- data/bot/al_kharid.nav-edges.toml | 8 ++-- data/bot/cooking.templates.toml | 14 +++---- data/bot/lumbridge.nav-edges.toml | 26 ++++++------- data/bot/teleport.shortcuts.toml | 5 ++- data/bot/varrock.nav-edges.toml | 10 ++--- .../src/main/kotlin/content/bot/BotManager.kt | 6 +-- .../kotlin/content/bot/action/ActionParser.kt | 14 +++---- .../kotlin/content/bot/action/Behaviour.kt | 2 + .../kotlin/content/bot/action/BotAction.kt | 37 ++++++++++++------- .../main/kotlin/content/bot/fact/Deficit.kt | 10 ++++- game/src/main/kotlin/content/bot/fact/Fact.kt | 20 +++++++--- .../kotlin/content/bot/fact/FactParser.kt | 11 ++++++ .../main/kotlin/content/bot/fact/Predicate.kt | 2 + .../kotlin/content/bot/fact/Requirement.kt | 1 + 14 files changed, 105 insertions(+), 61 deletions(-) diff --git a/data/bot/al_kharid.nav-edges.toml b/data/bot/al_kharid.nav-edges.toml index f4c9bf251f..a3c5447673 100644 --- a/data/bot/al_kharid.nav-edges.toml +++ b/data/bot/al_kharid.nav-edges.toml @@ -10,10 +10,10 @@ edges = [ { from_x = 3278, from_y = 3228, to_x = 3268, to_y = 3228 }, # al_kharid_crossroad_to_tollgate_north { from_x = 3278, from_y = 3228, to_x = 3268, to_y = 3227 }, # al_kharid_crossroad_to_tollgate { from_x = 3278, from_y = 3228, to_x = 3280, to_y = 3216 }, # al_kharid_crossroad_to_glider - { from_x = 3267, from_y = 3228, to_x = 3268, to_y = 3228, cost = 1, actions = [{ object = { option = "Pay-toll(10gp)", id = "toll_gate_al_kharid_north", x = 3268, y = 3228 } }], conditions = [{ carries = [{ id = "coins", amount = 10 }] }] }, # lumbridge_tollgate_north_to_al_kharid_tollgate - { from_x = 3268, from_y = 3228, to_x = 3267, to_y = 3228, cost = 1, actions = [{ object = { option = "Pay-toll(10gp)", id = "toll_gate_al_kharid_north", x = 3268, y = 3228 } }], conditions = [{ carries = [{ id = "coins", amount = 10 }] }] }, # al_kharid_tollgate_north_to_lumbridge_tollgate - { from_x = 3267, from_y = 3227, to_x = 3268, to_y = 3227, cost = 1, actions = [{ object = { option = "Pay-toll(10gp)", id = "toll_gate_al_kharid_south", x = 3268, y = 3227 } }], conditions = [{ carries = [{ id = "coins", amount = 10 }] }] }, # lumbridge_tollgate_to_al_kharid - { from_x = 3268, from_y = 3227, to_x = 3267, to_y = 3227, cost = 1, actions = [{ object = { option = "Pay-toll(10gp)", id = "toll_gate_al_kharid_south", x = 3268, y = 3227 } }], conditions = [{ carries = [{ id = "coins", amount = 10 }] }] }, # al_kharid_tollgate_to_lumbridge + { from_x = 3267, from_y = 3228, to_x = 3268, to_y = 3228, cost = 1, actions = [{ object = { option = "Pay-toll(10gp)", id = "toll_gate_al_kharid_north", x = 3268, y = 3228, success = { tile = { x = 3268, y = 3228 } } } }], conditions = [{ carries = [{ id = "coins", amount = 10 }] }] }, # lumbridge_tollgate_north_to_al_kharid_tollgate + { from_x = 3268, from_y = 3228, to_x = 3267, to_y = 3228, cost = 1, actions = [{ object = { option = "Pay-toll(10gp)", id = "toll_gate_al_kharid_north", x = 3268, y = 3228, success = { tile = { x = 3267, y = 3228 } } } }], conditions = [{ carries = [{ id = "coins", amount = 10 }] }] }, # al_kharid_tollgate_north_to_lumbridge_tollgate + { from_x = 3267, from_y = 3227, to_x = 3268, to_y = 3227, cost = 1, actions = [{ object = { option = "Pay-toll(10gp)", id = "toll_gate_al_kharid_south", x = 3268, y = 3227, success = { tile = { x = 3268, y = 3227 } } } }], conditions = [{ carries = [{ id = "coins", amount = 10 }] }] }, # lumbridge_tollgate_to_al_kharid + { from_x = 3268, from_y = 3227, to_x = 3267, to_y = 3227, cost = 1, actions = [{ object = { option = "Pay-toll(10gp)", id = "toll_gate_al_kharid_south", x = 3268, y = 3227, success = { tile = { x = 3267, y = 3227 } } } }], conditions = [{ carries = [{ id = "coins", amount = 10 }] }] }, # al_kharid_tollgate_to_lumbridge { from_x = 3268, from_y = 3227, to_x = 3280, to_y = 3216 }, # al_kharid_tollgate_to_glider { from_x = 3268, from_y = 3228, to_x = 3280, to_y = 3216 }, # al_kharid_tollgate_north_to_glider { from_x = 3280, from_y = 3216, to_x = 3292, to_y = 3215 }, # al_kharid_glider_to_musician diff --git a/data/bot/cooking.templates.toml b/data/bot/cooking.templates.toml index ec11f647ec..cdc69dfbe0 100644 --- a/data/bot/cooking.templates.toml +++ b/data/bot/cooking.templates.toml @@ -4,18 +4,18 @@ requires = [ { skill = { id = "cooking", min = "$level" } }, ] setup = [ - { carries = [{ id = "$raw", amount = 28 }] }, + { carries = { id = "$raw", amount = 28 } }, { area = { id = "$location" } }, ] actions = [ { item_on_object = { id = "$raw", object = "$obj", success = { interface_open = { id = "dialogue_skill_creation" } } } }, { interface = { option = "All", id = "skill_creation_amount:all" } }, { continue = { id = "dialogue_skill_creation:choice1" } }, - { restart = { wait_if = [{ has_queue = { id = "cooking" } }], success = { carries = [{ id = "$raw", max = 0 }] } } } + { restart = { wait_if = { has_queue = { id = "cooking" } }, success = { carries = { id = "$raw", max = 0 } } } } ] produces = [ { skill = { id = "cooking" } }, - { carries = [{ id = "$cooked" }] } + { carries = { id = "$cooked" } } ] # Beef has a different popup so can't use normal template @@ -25,7 +25,7 @@ requires = [ { skill = { id = "cooking", min = 1 } }, ] setup = [ - { carries = [{ id = "raw_beef", amount = 28 }] }, + { carries = { id = "raw_beef", amount = 28 } }, { area = { id = "$location" } }, ] actions = [ @@ -33,9 +33,9 @@ actions = [ { continue = { option = "Continue", id = "dialogue_multi2:line2", success = { interface_open = { id = "dialogue_skill_creation" } } } }, { interface = { option = "All", id = "skill_creation_amount:all" } }, { continue = { id = "dialogue_skill_creation:choice1" } }, - { restart = { wait_if = [{ has_queue = { id = "cooking" } }], success = { carries = [{ id = "raw_beef", max = 0 }] } } } + { restart = { wait_if = { has_queue = { id = "cooking" } }, success = { carries = { id = "raw_beef", max = 0 } } } } ] produces = [ { skill = { id = "cooking" } }, - { carries = [{ id = "beef" }] } -] \ No newline at end of file + { carries = { id = "beef" } } +] diff --git a/data/bot/lumbridge.nav-edges.toml b/data/bot/lumbridge.nav-edges.toml index bdd3e5f974..f21d73363d 100644 --- a/data/bot/lumbridge.nav-edges.toml +++ b/data/bot/lumbridge.nav-edges.toml @@ -4,10 +4,10 @@ edges = [ { from_x = 3236, from_y = 3205, to_x = 3243, to_y = 3209 }, # lumbridge_south_village_to_church { from_x = 3236, from_y = 3218, to_x = 3250, to_y = 3212 }, # lumbridge_gate_south_to_behind_church { from_x = 3250, from_y = 3212, to_x = 3258, to_y = 3206 }, # lumbridge_behind_church_to_church_fishing_spot - { from_x = 3236, from_y = 3205, to_x = 3231, to_y = 3203, cost = 5, actions = [{ object = { option = "Open", id = "door_720_closed", x = 3234, y = 3203 } }, { tile = { x = 3231, y = 3203 } }] }, # lumbridge_south_village_to_bobs_axes - { from_x = 3231, from_y = 3203, to_x = 3236, to_y = 3205, cost = 5, actions = [{ object = { option = "Open", id = "door_720_closed", x = 3234, y = 3203 } }, { tile = { x = 3236, y = 3205 } }] }, # lumbridge_bobs_axes_to_south_village - { from_x = 3236, from_y = 3205, to_x = 3231, to_y = 3197, cost = 5, actions = [{ object = { option = "Open", id = "door_720_closed", x = 3235, y = 3199 } }, { tile = { x = 3231, y = 3197 } }] }, # lumbridge_south_village_to_south_range_house - { from_x = 3231, from_y = 3197, to_x = 3236, to_y = 3205, cost = 5, actions = [{ object = { option = "Open", id = "door_720_closed", x = 3235, y = 3199 } }, { tile = { x = 3236, y = 3205 } }] }, # lumbridge_south_range_house_to_south_village + { from_x = 3236, from_y = 3205, to_x = 3231, to_y = 3203, cost = 5, actions = [{ object = { option = "Open", id = "door_720_closed", x = 3234, y = 3203, success = { object = { id = "door_720_opened", x = 3233, y = 3203 } } } }, { tile = { x = 3231, y = 3203 } }] }, # lumbridge_south_village_to_bobs_axes + { from_x = 3231, from_y = 3203, to_x = 3236, to_y = 3205, cost = 5, actions = [{ object = { option = "Open", id = "door_720_closed", x = 3234, y = 3203, success = { object = { id = "door_720_opened", x = 3233, y = 3203 } } } }, { tile = { x = 3236, y = 3205 } }] }, # lumbridge_bobs_axes_to_south_village + { from_x = 3236, from_y = 3205, to_x = 3231, to_y = 3197, cost = 5, actions = [{ object = { option = "Open", id = "door_720_closed", x = 3235, y = 3199, success = { object = { id = "door_720_opened", x = 3235, y = 3198 } } } }, { tile = { x = 3231, y = 3197 } }] }, # lumbridge_south_village_to_south_range_house + { from_x = 3231, from_y = 3197, to_x = 3236, to_y = 3205, cost = 5, actions = [{ object = { option = "Open", id = "door_720_closed", x = 3235, y = 3199, success = { object = { id = "door_720_opened", x = 3235, y = 3198 } } } }, { tile = { x = 3236, y = 3205 } }] }, # lumbridge_south_range_house_to_south_village { from_x = 3236, from_y = 3205, to_x = 3244, to_y = 3190 }, # lumbridge_south_village_to_graveyard_exit { from_x = 3244, from_y = 3190, to_x = 3253, to_y = 3200 }, # lumbridge_graveyard_exit_to_behind_graveyard { from_x = 3250, from_y = 3212, to_x = 3253, to_y = 3200 }, # lumbridge_behind_church_to_behind_graveyard @@ -26,7 +26,7 @@ edges = [ { from_x = 3199, from_y = 3218, to_x = 3193, to_y = 3236 }, # lumbridge_castle_tower_west_to_tree_patch { from_x = 3184, from_y = 3225, to_x = 3168, to_y = 3221 }, # lumbridge_castle_yew_trees_to_yew_trees_west { from_x = 3184, from_y = 3225, to_x = 3193, to_y = 3236 }, # lumbridge_castle_yew_trees_to_tree_patch - { from_x = 3226, from_y = 3214, to_x = 3227, to_y = 3214, cost = 3, actions = [{ object = { option = "Open", id = "door_627_closed", x = 3226, y = 3214 } }] }, # lumbridge_south_tower_to_ground_floor + { from_x = 3226, from_y = 3214, to_x = 3227, to_y = 3214, cost = 3, actions = [{ object = { option = "Open", id = "door_627_closed", x = 3226, y = 3214, success = { object = { id = "door_627_opened", x = 3227, y = 3214 } } } }] }, # lumbridge_south_tower_to_ground_floor { from_x = 3227, from_y = 3214, to_x = 3229, to_y = 3214, to_level = 1, cost = 1, actions = [{ object = { option = "Climb-up", id = "36768", x = 3229, y = 3213, success = { tile = { level = 1 } } } }] }, # lumbridge_south_tower_ground_floor_to_1st_floor { from_x = 3229, from_y = 3214, from_level = 1, to_x = 3229, to_y = 3214, to_level = 2, cost = 1, actions = [{ object = { option = "Climb-up", id = "36769", x = 3229, y = 3213, success = { tile = { level = 2 } } } }] }, # lumbridge_south_tower_1st_floor_to_2nd_floor { from_x = 3229, from_y = 3214, from_level = 1, to_x = 3229, to_y = 3214, cost = 1, actions = [{ object = { option = "Climb-down", id = "36769", x = 3229, y = 3213, success = { tile = { level = 0 } } } }] }, # lumbridge_south_tower_1st_floor_to_ground_floor @@ -46,10 +46,10 @@ edges = [ { from_x = 3251, from_y = 3252, to_x = 3258, to_y = 3250 }, { from_x = 3250, from_y = 3266, to_x = 3240, to_y = 3280 }, # lumbridge_cow_entrance_to_cow_path { from_x = 3240, from_y = 3280, to_x = 3238, to_y = 3295 }, # lumbridge_cow_path_to_chicken_entrance - { from_x = 3238, from_y = 3295, to_x = 3235, to_y = 3295, cost = 0, actions = [{ object = { option = "Open", id = "gate_235_closed", x = 3237, y = 3295 } }] }, # lumbridge_chicken_entrance_to_chicken_pen - { from_x = 3235, from_y = 3295, to_x = 3238, to_y = 3295, cost = 0, actions = [{ object = { option = "Open", id = "gate_237_closed", x = 3237, y = 3296 } }] }, # lumbridge_chicken_pen_to_chicken_entrance - { from_x = 3250, from_y = 3266, to_x = 3255, to_y = 3266, cost = 0, actions = [{ object = { option = "Open", id = "gate_241_closed", x = 3252, y = 3266 } }] }, # lumbridge_cow_entrance_to_cow_field - { from_x = 3255, from_y = 3266, to_x = 3250, to_y = 3266, cost = 0, actions = [{ object = { option = "Open", id = "gate_239_closed", x = 3252, y = 3267 } }] }, # lumbridge_cow_field_to_cow_entrance + { from_x = 3238, from_y = 3295, to_x = 3235, to_y = 3295, cost = 0, actions = [{ object = { option = "Open", id = "gate_235_closed", x = 3237, y = 3295, success = { object = { id = "gate_235_opened", x = 3236, y = 3295 } } } }] }, # lumbridge_chicken_entrance_to_chicken_pen + { from_x = 3235, from_y = 3295, to_x = 3238, to_y = 3295, cost = 0, actions = [{ object = { option = "Open", id = "gate_237_closed", x = 3237, y = 3296, success = { object = { id = "gate_235_opened", x = 3236, y = 3295 } } } }] }, # lumbridge_chicken_pen_to_chicken_entrance + { from_x = 3250, from_y = 3266, to_x = 3255, to_y = 3266, cost = 0, actions = [{ object = { option = "Open", id = "gate_241_closed", x = 3252, y = 3266, success = { object = { id = "gate_239_opened", x = 3253, y = 3267 } } } }] }, # lumbridge_cow_entrance_to_cow_field + { from_x = 3255, from_y = 3266, to_x = 3250, to_y = 3266, cost = 0, actions = [{ object = { option = "Open", id = "gate_239_closed", x = 3252, y = 3267, success = { object = { id = "gate_239_opened", x = 3253, y = 3267 } } } }] }, # lumbridge_cow_field_to_cow_entrance { from_x = 3244, from_y = 3190, to_x = 3241, to_y = 3176 }, # lumbridge_graveyard_exit_to_swamp_path { from_x = 3241, from_y = 3176, to_x = 3239, to_y = 3160 }, # lumbridge_swamp_path_to_swamp_cross_roads { from_x = 3244, from_y = 3190, to_x = 3231, to_y = 3188 }, # lumbridge_graveyard_exit_to_rats_north_east @@ -83,8 +83,8 @@ edges = [ { from_x = 3236, from_y = 3218, to_x = 3236, to_y = 3219 }, # lumbridge_courtyard_gate_south_to_gate_north { from_x = 3222, from_y = 3218, to_x = 3209, to_y = 3205 }, # lumbridge_courtyard_south_to_grounds_south { from_x = 3230, from_y = 3232, to_x = 3222, to_y = 3241 }, # lumbridge_unstable_house_to_general_store_east - { from_x = 3222, from_y = 3241, to_x = 3217, to_y = 3241, cost = 6, actions = [{ object = { option = "Open", id = "door_720_closed", x = 3219, y = 3241 } }, { tile = { x = 3217, y = 3241 } }] }, # lumbridge_general_store_east_to_general_store - { from_x = 3217, from_y = 3241, to_x = 3222, to_y = 3241, cost = 6, actions = [{ object = { option = "Open", id = "door_720_closed", x = 3219, y = 3241 } }, { tile = { x = 3222, y = 3241 } }] }, # lumbridge_general_store_to_general_store_east + { from_x = 3222, from_y = 3241, to_x = 3217, to_y = 3241, cost = 6, actions = [{ object = { option = "Open", id = "door_720_closed", x = 3219, y = 3241, success = { object = { id = "door_720_opened", x = 3218, y = 3241 } } } }, { tile = { x = 3217, y = 3241 } }] }, # lumbridge_general_store_east_to_general_store + { from_x = 3217, from_y = 3241, to_x = 3222, to_y = 3241, cost = 6, actions = [{ object = { option = "Open", id = "door_720_closed", x = 3219, y = 3241, success = { object = { id = "door_720_opened", x = 3218, y = 3241 } } } }, { tile = { x = 3222, y = 3241 } }] }, # lumbridge_general_store_to_general_store_east { from_x = 3222, from_y = 3241, to_x = 3226, to_y = 3245 }, # lumbridge_general_store_east_to_trees_west { from_x = 3226, from_y = 3245, to_x = 3235, to_y = 3261 }, # lumbridge_west_village_trees_to_bridge_north { from_x = 3222, from_y = 3241, to_x = 3204, to_y = 3247 }, # lumbridge_general_store_east_to_task_building @@ -116,8 +116,8 @@ edges = [ { from_x = 3190, from_y = 3283, to_x = 3190, to_y = 3294 }, { from_x = 3190, from_y = 3294, to_x = 3186, to_y = 3307 }, { from_x = 3186, from_y = 3307, to_x = 3177, to_y = 3315 }, - { from_x = 3177, from_y = 3315, to_x = 3177, to_y = 3316, cost = 0, actions = [{ object = { option = "Open", id = "gate_235_closed", x = 3177, y = 3316 } }] }, - { from_x = 3177, from_y = 3316, to_x = 3177, to_y = 3315, cost = 0, actions = [{ object = { option = "Open", id = "gate_235_closed", x = 3177, y = 3316 } }] }, + { from_x = 3177, from_y = 3315, to_x = 3177, to_y = 3316, cost = 0, actions = [{ object = { option = "Open", id = "gate_235_closed", x = 3177, y = 3316, success = { object = { id = "gate_235_opened", x = 3177, y = 3315 } } } }] }, + { from_x = 3177, from_y = 3316, to_x = 3177, to_y = 3315, cost = 0, actions = [{ object = { option = "Open", id = "gate_235_closed", x = 3177, y = 3316, success = { object = { id = "gate_235_opened", x = 3177, y = 3315 } } } }] }, { from_x = 3177, from_y = 3316, to_x = 3179, to_y = 3326 }, { from_x = 3179, from_y = 3326, to_x = 3178, to_y = 3334 }, { from_x = 3178, from_y = 3334, to_x = 3177, to_y = 3345 }, diff --git a/data/bot/teleport.shortcuts.toml b/data/bot/teleport.shortcuts.toml index 262e354700..43e2838afd 100644 --- a/data/bot/teleport.shortcuts.toml +++ b/data/bot/teleport.shortcuts.toml @@ -49,7 +49,7 @@ requires = [ ] actions = [ { go_to = { nearest = "bank" } }, - { object = { option = "Use-quickly", id = "bank_booth*" } }, + { object = { option = "Use-quickly", id = "bank_booth*", success = { interface_open = { id = "bank" } } } }, { interface = { option = "Withdraw-1", id = "bank:inventory:fire_rune" } }, { interface = { option = "Withdraw-X", id = "bank:inventory:air_rune" } }, { enter = { int = 3 } }, @@ -63,13 +63,14 @@ produces = [ ] [teleport_lumbridge] -weight = 5 +weight = 200 requires = [ { variable = { id = "spellbook_config", equals = 0, default = 0 } }, { clock = { id = "home_teleport_timeout", max = 0 } }, ] actions = [ { interface = { option = "Cast", id = "modern_spellbook:lumbridge_home_teleport" } }, + { wait = { ticks = 25 } }, ] produces = [ { area = { id = "lumbridge_teleport" } } diff --git a/data/bot/varrock.nav-edges.toml b/data/bot/varrock.nav-edges.toml index 4438095300..75b5fd3313 100644 --- a/data/bot/varrock.nav-edges.toml +++ b/data/bot/varrock.nav-edges.toml @@ -9,12 +9,12 @@ edges = [ { from_x = 3162, from_y = 3420, to_x = 3150, to_y = 3416 }, # varrock_west_oak_trees_to_cat_house { from_x = 3150, from_y = 3416, to_x = 3135, to_y = 3416 }, # varrock_west_cat_house_to_barbarian_path { from_x = 3135, from_y = 3416, to_x = 3128, to_y = 3407 }, # varrock_west_barbarian_path_to_air_altar_ruins - { from_x = 3128, from_y = 3407, to_x = 2841, to_y = 4830, cost = 3, actions = [{ object = { option = "null", id = "air_altar_ruins", x = 3126, y = 3404 } }] }, # varrock_west_air_altar_ruins_to_air_altar + { from_x = 3128, from_y = 3407, to_x = 2841, to_y = 4830, cost = 3, actions = [{ object = { option = "null", id = "air_altar_ruins", x = 3126, y = 3404, success = { tile = { x = 2841, y = 4830 } } } }] }, # varrock_west_air_altar_ruins_to_air_altar { from_x = 2841, from_y = 4830, to_x = 2841, to_y = 4829 }, # varrock_west_air_altar_to_exit - { from_x = 2841, from_y = 4829, to_x = 3128, to_y = 3407, cost = 3, actions = [{ object = { option = "Enter", id = "air_altar_portal", x = 2841, y = 4828 } }] }, # varrock_west_air_altar_exit_to_ruins - { from_x = 3304, from_y = 3474, to_x = 2655, to_y = 4831, cost = 3, actions = [{ object = { option = "Enter", id = "earth_altar_ruins", x = 3305, y = 3473 } }] }, # varrock_east_earth_altar_ruins_to_altar + { from_x = 2841, from_y = 4829, to_x = 3128, to_y = 3407, cost = 3, actions = [{ object = { option = "Enter", id = "air_altar_portal", x = 2841, y = 4828, success = { tile = { x = 3128, y = 3407 } } } }] }, # varrock_west_air_altar_exit_to_ruins + { from_x = 3304, from_y = 3474, to_x = 2655, to_y = 4831, cost = 3, actions = [{ object = { option = "Enter", id = "earth_altar_ruins", x = 3305, y = 3473, success = { tile = { x = 2655, y = 4831 } } } }] }, # varrock_east_earth_altar_ruins_to_altar { from_x = 2655, from_y = 4831, to_x = 2655, to_y = 4830 }, # varrock_east_earth_altar_to_exit - { from_x = 2655, from_y = 4830, to_x = 3304, to_y = 3474, cost = 3, actions = [{ object = { option = "Enter", id = "earth_altar_portal", x = 2655, y = 4829 } }] }, # varrock_east_earth_altar_exit_to_ruins + { from_x = 2655, from_y = 4830, to_x = 3304, to_y = 3474, cost = 3, actions = [{ object = { option = "Enter", id = "earth_altar_portal", x = 2655, y = 4829, success = { tile = { x = 3304, y = 3474 } } } }] }, # varrock_east_earth_altar_exit_to_ruins { from_x = 3254, from_y = 3422, to_x = 3265, to_y = 3428 }, # varrock_east_bank_to_dirt_crossroad { from_x = 3265, from_y = 3428, to_x = 3280, to_y = 3428 }, # varrock_east_dirt_crossroad_to_east_crossroad { from_x = 3280, from_y = 3428, to_x = 3287, to_y = 3414 }, # varrock_east_crossroad_to_east_path_south @@ -46,7 +46,7 @@ edges = [ { from_x = 3210, from_y = 3407, to_x = 3211, to_y = 3395 }, # varrock_south_dirt_crossroads_to_blue_moon_inn { from_x = 3210, from_y = 3407, to_x = 3209, to_y = 3399 }, { from_x = 3210, from_y = 3407, to_x = 3209, to_y = 3399 }, - { from_x = 3209, from_y = 3399, to_x = 3207, to_y = 3399, cost = 1, actions = [{ object = { option = "Open", id = "door_444_closed", x = 3209, y = 3399 } }, { tile = { x = 3207, y = 3399 } }] }, + { from_x = 3209, from_y = 3399, to_x = 3207, to_y = 3399, cost = 1, actions = [{ object = { option = "Open", id = "door_444_closed", x = 3209, y = 3399, success = { object = { id = "door_444_opened", x = 3208, y = 3399 } } } }, { tile = { x = 3207, y = 3399 } }] }, { from_x = 3211, from_y = 3395, to_x = 3209, to_y = 3399 }, # varrock_sword_shop { from_x = 3211, from_y = 3395, to_x = 3211, to_y = 3381 }, # varrock_blue_moon_inn_to_south_border { from_x = 3211, from_y = 3381, to_x = 3214, to_y = 3367 }, # varrock_south_border_to_dark_mages diff --git a/game/src/main/kotlin/content/bot/BotManager.kt b/game/src/main/kotlin/content/bot/BotManager.kt index bbb18b84e9..c72ee3407a 100644 --- a/game/src/main/kotlin/content/bot/BotManager.kt +++ b/game/src/main/kotlin/content/bot/BotManager.kt @@ -181,12 +181,12 @@ class BotManager( frame.start(bot) } - private fun availableResolvers(bot: Bot, condition: Requirement<*>): MutableList { + private fun availableResolvers(bot: Bot, requirement: Requirement<*>): MutableList { val options = mutableListOf() - for (deficit in condition.deficits(bot.player)) { + for (deficit in requirement.deficits(bot.player)) { options.add(deficit.resolve(bot.player) ?: continue) } - for (key in condition.fact.keys()) { + for (key in requirement.fact.keys()) { options.addAll(resolvers[key] ?: continue) } return options diff --git a/game/src/main/kotlin/content/bot/action/ActionParser.kt b/game/src/main/kotlin/content/bot/action/ActionParser.kt index a0f7bc80af..f661854b6e 100644 --- a/game/src/main/kotlin/content/bot/action/ActionParser.kt +++ b/game/src/main/kotlin/content/bot/action/ActionParser.kt @@ -91,8 +91,8 @@ sealed class ActionParser { } object InteractObjectParser : ActionParser() { - override val required = setOf("option", "id") - override val optional = setOf("delay", "success", "radius") + override val required = setOf("option", "id", "success") + override val optional = setOf("delay", "radius", "x", "y") override fun parse(map: Map): BotAction { val option = map["option"] as String @@ -100,19 +100,19 @@ sealed class ActionParser { val delay = map["delay"] as? Int ?: 0 val success = requirement(map, "success").singleOrNull() val radius = map["radius"] as? Int ?: 10 - return BotAction.InteractObject(option, id, delay, success, radius) + val x = map["x"] as? Int + val y = map["y"] as? Int + return BotAction.InteractObject(option, id, delay, success, radius, x, y) } } object GoToParser : ActionParser() { override val optional = setOf("area", "nearest") - override fun parse(map: Map) = when { map.containsKey("area") -> BotAction.GoTo(map["area"] as String) - map.containsKey("nearest") -> BotAction.GoTo(map["nearest"] as String) + map.containsKey("nearest") -> BotAction.GoToNearest(map["nearest"] as String) else -> error("Expected field 'area' or 'nearest', but got '${map["area"]}'") } - } object WalkToParser : ActionParser() { @@ -149,7 +149,7 @@ sealed class ActionParser { @Suppress("UNCHECKED_CAST") private fun requirement(map: Map, key: String): List> { val parent = map[key] as? Map ?: return listOf() - val key = parent.keys.single() + val key = parent.keys.singleOrNull() ?: error("Collection $map has more than one element.") val value = parent[key] ?: return listOf() val list = when (value) { is Map<*, *> -> listOf(value as Map) diff --git a/game/src/main/kotlin/content/bot/action/Behaviour.kt b/game/src/main/kotlin/content/bot/action/Behaviour.kt index 5bf0991081..44e8fd9c15 100644 --- a/game/src/main/kotlin/content/bot/action/Behaviour.kt +++ b/game/src/main/kotlin/content/bot/action/Behaviour.kt @@ -26,12 +26,14 @@ fun loadBehaviours( val templates = loadTemplates(files.list(Settings["bots.templates"])) loadActivities(activities, templates, files.list(Settings["bots.definitions"])) // Group activities by requirement types + var total = 0 for (activity in activities.values) { for (req in activity.requires) { for (key in req.fact.groups()) { groups.getOrPut(key) { mutableListOf() }.add(activity.id) } } + total += activity.capacity } loadSetups(resolvers, templates, files.list(Settings["bots.setups"])) loadShortcuts(shortcuts, templates, files.list(Settings["bots.shortcuts"])) diff --git a/game/src/main/kotlin/content/bot/action/BotAction.kt b/game/src/main/kotlin/content/bot/action/BotAction.kt index 979771fba5..b37d209bf7 100644 --- a/game/src/main/kotlin/content/bot/action/BotAction.kt +++ b/game/src/main/kotlin/content/bot/action/BotAction.kt @@ -18,15 +18,18 @@ import world.gregs.voidps.engine.entity.character.mode.interact.PlayerOnFloorIte import world.gregs.voidps.engine.entity.character.mode.interact.PlayerOnNPCInteract import world.gregs.voidps.engine.entity.character.mode.interact.PlayerOnObjectInteract import world.gregs.voidps.engine.entity.character.npc.NPCs +import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.entity.character.player.skill.Skill import world.gregs.voidps.engine.entity.item.Item import world.gregs.voidps.engine.entity.item.floor.FloorItems +import world.gregs.voidps.engine.entity.obj.GameObject import world.gregs.voidps.engine.entity.obj.GameObjects import world.gregs.voidps.engine.event.wildcardEquals import world.gregs.voidps.engine.get import world.gregs.voidps.engine.inv.inventory import world.gregs.voidps.engine.map.Spiral import world.gregs.voidps.network.client.instruction.* +import kotlin.collections.indexOf sealed interface BotAction { fun start(bot: Bot, frame: BehaviourFrame): BehaviourState = BehaviourState.Running @@ -279,6 +282,8 @@ sealed interface BotAction { val delay: Int = 0, val success: Requirement<*>? = null, val radius: Int = 10, + val x: Int? = null, + val y: Int? = null, ) : BotAction { override fun start(bot: Bot, frame: BehaviourFrame): BehaviourState { @@ -295,25 +300,30 @@ sealed interface BotAction { private fun search(bot: Bot): BehaviourState { val player = bot.player - for (tile in Spiral.spiral(player.tile, radius)) { + val start = if (x != null && y != null) player.tile.copy(x = x, y = y) else player.tile + for (tile in Spiral.spiral(start, radius)) { for (obj in GameObjects.at(tile)) { - if (!wildcardEquals(id, obj.id)) { - continue - } - val index = obj.def(player).options?.indexOf(option) - if (index == null || index == -1) { - continue - } - val valid = get().handle(bot.player, InteractObject(obj.intId, obj.x, obj.y, index + 1)) - if (!valid) { - return BehaviourState.Failed(Reason.Invalid("Invalid object interaction: $obj ${index + 1}")) - } - return BehaviourState.Running + return interact(player, obj) ?: continue } } return handleNoTarget() } + private fun interact(player: Player, obj: GameObject): BehaviourState? { + if (!wildcardEquals(id, obj.id)) { + return null + } + val index = obj.def(player).options?.indexOf(option) + if (index == null || index == -1) { + return null + } + val valid = get().handle(player, InteractObject(obj.intId, obj.x, obj.y, index + 1)) + if (!valid) { + return BehaviourState.Failed(Reason.Invalid("Invalid object interaction: $obj ${index + 1}")) + } + return BehaviourState.Running + } + private fun handleNoTarget(): BehaviourState { if (success == null) { return BehaviourState.Failed(Reason.NoTarget) @@ -543,6 +553,7 @@ sealed interface BotAction { * tidy up old bot code * move tags into edges not areas * item tags? + * track timeouts by comparing previous to current produce for progress * * Idea: Reactions? * A separate queue that runs "reactions" e.g. diff --git a/game/src/main/kotlin/content/bot/fact/Deficit.kt b/game/src/main/kotlin/content/bot/fact/Deficit.kt index 3a0fe5d715..2042507999 100644 --- a/game/src/main/kotlin/content/bot/fact/Deficit.kt +++ b/game/src/main/kotlin/content/bot/fact/Deficit.kt @@ -36,13 +36,13 @@ sealed interface Deficit { } iterator.remove() uniqueName.append("_${item.id}") - actions.add(BotAction.InterfaceOption("Wield", "bank:inventory:${item.id}")) + actions.add(BotAction.InterfaceOption("Equip", "inventory:inventory:${item.id}")) } } val spaceNeeded = withdraw(actions, player, entries, uniqueName) if (actions.isNotEmpty()) { return Resolver( - "withdraw_$uniqueName", weight = 20, + "withdraw$uniqueName", weight = 20, setup = listOf( Requirement(Fact.InventorySpace, Predicate.IntRange(spaceNeeded)) ), @@ -76,6 +76,9 @@ sealed interface Deficit { } } if (spaceNeeded > 0) { + if (player.inventory.spaces < spaceNeeded) { + actions.add(2, BotAction.InteractObject("Deposit carried items", "bank:carried", success = Requirement(Fact.InventorySpace, Predicate.IntEquals(28)))) + } actions.add(BotAction.CloseInterface) } return spaceNeeded @@ -112,6 +115,9 @@ sealed interface Deficit { } } if (spaceNeeded > 0) { + if (player.inventory.spaces < spaceNeeded) { + actions.add(2, BotAction.InteractObject("Deposit carried items", "bank:carried", success = Requirement(Fact.InventorySpace, Predicate.IntEquals(28)))) + } actions.add(BotAction.CloseInterface) return Resolver( "withdraw_$uniqueName", weight = 20, diff --git a/game/src/main/kotlin/content/bot/fact/Fact.kt b/game/src/main/kotlin/content/bot/fact/Fact.kt index 7e1c2fc1be..2f1efda816 100644 --- a/game/src/main/kotlin/content/bot/fact/Fact.kt +++ b/game/src/main/kotlin/content/bot/fact/Fact.kt @@ -6,9 +6,8 @@ import world.gregs.voidps.engine.client.variable.remaining import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.entity.character.player.combatLevel import world.gregs.voidps.engine.entity.character.player.skill.Skill -import world.gregs.voidps.engine.entity.character.player.skill.level.Level.hasRequirements -import world.gregs.voidps.engine.entity.character.player.skill.level.Level.hasRequirementsToUse -import world.gregs.voidps.engine.entity.item.Item +import world.gregs.voidps.engine.entity.obj.GameObject +import world.gregs.voidps.engine.entity.obj.GameObjects import world.gregs.voidps.engine.inv.equipment import world.gregs.voidps.engine.inv.inventory import world.gregs.voidps.engine.timer.epochSeconds @@ -111,6 +110,18 @@ sealed class Fact(val priority: Int) { override fun getValue(player: Player) = player.combatLevel } + data class ObjectExists(val filter: Predicate, val tile: Tile) : Fact(1) { + override fun keys() = setOf("object") + override fun getValue(player: Player): Boolean { + for (obj in GameObjects.at(tile)) { + if (filter.test(player, obj.id)) { + return true + } + } + return false + } + } + object AttackLevel : SkillLevel(Skill.Attack) object DefenceLevel : SkillLevel(Skill.Defence) object StrengthLevel : SkillLevel(Skill.Strength) @@ -136,7 +147,6 @@ sealed class Fact(val priority: Int) { object ConstructionLevel : SkillLevel(Skill.Construction) object SummoningLevel : SkillLevel(Skill.Summoning) object DungeoneeringLevel : SkillLevel(Skill.Dungeoneering) - data class ReferenceLevel(val key: String) : SkillLevel(null) abstract class SkillLevel( val skill: Skill?, @@ -172,7 +182,7 @@ sealed class Fact(val priority: Int) { "construction" -> ConstructionLevel "summoning" -> SummoningLevel "dungeoneering" -> DungeoneeringLevel - else -> if (skill.startsWith("$")) ReferenceLevel(skill) else throw IllegalArgumentException("Unknown skill: $skill") + else -> throw IllegalArgumentException("Unknown skill: $skill") } } } diff --git a/game/src/main/kotlin/content/bot/fact/FactParser.kt b/game/src/main/kotlin/content/bot/fact/FactParser.kt index 9ad71ee0c3..77fbeb95cf 100644 --- a/game/src/main/kotlin/content/bot/fact/FactParser.kt +++ b/game/src/main/kotlin/content/bot/fact/FactParser.kt @@ -122,6 +122,16 @@ sealed class FactParser { override fun predicate(map: Map) = Predicate.parseInt(map) } + object ObjectExists : FactParser() { + override val required = setOf("id", "x", "y") + override fun parse(map: Map): Fact.ObjectExists{ + val tile = Tile(map["x"] as Int, map["y"] as Int) + val id = map["id"] as String + return Fact.ObjectExists(Predicate.StringEquals(id), tile) + } + override fun predicate(map: Map) = Predicate.BooleanTrue + } + object Skill : FactParser() { override val required = setOf("id") override fun parse(map: Map) = Fact.SkillLevel.of(map["id"] as String) @@ -144,6 +154,7 @@ sealed class FactParser { "area" to PlayerTile, "combat_level" to CombatLevel, "skill" to Skill, + "object" to ObjectExists, ) } } diff --git a/game/src/main/kotlin/content/bot/fact/Predicate.kt b/game/src/main/kotlin/content/bot/fact/Predicate.kt index 8e4c8f60b4..4165ed928a 100644 --- a/game/src/main/kotlin/content/bot/fact/Predicate.kt +++ b/game/src/main/kotlin/content/bot/fact/Predicate.kt @@ -39,6 +39,7 @@ sealed class Predicate { } data class InArea(val name: String) : Predicate() { + override val evaluator = RequirementEvaluator.TileEval override fun test(player: Player, value: Tile) = value in Areas[name] } @@ -65,6 +66,7 @@ sealed class Predicate { } data class Within(val x: Int, val y: Int, val level: Int, val radius: Int) : Predicate() { + override val evaluator = RequirementEvaluator.TileEval override fun test(player: Player, value: Tile) = value.within(x, y, level, radius) } diff --git a/game/src/main/kotlin/content/bot/fact/Requirement.kt b/game/src/main/kotlin/content/bot/fact/Requirement.kt index ef708041fc..31f986d552 100644 --- a/game/src/main/kotlin/content/bot/fact/Requirement.kt +++ b/game/src/main/kotlin/content/bot/fact/Requirement.kt @@ -29,6 +29,7 @@ data class Requirement(val fact: Fact, val predicate: Predicate? = null } requirements.add(requirement) } + requirements.sortBy { it.fact.priority } return requirements } } From 1e8ca4199dea5cab956004926e910f14b8fdb8dd Mon Sep 17 00:00:00 2001 From: GregHib Date: Sun, 8 Feb 2026 13:45:54 +0000 Subject: [PATCH 069/101] New nav edge format --- data/bot/al_kharid.nav-edges.toml | 56 ++-- data/bot/draynor.nav-edges.toml | 36 +-- data/bot/lumbridge.nav-edges.toml | 278 +++++++++--------- data/bot/varrock.nav-edges.toml | 124 ++++---- .../kotlin/content/bot/interact/path/Graph.kt | 27 +- 5 files changed, 257 insertions(+), 264 deletions(-) diff --git a/data/bot/al_kharid.nav-edges.toml b/data/bot/al_kharid.nav-edges.toml index a3c5447673..6a5017cd8c 100644 --- a/data/bot/al_kharid.nav-edges.toml +++ b/data/bot/al_kharid.nav-edges.toml @@ -1,30 +1,30 @@ edges = [ - { from_x = 3283, from_y = 3331, to_x = 3284, to_y = 3313 }, # al_kharid_mine_north_entrance_to_west_path - { from_x = 3284, from_y = 3313, to_x = 3287, to_y = 3294 }, # al_kharid_mine_west_path_to_south - { from_x = 3287, from_y = 3294, to_x = 3298, to_y = 3280 }, # al_kharid_mine_west_path_to_entrance - { from_x = 3298, from_y = 3280, to_x = 3299, to_y = 3294 }, - { from_x = 3299, from_y = 3294, to_x = 3300, to_y = 3311 }, - { from_x = 3298, from_y = 3280, to_x = 3299, to_y = 3263 }, # al_kharid_mine_entrance_to_mine_south - { from_x = 3299, from_y = 3263, to_x = 3294, to_y = 3242 }, # al_kharid_mine_south_to_north_path - { from_x = 3294, from_y = 3242, to_x = 3278, to_y = 3228 }, # al_kharid_north_path_to_crossroad - { from_x = 3278, from_y = 3228, to_x = 3268, to_y = 3228 }, # al_kharid_crossroad_to_tollgate_north - { from_x = 3278, from_y = 3228, to_x = 3268, to_y = 3227 }, # al_kharid_crossroad_to_tollgate - { from_x = 3278, from_y = 3228, to_x = 3280, to_y = 3216 }, # al_kharid_crossroad_to_glider - { from_x = 3267, from_y = 3228, to_x = 3268, to_y = 3228, cost = 1, actions = [{ object = { option = "Pay-toll(10gp)", id = "toll_gate_al_kharid_north", x = 3268, y = 3228, success = { tile = { x = 3268, y = 3228 } } } }], conditions = [{ carries = [{ id = "coins", amount = 10 }] }] }, # lumbridge_tollgate_north_to_al_kharid_tollgate - { from_x = 3268, from_y = 3228, to_x = 3267, to_y = 3228, cost = 1, actions = [{ object = { option = "Pay-toll(10gp)", id = "toll_gate_al_kharid_north", x = 3268, y = 3228, success = { tile = { x = 3267, y = 3228 } } } }], conditions = [{ carries = [{ id = "coins", amount = 10 }] }] }, # al_kharid_tollgate_north_to_lumbridge_tollgate - { from_x = 3267, from_y = 3227, to_x = 3268, to_y = 3227, cost = 1, actions = [{ object = { option = "Pay-toll(10gp)", id = "toll_gate_al_kharid_south", x = 3268, y = 3227, success = { tile = { x = 3268, y = 3227 } } } }], conditions = [{ carries = [{ id = "coins", amount = 10 }] }] }, # lumbridge_tollgate_to_al_kharid - { from_x = 3268, from_y = 3227, to_x = 3267, to_y = 3227, cost = 1, actions = [{ object = { option = "Pay-toll(10gp)", id = "toll_gate_al_kharid_south", x = 3268, y = 3227, success = { tile = { x = 3267, y = 3227 } } } }], conditions = [{ carries = [{ id = "coins", amount = 10 }] }] }, # al_kharid_tollgate_to_lumbridge - { from_x = 3268, from_y = 3227, to_x = 3280, to_y = 3216 }, # al_kharid_tollgate_to_glider - { from_x = 3268, from_y = 3228, to_x = 3280, to_y = 3216 }, # al_kharid_tollgate_north_to_glider - { from_x = 3280, from_y = 3216, to_x = 3292, to_y = 3215 }, # al_kharid_glider_to_musician - { from_x = 3292, from_y = 3215, to_x = 3300, to_y = 3198 }, # al_kharid_musician_to_silk_path - { from_x = 3300, from_y = 3198, to_x = 3288, to_y = 3189 }, # al_kharid_silk_path_to_scimitar_shop - { from_x = 3280, from_y = 3216, to_x = 3280, to_y = 3200 }, # al_kharid_glider_to_west_shortcut - { from_x = 3280, from_y = 3200, to_x = 3288, to_y = 3189 }, # al_kharid_west_shortcut_to_scimitar_shop - { from_x = 3288, from_y = 3189, to_x = 3282, to_y = 3185 }, # al_kharid_scimitar_shop_to_furnace_entrance - { from_x = 3280, from_y = 3200, to_x = 3282, to_y = 3185 }, # al_kharid_west_shortcut_to_furnace_to_entrance - { from_x = 3282, from_y = 3185, to_x = 3278, to_y = 3177 }, # al_kharid_furnace_entrance_to_bank_crossroads - { from_x = 3278, from_y = 3177, to_x = 3276, to_y = 3168 }, # al_kharid_bank_crossroad_to_entrance - { from_x = 3278, from_y = 3177, to_x = 3274, to_y = 3180 }, # al_kharid_bank_crossroad_to_kebab_shop - { from_x = 3276, from_y = 3168, to_x = 3270, to_y = 3167 }, # al_kharid_bank_entrance_to_bank + { from = { x = 3283, y = 3331 }, to = { x = 3284, y = 3313 } }, # al_kharid_mine_north_entrance_to_west_path + { from = { x = 3284, y = 3313 }, to = { x = 3287, y = 3294 } }, # al_kharid_mine_west_path_to_south + { from = { x = 3287, y = 3294 }, to = { x = 3298, y = 3280 } }, # al_kharid_mine_west_path_to_entrance + { from = { x = 3298, y = 3280 }, to = { x = 3299, y = 3294 } }, + { from = { x = 3299, y = 3294 }, to = { x = 3300, y = 3311 } }, + { from = { x = 3298, y = 3280 }, to = { x = 3299, y = 3263 } }, # al_kharid_mine_entrance_to_mine_south + { from = { x = 3299, y = 3263 }, to = { x = 3294, y = 3242 } }, # al_kharid_mine_south_to_north_path + { from = { x = 3294, y = 3242 }, to = { x = 3278, y = 3228 } }, # al_kharid_north_path_to_crossroad + { from = { x = 3278, y = 3228 }, to = { x = 3268, y = 3228 } }, # al_kharid_crossroad_to_tollgate_north + { from = { x = 3278, y = 3228 }, to = { x = 3268, y = 3227 } }, # al_kharid_crossroad_to_tollgate + { from = { x = 3278, y = 3228 }, to = { x = 3280, y = 3216 } }, # al_kharid_crossroad_to_glider + { from = { x = 3267, y = 3228 }, to = { x = 3268, y = 3228 }, cost = 1, actions = [{ object = { option = "Pay-toll(10gp)", id = "toll_gate_al_kharid_north", x = 3268, y = 3228, success = { tile = { x = 3268, y = 3228 } } } }], conditions = [{ carries = [{ id = "coins", amount = 10 }] }] }, # lumbridge_tollgate_north_to_al_kharid_tollgate + { from = { x = 3268, y = 3228 }, to = { x = 3267, y = 3228 }, cost = 1, actions = [{ object = { option = "Pay-toll(10gp)", id = "toll_gate_al_kharid_north", x = 3268, y = 3228, success = { tile = { x = 3267, y = 3228 } } } }], conditions = [{ carries = [{ id = "coins", amount = 10 }] }] }, # al_kharid_tollgate_north_to_lumbridge_tollgate + { from = { x = 3267, y = 3227 }, to = { x = 3268, y = 3227 }, cost = 1, actions = [{ object = { option = "Pay-toll(10gp)", id = "toll_gate_al_kharid_south", x = 3268, y = 3227, success = { tile = { x = 3268, y = 3227 } } } }], conditions = [{ carries = [{ id = "coins", amount = 10 }] }] }, # lumbridge_tollgate_to_al_kharid + { from = { x = 3268, y = 3227 }, to = { x = 3267, y = 3227 }, cost = 1, actions = [{ object = { option = "Pay-toll(10gp)", id = "toll_gate_al_kharid_south", x = 3268, y = 3227, success = { tile = { x = 3267, y = 3227 } } } }], conditions = [{ carries = [{ id = "coins", amount = 10 }] }] }, # al_kharid_tollgate_to_lumbridge + { from = { x = 3268, y = 3227 }, to = { x = 3280, y = 3216 } }, # al_kharid_tollgate_to_glider + { from = { x = 3268, y = 3228 }, to = { x = 3280, y = 3216 } }, # al_kharid_tollgate_north_to_glider + { from = { x = 3280, y = 3216 }, to = { x = 3292, y = 3215 } }, # al_kharid_glider_to_musician + { from = { x = 3292, y = 3215 }, to = { x = 3300, y = 3198 } }, # al_kharid_musician_to_silk_path + { from = { x = 3300, y = 3198 }, to = { x = 3288, y = 3189 } }, # al_kharid_silk_path_to_scimitar_shop + { from = { x = 3280, y = 3216 }, to = { x = 3280, y = 3200 } }, # al_kharid_glider_to_west_shortcut + { from = { x = 3280, y = 3200 }, to = { x = 3288, y = 3189 } }, # al_kharid_west_shortcut_to_scimitar_shop + { from = { x = 3288, y = 3189 }, to = { x = 3282, y = 3185 } }, # al_kharid_scimitar_shop_to_furnace_entrance + { from = { x = 3280, y = 3200 }, to = { x = 3282, y = 3185 } }, # al_kharid_west_shortcut_to_furnace_to_entrance + { from = { x = 3282, y = 3185 }, to = { x = 3278, y = 3177 } }, # al_kharid_furnace_entrance_to_bank_crossroads + { from = { x = 3278, y = 3177 }, to = { x = 3276, y = 3168 } }, # al_kharid_bank_crossroad_to_entrance + { from = { x = 3278, y = 3177 }, to = { x = 3274, y = 3180 } }, # al_kharid_bank_crossroad_to_kebab_shop + { from = { x = 3276, y = 3168 }, to = { x = 3270, y = 3167 } }, # al_kharid_bank_entrance_to_bank ] \ No newline at end of file diff --git a/data/bot/draynor.nav-edges.toml b/data/bot/draynor.nav-edges.toml index bcd589ca1f..56a78a2ccd 100644 --- a/data/bot/draynor.nav-edges.toml +++ b/data/bot/draynor.nav-edges.toml @@ -1,20 +1,20 @@ edges = [ - { from_x = 3138, from_y = 3227, to_x = 3119, to_y = 3228 }, # draynor_east_to_jail_path_south - { from_x = 3119, from_y = 3228, to_x = 3105, to_y = 3238 }, # draynor_path_south_to_west - { from_x = 3105, from_y = 3238, to_x = 3104, to_y = 3248 }, # draynor_path_west_to_bank_crossroad - { from_x = 3104, from_y = 3248, to_x = 3093, to_y = 3245 }, # draynor_bank_crossroad_to_bank - { from_x = 3093, from_y = 3245, to_x = 3079, to_y = 3249 }, # draynor_bank_to_stalls - { from_x = 3093, from_y = 3245, to_x = 3099, to_y = 3246 }, # draynor_bank_to_trees - { from_x = 3104, from_y = 3248, to_x = 3099, to_y = 3246 }, # draynor_bank_crossroad_to_trees - { from_x = 3079, from_y = 3249, to_x = 3071, to_y = 3266 }, # draynor_stalls_to_pigsty - { from_x = 3079, from_y = 3249, to_x = 3086, to_y = 3237 }, # draynor_stall_to_willow_trees - { from_x = 3079, from_y = 3249, to_x = 3072, to_y = 3250 }, # draynor_stalls_to_west_trees - { from_x = 3079, from_y = 3249, to_x = 3079, to_y = 3265 }, # draynor_stalls_to_north_trees - { from_x = 3093, from_y = 3245, to_x = 3086, to_y = 3237 }, # draynor_bank_to_willow_trees - { from_x = 3086, from_y = 3237, to_x = 3097, to_y = 3235 }, # draynor_willow_trees_to_south - { from_x = 3086, from_y = 3237, to_x = 3086, to_y = 3231 }, # draynor_willow_trees_to_fishing_spot - { from_x = 3086, from_y = 3231, to_x = 3097, to_y = 3235 }, # draynor_fishing_spot_to_south - { from_x = 3105, from_y = 3238, to_x = 3097, to_y = 3235 }, # draynor_jail_path_west_to_south - { from_x = 3138, from_y = 3227, to_x = 3153, to_y = 3216 }, # draynor_east_path_to_swamp_north_wall - { from_x = 3268, from_y = 3331, to_x = 3283, to_y = 3331 }, # varrock_al_kharid_crossroad_to_north_entrance + { from = { x = 3138, y = 3227 }, to = { x = 3119, y = 3228 } }, # draynor_east_to_jail_path_south + { from = { x = 3119, y = 3228 }, to = { x = 3105, y = 3238 } }, # draynor_path_south_to_west + { from = { x = 3105, y = 3238 }, to = { x = 3104, y = 3248 } }, # draynor_path_west_to_bank_crossroad + { from = { x = 3104, y = 3248 }, to = { x = 3093, y = 3245 } }, # draynor_bank_crossroad_to_bank + { from = { x = 3093, y = 3245 }, to = { x = 3079, y = 3249 } }, # draynor_bank_to_stalls + { from = { x = 3093, y = 3245 }, to = { x = 3099, y = 3246 } }, # draynor_bank_to_trees + { from = { x = 3104, y = 3248 }, to = { x = 3099, y = 3246 } }, # draynor_bank_crossroad_to_trees + { from = { x = 3079, y = 3249 }, to = { x = 3071, y = 3266 } }, # draynor_stalls_to_pigsty + { from = { x = 3079, y = 3249 }, to = { x = 3086, y = 3237 } }, # draynor_stall_to_willow_trees + { from = { x = 3079, y = 3249 }, to = { x = 3072, y = 3250 } }, # draynor_stalls_to_west_trees + { from = { x = 3079, y = 3249 }, to = { x = 3079, y = 3265 } }, # draynor_stalls_to_north_trees + { from = { x = 3093, y = 3245 }, to = { x = 3086, y = 3237 } }, # draynor_bank_to_willow_trees + { from = { x = 3086, y = 3237 }, to = { x = 3097, y = 3235 } }, # draynor_willow_trees_to_south + { from = { x = 3086, y = 3237 }, to = { x = 3086, y = 3231 } }, # draynor_willow_trees_to_fishing_spot + { from = { x = 3086, y = 3231 }, to = { x = 3097, y = 3235 } }, # draynor_fishing_spot_to_south + { from = { x = 3105, y = 3238 }, to = { x = 3097, y = 3235 } }, # draynor_jail_path_west_to_south + { from = { x = 3138, y = 3227 }, to = { x = 3153, y = 3216 } }, # draynor_east_path_to_swamp_north_wall + { from = { x = 3268, y = 3331 }, to = { x = 3283, y = 3331 } }, # varrock_al_kharid_crossroad_to_north_entrance ] \ No newline at end of file diff --git a/data/bot/lumbridge.nav-edges.toml b/data/bot/lumbridge.nav-edges.toml index f21d73363d..d555d6118e 100644 --- a/data/bot/lumbridge.nav-edges.toml +++ b/data/bot/lumbridge.nav-edges.toml @@ -1,141 +1,141 @@ edges = [ - { from_x = 3236, from_y = 3218, to_x = 3236, to_y = 3205 }, # lumbridge_gate_south_to_village - { from_x = 3236, from_y = 3218, to_x = 3243, to_y = 3209 }, # lumbridge_gate_south_to_church - { from_x = 3236, from_y = 3205, to_x = 3243, to_y = 3209 }, # lumbridge_south_village_to_church - { from_x = 3236, from_y = 3218, to_x = 3250, to_y = 3212 }, # lumbridge_gate_south_to_behind_church - { from_x = 3250, from_y = 3212, to_x = 3258, to_y = 3206 }, # lumbridge_behind_church_to_church_fishing_spot - { from_x = 3236, from_y = 3205, to_x = 3231, to_y = 3203, cost = 5, actions = [{ object = { option = "Open", id = "door_720_closed", x = 3234, y = 3203, success = { object = { id = "door_720_opened", x = 3233, y = 3203 } } } }, { tile = { x = 3231, y = 3203 } }] }, # lumbridge_south_village_to_bobs_axes - { from_x = 3231, from_y = 3203, to_x = 3236, to_y = 3205, cost = 5, actions = [{ object = { option = "Open", id = "door_720_closed", x = 3234, y = 3203, success = { object = { id = "door_720_opened", x = 3233, y = 3203 } } } }, { tile = { x = 3236, y = 3205 } }] }, # lumbridge_bobs_axes_to_south_village - { from_x = 3236, from_y = 3205, to_x = 3231, to_y = 3197, cost = 5, actions = [{ object = { option = "Open", id = "door_720_closed", x = 3235, y = 3199, success = { object = { id = "door_720_opened", x = 3235, y = 3198 } } } }, { tile = { x = 3231, y = 3197 } }] }, # lumbridge_south_village_to_south_range_house - { from_x = 3231, from_y = 3197, to_x = 3236, to_y = 3205, cost = 5, actions = [{ object = { option = "Open", id = "door_720_closed", x = 3235, y = 3199, success = { object = { id = "door_720_opened", x = 3235, y = 3198 } } } }, { tile = { x = 3236, y = 3205 } }] }, # lumbridge_south_range_house_to_south_village - { from_x = 3236, from_y = 3205, to_x = 3244, to_y = 3190 }, # lumbridge_south_village_to_graveyard_exit - { from_x = 3244, from_y = 3190, to_x = 3253, to_y = 3200 }, # lumbridge_graveyard_exit_to_behind_graveyard - { from_x = 3250, from_y = 3212, to_x = 3253, to_y = 3200 }, # lumbridge_behind_church_to_behind_graveyard - { from_x = 3258, from_y = 3206, to_x = 3253, to_y = 3200 }, # lumbridge_church_fishing_spot_to_behind_graveyard - { from_x = 3205, from_y = 3209, from_level = 2, to_x = 3205, to_y = 3209, to_level = 1, cost = 1, actions = [{ object = { option = "Climb-down", id = "lumbridge_staircase_top", x = 3204, y = 3207, success = { tile = { level = 1 } } } }] }, # lumbridge_castle_2nd_floor_south_stairs_to_1st_floor - { from_x = 3208, from_y = 3219, from_level = 2, to_x = 3205, to_y = 3209, to_level = 2 }, # lumbridge_castle_2nd_floor_bank_to_south_stairs - { from_x = 3205, from_y = 3209, to_x = 3205, to_y = 3209, to_level = 1, cost = 1, actions = [{ object = { option = "Climb-up", id = "lumbridge_staircase", x = 3204, y = 3207, success = { tile = { level = 1 } } } }] }, # lumbridge_castle_ground_floor_south_stairs_to_1st_floor - { from_x = 3205, from_y = 3209, to_x = 3208, to_y = 3210 }, # lumbridge_castle_ground_floor_south_stairs_to_kitchen_corridor - { from_x = 3208, from_y = 3210, to_x = 3215, to_y = 3216 }, # lumbridge_castle_kitchen_corridor_to_castle_south_entrance - { from_x = 3208, from_y = 3210, to_x = 3211, to_y = 3214 }, # lumbridge_castle_kitchen_corridor_to_kitchen - { from_x = 3215, from_y = 3216, to_x = 3222, to_y = 3218 }, # lumbridge_castle_south_entrance_to_courtyard_south - { from_x = 3205, from_y = 3209, from_level = 1, to_x = 3205, to_y = 3209, cost = 1, actions = [{ object = { option = "Climb-down", id = "lumbridge_castle_staircase_south_middle", x = 3204, y = 3207, success = { tile = { level = 0 } } } }] }, # lumbridge_castle_1st_floor_south_stairs_to_ground_floor - { from_x = 3205, from_y = 3209, from_level = 1, to_x = 3205, to_y = 3209, to_level = 2, cost = 1, actions = [{ object = { option = "Climb-up", id = "lumbridge_castle_staircase_south_middle", x = 3204, y = 3207, success = { tile = { level = 2 } } } }] }, # lumbridge_castle_1st_floor_south_stairs_to_2nd_floor - { from_x = 3209, from_y = 3205, to_x = 3199, to_y = 3218 }, # lumbridge_castle_grounds_south_to_tower_west - { from_x = 3199, from_y = 3218, to_x = 3184, to_y = 3225 }, # lumbridge_castle_tower_west_to_yew_trees - { from_x = 3199, from_y = 3218, to_x = 3193, to_y = 3236 }, # lumbridge_castle_tower_west_to_tree_patch - { from_x = 3184, from_y = 3225, to_x = 3168, to_y = 3221 }, # lumbridge_castle_yew_trees_to_yew_trees_west - { from_x = 3184, from_y = 3225, to_x = 3193, to_y = 3236 }, # lumbridge_castle_yew_trees_to_tree_patch - { from_x = 3226, from_y = 3214, to_x = 3227, to_y = 3214, cost = 3, actions = [{ object = { option = "Open", id = "door_627_closed", x = 3226, y = 3214, success = { object = { id = "door_627_opened", x = 3227, y = 3214 } } } }] }, # lumbridge_south_tower_to_ground_floor - { from_x = 3227, from_y = 3214, to_x = 3229, to_y = 3214, to_level = 1, cost = 1, actions = [{ object = { option = "Climb-up", id = "36768", x = 3229, y = 3213, success = { tile = { level = 1 } } } }] }, # lumbridge_south_tower_ground_floor_to_1st_floor - { from_x = 3229, from_y = 3214, from_level = 1, to_x = 3229, to_y = 3214, to_level = 2, cost = 1, actions = [{ object = { option = "Climb-up", id = "36769", x = 3229, y = 3213, success = { tile = { level = 2 } } } }] }, # lumbridge_south_tower_1st_floor_to_2nd_floor - { from_x = 3229, from_y = 3214, from_level = 1, to_x = 3229, to_y = 3214, cost = 1, actions = [{ object = { option = "Climb-down", id = "36769", x = 3229, y = 3213, success = { tile = { level = 0 } } } }] }, # lumbridge_south_tower_1st_floor_to_ground_floor - { from_x = 3229, from_y = 3214, from_level = 2, to_x = 3229, to_y = 3214, to_level = 1, cost = 1, actions = [{ object = { option = "Climb-down", id = "36770", x = 3229, y = 3213, success = { tile = { level = 1 } } } }] }, # lumbridge_south_tower_2nd_floor_to_1st_floor - { from_x = 3236, from_y = 3219, to_x = 3236, to_y = 3225 }, # lumbridge_gate_north_to_bridge_west - { from_x = 3236, from_y = 3225, to_x = 3230, to_y = 3232 }, # lumbridge_bridge_west_to_unstable_house - { from_x = 3236, from_y = 3225, to_x = 3253, to_y = 3225 }, # lumbridge_bridge_west_to_bridge_east - { from_x = 3253, from_y = 3225, to_x = 3263, to_y = 3222 }, # lumbridge_bridge_east_to_trees_east - { from_x = 3253, from_y = 3225, to_x = 3260, to_y = 3228 }, # lumbridge_bridge_east_to_east_crossroad - { from_x = 3260, from_y = 3228, to_x = 3263, to_y = 3222 }, # lumbridge_east_crossroad_to_trees_east - { from_x = 3260, from_y = 3228, to_x = 3259, to_y = 3239 }, - { from_x = 3259, from_y = 3239, to_x = 3258, to_y = 3250 }, - { from_x = 3259, from_y = 3239, to_x = 3256, to_y = 3247 }, - { from_x = 3256, from_y = 3247, to_x = 3251, to_y = 3252 }, - { from_x = 3235, from_y = 3261, to_x = 3250, to_y = 3266 }, # lumbridge_bridge_north_to_cow_entrance - { from_x = 3251, from_y = 3252, to_x = 3250, to_y = 3266 }, # lumbridge_goblin_path_north_to_cow_entrance - { from_x = 3251, from_y = 3252, to_x = 3258, to_y = 3250 }, - { from_x = 3250, from_y = 3266, to_x = 3240, to_y = 3280 }, # lumbridge_cow_entrance_to_cow_path - { from_x = 3240, from_y = 3280, to_x = 3238, to_y = 3295 }, # lumbridge_cow_path_to_chicken_entrance - { from_x = 3238, from_y = 3295, to_x = 3235, to_y = 3295, cost = 0, actions = [{ object = { option = "Open", id = "gate_235_closed", x = 3237, y = 3295, success = { object = { id = "gate_235_opened", x = 3236, y = 3295 } } } }] }, # lumbridge_chicken_entrance_to_chicken_pen - { from_x = 3235, from_y = 3295, to_x = 3238, to_y = 3295, cost = 0, actions = [{ object = { option = "Open", id = "gate_237_closed", x = 3237, y = 3296, success = { object = { id = "gate_235_opened", x = 3236, y = 3295 } } } }] }, # lumbridge_chicken_pen_to_chicken_entrance - { from_x = 3250, from_y = 3266, to_x = 3255, to_y = 3266, cost = 0, actions = [{ object = { option = "Open", id = "gate_241_closed", x = 3252, y = 3266, success = { object = { id = "gate_239_opened", x = 3253, y = 3267 } } } }] }, # lumbridge_cow_entrance_to_cow_field - { from_x = 3255, from_y = 3266, to_x = 3250, to_y = 3266, cost = 0, actions = [{ object = { option = "Open", id = "gate_239_closed", x = 3252, y = 3267, success = { object = { id = "gate_239_opened", x = 3253, y = 3267 } } } }] }, # lumbridge_cow_field_to_cow_entrance - { from_x = 3244, from_y = 3190, to_x = 3241, to_y = 3176 }, # lumbridge_graveyard_exit_to_swamp_path - { from_x = 3241, from_y = 3176, to_x = 3239, to_y = 3160 }, # lumbridge_swamp_path_to_swamp_cross_roads - { from_x = 3244, from_y = 3190, to_x = 3231, to_y = 3188 }, # lumbridge_graveyard_exit_to_rats_north_east - { from_x = 3244, from_y = 3190, to_x = 3231, to_y = 3181 }, # lumbridge_graveyard_exit_to_rats_south_east - { from_x = 3241, from_y = 3176, to_x = 3231, to_y = 3188 }, # lumbridge_swamp_path_to_rats_north_east - { from_x = 3241, from_y = 3176, to_x = 3231, to_y = 3181 }, # lumbridge_swamp_path_to_rats_south_east - { from_x = 3231, from_y = 3188, to_x = 3231, to_y = 3181 }, # lumbridge_swamp_rats_north_east_to_south_east - { from_x = 3239, from_y = 3160, to_x = 3228, to_y = 3148 }, # lumbridge_swamp_cross_roads_to_copper_mine - { from_x = 3228, from_y = 3148, to_x = 3225, to_y = 3147 }, # lumbridge_swamp_copper_mine_to_tin_mine - { from_x = 3228, from_y = 3148, to_x = 3221, to_y = 3156 }, # lumbridge_swamp_copper_mine_to_east_mine - { from_x = 3239, from_y = 3160, to_x = 3221, to_y = 3156 }, # lumbridge_swamp_cross_roads_to_east_mine - { from_x = 3239, from_y = 3160, to_x = 3243, to_y = 3155 }, # lumbridge_swamp_cross_roads_to_fishing_spot - { from_x = 3221, from_y = 3156, to_x = 3201, to_y = 3155 }, # lumbridge_swamp_east_mine_to_urhney_house - { from_x = 3201, from_y = 3155, to_x = 3181, to_y = 3153 }, # lumbridge_swamp_urhney_house_to_water_altar - { from_x = 3181, from_y = 3153, to_x = 3161, to_y = 3151 }, # lumbridge_swamp_water_altar_to_south - { from_x = 3161, from_y = 3151, to_x = 3148, to_y = 3149 }, # lumbridge_swamp_south_to_west_mine - { from_x = 3148, from_y = 3149, to_x = 3146, to_y = 3147 }, # lumbridge_swamp_west_mine_to_mithril_mine - { from_x = 3148, from_y = 3149, to_x = 3146, to_y = 3150 }, # lumbridge_swamp_west_mine_to_coal_mine - { from_x = 3148, from_y = 3149, to_x = 3146, to_y = 3167 }, # lumbridge_swamp_west_mine_to_west_1 - { from_x = 3146, from_y = 3167, to_x = 3143, to_y = 3186 }, # lumbridge_swamp_west_1_to_2 - { from_x = 3143, from_y = 3186, to_x = 3142, to_y = 3203 }, # lumbridge_swamp_west_2_to_west_camp - { from_x = 3142, from_y = 3203, to_x = 3136, to_y = 3219 }, # lumbridge_swamp_west_camp_to_west_wall - { from_x = 3142, from_y = 3203, to_x = 3153, to_y = 3216 }, # lumbridge_swamp_west_camp_to_north_wall - { from_x = 3136, from_y = 3219, to_x = 3153, to_y = 3216 }, # lumbridge_swamp_west_wall_to_north_wall - { from_x = 3136, from_y = 3219, to_x = 3138, to_y = 3227 }, # lumbridge_swamp_west_wall_to_draynor_east_path - { from_x = 3136, from_y = 3219, to_x = 3119, to_y = 3228 }, # lumbridge_swamp_west_wall_to_draynor_jail_path_south - { from_x = 3222, from_y = 3218, to_x = 3226, to_y = 3214 }, # lumbridge_courtyard_south_to_tower_door - { from_x = 3222, from_y = 3218, to_x = 3222, to_y = 3219 }, # lumbridge_courtyard_south_to_north - { from_x = 3222, from_y = 3218, to_x = 3236, to_y = 3218 }, # lumbridge_courtyard_south_to_gate_north - { from_x = 3222, from_y = 3219, to_x = 3236, to_y = 3219 }, # lumbridge_courtyard_north_to_gate_north - { from_x = 3236, from_y = 3218, to_x = 3236, to_y = 3219 }, # lumbridge_courtyard_gate_south_to_gate_north - { from_x = 3222, from_y = 3218, to_x = 3209, to_y = 3205 }, # lumbridge_courtyard_south_to_grounds_south - { from_x = 3230, from_y = 3232, to_x = 3222, to_y = 3241 }, # lumbridge_unstable_house_to_general_store_east - { from_x = 3222, from_y = 3241, to_x = 3217, to_y = 3241, cost = 6, actions = [{ object = { option = "Open", id = "door_720_closed", x = 3219, y = 3241, success = { object = { id = "door_720_opened", x = 3218, y = 3241 } } } }, { tile = { x = 3217, y = 3241 } }] }, # lumbridge_general_store_east_to_general_store - { from_x = 3217, from_y = 3241, to_x = 3222, to_y = 3241, cost = 6, actions = [{ object = { option = "Open", id = "door_720_closed", x = 3219, y = 3241, success = { object = { id = "door_720_opened", x = 3218, y = 3241 } } } }, { tile = { x = 3222, y = 3241 } }] }, # lumbridge_general_store_to_general_store_east - { from_x = 3222, from_y = 3241, to_x = 3226, to_y = 3245 }, # lumbridge_general_store_east_to_trees_west - { from_x = 3226, from_y = 3245, to_x = 3235, to_y = 3261 }, # lumbridge_west_village_trees_to_bridge_north - { from_x = 3222, from_y = 3241, to_x = 3204, to_y = 3247 }, # lumbridge_general_store_east_to_task_building - { from_x = 3217, from_y = 3241, to_x = 3204, to_y = 3247 }, # lumbridge_general_store_to_task_building - { from_x = 3204, from_y = 3247, to_x = 3194, to_y = 3247 }, # lumbridge_task_building_to_fishing_shop_entrance - { from_x = 3194, from_y = 3247, to_x = 3172, to_y = 3239 }, # lumbridge_fishing_shop_entrance_to_west_path - { from_x = 3194, from_y = 3247, to_x = 3193, to_y = 3236 }, # lumbridge_fishing_shop_entrance_to_tree_patch - { from_x = 3194, from_y = 3247, to_x = 3195, to_y = 3251 }, # lumbridge_fishing_shop_entrance_to_fishing_shop - { from_x = 3172, from_y = 3239, to_x = 3157, to_y = 3234 }, # lumbridge_west_path_to_ham_path - { from_x = 3172, from_y = 3239, to_x = 3184, to_y = 3225 }, # lumbridge_west_path_to_yew_trees - { from_x = 3172, from_y = 3239, to_x = 3168, to_y = 3221 }, # lumbridge_west_path_to_yew_trees_west - { from_x = 3168, from_y = 3221, to_x = 3153, to_y = 3216 }, # lumbridge_yew_trees_west_to_swamp_north_wall - { from_x = 3157, from_y = 3234, to_x = 3138, to_y = 3227 }, # lumbridge_ham_path_to_draynor_east_path - { from_x = 3222, from_y = 3241, to_x = 3219, to_y = 3247 }, # lumbridge_general_store_east_to_west_crossroad - { from_x = 3219, from_y = 3247, to_x = 3212, to_y = 3247 }, # lumbridge_west_crossroad_to_combat_hall_east_entrance - { from_x = 3212, from_y = 3247, to_x = 3204, to_y = 3247 }, # lumbridge_combat_hall_east_entrance_to_task_building - { from_x = 3212, from_y = 3247, to_x = 3212, to_y = 3250 }, # lumbridge_combat_hall_entrance_to_east - { from_x = 3204, from_y = 3247, to_x = 3204, to_y = 3250 }, # lumbridge_task_building_to_combat_hall_west - { from_x = 3226, from_y = 3245, to_x = 3225, to_y = 3252 }, # lumbridge_village_trees_west_to_smiths_south - { from_x = 3225, from_y = 3252, to_x = 3222, to_y = 3255 }, # lumbridge_smiths_south_to_west - { from_x = 3222, from_y = 3255, to_x = 3218, to_y = 3255 }, # lumbridge_smiths_west_to_smiths_crossroad - { from_x = 3218, from_y = 3255, to_x = 3219, to_y = 3247 }, # lumbridge_smiths_crossroad_to_west_crossroad - { from_x = 3218, from_y = 3255, to_x = 3223, to_y = 3260 }, - { from_x = 3223, from_y = 3260, to_x = 3235, to_y = 3261 }, - { from_x = 3218, from_y = 3255, to_x = 3217, to_y = 3268 }, - { from_x = 3217, from_y = 3268, to_x = 3213, to_y = 3277 }, - { from_x = 3213, from_y = 3277, to_x = 3199, to_y = 3279 }, - { from_x = 3199, from_y = 3279, to_x = 3190, to_y = 3283 }, - { from_x = 3190, from_y = 3283, to_x = 3190, to_y = 3294 }, - { from_x = 3190, from_y = 3294, to_x = 3186, to_y = 3307 }, - { from_x = 3186, from_y = 3307, to_x = 3177, to_y = 3315 }, - { from_x = 3177, from_y = 3315, to_x = 3177, to_y = 3316, cost = 0, actions = [{ object = { option = "Open", id = "gate_235_closed", x = 3177, y = 3316, success = { object = { id = "gate_235_opened", x = 3177, y = 3315 } } } }] }, - { from_x = 3177, from_y = 3316, to_x = 3177, to_y = 3315, cost = 0, actions = [{ object = { option = "Open", id = "gate_235_closed", x = 3177, y = 3316, success = { object = { id = "gate_235_opened", x = 3177, y = 3315 } } } }] }, - { from_x = 3177, from_y = 3316, to_x = 3179, to_y = 3326 }, - { from_x = 3179, from_y = 3326, to_x = 3178, to_y = 3334 }, - { from_x = 3178, from_y = 3334, to_x = 3177, to_y = 3345 }, - { from_x = 3177, from_y = 3345, to_x = 3177, to_y = 3357 }, - { from_x = 3177, from_y = 3357, to_x = 3178, to_y = 3364 }, - { from_x = 3178, from_y = 3364, to_x = 3184, to_y = 3370 }, - { from_x = 3225, from_y = 3252, to_x = 3219, to_y = 3247 }, # lumbridge_smiths_south_to_west_crossroad - { from_x = 3253, from_y = 3225, to_x = 3245, to_y = 3238 }, # lumbridge_bridge_east_to_goblins_river - { from_x = 3253, from_y = 3225, to_x = 3254, to_y = 3239 }, # lumbridge_bridge_east_to_goblins - { from_x = 3254, from_y = 3239, to_x = 3258, to_y = 3250 }, # lumbridge_goblins_to_trees_east - { from_x = 3254, from_y = 3239, to_x = 3245, to_y = 3238 }, # lumbridge_goblins_to_river - { from_x = 3254, from_y = 3239, to_x = 3251, to_y = 3252 }, # lumbridge_goblins_to_path_north - { from_x = 3251, from_y = 3252, to_x = 3235, to_y = 3261 }, # lumbridge_goblins_path_north_to_bridge_north - { from_x = 3235, from_y = 3261, to_x = 3240, to_y = 3249 }, # lumbridge_bridge_north_to_goblin_fishing_spot - { from_x = 3251, from_y = 3252, to_x = 3240, to_y = 3249 }, # lumbridge_goblin_path_north_to_fishing_spot - { from_x = 3240, from_y = 3249, to_x = 3245, to_y = 3238 }, # lumbridge_goblin_fishing_spot_to_river - { from_x = 3260, from_y = 3228, to_x = 3254, to_y = 3239 }, # lumbridge_east_crossroad_to_goblins - { from_x = 3260, from_y = 3228, to_x = 3267, to_y = 3227 }, # lumbridge_east_crossroad_to_tollgate - { from_x = 3260, from_y = 3228, to_x = 3267, to_y = 3228 }, # lumbridge_east_crossroad_to_tollgate_north - { from_x = 3260, from_y = 3228, to_x = 3263, to_y = 3222 }, # lumbridge_east_crossroad_to_trees_east + { from = { x = 3236, y = 3218 }, to = { x = 3236, y = 3205 } }, # lumbridge_gate_south_to_village + { from = { x = 3236, y = 3218 }, to = { x = 3243, y = 3209 } }, # lumbridge_gate_south_to_church + { from = { x = 3236, y = 3205 }, to = { x = 3243, y = 3209 } }, # lumbridge_south_village_to_church + { from = { x = 3236, y = 3218 }, to = { x = 3250, y = 3212 } }, # lumbridge_gate_south_to_behind_church + { from = { x = 3250, y = 3212 }, to = { x = 3258, y = 3206 } }, # lumbridge_behind_church_to_church_fishing_spot + { from = { x = 3236, y = 3205 }, to = { x = 3231, y = 3203 }, cost = 5, actions = [{ object = { option = "Open", id = "door_720_closed", x = 3234, y = 3203, success = { object = { id = "door_720_opened", x = 3233, y = 3203 } } } }, { tile = { x = 3231, y = 3203 } }] }, # lumbridge_south_village_to_bobs_axes + { from = { x = 3231, y = 3203 }, to = { x = 3236, y = 3205 }, cost = 5, actions = [{ object = { option = "Open", id = "door_720_closed", x = 3234, y = 3203, success = { object = { id = "door_720_opened", x = 3233, y = 3203 } } } }, { tile = { x = 3236, y = 3205 } }] }, # lumbridge_bobs_axes_to_south_village + { from = { x = 3236, y = 3205 }, to = { x = 3231, y = 3197 }, cost = 5, actions = [{ object = { option = "Open", id = "door_720_closed", x = 3235, y = 3199, success = { object = { id = "door_720_opened", x = 3235, y = 3198 } } } }, { tile = { x = 3231, y = 3197 } }] }, # lumbridge_south_village_to_south_range_house + { from = { x = 3231, y = 3197 }, to = { x = 3236, y = 3205 }, cost = 5, actions = [{ object = { option = "Open", id = "door_720_closed", x = 3235, y = 3199, success = { object = { id = "door_720_opened", x = 3235, y = 3198 } } } }, { tile = { x = 3236, y = 3205 } }] }, # lumbridge_south_range_house_to_south_village + { from = { x = 3236, y = 3205 }, to = { x = 3244, y = 3190 } }, # lumbridge_south_village_to_graveyard_exit + { from = { x = 3244, y = 3190 }, to = { x = 3253, y = 3200 } }, # lumbridge_graveyard_exit_to_behind_graveyard + { from = { x = 3250, y = 3212 }, to = { x = 3253, y = 3200 } }, # lumbridge_behind_church_to_behind_graveyard + { from = { x = 3258, y = 3206 }, to = { x = 3253, y = 3200 } }, # lumbridge_church_fishing_spot_to_behind_graveyard + { from = { x = 3205, y = 3209, level = 2 }, to = { x = 3205, y = 3209, level = 1 }, cost = 1, actions = [{ object = { option = "Climb-down", id = "lumbridge_staircase_top", x = 3204, y = 3207, success = { tile = { level = 1 } } } }] }, # lumbridge_castle_2nd_floor_south_stairs_to_1st_floor + { from = { x = 3208, y = 3219, level = 2 }, to = { x = 3205, y = 3209, level = 2 } }, # lumbridge_castle_2nd_floor_bank_to_south_stairs + { from = { x = 3205, y = 3209 }, to = { x = 3205, y = 3209, level = 1 }, cost = 1, actions = [{ object = { option = "Climb-up", id = "lumbridge_staircase", x = 3204, y = 3207, success = { tile = { level = 1 } } } }] }, # lumbridge_castle_ground_floor_south_stairs_to_1st_floor + { from = { x = 3205, y = 3209 }, to = { x = 3208, y = 3210 } }, # lumbridge_castle_ground_floor_south_stairs_to_kitchen_corridor + { from = { x = 3208, y = 3210 }, to = { x = 3215, y = 3216 } }, # lumbridge_castle_kitchen_corridor_to_castle_south_entrance + { from = { x = 3208, y = 3210 }, to = { x = 3211, y = 3214 } }, # lumbridge_castle_kitchen_corridor_to_kitchen + { from = { x = 3215, y = 3216 }, to = { x = 3222, y = 3218 } }, # lumbridge_castle_south_entrance_to_courtyard_south + { from = { x = 3205, y = 3209, level = 1 }, to = { x = 3205, y = 3209 }, cost = 1, actions = [{ object = { option = "Climb-down", id = "lumbridge_castle_staircase_south_middle", x = 3204, y = 3207, success = { tile = { level = 0 } } } }] }, # lumbridge_castle_1st_floor_south_stairs_to_ground_floor + { from = { x = 3205, y = 3209, level = 1 }, to = { x = 3205, y = 3209, level = 2 }, cost = 1, actions = [{ object = { option = "Climb-up", id = "lumbridge_castle_staircase_south_middle", x = 3204, y = 3207, success = { tile = { level = 2 } } } }] }, # lumbridge_castle_1st_floor_south_stairs_to_2nd_floor + { from = { x = 3209, y = 3205 }, to = { x = 3199, y = 3218 } }, # lumbridge_castle_grounds_south_to_tower_west + { from = { x = 3199, y = 3218 }, to = { x = 3184, y = 3225 } }, # lumbridge_castle_tower_west_to_yew_trees + { from = { x = 3199, y = 3218 }, to = { x = 3193, y = 3236 } }, # lumbridge_castle_tower_west_to_tree_patch + { from = { x = 3184, y = 3225 }, to = { x = 3168, y = 3221 } }, # lumbridge_castle_yew_trees_to_yew_trees_west + { from = { x = 3184, y = 3225 }, to = { x = 3193, y = 3236 } }, # lumbridge_castle_yew_trees_to_tree_patch + { from = { x = 3226, y = 3214 }, to = { x = 3227, y = 3214 }, cost = 3, actions = [{ object = { option = "Open", id = "door_627_closed", x = 3226, y = 3214, success = { object = { id = "door_627_opened", x = 3227, y = 3214 } } } }] }, # lumbridge_south_tower_to_ground_floor + { from = { x = 3227, y = 3214 }, to = { x = 3229, y = 3214, level = 1 }, cost = 1, actions = [{ object = { option = "Climb-up", id = "36768", x = 3229, y = 3213, success = { tile = { level = 1 } } } }] }, # lumbridge_south_tower_ground_floor_to_1st_floor + { from = { x = 3229, y = 3214, level = 1 }, to = { x = 3229, y = 3214, level = 2 }, cost = 1, actions = [{ object = { option = "Climb-up", id = "36769", x = 3229, y = 3213, success = { tile = { level = 2 } } } }] }, # lumbridge_south_tower_1st_floor_to_2nd_floor + { from = { x = 3229, y = 3214, level = 1 }, to = { x = 3229, y = 3214 }, cost = 1, actions = [{ object = { option = "Climb-down", id = "36769", x = 3229, y = 3213, success = { tile = { level = 0 } } } }] }, # lumbridge_south_tower_1st_floor_to_ground_floor + { from = { x = 3229, y = 3214, level = 2 }, to = { x = 3229, y = 3214, level = 1 }, cost = 1, actions = [{ object = { option = "Climb-down", id = "36770", x = 3229, y = 3213, success = { tile = { level = 1 } } } }] }, # lumbridge_south_tower_2nd_floor_to_1st_floor + { from = { x = 3236, y = 3219 }, to = { x = 3236, y = 3225 } }, # lumbridge_gate_north_to_bridge_west + { from = { x = 3236, y = 3225 }, to = { x = 3230, y = 3232 } }, # lumbridge_bridge_west_to_unstable_house + { from = { x = 3236, y = 3225 }, to = { x = 3253, y = 3225 } }, # lumbridge_bridge_west_to_bridge_east + { from = { x = 3253, y = 3225 }, to = { x = 3263, y = 3222 } }, # lumbridge_bridge_east_to_trees_east + { from = { x = 3253, y = 3225 }, to = { x = 3260, y = 3228 } }, # lumbridge_bridge_east_to_east_crossroad + { from = { x = 3260, y = 3228 }, to = { x = 3263, y = 3222 } }, # lumbridge_east_crossroad_to_trees_east + { from = { x = 3260, y = 3228 }, to = { x = 3259, y = 3239 } }, + { from = { x = 3259, y = 3239 }, to = { x = 3258, y = 3250 } }, + { from = { x = 3259, y = 3239 }, to = { x = 3256, y = 3247 } }, + { from = { x = 3256, y = 3247 }, to = { x = 3251, y = 3252 } }, + { from = { x = 3235, y = 3261 }, to = { x = 3250, y = 3266 } }, # lumbridge_bridge_north_to_cow_entrance + { from = { x = 3251, y = 3252 }, to = { x = 3250, y = 3266 } }, # lumbridge_goblin_path_north_to_cow_entrance + { from = { x = 3251, y = 3252 }, to = { x = 3258, y = 3250 } }, + { from = { x = 3250, y = 3266 }, to = { x = 3240, y = 3280 } }, # lumbridge_cow_entrance_to_cow_path + { from = { x = 3240, y = 3280 }, to = { x = 3238, y = 3295 } }, # lumbridge_cow_path_to_chicken_entrance + { from = { x = 3238, y = 3295 }, to = { x = 3235, y = 3295 }, cost = 0, actions = [{ object = { option = "Open", id = "gate_235_closed", x = 3237, y = 3295, success = { object = { id = "gate_235_opened", x = 3236, y = 3295 } } } }] }, # lumbridge_chicken_entrance_to_chicken_pen + { from = { x = 3235, y = 3295 }, to = { x = 3238, y = 3295 }, cost = 0, actions = [{ object = { option = "Open", id = "gate_237_closed", x = 3237, y = 3296, success = { object = { id = "gate_235_opened", x = 3236, y = 3295 } } } }] }, # lumbridge_chicken_pen_to_chicken_entrance + { from = { x = 3250, y = 3266 }, to = { x = 3255, y = 3266 }, cost = 0, actions = [{ object = { option = "Open", id = "gate_241_closed", x = 3252, y = 3266, success = { object = { id = "gate_239_opened", x = 3253, y = 3267 } } } }] }, # lumbridge_cow_entrance_to_cow_field + { from = { x = 3255, y = 3266 }, to = { x = 3250, y = 3266 }, cost = 0, actions = [{ object = { option = "Open", id = "gate_239_closed", x = 3252, y = 3267, success = { object = { id = "gate_239_opened", x = 3253, y = 3267 } } } }] }, # lumbridge_cow_field_to_cow_entrance + { from = { x = 3244, y = 3190 }, to = { x = 3241, y = 3176 } }, # lumbridge_graveyard_exit_to_swamp_path + { from = { x = 3241, y = 3176 }, to = { x = 3239, y = 3160 } }, # lumbridge_swamp_path_to_swamp_cross_roads + { from = { x = 3244, y = 3190 }, to = { x = 3231, y = 3188 } }, # lumbridge_graveyard_exit_to_rats_north_east + { from = { x = 3244, y = 3190 }, to = { x = 3231, y = 3181 } }, # lumbridge_graveyard_exit_to_rats_south_east + { from = { x = 3241, y = 3176 }, to = { x = 3231, y = 3188 } }, # lumbridge_swamp_path_to_rats_north_east + { from = { x = 3241, y = 3176 }, to = { x = 3231, y = 3181 } }, # lumbridge_swamp_path_to_rats_south_east + { from = { x = 3231, y = 3188 }, to = { x = 3231, y = 3181 } }, # lumbridge_swamp_rats_north_east_to_south_east + { from = { x = 3239, y = 3160 }, to = { x = 3228, y = 3148 } }, # lumbridge_swamp_cross_roads_to_copper_mine + { from = { x = 3228, y = 3148 }, to = { x = 3225, y = 3147 } }, # lumbridge_swamp_copper_mine_to_tin_mine + { from = { x = 3228, y = 3148 }, to = { x = 3221, y = 3156 } }, # lumbridge_swamp_copper_mine_to_east_mine + { from = { x = 3239, y = 3160 }, to = { x = 3221, y = 3156 } }, # lumbridge_swamp_cross_roads_to_east_mine + { from = { x = 3239, y = 3160 }, to = { x = 3243, y = 3155 } }, # lumbridge_swamp_cross_roads_to_fishing_spot + { from = { x = 3221, y = 3156 }, to = { x = 3201, y = 3155 } }, # lumbridge_swamp_east_mine_to_urhney_house + { from = { x = 3201, y = 3155 }, to = { x = 3181, y = 3153 } }, # lumbridge_swamp_urhney_house_to_water_altar + { from = { x = 3181, y = 3153 }, to = { x = 3161, y = 3151 } }, # lumbridge_swamp_water_altar_to_south + { from = { x = 3161, y = 3151 }, to = { x = 3148, y = 3149 } }, # lumbridge_swamp_south_to_west_mine + { from = { x = 3148, y = 3149 }, to = { x = 3146, y = 3147 } }, # lumbridge_swamp_west_mine_to_mithril_mine + { from = { x = 3148, y = 3149 }, to = { x = 3146, y = 3150 } }, # lumbridge_swamp_west_mine_to_coal_mine + { from = { x = 3148, y = 3149 }, to = { x = 3146, y = 3167 } }, # lumbridge_swamp_west_mine_to_west_1 + { from = { x = 3146, y = 3167 }, to = { x = 3143, y = 3186 } }, # lumbridge_swamp_west_1_to_2 + { from = { x = 3143, y = 3186 }, to = { x = 3142, y = 3203 } }, # lumbridge_swamp_west_2_to_west_camp + { from = { x = 3142, y = 3203 }, to = { x = 3136, y = 3219 } }, # lumbridge_swamp_west_camp_to_west_wall + { from = { x = 3142, y = 3203 }, to = { x = 3153, y = 3216 } }, # lumbridge_swamp_west_camp_to_north_wall + { from = { x = 3136, y = 3219 }, to = { x = 3153, y = 3216 } }, # lumbridge_swamp_west_wall_to_north_wall + { from = { x = 3136, y = 3219 }, to = { x = 3138, y = 3227 } }, # lumbridge_swamp_west_wall_to_draynor_east_path + { from = { x = 3136, y = 3219 }, to = { x = 3119, y = 3228 } }, # lumbridge_swamp_west_wall_to_draynor_jail_path_south + { from = { x = 3222, y = 3218 }, to = { x = 3226, y = 3214 } }, # lumbridge_courtyard_south_to_tower_door + { from = { x = 3222, y = 3218 }, to = { x = 3222, y = 3219 } }, # lumbridge_courtyard_south_to_north + { from = { x = 3222, y = 3218 }, to = { x = 3236, y = 3218 } }, # lumbridge_courtyard_south_to_gate_north + { from = { x = 3222, y = 3219 }, to = { x = 3236, y = 3219 } }, # lumbridge_courtyard_north_to_gate_north + { from = { x = 3236, y = 3218 }, to = { x = 3236, y = 3219 } }, # lumbridge_courtyard_gate_south_to_gate_north + { from = { x = 3222, y = 3218 }, to = { x = 3209, y = 3205 } }, # lumbridge_courtyard_south_to_grounds_south + { from = { x = 3230, y = 3232 }, to = { x = 3222, y = 3241 } }, # lumbridge_unstable_house_to_general_store_east + { from = { x = 3222, y = 3241 }, to = { x = 3217, y = 3241 }, cost = 6, actions = [{ object = { option = "Open", id = "door_720_closed", x = 3219, y = 3241, success = { object = { id = "door_720_opened", x = 3218, y = 3241 } } } }, { tile = { x = 3217, y = 3241 } }] }, # lumbridge_general_store_east_to_general_store + { from = { x = 3217, y = 3241 }, to = { x = 3222, y = 3241 }, cost = 6, actions = [{ object = { option = "Open", id = "door_720_closed", x = 3219, y = 3241, success = { object = { id = "door_720_opened", x = 3218, y = 3241 } } } }, { tile = { x = 3222, y = 3241 } }] }, # lumbridge_general_store_to_general_store_east + { from = { x = 3222, y = 3241 }, to = { x = 3226, y = 3245 } }, # lumbridge_general_store_east_to_trees_west + { from = { x = 3226, y = 3245 }, to = { x = 3235, y = 3261 } }, # lumbridge_west_village_trees_to_bridge_north + { from = { x = 3222, y = 3241 }, to = { x = 3204, y = 3247 } }, # lumbridge_general_store_east_to_task_building + { from = { x = 3217, y = 3241 }, to = { x = 3204, y = 3247 } }, # lumbridge_general_store_to_task_building + { from = { x = 3204, y = 3247 }, to = { x = 3194, y = 3247 } }, # lumbridge_task_building_to_fishing_shop_entrance + { from = { x = 3194, y = 3247 }, to = { x = 3172, y = 3239 } }, # lumbridge_fishing_shop_entrance_to_west_path + { from = { x = 3194, y = 3247 }, to = { x = 3193, y = 3236 } }, # lumbridge_fishing_shop_entrance_to_tree_patch + { from = { x = 3194, y = 3247 }, to = { x = 3195, y = 3251 } }, # lumbridge_fishing_shop_entrance_to_fishing_shop + { from = { x = 3172, y = 3239 }, to = { x = 3157, y = 3234 } }, # lumbridge_west_path_to_ham_path + { from = { x = 3172, y = 3239 }, to = { x = 3184, y = 3225 } }, # lumbridge_west_path_to_yew_trees + { from = { x = 3172, y = 3239 }, to = { x = 3168, y = 3221 } }, # lumbridge_west_path_to_yew_trees_west + { from = { x = 3168, y = 3221 }, to = { x = 3153, y = 3216 } }, # lumbridge_yew_trees_west_to_swamp_north_wall + { from = { x = 3157, y = 3234 }, to = { x = 3138, y = 3227 } }, # lumbridge_ham_path_to_draynor_east_path + { from = { x = 3222, y = 3241 }, to = { x = 3219, y = 3247 } }, # lumbridge_general_store_east_to_west_crossroad + { from = { x = 3219, y = 3247 }, to = { x = 3212, y = 3247 } }, # lumbridge_west_crossroad_to_combat_hall_east_entrance + { from = { x = 3212, y = 3247 }, to = { x = 3204, y = 3247 } }, # lumbridge_combat_hall_east_entrance_to_task_building + { from = { x = 3212, y = 3247 }, to = { x = 3212, y = 3250 } }, # lumbridge_combat_hall_entrance_to_east + { from = { x = 3204, y = 3247 }, to = { x = 3204, y = 3250 } }, # lumbridge_task_building_to_combat_hall_west + { from = { x = 3226, y = 3245 }, to = { x = 3225, y = 3252 } }, # lumbridge_village_trees_west_to_smiths_south + { from = { x = 3225, y = 3252 }, to = { x = 3222, y = 3255 } }, # lumbridge_smiths_south_to_west + { from = { x = 3222, y = 3255 }, to = { x = 3218, y = 3255 } }, # lumbridge_smiths_west_to_smiths_crossroad + { from = { x = 3218, y = 3255 }, to = { x = 3219, y = 3247 } }, # lumbridge_smiths_crossroad_to_west_crossroad + { from = { x = 3218, y = 3255 }, to = { x = 3223, y = 3260 } }, + { from = { x = 3223, y = 3260 }, to = { x = 3235, y = 3261 } }, + { from = { x = 3218, y = 3255 }, to = { x = 3217, y = 3268 } }, + { from = { x = 3217, y = 3268 }, to = { x = 3213, y = 3277 } }, + { from = { x = 3213, y = 3277 }, to = { x = 3199, y = 3279 } }, + { from = { x = 3199, y = 3279 }, to = { x = 3190, y = 3283 } }, + { from = { x = 3190, y = 3283 }, to = { x = 3190, y = 3294 } }, + { from = { x = 3190, y = 3294 }, to = { x = 3186, y = 3307 } }, + { from = { x = 3186, y = 3307 }, to = { x = 3177, y = 3315 } }, + { from = { x = 3177, y = 3315 }, to = { x = 3177, y = 3316 }, cost = 0, actions = [{ object = { option = "Open", id = "gate_235_closed", x = 3177, y = 3316, success = { object = { id = "gate_235_opened", x = 3177, y = 3315 } } } }] }, + { from = { x = 3177, y = 3316 }, to = { x = 3177, y = 3315 }, cost = 0, actions = [{ object = { option = "Open", id = "gate_235_closed", x = 3177, y = 3316, success = { object = { id = "gate_235_opened", x = 3177, y = 3315 } } } }] }, + { from = { x = 3177, y = 3316 }, to = { x = 3179, y = 3326 } }, + { from = { x = 3179, y = 3326 }, to = { x = 3178, y = 3334 } }, + { from = { x = 3178, y = 3334 }, to = { x = 3177, y = 3345 } }, + { from = { x = 3177, y = 3345 }, to = { x = 3177, y = 3357 } }, + { from = { x = 3177, y = 3357 }, to = { x = 3178, y = 3364 } }, + { from = { x = 3178, y = 3364 }, to = { x = 3184, y = 3370 } }, + { from = { x = 3225, y = 3252 }, to = { x = 3219, y = 3247 } }, # lumbridge_smiths_south_to_west_crossroad + { from = { x = 3253, y = 3225 }, to = { x = 3245, y = 3238 } }, # lumbridge_bridge_east_to_goblins_river + { from = { x = 3253, y = 3225 }, to = { x = 3254, y = 3239 } }, # lumbridge_bridge_east_to_goblins + { from = { x = 3254, y = 3239 }, to = { x = 3258, y = 3250 } }, # lumbridge_goblins_to_trees_east + { from = { x = 3254, y = 3239 }, to = { x = 3245, y = 3238 } }, # lumbridge_goblins_to_river + { from = { x = 3254, y = 3239 }, to = { x = 3251, y = 3252 } }, # lumbridge_goblins_to_path_north + { from = { x = 3251, y = 3252 }, to = { x = 3235, y = 3261 } }, # lumbridge_goblins_path_north_to_bridge_north + { from = { x = 3235, y = 3261 }, to = { x = 3240, y = 3249 } }, # lumbridge_bridge_north_to_goblin_fishing_spot + { from = { x = 3251, y = 3252 }, to = { x = 3240, y = 3249 } }, # lumbridge_goblin_path_north_to_fishing_spot + { from = { x = 3240, y = 3249 }, to = { x = 3245, y = 3238 } }, # lumbridge_goblin_fishing_spot_to_river + { from = { x = 3260, y = 3228 }, to = { x = 3254, y = 3239 } }, # lumbridge_east_crossroad_to_goblins + { from = { x = 3260, y = 3228 }, to = { x = 3267, y = 3227 } }, # lumbridge_east_crossroad_to_tollgate + { from = { x = 3260, y = 3228 }, to = { x = 3267, y = 3228 } }, # lumbridge_east_crossroad_to_tollgate_north + { from = { x = 3260, y = 3228 }, to = { x = 3263, y = 3222 } }, # lumbridge_east_crossroad_to_trees_east ] diff --git a/data/bot/varrock.nav-edges.toml b/data/bot/varrock.nav-edges.toml index 75b5fd3313..699881b4ec 100644 --- a/data/bot/varrock.nav-edges.toml +++ b/data/bot/varrock.nav-edges.toml @@ -1,64 +1,64 @@ edges = [ - { from_x = 3213, from_y = 3428, to_x = 3223, to_y = 3429 }, # varrock_teleport_to_centre_east - { from_x = 3213, from_y = 3428, to_x = 3200, to_y = 3429 }, # varrock_teleport_to_centre_west - { from_x = 3213, from_y = 3428, to_x = 3211, to_y = 3420 }, # varrock_teleport_to_centre_south - { from_x = 3200, from_y = 3429, to_x = 3186, to_y = 3430 }, # varrock_centre_west_to_west_bank_south_entrance - { from_x = 3186, from_y = 3430, to_x = 3186, to_y = 3435 }, # varrock_west_bank_south_entrance_to_bank_south - { from_x = 3186, from_y = 3430, to_x = 3171, to_y = 3429 }, # varrock_west_bank_south_entrance_to_romeo_crossroad - { from_x = 3171, from_y = 3429, to_x = 3162, to_y = 3420 }, # varrock_west_romeo_crossroads_to_oak_tress - { from_x = 3162, from_y = 3420, to_x = 3150, to_y = 3416 }, # varrock_west_oak_trees_to_cat_house - { from_x = 3150, from_y = 3416, to_x = 3135, to_y = 3416 }, # varrock_west_cat_house_to_barbarian_path - { from_x = 3135, from_y = 3416, to_x = 3128, to_y = 3407 }, # varrock_west_barbarian_path_to_air_altar_ruins - { from_x = 3128, from_y = 3407, to_x = 2841, to_y = 4830, cost = 3, actions = [{ object = { option = "null", id = "air_altar_ruins", x = 3126, y = 3404, success = { tile = { x = 2841, y = 4830 } } } }] }, # varrock_west_air_altar_ruins_to_air_altar - { from_x = 2841, from_y = 4830, to_x = 2841, to_y = 4829 }, # varrock_west_air_altar_to_exit - { from_x = 2841, from_y = 4829, to_x = 3128, to_y = 3407, cost = 3, actions = [{ object = { option = "Enter", id = "air_altar_portal", x = 2841, y = 4828, success = { tile = { x = 3128, y = 3407 } } } }] }, # varrock_west_air_altar_exit_to_ruins - { from_x = 3304, from_y = 3474, to_x = 2655, to_y = 4831, cost = 3, actions = [{ object = { option = "Enter", id = "earth_altar_ruins", x = 3305, y = 3473, success = { tile = { x = 2655, y = 4831 } } } }] }, # varrock_east_earth_altar_ruins_to_altar - { from_x = 2655, from_y = 4831, to_x = 2655, to_y = 4830 }, # varrock_east_earth_altar_to_exit - { from_x = 2655, from_y = 4830, to_x = 3304, to_y = 3474, cost = 3, actions = [{ object = { option = "Enter", id = "earth_altar_portal", x = 2655, y = 4829, success = { tile = { x = 3304, y = 3474 } } } }] }, # varrock_east_earth_altar_exit_to_ruins - { from_x = 3254, from_y = 3422, to_x = 3265, to_y = 3428 }, # varrock_east_bank_to_dirt_crossroad - { from_x = 3265, from_y = 3428, to_x = 3280, to_y = 3428 }, # varrock_east_dirt_crossroad_to_east_crossroad - { from_x = 3280, from_y = 3428, to_x = 3287, to_y = 3414 }, # varrock_east_crossroad_to_east_path_south - { from_x = 3287, from_y = 3414, to_x = 3291, to_y = 3399 }, # varrock_east_path_to_south - { from_x = 3291, from_y = 3399, to_x = 3293, to_y = 3384 }, # varrock_east_path_south_to_east_border - { from_x = 3280, from_y = 3428, to_x = 3286, to_y = 3442 }, # varrock_east_crossroad_to_path_north - { from_x = 3286, from_y = 3442, to_x = 3287, to_y = 3457 }, # varrock_east_path_to_north - { from_x = 3287, from_y = 3457, to_x = 3296, to_y = 3462 }, # varrock_east_path_north_to_sawmill_crossroad - { from_x = 3296, from_y = 3462, to_x = 3304, to_y = 3474 }, # varrock_east_sawmill_crossroad_to_eath_altar_ruins - { from_x = 3265, from_y = 3428, to_x = 3246, to_y = 3429 }, # varrock_east_dirt_crossroad_to_bank_crossroad - { from_x = 3246, from_y = 3429, to_x = 3254, to_y = 3422 }, # varrock_east_bank_crossroad_to_east_bank - { from_x = 3230, from_y = 3430, to_x = 3246, to_y = 3429 }, # varrock_east_armour_shop_to_bank_crossroad - { from_x = 3230, from_y = 3430, to_x = 3223, to_y = 3429 }, # varrock_east_armour_shop_to_centre_east - { from_x = 3238, from_y = 3295, to_x = 3238, to_y = 3304 }, # lumbridge_chicken_entrance_to_varrock_south_split - { from_x = 3238, from_y = 3304, to_x = 3251, to_y = 3319 }, # varrock_south_split_to_al_kharid_path_west - { from_x = 3238, from_y = 3304, to_x = 3238, to_y = 3320 }, # varrock_south_split_to_al_kharid_path_west - { from_x = 3238, from_y = 3320, to_x = 3240, to_y = 3336 }, # varrock_south_split_to_al_kharid_path_west - { from_x = 3251, from_y = 3319, to_x = 3268, to_y = 3331 }, # varrock_al_kharid_path_west_to_crossroads - { from_x = 3283, from_y = 3331, to_x = 3295, to_y = 3334 }, # al_kharid_north_entrance_to_varrock_north_west - { from_x = 3295, from_y = 3334, to_x = 3304, to_y = 3335 }, # varrock_al_kharid_north_west_to_crossroad - { from_x = 3295, from_y = 3334, to_x = 3299, to_y = 3346 }, # varrock_al_kharid_north_west_to_south_east_path - { from_x = 3304, from_y = 3335, to_x = 3299, to_y = 3346 }, # varrock_al_kharid_north_west_crossroad_to_south_east_path - { from_x = 3299, from_y = 3346, to_x = 3298, to_y = 3359 }, # varrock_south_east_path_to_east_mine_path_south - { from_x = 3298, from_y = 3359, to_x = 3295, to_y = 3372 }, # varrock_south_east_mine_south_to_path - { from_x = 3295, from_y = 3372, to_x = 3286, to_y = 3371 }, # varrock_south_east_mine_path_to_east - { from_x = 3286, from_y = 3371, to_x = 3293, to_y = 3384 }, # varrock_south_east_mine_to_border - { from_x = 3295, from_y = 3372, to_x = 3293, to_y = 3384 }, # varrock_south_east_mine_path_to_border - { from_x = 3211, from_y = 3420, to_x = 3210, to_y = 3407 }, # varrock_centre_south_to_dirt_crossroads - { from_x = 3210, from_y = 3407, to_x = 3211, to_y = 3395 }, # varrock_south_dirt_crossroads_to_blue_moon_inn - { from_x = 3210, from_y = 3407, to_x = 3209, to_y = 3399 }, - { from_x = 3210, from_y = 3407, to_x = 3209, to_y = 3399 }, - { from_x = 3209, from_y = 3399, to_x = 3207, to_y = 3399, cost = 1, actions = [{ object = { option = "Open", id = "door_444_closed", x = 3209, y = 3399, success = { object = { id = "door_444_opened", x = 3208, y = 3399 } } } }, { tile = { x = 3207, y = 3399 } }] }, - { from_x = 3211, from_y = 3395, to_x = 3209, to_y = 3399 }, # varrock_sword_shop - { from_x = 3211, from_y = 3395, to_x = 3211, to_y = 3381 }, # varrock_blue_moon_inn_to_south_border - { from_x = 3211, from_y = 3381, to_x = 3214, to_y = 3367 }, # varrock_south_border_to_dark_mages - { from_x = 3214, from_y = 3367, to_x = 3226, to_y = 3352 }, # varrock_south_dark_mages_to_south - { from_x = 3214, from_y = 3367, to_x = 3206, to_y = 3376 }, - { from_x = 3211, from_y = 3381, to_x = 3206, to_y = 3376 }, - { from_x = 3206, from_y = 3376, to_x = 3197, to_y = 3373 }, - { from_x = 3197, from_y = 3373, to_x = 3191, to_y = 3367 }, - { from_x = 3197, from_y = 3373, to_x = 3184, to_y = 3370 }, - { from_x = 3191, from_y = 3367, to_x = 3184, to_y = 3370 }, - { from_x = 3226, from_y = 3352, to_x = 3228, to_y = 3337 }, # varrock_dark_mages_to_south_fields_path - { from_x = 3228, from_y = 3337, to_x = 3240, to_y = 3336 }, # varrock_south_fields_path_to_stile - { from_x = 3240, from_y = 3336, to_x = 3254, to_y = 3333 }, # varrock_south_stile_to_path - { from_x = 3254, from_y = 3333, to_x = 3268, to_y = 3331 }, # varrock_south_stile_to_al_kharid_crossroad + { from = { x = 3213, y = 3428 }, to = { x = 3223, y = 3429 } }, # varrock_teleport_to_centre_east + { from = { x = 3213, y = 3428 }, to = { x = 3200, y = 3429 } }, # varrock_teleport_to_centre_west + { from = { x = 3213, y = 3428 }, to = { x = 3211, y = 3420 } }, # varrock_teleport_to_centre_south + { from = { x = 3200, y = 3429 }, to = { x = 3186, y = 3430 } }, # varrock_centre_west_to_west_bank_south_entrance + { from = { x = 3186, y = 3430 }, to = { x = 3186, y = 3435 } }, # varrock_west_bank_south_entrance_to_bank_south + { from = { x = 3186, y = 3430 }, to = { x = 3171, y = 3429 } }, # varrock_west_bank_south_entrance_to_romeo_crossroad + { from = { x = 3171, y = 3429 }, to = { x = 3162, y = 3420 } }, # varrock_west_romeo_crossroads_to_oak_tress + { from = { x = 3162, y = 3420 }, to = { x = 3150, y = 3416 } }, # varrock_west_oak_trees_to_cat_house + { from = { x = 3150, y = 3416 }, to = { x = 3135, y = 3416 } }, # varrock_west_cat_house_to_barbarian_path + { from = { x = 3135, y = 3416 }, to = { x = 3128, y = 3407 } }, # varrock_west_barbarian_path_to_air_altar_ruins + { from = { x = 3128, y = 3407 }, to = { x = 2841, y = 4830 }, cost = 3, actions = [{ object = { option = "null", id = "air_altar_ruins", x = 3126, y = 3404, success = { tile = { x = 2841, y = 4830 } } } }] }, # varrock_west_air_altar_ruins_to_air_altar + { from = { x = 2841, y = 4830 }, to = { x = 2841, y = 4829 } }, # varrock_west_air_altar_to_exit + { from = { x = 2841, y = 4829 }, to = { x = 3128, y = 3407 }, cost = 3, actions = [{ object = { option = "Enter", id = "air_altar_portal", x = 2841, y = 4828, success = { tile = { x = 3128, y = 3407 } } } }] }, # varrock_west_air_altar_exit_to_ruins + { from = { x = 3304, y = 3474 }, to = { x = 2655, y = 4831 }, cost = 3, actions = [{ object = { option = "Enter", id = "earth_altar_ruins", x = 3305, y = 3473, success = { tile = { x = 2655, y = 4831 } } } }] }, # varrock_east_earth_altar_ruins_to_altar + { from = { x = 2655, y = 4831 }, to = { x = 2655, y = 4830 } }, # varrock_east_earth_altar_to_exit + { from = { x = 2655, y = 4830 }, to = { x = 3304, y = 3474 }, cost = 3, actions = [{ object = { option = "Enter", id = "earth_altar_portal", x = 2655, y = 4829, success = { tile = { x = 3304, y = 3474 } } } }] }, # varrock_east_earth_altar_exit_to_ruins + { from = { x = 3254, y = 3422 }, to = { x = 3265, y = 3428 } }, # varrock_east_bank_to_dirt_crossroad + { from = { x = 3265, y = 3428 }, to = { x = 3280, y = 3428 } }, # varrock_east_dirt_crossroad_to_east_crossroad + { from = { x = 3280, y = 3428 }, to = { x = 3287, y = 3414 } }, # varrock_east_crossroad_to_east_path_south + { from = { x = 3287, y = 3414 }, to = { x = 3291, y = 3399 } }, # varrock_east_path_to_south + { from = { x = 3291, y = 3399 }, to = { x = 3293, y = 3384 } }, # varrock_east_path_south_to_east_border + { from = { x = 3280, y = 3428 }, to = { x = 3286, y = 3442 } }, # varrock_east_crossroad_to_path_north + { from = { x = 3286, y = 3442 }, to = { x = 3287, y = 3457 } }, # varrock_east_path_to_north + { from = { x = 3287, y = 3457 }, to = { x = 3296, y = 3462 } }, # varrock_east_path_north_to_sawmill_crossroad + { from = { x = 3296, y = 3462 }, to = { x = 3304, y = 3474 } }, # varrock_east_sawmill_crossroad_to_eath_altar_ruins + { from = { x = 3265, y = 3428 }, to = { x = 3246, y = 3429 } }, # varrock_east_dirt_crossroad_to_bank_crossroad + { from = { x = 3246, y = 3429 }, to = { x = 3254, y = 3422 } }, # varrock_east_bank_crossroad_to_east_bank + { from = { x = 3230, y = 3430 }, to = { x = 3246, y = 3429 } }, # varrock_east_armour_shop_to_bank_crossroad + { from = { x = 3230, y = 3430 }, to = { x = 3223, y = 3429 } }, # varrock_east_armour_shop_to_centre_east + { from = { x = 3238, y = 3295 }, to = { x = 3238, y = 3304 } }, # lumbridge_chicken_entrance_to_varrock_south_split + { from = { x = 3238, y = 3304 }, to = { x = 3251, y = 3319 } }, # varrock_south_split_to_al_kharid_path_west + { from = { x = 3238, y = 3304 }, to = { x = 3238, y = 3320 } }, # varrock_south_split_to_al_kharid_path_west + { from = { x = 3238, y = 3320 }, to = { x = 3240, y = 3336 } }, # varrock_south_split_to_al_kharid_path_west + { from = { x = 3251, y = 3319 }, to = { x = 3268, y = 3331 } }, # varrock_al_kharid_path_west_to_crossroads + { from = { x = 3283, y = 3331 }, to = { x = 3295, y = 3334 } }, # al_kharid_north_entrance_to_varrock_north_west + { from = { x = 3295, y = 3334 }, to = { x = 3304, y = 3335 } }, # varrock_al_kharid_north_west_to_crossroad + { from = { x = 3295, y = 3334 }, to = { x = 3299, y = 3346 } }, # varrock_al_kharid_north_west_to_south_east_path + { from = { x = 3304, y = 3335 }, to = { x = 3299, y = 3346 } }, # varrock_al_kharid_north_west_crossroad_to_south_east_path + { from = { x = 3299, y = 3346 }, to = { x = 3298, y = 3359 } }, # varrock_south_east_path_to_east_mine_path_south + { from = { x = 3298, y = 3359 }, to = { x = 3295, y = 3372 } }, # varrock_south_east_mine_south_to_path + { from = { x = 3295, y = 3372 }, to = { x = 3286, y = 3371 } }, # varrock_south_east_mine_path_to_east + { from = { x = 3286, y = 3371 }, to = { x = 3293, y = 3384 } }, # varrock_south_east_mine_to_border + { from = { x = 3295, y = 3372 }, to = { x = 3293, y = 3384 } }, # varrock_south_east_mine_path_to_border + { from = { x = 3211, y = 3420 }, to = { x = 3210, y = 3407 } }, # varrock_centre_south_to_dirt_crossroads + { from = { x = 3210, y = 3407 }, to = { x = 3211, y = 3395 } }, # varrock_south_dirt_crossroads_to_blue_moon_inn + { from = { x = 3210, y = 3407 }, to = { x = 3209, y = 3399 } }, + { from = { x = 3210, y = 3407 }, to = { x = 3209, y = 3399 } }, + { from = { x = 3209, y = 3399 }, to = { x = 3207, y = 3399 }, cost = 1, actions = [{ object = { option = "Open", id = "door_444_closed", x = 3209, y = 3399, success = { object = { id = "door_444_opened", x = 3208, y = 3399 } } } }, { tile = { x = 3207, y = 3399 } }] }, + { from = { x = 3211, y = 3395 }, to = { x = 3209, y = 3399 } }, # varrock_sword_shop + { from = { x = 3211, y = 3395 }, to = { x = 3211, y = 3381 } }, # varrock_blue_moon_inn_to_south_border + { from = { x = 3211, y = 3381 }, to = { x = 3214, y = 3367 } }, # varrock_south_border_to_dark_mages + { from = { x = 3214, y = 3367 }, to = { x = 3226, y = 3352 } }, # varrock_south_dark_mages_to_south + { from = { x = 3214, y = 3367 }, to = { x = 3206, y = 3376 } }, + { from = { x = 3211, y = 3381 }, to = { x = 3206, y = 3376 } }, + { from = { x = 3206, y = 3376 }, to = { x = 3197, y = 3373 } }, + { from = { x = 3197, y = 3373 }, to = { x = 3191, y = 3367 } }, + { from = { x = 3197, y = 3373 }, to = { x = 3184, y = 3370 } }, + { from = { x = 3191, y = 3367 }, to = { x = 3184, y = 3370 } }, + { from = { x = 3226, y = 3352 }, to = { x = 3228, y = 3337 } }, # varrock_dark_mages_to_south_fields_path + { from = { x = 3228, y = 3337 }, to = { x = 3240, y = 3336 } }, # varrock_south_fields_path_to_stile + { from = { x = 3240, y = 3336 }, to = { x = 3254, y = 3333 } }, # varrock_south_stile_to_path + { from = { x = 3254, y = 3333 }, to = { x = 3268, y = 3331 } }, # varrock_south_stile_to_al_kharid_crossroad ] \ No newline at end of file diff --git a/game/src/main/kotlin/content/bot/interact/path/Graph.kt b/game/src/main/kotlin/content/bot/interact/path/Graph.kt index 9822cd0cfc..b1d7b49d1d 100644 --- a/game/src/main/kotlin/content/bot/interact/path/Graph.kt +++ b/game/src/main/kotlin/content/bot/interact/path/Graph.kt @@ -8,6 +8,7 @@ import content.bot.action.requirements import content.bot.bot import content.bot.fact.Predicate import content.bot.fact.Requirement +import content.bot.interact.navigation.graph.readTile import content.bot.isBot import world.gregs.config.Config import world.gregs.voidps.engine.data.definition.Areas @@ -238,23 +239,15 @@ class Graph( val list = key() assert(list == "edges") { "Expected edges list, got: $list ${exception()}" } while (nextElement()) { - var fromX = 0 - var fromY = 0 - var fromLevel = 0 - var toX = 0 - var toY = 0 - var toLevel = 0 + var from = Tile.EMPTY + var to = Tile.EMPTY var cost = 0 val actions: MutableList>> = mutableListOf() val requirements = mutableListOf>>>() while (nextEntry()) { when (val key = key()) { - "from_x" -> fromX = int() - "from_y" -> fromY = int() - "from_level" -> fromLevel = int() - "to_x" -> toX = int() - "to_y" -> toY = int() - "to_level" -> toLevel = int() + "from" -> from = readTile() + "to" -> to = readTile() "cost" -> cost = int() "actions" -> actions(actions) "conditions" -> requirements(requirements) @@ -263,12 +256,12 @@ class Graph( } when { actions.isEmpty() -> { - val cost = Distance.manhattan(fromX, fromY, toX, toY) - builder.addEdge(Tile(fromX, fromY, fromLevel), Tile(toX, toY, toLevel), cost, listOf(BotAction.WalkTo(toX, toY)), null) - builder.addEdge(Tile(toX, toY, toLevel), Tile(fromX, fromY, fromLevel), cost, listOf(BotAction.WalkTo(fromX, fromY)), null) + val cost = Distance.manhattan(from.x, from.y, to.x, to.y) + builder.addEdge(Tile(from.x, from.y, from.level), Tile(to.x, to.y, to.level), cost, listOf(BotAction.WalkTo(to.x, to.y)), null) + builder.addEdge(Tile(to.x, to.y, to.level), Tile(from.x, from.y, from.level), cost, listOf(BotAction.WalkTo(from.x, from.y)), null) } - requirements.isEmpty() -> builder.addEdge(Tile(fromX, fromY, fromLevel), Tile(toX, toY, toLevel), cost, ActionParser.parse(actions, exception()), null) - else -> builder.addEdge(Tile(fromX, fromY, fromLevel), Tile(toX, toY, toLevel), cost, ActionParser.parse(actions, exception()), Requirement.parse(requirements, exception())) + requirements.isEmpty() -> builder.addEdge(Tile(from.x, from.y, from.level), Tile(to.x, to.y, to.level), cost, ActionParser.parse(actions, exception()), null) + else -> builder.addEdge(Tile(from.x, from.y, from.level), Tile(to.x, to.y, to.level), cost, ActionParser.parse(actions, exception()), Requirement.parse(requirements, exception())) } } } From 950b95f06d2ed63c6e87e276d9e979734c4c5e7f Mon Sep 17 00:00:00 2001 From: GregHib Date: Sun, 8 Feb 2026 13:46:21 +0000 Subject: [PATCH 070/101] New nav edge format --- data/bot/al_kharid.nav-edges.toml | 8 ++++---- game/src/main/kotlin/content/bot/interact/path/Graph.kt | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/data/bot/al_kharid.nav-edges.toml b/data/bot/al_kharid.nav-edges.toml index 6a5017cd8c..7bf93942dd 100644 --- a/data/bot/al_kharid.nav-edges.toml +++ b/data/bot/al_kharid.nav-edges.toml @@ -10,10 +10,10 @@ edges = [ { from = { x = 3278, y = 3228 }, to = { x = 3268, y = 3228 } }, # al_kharid_crossroad_to_tollgate_north { from = { x = 3278, y = 3228 }, to = { x = 3268, y = 3227 } }, # al_kharid_crossroad_to_tollgate { from = { x = 3278, y = 3228 }, to = { x = 3280, y = 3216 } }, # al_kharid_crossroad_to_glider - { from = { x = 3267, y = 3228 }, to = { x = 3268, y = 3228 }, cost = 1, actions = [{ object = { option = "Pay-toll(10gp)", id = "toll_gate_al_kharid_north", x = 3268, y = 3228, success = { tile = { x = 3268, y = 3228 } } } }], conditions = [{ carries = [{ id = "coins", amount = 10 }] }] }, # lumbridge_tollgate_north_to_al_kharid_tollgate - { from = { x = 3268, y = 3228 }, to = { x = 3267, y = 3228 }, cost = 1, actions = [{ object = { option = "Pay-toll(10gp)", id = "toll_gate_al_kharid_north", x = 3268, y = 3228, success = { tile = { x = 3267, y = 3228 } } } }], conditions = [{ carries = [{ id = "coins", amount = 10 }] }] }, # al_kharid_tollgate_north_to_lumbridge_tollgate - { from = { x = 3267, y = 3227 }, to = { x = 3268, y = 3227 }, cost = 1, actions = [{ object = { option = "Pay-toll(10gp)", id = "toll_gate_al_kharid_south", x = 3268, y = 3227, success = { tile = { x = 3268, y = 3227 } } } }], conditions = [{ carries = [{ id = "coins", amount = 10 }] }] }, # lumbridge_tollgate_to_al_kharid - { from = { x = 3268, y = 3227 }, to = { x = 3267, y = 3227 }, cost = 1, actions = [{ object = { option = "Pay-toll(10gp)", id = "toll_gate_al_kharid_south", x = 3268, y = 3227, success = { tile = { x = 3267, y = 3227 } } } }], conditions = [{ carries = [{ id = "coins", amount = 10 }] }] }, # al_kharid_tollgate_to_lumbridge + { from = { x = 3267, y = 3228 }, to = { x = 3268, y = 3228 }, cost = 1, actions = [{ object = { option = "Pay-toll(10gp)", id = "toll_gate_al_kharid_north", x = 3268, y = 3228, success = { tile = { x = 3268, y = 3228 } } } }], requires = [{ carries = [{ id = "coins", amount = 10 }] }] }, # lumbridge_tollgate_north_to_al_kharid_tollgate + { from = { x = 3268, y = 3228 }, to = { x = 3267, y = 3228 }, cost = 1, actions = [{ object = { option = "Pay-toll(10gp)", id = "toll_gate_al_kharid_north", x = 3268, y = 3228, success = { tile = { x = 3267, y = 3228 } } } }], requires = [{ carries = [{ id = "coins", amount = 10 }] }] }, # al_kharid_tollgate_north_to_lumbridge_tollgate + { from = { x = 3267, y = 3227 }, to = { x = 3268, y = 3227 }, cost = 1, actions = [{ object = { option = "Pay-toll(10gp)", id = "toll_gate_al_kharid_south", x = 3268, y = 3227, success = { tile = { x = 3268, y = 3227 } } } }], requires = [{ carries = [{ id = "coins", amount = 10 }] }] }, # lumbridge_tollgate_to_al_kharid + { from = { x = 3268, y = 3227 }, to = { x = 3267, y = 3227 }, cost = 1, actions = [{ object = { option = "Pay-toll(10gp)", id = "toll_gate_al_kharid_south", x = 3268, y = 3227, success = { tile = { x = 3267, y = 3227 } } } }], requires = [{ carries = [{ id = "coins", amount = 10 }] }] }, # al_kharid_tollgate_to_lumbridge { from = { x = 3268, y = 3227 }, to = { x = 3280, y = 3216 } }, # al_kharid_tollgate_to_glider { from = { x = 3268, y = 3228 }, to = { x = 3280, y = 3216 } }, # al_kharid_tollgate_north_to_glider { from = { x = 3280, y = 3216 }, to = { x = 3292, y = 3215 } }, # al_kharid_glider_to_musician diff --git a/game/src/main/kotlin/content/bot/interact/path/Graph.kt b/game/src/main/kotlin/content/bot/interact/path/Graph.kt index b1d7b49d1d..1c9a23a2cb 100644 --- a/game/src/main/kotlin/content/bot/interact/path/Graph.kt +++ b/game/src/main/kotlin/content/bot/interact/path/Graph.kt @@ -250,7 +250,7 @@ class Graph( "to" -> to = readTile() "cost" -> cost = int() "actions" -> actions(actions) - "conditions" -> requirements(requirements) + "requires" -> requirements(requirements) else -> throw IllegalArgumentException("Unexpected key: '$key' ${exception()}") } } From 6663c5cc483fc114ec7a686b0bf7116e87a05024 Mon Sep 17 00:00:00 2001 From: GregHib Date: Sun, 8 Feb 2026 13:59:41 +0000 Subject: [PATCH 071/101] Remove old bot and navigation system --- data/entity/bot/nav_graph.toml | 965 ------------------ game/src/main/kotlin/ContentLoader.kt | 2 - game/src/main/kotlin/GameModules.kt | 24 +- game/src/main/kotlin/GameTick.kt | 7 - game/src/main/kotlin/content/bot/Bot.kt | 7 +- game/src/main/kotlin/content/bot/BotSpawns.kt | 3 - .../src/main/kotlin/content/bot/BotUpdates.kt | 3 + game/src/main/kotlin/content/bot/Bots.kt | 133 --- .../main/kotlin/content/bot/DecisionMaking.kt | 81 -- .../main/kotlin/content/bot/InterfaceBot.kt | 21 - game/src/main/kotlin/content/bot/Task.kt | 17 - .../main/kotlin/content/bot/TaskManager.kt | 34 - .../src/main/kotlin/content/bot/WalkingBot.kt | 30 - .../content/bot/interact/bank/BankBot.kt | 142 --- .../content/bot/interact/item/FloorItemBot.kt | 22 - .../content/bot/interact/item/PickupBot.kt | 24 - .../content/bot/interact/navigation/GoTo.kt | 136 --- .../bot/interact/navigation/Navigation.kt | 85 -- .../interact/navigation/graph/Condition.kt | 7 - .../bot/interact/navigation/graph/Edge.kt | 23 - .../navigation/graph/HasInventoryItem.kt | 8 - .../navigation/graph/NavigationGraph.kt | 146 --- .../content/bot/interact/path/AreaStrategy.kt | 11 - .../bot/interact/path/ConditionalStrategy.kt | 26 - .../content/bot/interact/path/Dijkstra.kt | 62 -- .../bot/interact/path/DijkstraFrontier.kt | 47 - .../bot/interact/path/EdgeTraversal.kt | 8 - .../bot/interact/path/NodeTargetStrategy.kt | 5 - .../content/bot/interact/shop/ShopBot.kt | 80 -- .../main/kotlin/content/bot/skill/SkillBot.kt | 1 - .../content/bot/skill/combat/CombatBot.kt | 209 ---- .../content/bot/skill/combat/CombatGear.kt | 143 --- .../content/bot/skill/combat/TrainingBot.kt | 180 ---- .../content/bot/skill/cooking/CookingBot.kt | 93 -- .../bot/skill/firemaking/FiremakingBot.kt | 83 -- .../content/bot/skill/fishing/FishingBot.kt | 106 -- .../content/bot/skill/mining/MiningBot.kt | 141 --- .../bot/skill/runecrafting/RunecraftingBot.kt | 60 -- .../content/bot/skill/smithing/SmeltingBot.kt | 95 -- .../content/bot/skill/smithing/SmithingBot.kt | 92 -- .../bot/skill/woodcutting/WoodcuttingBot.kt | 88 -- .../player/command/PathFindingCommands.kt | 31 +- .../entity/player/command/ServerCommands.kt | 4 +- game/src/main/resources/game.properties | 6 - .../kotlin/content/bot/path/DijkstraTest.kt | 189 ---- .../gregs/voidps/tools/map/view/MapViewer.kt | 2 - 46 files changed, 17 insertions(+), 3665 deletions(-) delete mode 100644 data/entity/bot/nav_graph.toml delete mode 100644 game/src/main/kotlin/content/bot/Bots.kt delete mode 100644 game/src/main/kotlin/content/bot/DecisionMaking.kt delete mode 100644 game/src/main/kotlin/content/bot/InterfaceBot.kt delete mode 100644 game/src/main/kotlin/content/bot/Task.kt delete mode 100644 game/src/main/kotlin/content/bot/TaskManager.kt delete mode 100644 game/src/main/kotlin/content/bot/WalkingBot.kt delete mode 100644 game/src/main/kotlin/content/bot/interact/bank/BankBot.kt delete mode 100644 game/src/main/kotlin/content/bot/interact/item/FloorItemBot.kt delete mode 100644 game/src/main/kotlin/content/bot/interact/item/PickupBot.kt delete mode 100644 game/src/main/kotlin/content/bot/interact/navigation/GoTo.kt delete mode 100644 game/src/main/kotlin/content/bot/interact/navigation/Navigation.kt delete mode 100644 game/src/main/kotlin/content/bot/interact/navigation/graph/Condition.kt delete mode 100644 game/src/main/kotlin/content/bot/interact/navigation/graph/Edge.kt delete mode 100644 game/src/main/kotlin/content/bot/interact/navigation/graph/HasInventoryItem.kt delete mode 100644 game/src/main/kotlin/content/bot/interact/path/AreaStrategy.kt delete mode 100644 game/src/main/kotlin/content/bot/interact/path/ConditionalStrategy.kt delete mode 100644 game/src/main/kotlin/content/bot/interact/path/Dijkstra.kt delete mode 100644 game/src/main/kotlin/content/bot/interact/path/DijkstraFrontier.kt delete mode 100644 game/src/main/kotlin/content/bot/interact/path/EdgeTraversal.kt delete mode 100644 game/src/main/kotlin/content/bot/interact/path/NodeTargetStrategy.kt delete mode 100644 game/src/main/kotlin/content/bot/interact/shop/ShopBot.kt delete mode 100644 game/src/main/kotlin/content/bot/skill/combat/CombatBot.kt delete mode 100644 game/src/main/kotlin/content/bot/skill/combat/CombatGear.kt delete mode 100644 game/src/main/kotlin/content/bot/skill/combat/TrainingBot.kt delete mode 100644 game/src/main/kotlin/content/bot/skill/cooking/CookingBot.kt delete mode 100644 game/src/main/kotlin/content/bot/skill/firemaking/FiremakingBot.kt delete mode 100644 game/src/main/kotlin/content/bot/skill/fishing/FishingBot.kt delete mode 100644 game/src/main/kotlin/content/bot/skill/mining/MiningBot.kt delete mode 100644 game/src/main/kotlin/content/bot/skill/runecrafting/RunecraftingBot.kt delete mode 100644 game/src/main/kotlin/content/bot/skill/smithing/SmeltingBot.kt delete mode 100644 game/src/main/kotlin/content/bot/skill/smithing/SmithingBot.kt delete mode 100644 game/src/main/kotlin/content/bot/skill/woodcutting/WoodcuttingBot.kt delete mode 100644 game/src/test/kotlin/content/bot/path/DijkstraTest.kt diff --git a/data/entity/bot/nav_graph.toml b/data/entity/bot/nav_graph.toml deleted file mode 100644 index a8c18c42bd..0000000000 --- a/data/entity/bot/nav_graph.toml +++ /dev/null @@ -1,965 +0,0 @@ -[lumbridge_gate_south_to_village] -from = { x = 3234, y = 3218 } -to = { x = 3236, y = 3205 } - -[lumbridge_gate_south_to_church] -from = { x = 3234, y = 3218 } -to = { x = 3243, y = 3209 } - -[lumbridge_south_village_to_church] -from = { x = 3236, y = 3205 } -to = { x = 3243, y = 3209 } - -[lumbridge_gate_south_to_behind_church] -from = { x = 3234, y = 3218 } -to = { x = 3250, y = 3212 } - -[lumbridge_behind_church_to_church_fishing_spot] -from = { x = 3250, y = 3212 } -to = { x = 3258, y = 3206 } - -[lumbridge_south_village_to_bobs_axes] -from = { x = 3236, y = 3205 } -to = { x = 3231, y = 3203 } -cost = 5 -steps = [ - { option = "Open", object = "door_720_closed", tile = { x = 3234, y = 3203 } }, - { tile = { x = 3231, y = 3203 } }, -] - -[lumbridge_bobs_axes_to_south_village] -from = { x = 3231, y = 3203 } -to = { x = 3236, y = 3205 } -cost = 5 -steps = [ - { option = "Open", object = "door_720_closed", tile = { x = 3234, y = 3203 } }, - { tile = { x = 3236, y = 3205 } }, -] - -[lumbridge_south_village_to_south_range_house] -from = { x = 3236, y = 3205 } -to = { x = 3231, y = 3197 } -cost = 5 -steps = [ - { option = "Open", object = "door_720_closed", tile = { x = 3235, y = 3199 } }, - { tile = { x = 3231, y = 3197 } }, -] - -[lumbridge_south_range_house_to_south_village] -from = { x = 3231, y = 3197 } -to = { x = 3236, y = 3205 } -cost = 5 -steps = [ - { option = "Open", object = "door_720_closed", tile = { x = 3235, y = 3199 } }, - { tile = { x = 3236, y = 3205 } }, -] - -[lumbridge_south_village_to_graveyard_exit] -from = { x = 3236, y = 3205 } -to = { x = 3244, y = 3190 } - -[lumbridge_graveyard_exit_to_behind_graveyard] -from = { x = 3244, y = 3190 } -to = { x = 3253, y = 3200 } - -[lumbridge_behind_church_to_behind_graveyard] -from = { x = 3250, y = 3212 } -to = { x = 3253, y = 3200 } - -[lumbridge_church_fishing_spot_to_behind_graveyard] -from = { x = 3258, y = 3206 } -to = { x = 3253, y = 3200 } - -[lumbridge_castle_2nd_floor_south_stairs_to_1st_floor] -from = { x = 3205, y = 3209, level = 2 } -to = { x = 3205, y = 3209, level = 1 } -cost = 1 -steps = [ - { option = "Climb-down", object = "lumbridge_staircase_top", tile = { x = 3204, y = 3207, level = 2 } }, -] - -[lumbridge_castle_2nd_floor_bank_to_south_stairs] -from = { x = 3208, y = 3219, level = 2 } -to = { x = 3205, y = 3209, level = 2 } - -[lumbridge_castle_ground_floor_south_stairs_to_1st_floor] -from = { x = 3205, y = 3209 } -to = { x = 3205, y = 3209, level = 1 } -cost = 1 -steps = [ - { option = "Climb-up", object = "lumbridge_staircase", tile = { x = 3204, y = 3207 } }, -] - -[lumbridge_castle_ground_floor_south_stairs_to_kitchen_corridor] -from = { x = 3205, y = 3209 } -to = { x = 3208, y = 3210 } - -[lumbridge_castle_kitchen_corridor_to_castle_south_entrance] -from = { x = 3208, y = 3210 } -to = { x = 3215, y = 3216 } - -[lumbridge_castle_kitchen_corridor_to_kitchen] -from = { x = 3208, y = 3210 } -to = { x = 3211, y = 3214 } - -[lumbridge_castle_south_entrance_to_courtyard_south] -from = { x = 3215, y = 3216 } -to = { x = 3222, y = 3218 } - -[lumbridge_castle_1st_floor_south_stairs_to_ground_floor] -from = { x = 3205, y = 3209, level = 1 } -to = { x = 3205, y = 3209 } -cost = 1 -steps = [ - { option = "Climb-down", object = "lumbridge_castle_staircase_south_middle", tile = { x = 3204, y = 3207, level = 1 } }, -] - -[lumbridge_castle_1st_floor_south_stairs_to_2nd_floor] -from = { x = 3205, y = 3209, level = 1 } -to = { x = 3205, y = 3209, level = 2 } -cost = 1 -steps = [ - { option = "Climb-up", object = "lumbridge_castle_staircase_south_middle", tile = { x = 3204, y = 3207, level = 1 } }, -] - -[lumbridge_castle_grounds_south_to_tower_west] -from = { x = 3209, y = 3205 } -to = { x = 3199, y = 3218 } - -[lumbridge_castle_tower_west_to_yew_trees] -from = { x = 3199, y = 3218 } -to = { x = 3184, y = 3225 } - -[lumbridge_castle_tower_west_to_tree_patch] -from = { x = 3199, y = 3218 } -to = { x = 3193, y = 3236 } - -[lumbridge_castle_yew_trees_to_yew_trees_west] -from = { x = 3184, y = 3225 } -to = { x = 3168, y = 3221 } - -[lumbridge_castle_yew_trees_to_tree_patch] -from = { x = 3184, y = 3225 } -to = { x = 3193, y = 3236 } - -[lumbridge_south_tower_to_ground_floor] -from = { x = 3226, y = 3214 } -to = { x = 3227, y = 3214 } -cost = 3 -steps = [ - { option = "Open", object = "door_627_closed", tile = { x = 3226, y = 3214 } }, -] - -[lumbridge_south_tower_ground_floor_to_1st_floor] -from = { x = 3227, y = 3214 } -to = { x = 3229, y = 3214, level = 1 } -cost = 1 -steps = [ - { option = "Climb-up", object = "36768", tile = { x = 3229, y = 3213 } }, -] - -[lumbridge_south_tower_1st_floor_to_2nd_floor] -from = { x = 3229, y = 3214, level = 1 } -to = { x = 3229, y = 3214, level = 2 } -cost = 1 -steps = [ - { option = "Climb-up", object = "36769", tile = { x = 3229, y = 3213, level = 1 } }, -] - -[lumbridge_south_tower_1st_floor_to_ground_floor] -from = { x = 3229, y = 3214, level = 1 } -to = { x = 3229, y = 3214 } -cost = 1 -steps = [ - { option = "Climb-down", object = "36769", tile = { x = 3229, y = 3213, level = 1 } }, -] - -[lumbridge_south_tower_2nd_floor_to_1st_floor] -from = { x = 3229, y = 3214, level = 2 } -to = { x = 3229, y = 3214, level = 1 } -cost = 1 -steps = [ - { option = "Climb-down", object = "36770", tile = { x = 3229, y = 3213, level = 2 } }, -] - -[lumbridge_gate_north_to_bridge_west] -from = { x = 3234, y = 3220 } -to = { x = 3236, y = 3225 } - -[lumbridge_bridge_west_to_unstable_house] -from = { x = 3236, y = 3225 } -to = { x = 3230, y = 3232 } - -[lumbridge_bridge_west_to_bridge_east] -from = { x = 3236, y = 3225 } -to = { x = 3253, y = 3225 } - -[lumbridge_bridge_east_to_trees_east] -from = { x = 3253, y = 3225 } -to = { x = 3263, y = 3222 } - -[lumbridge_bridge_east_to_east_crossroad] -from = { x = 3253, y = 3225 } -to = { x = 3260, y = 3228 } - -[lumbridge_east_crossroad_to_trees_east] -from = { x = 3260, y = 3228 } -to = { x = 3263, y = 3222 } - -[lumbridge_bridge_north_to_cow_entrance] -from = { x = 3235, y = 3261 } -to = { x = 3250, y = 3266 } - -[lumbridge_goblin_path_north_to_cow_entrance] -from = { x = 3251, y = 3252 } -to = { x = 3250, y = 3266 } - -[lumbridge_cow_entrance_to_cow_path] -from = { x = 3250, y = 3266 } -to = { x = 3240, y = 3280 } - -[lumbridge_cow_path_to_chicken_entrance] -from = { x = 3240, y = 3280 } -to = { x = 3238, y = 3295 } - -[lumbridge_chicken_entrance_to_chicken_pen] -from = { x = 3238, y = 3295 } -to = { x = 3235, y = 3295 } -steps = [ - { option = "Open", object = "gate_235_closed", tile = { x = 3237, y = 3295 } }, -] - -[lumbridge_chicken_pen_to_chicken_entrance] -from = { x = 3235, y = 3295 } -to = { x = 3238, y = 3295 } -steps = [ - { option = "Open", object = "gate_237_closed", tile = { x = 3237, y = 3296 } }, -] - -[lumbridge_cow_entrance_to_cow_field] -from = { x = 3250, y = 3266 } -to = { x = 3255, y = 3266 } -steps = [ - { option = "Open", object = "gate_241_closed", tile = { x = 3252, y = 3266 } }, -] - -[lumbridge_cow_field_to_cow_entrance] -from = { x = 3255, y = 3266 } -to = { x = 3250, y = 3266 } -steps = [ - { option = "Open", object = "gate_239_closed", tile = { x = 3252, y = 3267 } }, -] - -[lumbridge_graveyard_exit_to_swamp_path] -from = { x = 3244, y = 3190 } -to = { x = 3241, y = 3176 } - -[lumbridge_swamp_path_to_swamp_cross_roads] -from = { x = 3241, y = 3176 } -to = { x = 3239, y = 3160 } - -[lumbridge_graveyard_exit_to_rats_north_east] -from = { x = 3244, y = 3190 } -to = { x = 3231, y = 3188 } - -[lumbridge_graveyard_exit_to_rats_south_east] -from = { x = 3244, y = 3190 } -to = { x = 3231, y = 3181 } - -[lumbridge_swamp_path_to_rats_north_east] -from = { x = 3241, y = 3176 } -to = { x = 3231, y = 3188 } - -[lumbridge_swamp_path_to_rats_south_east] -from = { x = 3241, y = 3176 } -to = { x = 3231, y = 3181 } - -[lumbridge_swamp_rats_north_east_to_south_east] -from = { x = 3231, y = 3188 } -to = { x = 3231, y = 3181 } - -[lumbridge_swamp_cross_roads_to_copper_mine] -from = { x = 3239, y = 3160 } -to = { x = 3228, y = 3148 } - -[lumbridge_swamp_copper_mine_to_tin_mine] -from = { x = 3228, y = 3148 } -to = { x = 3225, y = 3147 } - -[lumbridge_swamp_copper_mine_to_east_mine] -from = { x = 3228, y = 3148 } -to = { x = 3221, y = 3156 } - -[lumbridge_swamp_cross_roads_to_east_mine] -from = { x = 3239, y = 3160 } -to = { x = 3221, y = 3156 } - -[lumbridge_swamp_cross_roads_to_fishing_spot] -from = { x = 3239, y = 3160 } -to = { x = 3243, y = 3155 } - -[lumbridge_swamp_east_mine_to_urhney_house] -from = { x = 3221, y = 3156 } -to = { x = 3201, y = 3155 } - -[lumbridge_swamp_urhney_house_to_water_altar] -from = { x = 3201, y = 3155 } -to = { x = 3181, y = 3153 } - -[lumbridge_swamp_water_altar_to_south] -from = { x = 3181, y = 3153 } -to = { x = 3161, y = 3151 } - -[lumbridge_swamp_south_to_west_mine] -from = { x = 3161, y = 3151 } -to = { x = 3148, y = 3149 } - -[lumbridge_swamp_west_mine_to_mithril_mine] -from = { x = 3148, y = 3149 } -to = { x = 3146, y = 3147 } - -[lumbridge_swamp_west_mine_to_coal_mine] -from = { x = 3148, y = 3149 } -to = { x = 3146, y = 3150 } - -[lumbridge_swamp_west_mine_to_west_1] -from = { x = 3148, y = 3149 } -to = { x = 3146, y = 3167 } - -[lumbridge_swamp_west_1_to_2] -from = { x = 3146, y = 3167 } -to = { x = 3143, y = 3186 } - -[lumbridge_swamp_west_2_to_west_camp] -from = { x = 3143, y = 3186 } -to = { x = 3142, y = 3203 } - -[lumbridge_swamp_west_camp_to_west_wall] -from = { x = 3142, y = 3203 } -to = { x = 3136, y = 3219 } - -[lumbridge_swamp_west_camp_to_north_wall] -from = { x = 3142, y = 3203 } -to = { x = 3153, y = 3216 } - -[lumbridge_swamp_west_wall_to_north_wall] -from = { x = 3136, y = 3219 } -to = { x = 3153, y = 3216 } - -[lumbridge_swamp_west_wall_to_draynor_east_path] -from = { x = 3136, y = 3219 } -to = { x = 3138, y = 3227 } - -[lumbridge_swamp_west_wall_to_draynor_jail_path_south] -from = { x = 3136, y = 3219 } -to = { x = 3119, y = 3228 } - -[lumbridge_courtyard_south_to_tower_door] -from = { x = 3222, y = 3218 } -to = { x = 3226, y = 3214 } - -[lumbridge_courtyard_south_to_north] -from = { x = 3222, y = 3218 } -to = { x = 3222, y = 3220 } - -[lumbridge_courtyard_south_to_gate_north] -from = { x = 3222, y = 3218 } -to = { x = 3234, y = 3218 } - -[lumbridge_courtyard_north_to_gate_north] -from = { x = 3222, y = 3220 } -to = { x = 3234, y = 3220 } - -[lumbridge_courtyard_gate_south_to_gate_north] -from = { x = 3234, y = 3218 } -to = { x = 3234, y = 3220 } - -[lumbridge_courtyard_south_to_grounds_south] -from = { x = 3222, y = 3218 } -to = { x = 3209, y = 3205 } - -[lumbridge_unstable_house_to_general_store_east] -from = { x = 3230, y = 3232 } -to = { x = 3222, y = 3241 } - -[lumbridge_general_store_east_to_general_store] -from = { x = 3222, y = 3241 } -to = { x = 3217, y = 3241 } -cost = 6 -steps = [ - { option = "Open", object = "door_720_closed", tile = { x = 3219, y = 3241 } }, - { tile = { x = 3217, y = 3241 } }, -] - -[lumbridge_general_store_to_general_store_east] -from = { x = 3217, y = 3241 } -to = { x = 3222, y = 3241 } -cost = 6 -steps = [ - { option = "Open", object = "door_720_closed", tile = { x = 3219, y = 3241 } }, - { tile = { x = 3222, y = 3241 } }, -] - -[lumbridge_general_store_east_to_trees_west] -from = { x = 3222, y = 3241 } -to = { x = 3226, y = 3245 } - -[lumbridge_west_village_trees_to_bridge_north] -from = { x = 3226, y = 3245 } -to = { x = 3235, y = 3261 } - -[lumbridge_general_store_east_to_task_building] -from = { x = 3222, y = 3241 } -to = { x = 3204, y = 3247 } - -[lumbridge_general_store_to_task_building] -from = { x = 3217, y = 3241 } -to = { x = 3204, y = 3247 } - -[lumbridge_task_building_to_fishing_shop_entrance] -from = { x = 3204, y = 3247 } -to = { x = 3194, y = 3247 } - -[lumbridge_fishing_shop_entrance_to_west_path] -from = { x = 3194, y = 3247 } -to = { x = 3172, y = 3239 } - -[lumbridge_fishing_shop_entrance_to_tree_patch] -from = { x = 3194, y = 3247 } -to = { x = 3193, y = 3236 } - -[lumbridge_fishing_shop_entrance_to_fishing_shop] -from = { x = 3194, y = 3247 } -to = { x = 3195, y = 3251 } - -[lumbridge_west_path_to_ham_path] -from = { x = 3172, y = 3239 } -to = { x = 3157, y = 3234 } - -[lumbridge_west_path_to_yew_trees] -from = { x = 3172, y = 3239 } -to = { x = 3184, y = 3225 } - -[lumbridge_west_path_to_yew_trees_west] -from = { x = 3172, y = 3239 } -to = { x = 3168, y = 3221 } - -[lumbridge_yew_trees_west_to_swamp_north_wall] -from = { x = 3168, y = 3221 } -to = { x = 3153, y = 3216 } - -[lumbridge_ham_path_to_draynor_east_path] -from = { x = 3157, y = 3234 } -to = { x = 3138, y = 3227 } - -[lumbridge_general_store_east_to_west_crossroad] -from = { x = 3222, y = 3241 } -to = { x = 3219, y = 3247 } - -[lumbridge_west_crossroad_to_combat_hall_east_entrance] -from = { x = 3219, y = 3247 } -to = { x = 3212, y = 3247 } - -[lumbridge_combat_hall_east_entrance_to_task_building] -from = { x = 3212, y = 3247 } -to = { x = 3204, y = 3247 } - -[lumbridge_combat_hall_entrance_to_east] -from = { x = 3212, y = 3247 } -to = { x = 3212, y = 3250 } - -[lumbridge_task_building_to_combat_hall_west] -from = { x = 3204, y = 3247 } -to = { x = 3204, y = 3250 } - -[lumbridge_village_trees_west_to_smiths_south] -from = { x = 3226, y = 3245 } -to = { x = 3225, y = 3252 } - -[lumbridge_smiths_south_to_west] -from = { x = 3225, y = 3252 } -to = { x = 3222, y = 3255 } - -[lumbridge_smiths_west_to_smiths_crossroad] -from = { x = 3222, y = 3255 } -to = { x = 3218, y = 3255 } - -[lumbridge_smiths_crossroad_to_west_crossroad] -from = { x = 3218, y = 3255 } -to = { x = 3219, y = 3247 } - -[lumbridge_smiths_south_to_west_crossroad] -from = { x = 3225, y = 3252 } -to = { x = 3219, y = 3247 } - -[lumbridge_bridge_east_to_goblins_river] -from = { x = 3253, y = 3225 } -to = { x = 3245, y = 3238 } - -[lumbridge_bridge_east_to_goblins] -from = { x = 3253, y = 3225 } -to = { x = 3254, y = 3239 } - -[lumbridge_goblins_to_trees_east] -from = { x = 3254, y = 3239 } -to = { x = 3258, y = 3250 } - -[lumbridge_goblins_to_river] -from = { x = 3254, y = 3239 } -to = { x = 3245, y = 3238 } - -[lumbridge_goblins_to_path_north] -from = { x = 3254, y = 3239 } -to = { x = 3251, y = 3252 } - -[lumbridge_goblins_path_north_to_bridge_north] -from = { x = 3251, y = 3252 } -to = { x = 3235, y = 3261 } - -[lumbridge_bridge_north_to_goblin_fishing_spot] -from = { x = 3235, y = 3261 } -to = { x = 3240, y = 3249 } - -[lumbridge_goblin_path_north_to_fishing_spot] -from = { x = 3251, y = 3252 } -to = { x = 3240, y = 3249 } - -[lumbridge_goblin_fishing_spot_to_river] -from = { x = 3240, y = 3249 } -to = { x = 3245, y = 3238 } - -[lumbridge_east_crossroad_to_goblins] -from = { x = 3260, y = 3228 } -to = { x = 3254, y = 3239 } - -[lumbridge_east_crossroad_to_tollgate] -from = { x = 3260, y = 3228 } -to = { x = 3267, y = 3227 } - -[lumbridge_east_crossroad_to_tollgate_north] -from = { x = 3260, y = 3228 } -to = { x = 3267, y = 3228 } - -[lumbridge_east_crossroad_to_trees_east] -from = { x = 3260, y = 3228 } -to = { x = 3263, y = 3222 } - -[varrock_teleport_to_centre_east] -from = { x = 3213, y = 3428 } -to = { x = 3223, y = 3429 } - -[varrock_teleport_to_centre_west] -from = { x = 3213, y = 3428 } -to = { x = 3200, y = 3429 } - -[varrock_teleport_to_centre_south] -from = { x = 3213, y = 3428 } -to = { x = 3211, y = 3420 } - -[varrock_centre_west_to_west_bank_south_entrance] -from = { x = 3200, y = 3429 } -to = { x = 3186, y = 3430 } - -[varrock_west_bank_south_entrance_to_bank_south] -from = { x = 3186, y = 3430 } -to = { x = 3186, y = 3435 } - -[varrock_west_bank_south_entrance_to_romeo_crossroad] -from = { x = 3186, y = 3430 } -to = { x = 3171, y = 3429 } - -[varrock_west_romeo_crossroads_to_oak_tress] -from = { x = 3171, y = 3429 } -to = { x = 3162, y = 3420 } - -[varrock_west_oak_trees_to_cat_house] -from = { x = 3162, y = 3420 } -to = { x = 3150, y = 3416 } - -[varrock_west_cat_house_to_barbarian_path] -from = { x = 3150, y = 3416 } -to = { x = 3135, y = 3416 } - -[varrock_west_barbarian_path_to_air_altar_ruins] -from = { x = 3135, y = 3416 } -to = { x = 3128, y = 3407 } - -[varrock_west_air_altar_ruins_to_air_altar] -from = { x = 3128, y = 3407 } -to = { x = 2841, y = 4830 } -cost = 3 -steps = [ - { option = "Enter", object = "air_altar_ruins", tile = { x = 3126, y = 3404 } }, -] - -[varrock_west_air_altar_to_exit] -from = { x = 2841, y = 4830 } -to = { x = 2841, y = 4829 } - -[varrock_west_air_altar_exit_to_ruins] -from = { x = 2841, y = 4829 } -to = { x = 3128, y = 3407 } -cost = 3 -steps = [ - { option = "Enter", object = "air_altar_portal", tile = { x = 2841, y = 4828 } }, -] - -[varrock_east_earth_altar_ruins_to_altar] -from = { x = 3304, y = 3474 } -to = { x = 2655, y = 4831 } -cost = 3 -steps = [ - { option = "Enter", object = "earth_altar_ruins", tile = { x = 3305, y = 3473 } }, -] - -[varrock_east_earth_altar_to_exit] -from = { x = 2655, y = 4831 } -to = { x = 2655, y = 4830 } - -[varrock_east_earth_altar_exit_to_ruins] -from = { x = 2655, y = 4830 } -to = { x = 3304, y = 3474 } -cost = 3 -steps = [ - { option = "Enter", object = "earth_altar_portal", tile = { x = 2655, y = 4829 } }, -] - -[varrock_east_bank_to_dirt_crossroad] -from = { x = 3254, y = 3422 } -to = { x = 3265, y = 3428 } - -[varrock_east_dirt_crossroad_to_east_crossroad] -from = { x = 3265, y = 3428 } -to = { x = 3280, y = 3428 } - -[varrock_east_crossroad_to_east_path_south] -from = { x = 3280, y = 3428 } -to = { x = 3287, y = 3414 } - -[varrock_east_path_to_south] -from = { x = 3287, y = 3414 } -to = { x = 3291, y = 3399 } - -[varrock_east_path_south_to_east_border] -from = { x = 3291, y = 3399 } -to = { x = 3293, y = 3384 } - -[varrock_east_crossroad_to_path_north] -from = { x = 3280, y = 3428 } -to = { x = 3286, y = 3442 } - -[varrock_east_path_to_north] -from = { x = 3286, y = 3442 } -to = { x = 3287, y = 3457 } - -[varrock_east_path_north_to_sawmill_crossroad] -from = { x = 3287, y = 3457 } -to = { x = 3296, y = 3462 } - -[varrock_east_sawmill_crossroad_to_eath_altar_ruins] -from = { x = 3296, y = 3462 } -to = { x = 3304, y = 3474 } - -[varrock_east_dirt_crossroad_to_bank_crossroad] -from = { x = 3265, y = 3428 } -to = { x = 3246, y = 3429 } - -[varrock_east_bank_crossroad_to_east_bank] -from = { x = 3246, y = 3429 } -to = { x = 3254, y = 3422 } - -[varrock_east_armour_shop_to_bank_crossroad] -from = { x = 3230, y = 3430 } -to = { x = 3246, y = 3429 } - -[varrock_east_armour_shop_to_centre_east] -from = { x = 3230, y = 3430 } -to = { x = 3223, y = 3429 } - -[lumbridge_chicken_entrance_to_varrock_south_split] -from = { x = 3238, y = 3295 } -to = { x = 3238, y = 3304 } - -[varrock_south_split_to_al_kharid_path_west] -from = { x = 3238, y = 3304 } -to = { x = 3251, y = 3319 } - -[varrock_al_kharid_path_west_to_crossroads] -from = { x = 3251, y = 3319 } -to = { x = 3268, y = 3331 } - -[al_kharid_north_entrance_to_varrock_north_west] -from = { x = 3283, y = 3331 } -to = { x = 3295, y = 3334 } - -[varrock_al_kharid_north_west_to_crossroad] -from = { x = 3295, y = 3334 } -to = { x = 3304, y = 3335 } - -[varrock_al_kharid_north_west_to_south_east_path] -from = { x = 3295, y = 3334 } -to = { x = 3299, y = 3346 } - -[varrock_al_kharid_north_west_crossroad_to_south_east_path] -from = { x = 3304, y = 3335 } -to = { x = 3299, y = 3346 } - -[varrock_south_east_path_to_east_mine_path_south] -from = { x = 3299, y = 3346 } -to = { x = 3298, y = 3359 } - -[varrock_south_east_mine_south_to_path] -from = { x = 3298, y = 3359 } -to = { x = 3295, y = 3372 } - -[varrock_south_east_mine_path_to_east] -from = { x = 3295, y = 3372 } -to = { x = 3286, y = 3371 } - -[varrock_south_east_mine_to_border] -from = { x = 3286, y = 3371 } -to = { x = 3293, y = 3384 } - -[varrock_south_east_mine_path_to_border] -from = { x = 3295, y = 3372 } -to = { x = 3293, y = 3384 } - -[varrock_centre_south_to_dirt_crossroads] -from = { x = 3211, y = 3420 } -to = { x = 3210, y = 3407 } - -[varrock_south_dirt_crossroads_to_blue_moon_inn] -from = { x = 3210, y = 3407 } -to = { x = 3211, y = 3395 } - -[varrock_blue_moon_inn_to_south_border] -from = { x = 3211, y = 3395 } -to = { x = 3211, y = 3381 } - -[varrock_south_border_to_dark_mages] -from = { x = 3211, y = 3381 } -to = { x = 3214, y = 3367 } - -[varrock_south_dark_mages_to_south] -from = { x = 3214, y = 3367 } -to = { x = 3226, y = 3352 } - -[varrock_dark_mages_to_south_fields_path] -from = { x = 3226, y = 3352 } -to = { x = 3228, y = 3337 } - -[varrock_south_fields_path_to_stile] -from = { x = 3228, y = 3337 } -to = { x = 3240, y = 3336 } - -[varrock_south_stile_to_path] -from = { x = 3240, y = 3336 } -to = { x = 3254, y = 3333 } - -[varrock_south_stile_to_al_kharid_crossroad] -from = { x = 3254, y = 3333 } -to = { x = 3268, y = 3331 } - -[draynor_east_to_jail_path_south] -from = { x = 3138, y = 3227 } -to = { x = 3119, y = 3228 } - -[draynor_path_south_to_west] -from = { x = 3119, y = 3228 } -to = { x = 3105, y = 3238 } - -[draynor_path_west_to_bank_crossroad] -from = { x = 3105, y = 3238 } -to = { x = 3104, y = 3248 } - -[draynor_bank_crossroad_to_bank] -from = { x = 3104, y = 3248 } -to = { x = 3093, y = 3245 } - -[draynor_bank_to_stalls] -from = { x = 3093, y = 3245 } -to = { x = 3079, y = 3249 } - -[draynor_bank_to_trees] -from = { x = 3093, y = 3245 } -to = { x = 3099, y = 3246 } - -[draynor_bank_crossroad_to_trees] -from = { x = 3104, y = 3248 } -to = { x = 3099, y = 3246 } - -[draynor_stalls_to_pigsty] -from = { x = 3079, y = 3249 } -to = { x = 3071, y = 3266 } - -[draynor_stall_to_willow_trees] -from = { x = 3079, y = 3249 } -to = { x = 3086, y = 3237 } - -[draynor_stalls_to_west_trees] -from = { x = 3079, y = 3249 } -to = { x = 3072, y = 3250 } - -[draynor_stalls_to_north_trees] -from = { x = 3079, y = 3249 } -to = { x = 3079, y = 3265 } - -[draynor_bank_to_willow_trees] -from = { x = 3093, y = 3245 } -to = { x = 3086, y = 3237 } - -[draynor_willow_trees_to_south] -from = { x = 3086, y = 3237 } -to = { x = 3097, y = 3235 } - -[draynor_willow_trees_to_fishing_spot] -from = { x = 3086, y = 3237 } -to = { x = 3086, y = 3231 } - -[draynor_fishing_spot_to_south] -from = { x = 3086, y = 3231 } -to = { x = 3097, y = 3235 } - -[draynor_jail_path_west_to_south] -from = { x = 3105, y = 3238 } -to = { x = 3097, y = 3235 } - -[draynor_east_path_to_swamp_north_wall] -from = { x = 3138, y = 3227 } -to = { x = 3153, y = 3216 } - -[varrock_al_kharid_crossroad_to_north_entrance] -from = { x = 3268, y = 3331 } -to = { x = 3283, y = 3331 } - -[al_kharid_mine_north_entrance_to_west_path] -from = { x = 3283, y = 3331 } -to = { x = 3284, y = 3313 } - -[al_kharid_mine_west_path_to_south] -from = { x = 3284, y = 3313 } -to = { x = 3287, y = 3294 } - -[al_kharid_mine_west_path_to_entrance] -from = { x = 3287, y = 3294 } -to = { x = 3298, y = 3280 } - -[al_kharid_mine_entrance_to_mine_south] -from = { x = 3298, y = 3280 } -to = { x = 3299, y = 3263 } - -[al_kharid_mine_south_to_north_path] -from = { x = 3299, y = 3263 } -to = { x = 3294, y = 3242 } - -[al_kharid_north_path_to_crossroad] -from = { x = 3294, y = 3242 } -to = { x = 3278, y = 3228 } - -[al_kharid_crossroad_to_tollgate_north] -from = { x = 3278, y = 3228 } -to = { x = 3268, y = 3228 } - -[al_kharid_crossroad_to_tollgate] -from = { x = 3278, y = 3228 } -to = { x = 3268, y = 3227 } - -[al_kharid_crossroad_to_glider] -from = { x = 3278, y = 3228 } -to = { x = 3280, y = 3216 } - -[lumbridge_tollgate_north_to_al_kharid_tollgate] -from = { x = 3267, y = 3228 } -to = { x = 3268, y = 3228 } -cost = 1 -steps = [ - { option = "Pay-toll(10gp)", object = "toll_gate_al_kharid_north", tile = { x = 3268, y = 3228 } }, -] -conditions = [ - { type = "inventory_item", item = "coins", amount = 10 }, -] - -[al_kharid_tollgate_north_to_lumbridge_tollgate] -from = { x = 3268, y = 3228 } -to = { x = 3267, y = 3228 } -cost = 1 -steps = [ - { option = "Pay-toll(10gp)", object = "toll_gate_al_kharid_north", tile = { x = 3268, y = 3228 } }, -] -conditions = [ - { type = "inventory_item", item = "coins", amount = 10 }, -] - -[lumbridge_tollgate_to_al_kharid] -from = { x = 3267, y = 3227 } -to = { x = 3268, y = 3227 } -cost = 1 -steps = [ - { option = "Pay-toll(10gp)", object = "toll_gate_al_kharid_south", tile = { x = 3268, y = 3227 } }, -] -conditions = [ - { type = "inventory_item", item = "coins", amount = 10 }, -] - -[al_kharid_tollgate_to_lumbridge] -from = { x = 3268, y = 3227 } -to = { x = 3267, y = 3227 } -cost = 1 -steps = [ - { option = "Pay-toll(10gp)", object = "toll_gate_al_kharid_south", tile = { x = 3268, y = 3227 } }, -] -conditions = [ - { type = "inventory_item", item = "coins", amount = 10 }, -] - -[al_kharid_tollgate_to_glider] -from = { x = 3268, y = 3227 } -to = { x = 3280, y = 3216 } - -[al_kharid_tollgate_north_to_glider] -from = { x = 3268, y = 3228 } -to = { x = 3280, y = 3216 } - -[al_kharid_glider_to_musician] -from = { x = 3280, y = 3216 } -to = { x = 3292, y = 3215 } - -[al_kharid_musician_to_silk_path] -from = { x = 3292, y = 3215 } -to = { x = 3300, y = 3198 } - -[al_kharid_silk_path_to_scimitar_shop] -from = { x = 3300, y = 3198 } -to = { x = 3288, y = 3189 } - -[al_kharid_glider_to_west_shortcut] -from = { x = 3280, y = 3216 } -to = { x = 3280, y = 3200 } - -[al_kharid_west_shortcut_to_scimitar_shop] -from = { x = 3280, y = 3200 } -to = { x = 3288, y = 3189 } - -[al_kharid_scimitar_shop_to_furnace_entrance] -from = { x = 3288, y = 3189 } -to = { x = 3282, y = 3185 } - -[al_kharid_west_shortcut_to_furnace_to_entrance] -from = { x = 3280, y = 3200 } -to = { x = 3282, y = 3185 } - -[al_kharid_furnace_entrance_to_bank_crossroads] -from = { x = 3282, y = 3185 } -to = { x = 3278, y = 3177 } - -[al_kharid_bank_crossroad_to_entrance] -from = { x = 3278, y = 3177 } -to = { x = 3276, y = 3168 } - -[al_kharid_bank_crossroad_to_kebab_shop] -from = { x = 3278, y = 3177 } -to = { x = 3274, y = 3180 } - -[al_kharid_bank_entrance_to_bank] -from = { x = 3276, y = 3168 } -to = { x = 3270, y = 3167 } diff --git a/game/src/main/kotlin/ContentLoader.kt b/game/src/main/kotlin/ContentLoader.kt index ac88b95b9f..6dcc8af0e9 100644 --- a/game/src/main/kotlin/ContentLoader.kt +++ b/game/src/main/kotlin/ContentLoader.kt @@ -1,5 +1,4 @@ import com.github.michaelbull.logging.InlineLogger -import content.bot.Bots import content.skill.prayer.PrayerApi import world.gregs.voidps.engine.Script import world.gregs.voidps.engine.client.ui.chat.plural @@ -43,7 +42,6 @@ class ContentLoader { private fun loadContentApis() { Script.interfaces.add(PrayerApi) - Script.interfaces.add(Bots) } private fun loadScript(name: String): Any { diff --git a/game/src/main/kotlin/GameModules.kt b/game/src/main/kotlin/GameModules.kt index 37dc868dff..e2f4df9bab 100644 --- a/game/src/main/kotlin/GameModules.kt +++ b/game/src/main/kotlin/GameModules.kt @@ -1,8 +1,4 @@ import content.bot.BotManager -import content.bot.TaskManager -import content.bot.interact.navigation.graph.NavigationGraph -import content.bot.interact.path.Dijkstra -import content.bot.interact.path.DijkstraFrontier import content.entity.obj.ship.CharterShips import content.entity.player.modal.book.Books import content.entity.world.music.MusicTracks @@ -10,33 +6,19 @@ import content.quest.member.fairy_tale_part_2.fairy_ring.FairyRingCodes import content.skill.farming.FarmingDefinitions import content.social.trade.exchange.GrandExchange import content.social.trade.exchange.history.ExchangeHistory -import kotlinx.io.pool.DefaultPool import org.koin.dsl.module import world.gregs.voidps.engine.client.instruction.InstructionHandlers import world.gregs.voidps.engine.client.instruction.InterfaceHandler -import world.gregs.voidps.engine.data.* -import world.gregs.voidps.engine.data.definition.ObjectDefinitions +import world.gregs.voidps.engine.data.ConfigFiles +import world.gregs.voidps.engine.data.Settings +import world.gregs.voidps.engine.data.Storage import world.gregs.voidps.engine.data.file.FileStorage import world.gregs.voidps.engine.entity.item.floor.ItemSpawns import java.io.File fun gameModule(files: ConfigFiles) = module { single { ItemSpawns() } - single { TaskManager() } single { BotManager().load(files) } - single { - val size = get().size - Dijkstra( - get(), - object : DefaultPool(10) { - override fun produceInstance() = DijkstraFrontier(size) - }, - ) - } - single(createdAtStart = true) { - get() - NavigationGraph().load(files.find(Settings["map.navGraph"])) - } single(createdAtStart = true) { Books().load(files.list(Settings["definitions.books"])) } single(createdAtStart = true) { MusicTracks().load(files.find(Settings["map.music"])) } single(createdAtStart = true) { FairyRingCodes().load(files.find(Settings["definitions.fairyCodes"])) } diff --git a/game/src/main/kotlin/GameTick.kt b/game/src/main/kotlin/GameTick.kt index abecd7965b..cb2f99fc35 100644 --- a/game/src/main/kotlin/GameTick.kt +++ b/game/src/main/kotlin/GameTick.kt @@ -78,13 +78,6 @@ fun getTickStages( ) } -object AiTick : Runnable { - var method: (() -> Unit)? = null - override fun run() { - method?.invoke() - } -} - private class SaveLogs : Runnable { private val directory = File(Settings["storage.players.logs"]) private var ticks = TimeUnit.SECONDS.toTicks(Settings["storage.players.logs.seconds", 10]) diff --git a/game/src/main/kotlin/content/bot/Bot.kt b/game/src/main/kotlin/content/bot/Bot.kt index b152cfbce1..a57a144e4b 100644 --- a/game/src/main/kotlin/content/bot/Bot.kt +++ b/game/src/main/kotlin/content/bot/Bot.kt @@ -11,7 +11,6 @@ import world.gregs.voidps.network.client.Instruction import java.util.Stack data class Bot(val player: Player) : Character by player { - var step: Instruction? = null val blocked: MutableSet = mutableSetOf() var previous: BotActivity? = null val frames = Stack() @@ -42,3 +41,9 @@ data class Bot(val player: Player) : Character by player { return "BOT ${player.accountName}" } } + +val Player.isBot: Boolean + get() = contains("bot") + +val Player.bot: Bot + get() = get("bot")!! diff --git a/game/src/main/kotlin/content/bot/BotSpawns.kt b/game/src/main/kotlin/content/bot/BotSpawns.kt index be7cc2ee70..133b9f9e01 100644 --- a/game/src/main/kotlin/content/bot/BotSpawns.kt +++ b/game/src/main/kotlin/content/bot/BotSpawns.kt @@ -35,7 +35,6 @@ import kotlin.text.toIntOrNull class BotSpawns( val enums: EnumDefinitions, val structs: StructDefinitions, - val tasks: TaskManager, val loader: PlayerAccountLoader, val manager: BotManager, val accounts: AccountManager, @@ -141,7 +140,6 @@ class BotSpawns( bot.available.add(name) manager.assign(bot, name) } - Bots.start(player) player.message("Bot enabled.") } } @@ -170,7 +168,6 @@ class BotSpawns( if (player.inventory.isEmpty()) { player.inventory.add("coins", 10000) } - Bots.start(player) player.viewport?.loaded = true delay(3) manager.add(bot) diff --git a/game/src/main/kotlin/content/bot/BotUpdates.kt b/game/src/main/kotlin/content/bot/BotUpdates.kt index e7c26d80fd..d7f0776b9c 100644 --- a/game/src/main/kotlin/content/bot/BotUpdates.kt +++ b/game/src/main/kotlin/content/bot/BotUpdates.kt @@ -2,6 +2,9 @@ package content.bot import world.gregs.voidps.engine.Script +/** + * Listen for state changes which would change which activities are available to a bot + */ class BotUpdates(val manager: BotManager) : Script { init { levelChanged { skill, _, _ -> diff --git a/game/src/main/kotlin/content/bot/Bots.kt b/game/src/main/kotlin/content/bot/Bots.kt deleted file mode 100644 index b3cabfc4f1..0000000000 --- a/game/src/main/kotlin/content/bot/Bots.kt +++ /dev/null @@ -1,133 +0,0 @@ -package content.bot - -import content.bot.interact.bank.closeBank -import content.bot.interact.bank.depositAll -import content.bot.interact.bank.openBank -import content.bot.interact.bank.withdrawCoins -import content.bot.interact.navigation.await -import content.bot.interact.shop.buy -import content.bot.interact.shop.closeShop -import content.bot.interact.shop.openNearestShop -import content.entity.player.bank.bank -import world.gregs.voidps.engine.client.ui.dialogue -import world.gregs.voidps.engine.data.definition.InterfaceDefinitions -import world.gregs.voidps.engine.data.definition.ItemDefinitions -import world.gregs.voidps.engine.entity.character.npc.NPC -import world.gregs.voidps.engine.entity.character.player.Player -import world.gregs.voidps.engine.entity.item.slot -import world.gregs.voidps.engine.entity.obj.GameObject -import world.gregs.voidps.engine.entity.obj.GameObjects -import world.gregs.voidps.engine.get -import world.gregs.voidps.engine.inv.inventory -import world.gregs.voidps.engine.map.spiral -import world.gregs.voidps.network.client.instruction.InteractDialogue -import world.gregs.voidps.network.client.instruction.InteractInterface -import world.gregs.voidps.network.client.instruction.InteractNPC -import world.gregs.voidps.network.client.instruction.InteractObject -import world.gregs.voidps.network.login.protocol.visual.update.player.EquipSlot - -object Bots : AutoCloseable { - fun start(bot: Player) { - start?.invoke(bot) - } - - var start: ((Player) -> Unit)? = null - - override fun close() { - start = null - } -} - -val Player.isBot: Boolean - get() = contains("bot") - -val Player.bot: Bot - get() = get("bot")!! - -fun Bot.hasCoins(amount: Int, bank: Boolean = true): Boolean { - if (player.inventory.contains("coins") && player.inventory.count("coins") >= amount) { - return true - } - if (bank && player.bank.contains("coins") && player.bank.count("coins") >= amount) { - return true - } - return false -} - -suspend fun Bot.buyItem(item: String, amount: Int = 1): Boolean { - if (player.inventory.isFull()) { - openBank() - depositAll() - closeBank() - } - withdrawCoins() - val success = try { - openNearestShop(item) - } catch (e: Exception) { - false - } - if (success) { - buy(item, amount) - closeShop() - } - return success -} - -fun Bot.equip(item: String) { - val def = ItemDefinitions.getOrNull(item) ?: return - if (def.slot == EquipSlot.None) { - return - } - val index = player.inventory.indexOf(item) - if (index != -1) { - player.instructions.trySend(InteractInterface(interfaceId = 149, componentId = 0, itemId = def.id, itemSlot = index, option = 1)) - } -} - -fun Bot.inventoryOption(item: String, option: String) { - val index = player.inventory.indexOf(item) - if (index != -1) { - val def = ItemDefinitions.getOrNull(item) ?: return - player.instructions.trySend(InteractInterface(interfaceId = 149, componentId = 0, itemId = def.id, itemSlot = index, option = def.options.indexOf(option))) - } -} - -suspend fun Bot.npcOption(npc: NPC, option: String) { - player.instructions.send(InteractNPC(npc.index, npc.def.options.indexOf(option) + 1)) -} - -suspend fun Bot.objectOption(obj: GameObject, option: String) { - player.instructions.send(InteractObject(obj.def.id, obj.tile.x, obj.tile.y, obj.def.optionsIndex(option) + 1)) -} - -suspend fun Bot.dialogueOption(option: String) { - val current = player.dialogue!! - val definitions = get() - val def = definitions.get(current) - player.instructions.send(InteractDialogue(def.id, definitions.getComponentId(current, option)!!, -1)) - await("tick") -} - -fun Bot.getObject(filter: (GameObject) -> Boolean): GameObject? { - for (zone in player.tile.zone.spiral(2)) { - val obj = zone.toCuboid() - .flatMap { tile -> GameObjects.at(tile) } - .firstOrNull(filter) - if (obj != null) { - return obj - } - } - return null -} - -fun Bot.getObjects(filter: (GameObject) -> Boolean): List { - val list = mutableListOf() - for (zone in player.tile.zone.spiral(2)) { - list.addAll( - zone.toCuboid() - .flatMap { tile -> GameObjects.at(tile) } - .filter(filter), - ) - } - return list -} diff --git a/game/src/main/kotlin/content/bot/DecisionMaking.kt b/game/src/main/kotlin/content/bot/DecisionMaking.kt deleted file mode 100644 index 1fd75606ee..0000000000 --- a/game/src/main/kotlin/content/bot/DecisionMaking.kt +++ /dev/null @@ -1,81 +0,0 @@ -package content.bot - -import com.github.michaelbull.logging.InlineLogger -import content.bot.interact.navigation.resume -import kotlinx.coroutines.CancellableContinuation -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch -import world.gregs.voidps.engine.Contexts -import world.gregs.voidps.engine.Script -import world.gregs.voidps.engine.entity.character.player.Player -import world.gregs.voidps.engine.entity.character.player.Players -import kotlin.coroutines.resume - -class DecisionMaking( - val tasks: TaskManager, -) : Script { - - val scope = CoroutineScope(Contexts.Game) - val logger = InlineLogger("Bot") - - init { - Bots.start = start@{ bot -> - if (!bot.contains("task_bot") || bot.contains("task_started")) { - return@start - } - val name: String = bot["task_bot"]!! - val task = tasks.get(name) - if (task == null) { - bot.clear("task_bot") - } else { - assign(bot, task) - } - } - - AiTick.method = { - for (player in Players) { - if (!player.isBot) { - continue - } - val bot: Bot = player["bot"]!! - if (!bot.contains("task_bot")) { - val lastTask: String? = bot["last_task_bot"] - assign(player, tasks.assign(bot, lastTask)) - } - player.bot.resume("tick") - handleNewSuspensions(player) - } - } - } - - fun handleNewSuspensions(player: Player) { - val suspensions: MutableList Boolean, CancellableContinuation>> = player["bot_new_suspensions"] ?: return - suspensions.removeIf { - val success = it.first(player) - if (success) it.second.resume(Unit) - success - } - } - - fun assign(bot: Player, task: Task) { - if (bot["debug", false]) { - logger.debug { "Task assigned: ${bot.accountName} - ${task.name}" } - } - val last = bot.get("task_bot") - if (last != null) { - bot["last_task_bot"] = last - } - bot["task_bot"] = task.name - bot["task_started"] = true - task.spaces-- - scope.launch { - try { - task.block.invoke(bot) - } catch (t: Throwable) { - logger.warn(t) { "Task cancelled for $bot" } - } - bot.clear("task_bot") - task.spaces++ - } - } -} diff --git a/game/src/main/kotlin/content/bot/InterfaceBot.kt b/game/src/main/kotlin/content/bot/InterfaceBot.kt deleted file mode 100644 index b16e6318a7..0000000000 --- a/game/src/main/kotlin/content/bot/InterfaceBot.kt +++ /dev/null @@ -1,21 +0,0 @@ -package content.bot - -import world.gregs.voidps.engine.entity.item.Item -import world.gregs.voidps.engine.entity.obj.GameObject -import world.gregs.voidps.engine.inv.inventory -import world.gregs.voidps.network.client.instruction.InteractInterface -import world.gregs.voidps.network.client.instruction.InteractInterfaceObject -import world.gregs.voidps.network.client.instruction.InterfaceClosedInstruction - -suspend fun Bot.closeInterface(id: Int, component: Int) { - player.instructions.send(InterfaceClosedInstruction) - clickInterface(id, component, 0) -} - -suspend fun Bot.clickInterface(id: Int, component: Int, option: Int = 0, itemId: Int = -1, itemSlot: Int = -1) { - player.instructions.send(InteractInterface(interfaceId = id, componentId = component, itemId = itemId, itemSlot = itemSlot, option = option)) -} - -suspend fun Bot.itemOnObject(item: Item, obj: GameObject, interfaceId: Int = 149, component: Int = 0) { - player.instructions.send(InteractInterfaceObject(obj.def.id, obj.tile.x, obj.tile.y, interfaceId, component, item.def.id, player.inventory.indexOf(item.id))) -} diff --git a/game/src/main/kotlin/content/bot/Task.kt b/game/src/main/kotlin/content/bot/Task.kt deleted file mode 100644 index 9414951848..0000000000 --- a/game/src/main/kotlin/content/bot/Task.kt +++ /dev/null @@ -1,17 +0,0 @@ -package content.bot - -import world.gregs.voidps.engine.entity.character.player.Player -import world.gregs.voidps.type.Area -import world.gregs.voidps.type.Tile - -data class Task( - val name: String, - val block: suspend Player.() -> Unit, - val area: Area? = null, - var spaces: Int = 1, - val requirements: Collection Boolean> = emptySet(), -) { - fun full() = spaces <= 0 - - fun distanceTo(tile: Tile): Int = if (area == null) 0 else tile.distanceTo(area.random()) -} diff --git a/game/src/main/kotlin/content/bot/TaskManager.kt b/game/src/main/kotlin/content/bot/TaskManager.kt deleted file mode 100644 index 98992e66b5..0000000000 --- a/game/src/main/kotlin/content/bot/TaskManager.kt +++ /dev/null @@ -1,34 +0,0 @@ -package content.bot - -import content.bot.interact.navigation.await -import world.gregs.voidps.type.random -import java.util.* - -class TaskManager { - val names = mutableSetOf() - private val queue = LinkedList() - private var idle = Task( - name = "do nothing", - block = { - repeat(random.nextInt(10, 100)) { - bot.await("tick") - } - }, - spaces = Int.MAX_VALUE, - ) - - fun register(task: Task) { - names.add(task.name) - queue.add(task) - } - - fun get(name: String): Task? = queue.firstOrNull { it.name == name } - - fun assign(bot: Bot, last: String?): Task = queue - .filter { it.name != last && !it.full() && it.requirements.all { req -> req(bot.player) } } - .minByOrNull { it.distanceTo(bot.tile) } ?: idle - - fun idle(task: Task) { - idle = task - } -} diff --git a/game/src/main/kotlin/content/bot/WalkingBot.kt b/game/src/main/kotlin/content/bot/WalkingBot.kt deleted file mode 100644 index 58e7db9ce1..0000000000 --- a/game/src/main/kotlin/content/bot/WalkingBot.kt +++ /dev/null @@ -1,30 +0,0 @@ -package content.bot - -import content.bot.interact.navigation.await -import world.gregs.voidps.engine.Script -import world.gregs.voidps.engine.data.Settings -import world.gregs.voidps.network.client.instruction.Walk - -class WalkingBot(val tasks: TaskManager) : Script { - - init { - worldSpawn { - val task = Task( - name = "walk randomly", - block = { - while (true) { - val tile = tile.toCuboid(10).random() - instructions.send(Walk(tile.x, tile.y)) - bot.await("tick") - } - }, - area = null, - spaces = Int.MAX_VALUE, - requirements = emptyList(), - ) - if (Settings["bots.idle", "nothing"] == "randomWalk") { - tasks.idle(task) - } - } - } -} diff --git a/game/src/main/kotlin/content/bot/interact/bank/BankBot.kt b/game/src/main/kotlin/content/bot/interact/bank/BankBot.kt deleted file mode 100644 index c6220cd99a..0000000000 --- a/game/src/main/kotlin/content/bot/interact/bank/BankBot.kt +++ /dev/null @@ -1,142 +0,0 @@ -package content.bot.interact.bank - -import content.bot.* -import content.bot.bot -import content.bot.interact.navigation.await -import content.bot.interact.navigation.cancel -import content.bot.interact.navigation.goToNearest -import content.bot.interact.navigation.resume -import content.bot.isBot -import content.entity.player.bank.bank -import world.gregs.voidps.engine.Script -import world.gregs.voidps.engine.client.ui.menu -import world.gregs.voidps.engine.data.definition.ItemDefinitions -import world.gregs.voidps.engine.inv.equipment -import world.gregs.voidps.engine.inv.inventory -import world.gregs.voidps.network.client.instruction.EnterInt -import world.gregs.voidps.network.client.instruction.InteractInterface - -private fun getItemId(id: String): Int? = ItemDefinitions.getOrNull(id)?.id - -suspend fun Bot.openBank() { - if (player.menu == "bank") { - return - } - goToNearest("bank") - val bank = getObject { it.def.containsOption(1, "Use-quickly") } ?: return cancel() - objectOption(bank, "Use-quickly") - await("bank") -} - -suspend fun Bot.depositAll() { - if (player.inventory.isEmpty()) { - return - } - clickInterface(762, 33, 0) - await("tick") - await("tick") -} - -suspend fun Bot.depositWornItems() { - if (player.equipment.isEmpty()) { - return - } - clickInterface(762, 35, 0) - await("tick") - await("tick") -} - -suspend fun Bot.depositAll(item: String, slot: Int = player.inventory.indexOf(item)) { - if (slot == -1) { - return - } - player.instructions.send(InteractInterface(interfaceId = 763, componentId = 0, itemId = getItemId(item) ?: return, itemSlot = slot, option = 5)) - await("tick") -} - -suspend fun Bot.deposit(item: String, slot: Int = player.inventory.indexOf(item), amount: Int = 1) { - if (slot == -1) { - return - } - val option = when (amount) { - 1 -> 0 - 5 -> 1 - 10 -> 2 - else -> 4 - } - player.instructions.send(InteractInterface(interfaceId = 763, componentId = 0, itemId = getItemId(item) ?: return, itemSlot = slot, option = option)) - if (option == 4) { - await("tick") - player.instructions.send(EnterInt(value = amount)) - } - await("tick") -} - -suspend fun Bot.withdraw(item: String, slot: Int = player.bank.indexOf(item), amount: Int = 1) { - if (slot == -1) { - return - } - val option = when (amount) { - 1 -> 0 - 5 -> 1 - 10 -> 2 - else -> 4 - } - player.instructions.send(InteractInterface(interfaceId = 762, componentId = 93, itemId = getItemId(item) ?: return, itemSlot = slot, option = option)) - if (option == 4) { - await("tick") - player.instructions.send(EnterInt(value = amount)) - } - await("tick") -} - -suspend fun Bot.withdrawAll(vararg items: String) { - var open = false - for (item in items) { - if (player.bank.contains(item) && !open) { - openBank() - open = true - } - withdrawAll(item) - } - if (open) { - closeBank() - } -} - -suspend fun Bot.withdrawAll(item: String, slot: Int = player.bank.indexOf(item)) { - if (slot == -1) { - return - } - player.instructions.send(InteractInterface(interfaceId = 762, componentId = 93, itemId = getItemId(item) ?: return, itemSlot = slot, option = 5)) - await("tick") -} - -suspend fun Bot.withdrawAllButOne(item: String, slot: Int = player.bank.indexOf(item)) { - if (slot == -1) { - return - } - player.instructions.send(InteractInterface(interfaceId = 762, componentId = 93, itemId = getItemId(item) ?: return, itemSlot = slot, option = 6)) - await("tick") -} - -suspend fun Bot.closeBank() = closeInterface(762, 43) - -suspend fun Bot.withdrawCoins() { - if (!player.inventory.contains("coins")) { - openBank() - withdrawAllButOne("coins") - closeBank() - } -} - -class BankBot : Script { - - init { - interfaceOpened("bank") { - if (isBot) { - bot.resume("bank") - } - } - } -} diff --git a/game/src/main/kotlin/content/bot/interact/item/FloorItemBot.kt b/game/src/main/kotlin/content/bot/interact/item/FloorItemBot.kt deleted file mode 100644 index 83fa73f73c..0000000000 --- a/game/src/main/kotlin/content/bot/interact/item/FloorItemBot.kt +++ /dev/null @@ -1,22 +0,0 @@ -package content.bot.interact.item - -import content.bot.Bot -import kotlinx.coroutines.suspendCancellableCoroutine -import kotlinx.coroutines.withTimeoutOrNull -import world.gregs.voidps.engine.entity.item.floor.FloorItem -import world.gregs.voidps.engine.inv.inventory -import world.gregs.voidps.engine.timer.TICKS -import world.gregs.voidps.network.client.instruction.InteractFloorItem - -suspend fun Bot.pickup(floorItem: FloorItem) { - player.instructions.send(InteractFloorItem(floorItem.def.id, floorItem.tile.x, floorItem.tile.y, 2)) - if (player.inventory.isFull()) { - return - } - withTimeoutOrNull(TICKS.toMillis(2)) { - suspendCancellableCoroutine { cont -> - this@pickup["floor_item_job"] = cont - this@pickup["floor_item_hash"] = floorItem.hashCode() - } - } -} diff --git a/game/src/main/kotlin/content/bot/interact/item/PickupBot.kt b/game/src/main/kotlin/content/bot/interact/item/PickupBot.kt deleted file mode 100644 index 0f1d6c5dbf..0000000000 --- a/game/src/main/kotlin/content/bot/interact/item/PickupBot.kt +++ /dev/null @@ -1,24 +0,0 @@ -package content.bot.interact.item - -import content.bot.isBot -import kotlinx.coroutines.CancellableContinuation -import world.gregs.voidps.engine.Script -import world.gregs.voidps.engine.entity.character.player.Players -import kotlin.coroutines.resume - -class PickupBot : Script { - - init { - floorItemDespawn { - val hash = hashCode() - for (bot in Players) { - if (!bot.isBot || !bot.contains("floor_item_job") || bot["floor_item_hash", -1] != hash) { - continue - } - val job: CancellableContinuation = bot.remove("floor_item_job") ?: return@floorItemDespawn - bot.clear("floor_item_hash") - job.resume(Unit) - } - } - } -} diff --git a/game/src/main/kotlin/content/bot/interact/navigation/GoTo.kt b/game/src/main/kotlin/content/bot/interact/navigation/GoTo.kt deleted file mode 100644 index 4cfcea44d0..0000000000 --- a/game/src/main/kotlin/content/bot/interact/navigation/GoTo.kt +++ /dev/null @@ -1,136 +0,0 @@ -package content.bot.interact.navigation - -import com.github.michaelbull.logging.InlineLogger -import content.bot.Bot -import content.bot.bot -import content.bot.interact.navigation.graph.Edge -import content.bot.interact.navigation.graph.NavigationGraph -import content.bot.interact.navigation.graph.waypoints -import content.bot.interact.path.* -import content.bot.isBot -import content.entity.player.effect.energy.energyPercent -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.withTimeoutOrNull -import world.gregs.voidps.engine.Script -import world.gregs.voidps.engine.client.update.view.Viewport.Companion.VIEW_RADIUS -import world.gregs.voidps.engine.data.definition.AreaDefinition -import world.gregs.voidps.engine.entity.character.move.running -import world.gregs.voidps.engine.entity.character.npc.NPCs -import world.gregs.voidps.engine.entity.distanceTo -import world.gregs.voidps.engine.entity.obj.GameObjects -import world.gregs.voidps.engine.get -import world.gregs.voidps.engine.timer.TICKS -import world.gregs.voidps.network.client.instruction.InteractInterface -import world.gregs.voidps.network.client.instruction.InteractNPC -import world.gregs.voidps.network.client.instruction.InteractObject -import world.gregs.voidps.network.client.instruction.Walk -import world.gregs.voidps.type.Tile - -private val logger = InlineLogger() - -suspend fun Bot.goToNearest(tag: String) = goToNearest { it.tags.contains(tag) } - -suspend fun Bot.goToNearest(block: (AreaDefinition) -> Boolean): Boolean { - val current: AreaDefinition? = this["area"] - if (current != null && block.invoke(current)) { - return true - } - val graph: NavigationGraph = get() - val strategy = ConditionalStrategy(graph, block) - val result = goTo(strategy) - val area: AreaDefinition? = strategy.area - assert(result != null) { "Unable to find path." } - assert(area != null) { "Unable to find path target." } - if (result != null && area != null) { - this["area"] = area - return true - } - return false -} - -suspend fun Bot.goToArea(map: AreaDefinition) { - if (map.area.contains(player.tile)) { - return - } - val result = goTo(AreaStrategy(map.area)) - if (result != null) { - this["area"] = map - } else { - throw IllegalStateException("Failed to find path to ${map.name} from ${player.tile}") - } -} - -private suspend fun Bot.goTo(strategy: NodeTargetStrategy): Tile? { - player.waypoints.clear() - if (strategy.reached(player.tile)) { - return player.tile - } - - updateGraph(this) - val result = get().find(player, strategy, EdgeTraversal()) - this["navigating"] = result == null - if (result != null) { - navigate() - } - return result -} - -fun updateGraph(bot: Bot) { - val graph: NavigationGraph = get() - val edges = graph.get(bot.player) - edges.clear() - graph.nodes.filter { it is Tile && it.within(bot.tile, 20) }.forEach { - val tile = it as Tile - val distance = tile.distanceTo(bot.tile) - edges.add(Edge("", bot, tile, distance, listOf(Walk(tile.x, tile.y)))) - } -} - -private suspend fun Bot.rest() { - val musician = NPCs.firstOrNull { it.tile.within(player.tile, VIEW_RADIUS) && it.def.options.contains("Listen-to") } - if (musician != null && player.tile.distanceTo(musician) < 10) { - player.instructions.send(InteractNPC(npcIndex = 49, option = musician.def.options.indexOfFirst { it == "Listen-to" } + 1)) - repeat(32) { - await("tick") - } - } else { - player.instructions.send(InteractInterface(interfaceId = 750, componentId = 1, itemId = -1, itemSlot = -1, option = 1)) - repeat(50) { - await("tick") - } - } -} - -private suspend fun Bot.run() { - player.instructions.send(InteractInterface(interfaceId = 750, componentId = 1, itemId = -1, itemSlot = -1, option = 0)) -} - -suspend fun Bot.navigate() { - val waypoints = player.waypoints.toMutableList().iterator() - while (waypoints.hasNext()) { - val waypoint = waypoints.next() - for (step in waypoint.steps) { - if (player.energyPercent() <= 25) { - rest() - } else if (!player.running) { - run() - } - this.step = step - player.instructions.send(step) - val timeout = withTimeoutOrNull(TICKS.toMillis(20)) { - if (step is InteractObject && GameObjects.findOrNull(player.tile.copy(step.x, step.y), step.objectId) == null) { - await("tick") - } else { - await("move") - } - } - if (timeout == null && player["debug", false]) { - logger.debug { "Bot $player got stuck at $step $waypoint" } - } - } - waypoints.remove() - } - player["navigating"] = false - frame().success() -} diff --git a/game/src/main/kotlin/content/bot/interact/navigation/Navigation.kt b/game/src/main/kotlin/content/bot/interact/navigation/Navigation.kt deleted file mode 100644 index 1fe35d3ef1..0000000000 --- a/game/src/main/kotlin/content/bot/interact/navigation/Navigation.kt +++ /dev/null @@ -1,85 +0,0 @@ -package content.bot.interact.navigation - -import content.bot.Bot -import content.bot.bot -import content.bot.isBot -import content.entity.obj.door.Door -import kotlinx.coroutines.CancellableContinuation -import kotlinx.coroutines.suspendCancellableCoroutine -import kotlinx.coroutines.withTimeoutOrNull -import world.gregs.voidps.engine.Script -import world.gregs.voidps.engine.client.variable.hasClock -import world.gregs.voidps.engine.entity.character.mode.EmptyMode -import world.gregs.voidps.engine.entity.character.mode.interact.Interact -import world.gregs.voidps.engine.entity.character.mode.move.Movement -import world.gregs.voidps.engine.entity.character.player.Player -import world.gregs.voidps.engine.timer.TICKS -import kotlin.coroutines.resume - -suspend fun Bot.await(type: Any, timeout: Int = -1) { - if (timeout > 0) { - withTimeoutOrNull(TICKS.toMillis(timeout)) { - suspendCancellableCoroutine { cont -> - player["suspension"] = type - player["cont"] = cont - } - } - } else { - suspendCancellableCoroutine { cont -> - player["suspension"] = type - player["cont"] = cont - } - } -} - -suspend fun Bot.awaitInteract(timeout: Int = -1) { - await("tick", timeout) - while (player.mode is Interact || player.hasClock("movement_delay")) { - await("tick", timeout) - } -} - -suspend inline fun Bot.await( - noinline condition: Player.() -> Boolean = { true }, -) { - suspendCancellableCoroutine { cont -> - player.getOrPut("bot_new_suspensions") { mutableListOf Boolean, CancellableContinuation>>() }.add(condition to cont) - } -} - -fun Bot.resume(type: Any) = resume(type, Unit) - -fun Bot.resume(type: Any, value: T) { - if (player.get("suspension") == type) { - val cont: CancellableContinuation? = player.remove("cont") - cont?.resume(value) - } -} - -fun Bot.cancel(cause: Throwable? = null) { - val cont: CancellableContinuation<*>? = player.remove("cont") - cont?.cancel(cause) -} - -class Navigation : Script { - - init { - moved { - if (isBot && ((mode is Movement && steps.size <= 1) || mode == EmptyMode)) { - bot.resume("move") - } - } - - Door.opened = { - if (isBot) { - bot.resume("move") - } - } - - objTeleportLand { _, _ -> - if (isBot) { - bot.resume("move") - } - } - } -} diff --git a/game/src/main/kotlin/content/bot/interact/navigation/graph/Condition.kt b/game/src/main/kotlin/content/bot/interact/navigation/graph/Condition.kt deleted file mode 100644 index 4273d6dd27..0000000000 --- a/game/src/main/kotlin/content/bot/interact/navigation/graph/Condition.kt +++ /dev/null @@ -1,7 +0,0 @@ -package content.bot.interact.navigation.graph - -import world.gregs.voidps.engine.entity.character.player.Player - -interface Condition { - fun has(player: Player): Boolean -} diff --git a/game/src/main/kotlin/content/bot/interact/navigation/graph/Edge.kt b/game/src/main/kotlin/content/bot/interact/navigation/graph/Edge.kt deleted file mode 100644 index 88324cd0e3..0000000000 --- a/game/src/main/kotlin/content/bot/interact/navigation/graph/Edge.kt +++ /dev/null @@ -1,23 +0,0 @@ -package content.bot.interact.navigation.graph - -import world.gregs.voidps.engine.entity.character.player.Player -import world.gregs.voidps.engine.entity.character.player.name -import world.gregs.voidps.network.client.Instruction -import java.util.* - -data class Edge( - val name: String, - val start: Any, - val end: Any, - val cost: Int, - val steps: List = emptyList(), - val requirements: List = emptyList(), -) : Comparable { - - override fun compareTo(other: Edge): Int = cost.compareTo(other.cost) - - override fun toString(): String = "Edge(${"$name ".trimStart()}${if (start is Player) start.name else start} - $end)" -} - -val Player.waypoints: LinkedList - get() = getOrPut("waypoints") { LinkedList() } diff --git a/game/src/main/kotlin/content/bot/interact/navigation/graph/HasInventoryItem.kt b/game/src/main/kotlin/content/bot/interact/navigation/graph/HasInventoryItem.kt deleted file mode 100644 index 0e327ab2fd..0000000000 --- a/game/src/main/kotlin/content/bot/interact/navigation/graph/HasInventoryItem.kt +++ /dev/null @@ -1,8 +0,0 @@ -package content.bot.interact.navigation.graph - -import world.gregs.voidps.engine.entity.character.player.Player -import world.gregs.voidps.engine.inv.inventory - -class HasInventoryItem(val id: String, val amount: Int) : Condition { - override fun has(player: Player): Boolean = player.inventory.contains(id, amount) -} diff --git a/game/src/main/kotlin/content/bot/interact/navigation/graph/NavigationGraph.kt b/game/src/main/kotlin/content/bot/interact/navigation/graph/NavigationGraph.kt index b0ac93d99e..bf8df95c24 100644 --- a/game/src/main/kotlin/content/bot/interact/navigation/graph/NavigationGraph.kt +++ b/game/src/main/kotlin/content/bot/interact/navigation/graph/NavigationGraph.kt @@ -1,154 +1,8 @@ package content.bot.interact.navigation.graph -import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap -import it.unimi.dsi.fastutil.objects.ObjectArrayList -import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet -import world.gregs.config.Config import world.gregs.config.ConfigReader -import world.gregs.voidps.engine.data.definition.AreaDefinition -import world.gregs.voidps.engine.data.definition.Areas -import world.gregs.voidps.engine.data.definition.ObjectDefinitions -import world.gregs.voidps.engine.entity.obj.GameObject -import world.gregs.voidps.engine.timedLoad -import world.gregs.voidps.network.client.Instruction -import world.gregs.voidps.network.client.instruction.InteractObject -import world.gregs.voidps.network.client.instruction.Walk -import world.gregs.voidps.type.Distance import world.gregs.voidps.type.Tile -class NavigationGraph { - - private var adjacencyList: Object2ObjectOpenHashMap> = Object2ObjectOpenHashMap>() - private val tags = Object2ObjectOpenHashMap>() - - val nodes: Set - get() = adjacencyList.keys - val size: Int - get() = adjacencyList.size - - fun contains(node: Any) = adjacencyList.containsKey(node) - - fun getAdjacent(node: Any): Set = adjacencyList.getOrDefault(node, empty) - - fun get(node: Any): ObjectOpenHashSet = adjacencyList.getOrPut(node) { ObjectOpenHashSet() } - - fun areas(node: Any): Set = tags[node] ?: emptyTags - - fun add(node: Any, set: ObjectOpenHashSet) { - adjacencyList[node] = set - } - - fun remove(node: Any) { - adjacencyList.remove(node) - } - - fun load(path: String): NavigationGraph { - timedLoad("ai nav graph edge") { - val map = Object2ObjectOpenHashMap>() - var count = 0 - Config.fileReader(path) { - while (nextSection()) { - val name = section() - var from = Tile.EMPTY - var to = Tile.EMPTY - var cost = 0 - val steps = ObjectArrayList() - val conditions = ObjectArrayList() - while (nextPair()) { - when (val key = key()) { - "from" -> from = readTile() - "to" -> to = readTile() - "cost" -> cost = int() - "steps" -> { - while (nextElement()) { - var option = "" - var objectId = "" - var transform: Int? = null - var tile = Tile.EMPTY - while (nextEntry()) { - when (val stepKey = key()) { - "option" -> option = string() - "object" -> objectId = string() - "transform" -> transform = int() - "tile" -> tile = readTile() - else -> throw IllegalArgumentException("Unexpected key: '$stepKey' ${exception()}") - } - } - val instruction = when { - objectId != "" -> { - var def = ObjectDefinitions.getOrNull(objectId) ?: continue - if (transform != null) { - val id = def.transforms?.get(transform) ?: continue - def = ObjectDefinitions.getOrNull(id) ?: continue - } - val optionIndex = def.optionsIndex(option) + 1 - InteractObject(def.id, tile.x, tile.y, optionIndex) - } - else -> Walk(tile.x, tile.y) - } - steps.add(instruction) - } - } - "conditions" -> { - while (nextElement()) { - require(nextEntry()) { "Expected condition type. ${exception()}" } - require(key() == "type") { "Expected condition type. ${exception()}" } - when (val type = string()) { - "inventory_item" -> { - var item = "" - var amount = 0 - while (nextEntry()) { - when (val k = key()) { - "item" -> item = string() - "amount" -> amount = int() - else -> throw IllegalArgumentException("Unexpected key: '$k' ${exception()}") - } - } - conditions.add(HasInventoryItem(item, amount)) - } - else -> throw IllegalArgumentException("Unexpected type: '$type' ${exception()}") - } - } - } - else -> throw IllegalArgumentException("Unexpected key: '$key' ${exception()}") - } - } - val walk = steps.isEmpty - if (steps.isEmpty) { - cost = Distance.manhattan(from.x, from.y, to.x, to.y) - steps.add(Walk(to.x, to.y)) - } - count++ - map.getOrPut(from) { ObjectOpenHashSet(1) }.add(Edge(name, from, to, cost, steps, conditions)) - if (walk) { // Bidirectional - map.getOrPut(to) { ObjectOpenHashSet(1) }.add(Edge(name, to, from, cost, listOf(Walk(from.x, from.y)), conditions)) - } - } - } - this.adjacencyList = map - tagAreas() - count - } - return this - } - - private fun tagAreas() { - adjacencyList.forEach { (node, _) -> - val tile = when (node) { - is Tile -> node - is GameObject -> node.tile - else -> return@forEach - } - tags[node] = Areas.getAll().filter { it.area.contains(tile) }.toSet() - } - } - - companion object { - private val empty = emptySet() - private val emptyTags = emptySet() - } -} - fun ConfigReader.readTile(): Tile { var x = 0 var y = 0 diff --git a/game/src/main/kotlin/content/bot/interact/path/AreaStrategy.kt b/game/src/main/kotlin/content/bot/interact/path/AreaStrategy.kt deleted file mode 100644 index a4983c511e..0000000000 --- a/game/src/main/kotlin/content/bot/interact/path/AreaStrategy.kt +++ /dev/null @@ -1,11 +0,0 @@ -package content.bot.interact.path - -import world.gregs.voidps.type.Area -import world.gregs.voidps.type.Tile - -class AreaStrategy( - val area: Area, -) : NodeTargetStrategy() { - - override fun reached(node: Any): Boolean = node is Tile && node in area -} diff --git a/game/src/main/kotlin/content/bot/interact/path/ConditionalStrategy.kt b/game/src/main/kotlin/content/bot/interact/path/ConditionalStrategy.kt deleted file mode 100644 index 2ec472ed01..0000000000 --- a/game/src/main/kotlin/content/bot/interact/path/ConditionalStrategy.kt +++ /dev/null @@ -1,26 +0,0 @@ -package content.bot.interact.path - -import content.bot.interact.navigation.graph.NavigationGraph -import world.gregs.voidps.engine.data.definition.AreaDefinition -import world.gregs.voidps.type.Tile - -class ConditionalStrategy( - val graph: NavigationGraph, - val block: (AreaDefinition) -> Boolean, -) : NodeTargetStrategy() { - - var area: AreaDefinition? = null - - override fun reached(node: Any): Boolean { - if (node !is Tile) { - return false - } - for (area in graph.areas(node)) { - if (block(area)) { - this.area = area - return true - } - } - return false - } -} diff --git a/game/src/main/kotlin/content/bot/interact/path/Dijkstra.kt b/game/src/main/kotlin/content/bot/interact/path/Dijkstra.kt deleted file mode 100644 index 77d33270a6..0000000000 --- a/game/src/main/kotlin/content/bot/interact/path/Dijkstra.kt +++ /dev/null @@ -1,62 +0,0 @@ -package content.bot.interact.path - -import content.bot.interact.navigation.graph.Edge -import content.bot.interact.navigation.graph.NavigationGraph -import content.bot.interact.navigation.graph.waypoints -import content.bot.interact.path.DijkstraFrontier.Companion.MAX_COST -import kotlinx.io.pool.ObjectPool -import world.gregs.voidps.engine.entity.character.player.Player -import world.gregs.voidps.type.Tile -import java.util.* - -class Dijkstra( - private val graph: NavigationGraph, - private val pool: ObjectPool, -) { - - fun find(player: Player, strategy: NodeTargetStrategy, traversal: EdgeTraversal): Tile? { - val frontier = pool.borrow() - frontier.reset(player) - var target: Edge? = null - while (frontier.isNotEmpty()) { - val (parent, parentEdge, parentCost) = frontier.poll() - if (strategy.reached(parent)) { - target = parentEdge - break - } - for (edge in graph.getAdjacent(parent)) { - if (traversal.blocked(player, edge)) { - continue - } - val cost = parentCost + edge.cost - if (frontier.cost(edge) > cost) { - frontier.visit(edge.end, edge, parentEdge, cost) - } - } - } - val result = backtrace(frontier, player.waypoints, player, target) - pool.recycle(frontier) - return result - } - - private fun backtrace(frontier: DijkstraFrontier, waypoints: LinkedList, start: Any, target: Edge?): Tile? { - if (target != null && frontier.cost(target) != MAX_COST) { - var edge: Edge? = target - waypoints.clear() - while (edge != null) { - waypoints.add(0, edge) - if (edge.start == start) { - break - } - edge = frontier.parent(edge) - } - val end = target.end - if (end is Int) { - return Tile(end) - } else if (end is Tile) { - return end - } - } - return null - } -} diff --git a/game/src/main/kotlin/content/bot/interact/path/DijkstraFrontier.kt b/game/src/main/kotlin/content/bot/interact/path/DijkstraFrontier.kt deleted file mode 100644 index 694d92e26b..0000000000 --- a/game/src/main/kotlin/content/bot/interact/path/DijkstraFrontier.kt +++ /dev/null @@ -1,47 +0,0 @@ -package content.bot.interact.path - -import content.bot.interact.navigation.graph.Edge -import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap -import java.util.* - -/** - * All the graph nodes visited or to be visited by the [Dijkstra] algorithm - */ -class DijkstraFrontier(size: Int) { - private val queue = PriorityQueue() - private val visited = Object2ObjectOpenHashMap>(size + 1) - - fun isNotEmpty() = queue.isNotEmpty() - - fun add(node: Any, edge: Edge, cost: Int) { - queue.add(Weighted(node, edge, cost)) - } - - fun poll(): Triple = queue.poll().let { Triple(it.node, it.edge, it.cost) } - - fun visit(node: Any, edge: Edge, parent: Edge?, cost: Int) { - add(node, edge, cost) - visited[edge] = Pair(parent, cost) - } - - fun cost(edge: Edge): Int { - return visited[edge]?.second ?: return MAX_COST - } - - fun parent(edge: Edge): Edge? = visited[edge]?.first - - fun reset(node: Any) { - queue.clear() - visited.clear() - queue.add(Weighted(node, null, 0)) - } - - private class Weighted(val node: Any, val edge: Edge?, val cost: Int) : Comparable { - - override fun compareTo(other: Weighted): Int = cost.compareTo(other.cost) - } - - companion object { - const val MAX_COST = 0xffff - } -} diff --git a/game/src/main/kotlin/content/bot/interact/path/EdgeTraversal.kt b/game/src/main/kotlin/content/bot/interact/path/EdgeTraversal.kt deleted file mode 100644 index 0c26943fc8..0000000000 --- a/game/src/main/kotlin/content/bot/interact/path/EdgeTraversal.kt +++ /dev/null @@ -1,8 +0,0 @@ -package content.bot.interact.path - -import content.bot.interact.navigation.graph.Edge -import world.gregs.voidps.engine.entity.character.player.Player - -class EdgeTraversal { - fun blocked(player: Player, edge: Edge): Boolean = edge.requirements.any { !it.has(player) } -} diff --git a/game/src/main/kotlin/content/bot/interact/path/NodeTargetStrategy.kt b/game/src/main/kotlin/content/bot/interact/path/NodeTargetStrategy.kt deleted file mode 100644 index cceb42951e..0000000000 --- a/game/src/main/kotlin/content/bot/interact/path/NodeTargetStrategy.kt +++ /dev/null @@ -1,5 +0,0 @@ -package content.bot.interact.path - -abstract class NodeTargetStrategy { - abstract fun reached(node: Any): Boolean -} diff --git a/game/src/main/kotlin/content/bot/interact/shop/ShopBot.kt b/game/src/main/kotlin/content/bot/interact/shop/ShopBot.kt deleted file mode 100644 index 2f971e1a4e..0000000000 --- a/game/src/main/kotlin/content/bot/interact/shop/ShopBot.kt +++ /dev/null @@ -1,80 +0,0 @@ -package content.bot.interact.shop - -import content.bot.Bot -import content.bot.bot -import content.bot.closeInterface -import content.bot.interact.navigation.await -import content.bot.interact.navigation.goToArea -import content.bot.interact.navigation.goToNearest -import content.bot.interact.navigation.resume -import content.bot.isBot -import content.entity.npc.shop.shopInventory -import world.gregs.voidps.engine.Script -import world.gregs.voidps.engine.client.update.view.Viewport -import world.gregs.voidps.engine.data.definition.AreaDefinition -import world.gregs.voidps.engine.data.definition.Areas -import world.gregs.voidps.engine.entity.character.npc.NPC -import world.gregs.voidps.engine.entity.character.npc.NPCs -import world.gregs.voidps.network.client.instruction.InteractInterface -import world.gregs.voidps.network.client.instruction.InteractNPC - -suspend fun Bot.openShop(id: String): NPC = openShop(Areas.getOrNull(id)!!) - -suspend fun Bot.openNearestShop(id: String): Boolean { - val reached = goToNearest { it["items", emptyList()].contains(id) } - openShop() - return reached -} - -suspend fun Bot.openShop(map: AreaDefinition): NPC { - goToArea(map) - return openShop() -} - -private suspend fun Bot.openShop(): NPC { - val shop = NPCs.first { it.tile.within(player.tile, Viewport.VIEW_RADIUS) && it.def.options.contains("Trade") } - player.instructions.send(InteractNPC(npcIndex = shop.index, option = shop.def.options.indexOfFirst { it == "Trade" } + 1)) - await("shop") - return shop -} - -suspend fun Bot.closeShop() = closeInterface(620, 18) - -suspend fun Bot.buy(item: String, amount: Int = 1) { - val shop = player.shopInventory() - val index = shop.indexOf(item) - if (index == -1) { - throw IllegalArgumentException("Shop doesn't contain item '$item'") - } - val slot = index * 6 - var remaining = amount - while (remaining > 0) { - val option = when { - remaining >= 500 -> 5 - remaining >= 50 -> 4 - remaining >= 10 -> 3 - remaining >= 5 -> 2 - else -> 1 - } - player.instructions.send(InteractInterface(interfaceId = 620, componentId = 25, itemId = -1, itemSlot = slot, option = option)) - remaining -= when { - remaining >= 500 -> 500 - remaining >= 50 -> 50 - remaining >= 10 -> 10 - remaining >= 5 -> 5 - else -> 1 - } - } - await("tick") -} - -class ShopBot : Script { - - init { - interfaceOpened("shop") { - if (isBot) { - bot.resume("shop") - } - } - } -} diff --git a/game/src/main/kotlin/content/bot/skill/SkillBot.kt b/game/src/main/kotlin/content/bot/skill/SkillBot.kt index 767a730eec..9e33f1a55a 100644 --- a/game/src/main/kotlin/content/bot/skill/SkillBot.kt +++ b/game/src/main/kotlin/content/bot/skill/SkillBot.kt @@ -5,7 +5,6 @@ import world.gregs.voidps.engine.Script import world.gregs.voidps.network.client.instruction.InteractDialogue class SkillBot : Script { - init { interfaceOpened("dialogue_level_up") { if (isBot) { diff --git a/game/src/main/kotlin/content/bot/skill/combat/CombatBot.kt b/game/src/main/kotlin/content/bot/skill/combat/CombatBot.kt deleted file mode 100644 index 7b49b8c05f..0000000000 --- a/game/src/main/kotlin/content/bot/skill/combat/CombatBot.kt +++ /dev/null @@ -1,209 +0,0 @@ -package content.bot.skill.combat - -import content.bot.* -import content.bot.Bot -import content.bot.interact.item.pickup -import content.bot.interact.navigation.await -import content.bot.interact.navigation.cancel -import content.bot.interact.navigation.goToArea -import content.bot.interact.navigation.resume -import content.entity.combat.attackers -import content.entity.combat.underAttack -import content.entity.death.weightedSample -import content.skill.magic.spell.removeSpellItems -import content.skill.magic.spell.spell -import content.skill.magic.spell.spellBook -import content.skill.slayer.categories -import net.pearx.kasechange.toLowerSpaceCase -import net.pearx.kasechange.toSnakeCase -import world.gregs.voidps.engine.Script -import world.gregs.voidps.engine.client.ui.chat.toIntRange -import world.gregs.voidps.engine.client.update.view.Viewport -import world.gregs.voidps.engine.data.definition.AmmoDefinitions -import world.gregs.voidps.engine.data.definition.AreaDefinition -import world.gregs.voidps.engine.data.definition.Areas -import world.gregs.voidps.engine.data.definition.InterfaceDefinitions -import world.gregs.voidps.engine.entity.character.npc.NPC -import world.gregs.voidps.engine.entity.character.npc.NPCs -import world.gregs.voidps.engine.entity.character.player.Player -import world.gregs.voidps.engine.entity.character.player.combatLevel -import world.gregs.voidps.engine.entity.character.player.equip.equipped -import world.gregs.voidps.engine.entity.character.player.equip.has -import world.gregs.voidps.engine.entity.character.player.skill.Skill -import world.gregs.voidps.engine.entity.character.player.skill.level.Level.hasRequirements -import world.gregs.voidps.engine.entity.distanceTo -import world.gregs.voidps.engine.entity.item.floor.FloorItems -import world.gregs.voidps.engine.get -import world.gregs.voidps.engine.inv.inventory -import world.gregs.voidps.network.client.instruction.InteractInterface -import world.gregs.voidps.network.login.protocol.visual.update.player.EquipSlot -import world.gregs.voidps.type.Tile -import world.gregs.voidps.type.random - -suspend fun Bot.setAttackStyle(skill: Skill) { - setAttackStyle( - when (skill) { - Skill.Strength -> 1 - Skill.Defence -> 3 - else -> 0 - }, - ) -} - -suspend fun Bot.setAutoCast(spell: String) { - val definitions = get() - val def = definitions.get(player.spellBook) - player.instructions.send(InteractInterface(def.id, definitions.getComponentId(player.spellBook, spell) ?: return, -1, -1, 0)) -} - -suspend fun Bot.setAttackStyle(style: Int) { - player.instructions.send(InteractInterface(interfaceId = 884, componentId = style + 11, itemId = -1, itemSlot = -1, option = 0)) -} - -class CombatBot(val tasks: TaskManager) : Script { - - init { - worldSpawn { - for (area in Areas.tagged("combat_training")) { - val spaces: Int = area["spaces", 1] - val types = area["npcs", emptyList()].toSet() - val range = area["levels", "1-5"].toIntRange() - val skills = listOf(Skill.Attack, Skill.Strength, Skill.Defence, Skill.Ranged, Skill.Magic).shuffled().take(spaces) - for (skill in skills) { - val task = Task( - name = "train ${skill.name} killing ${types.joinToString(", ")} at ${area.name}".toLowerSpaceCase(), - block = { - while (levels.getMax(skill) < range.last + 1) { - bot.fight(area, skill, types) - } - }, - area = area.area, - spaces = 1, - requirements = listOf( - { levels.getMax(skill) in range }, - { bot.hasExactGear(skill) || bot.hasCoins(2000) }, - ), - ) - tasks.register(task) - } - } - } - - levelChanged(Skill.Constitution, ::eat) - - variableSet("under_attack") { _, _, to -> - if (to == 1 && isBot) { - bot.resume("combat") - } - } - - playerDeath { - if (isBot) { - clear("area") - bot.cancel() - } - } - } - - fun eat(player: Player, skill: Skill, from: Int, to: Int) { - if (player.isBot && player.levels.getPercent(Skill.Constitution) < 50.0) { - val food = player.inventory.items.firstOrNull { it.def.contains("heals") } ?: return - player.bot.inventoryOption(food.id, "Eat") - } - } - - suspend fun Bot.fight(map: AreaDefinition, skill: Skill, races: Set) { - setupGear(skill) - goToArea(map) - setAttackStyle(skill) - while (player.inventory.spaces > 0 && player.isRangedNotOutOfAmmo(skill) && player.isMagicNotOutOfRunes(skill)) { - val targets = NPCs - .filter { isAvailableTarget(map, it, races) } - .map { it to tile.distanceTo(it) } - val target = weightedSample(targets, invert = true) - if (target == null) { - await("tick") - if (player.inventory.spaces < 4) { - break - } - continue - } - npcOption(target, "Attack") - await("combat", timeout = 30) - target.get("death_tile")?.let { - pickupItems(it, 4) - } - equipAmmo(skill) - // TODO on death run back and pickup stuff - } - } - - fun Player.isRangedNotOutOfAmmo(skill: Skill): Boolean { - if (skill != Skill.Ranged) { - return true - } - return has(EquipSlot.Ammo) - } - - fun Player.isMagicNotOutOfRunes(skill: Skill): Boolean { - if (skill != Skill.Magic) { - return true - } - val spell = spell - return removeSpellItems(spell) - } - - suspend fun Bot.pickupItems(tile: Tile, amount: Int) { - repeat(random.nextInt(2, 8)) { - if (player.inventory.contains("bones")) { - inventoryOption("bones", "Bury") - await("tick") - } - await("tick") - } - repeat(amount) { - val item = FloorItems.at(tile).firstOrNull() ?: return@repeat - pickup(item) - } - } - - fun Bot.isAvailableTarget(map: AreaDefinition, npc: NPC, races: Set): Boolean { - if (!npc.tile.within(player.tile, Viewport.VIEW_RADIUS)) { - return false - } - if (player.attackers.isNotEmpty()) { - return player.attackers.contains(npc) - } - if (npc.underAttack) { - return false - } - if (!npc.def.options.contains("Attack")) { - return false - } - if (!races.contains(npc.def.name.toSnakeCase()) && npc.categories.none { races.contains(it) }) { - return false - } - if (!map.area.contains(npc.tile)) { - return false - } - val difference = npc.def.combat - player.combatLevel - return difference < 5 - } - - fun Bot.equipAmmo(skill: Skill) { - if (skill == Skill.Ranged) { - val ammo = player.equipped(EquipSlot.Ammo) - if (ammo.isEmpty()) { - val weapon = player.equipped(EquipSlot.Weapon) - val ammoDefinitions: AmmoDefinitions = get() - player.inventory.items - .firstOrNull { player.hasRequirements(it) && ammoDefinitions.get(weapon.def["ammo_group", ""]).items.contains(it.id) } - ?.let { - equip(it.id) - } - } else if (player.inventory.contains(ammo.id)) { - equip(ammo.id) - } - } - } -} diff --git a/game/src/main/kotlin/content/bot/skill/combat/CombatGear.kt b/game/src/main/kotlin/content/bot/skill/combat/CombatGear.kt deleted file mode 100644 index 7be88463b8..0000000000 --- a/game/src/main/kotlin/content/bot/skill/combat/CombatGear.kt +++ /dev/null @@ -1,143 +0,0 @@ -package content.bot.skill.combat - -import content.bot.Bot -import content.bot.buyItem -import content.bot.equip -import content.bot.interact.bank.* -import content.bot.interact.navigation.await -import content.entity.player.bank.bank -import content.entity.player.bank.ownsItem -import kotlinx.coroutines.CancellationException -import world.gregs.voidps.engine.data.config.GearDefinition -import world.gregs.voidps.engine.data.definition.GearDefinitions -import world.gregs.voidps.engine.entity.character.player.Player -import world.gregs.voidps.engine.entity.character.player.skill.Skill -import world.gregs.voidps.engine.entity.character.player.skill.level.Level.hasRequirements -import world.gregs.voidps.engine.entity.character.player.skill.level.Level.hasRequirementsToUse -import world.gregs.voidps.engine.entity.item.Item -import world.gregs.voidps.engine.get -import world.gregs.voidps.engine.inv.inventory - -suspend fun Bot.setupGear(gear: GearDefinition, buy: Boolean = true) { - openBank() - depositAll() - if (gear.equipment.isNotEmpty()) { - depositWornItems() - } - setupGearAndInv(gear, buy) -} - -suspend fun Bot.setupGear(skill: Skill, buy: Boolean = true) { - val gear = getGear(skill) ?: return - setupGear(gear, buy) -} - -fun Bot.getGear(skill: Skill): GearDefinition? { - val style = when (skill) { - Skill.Attack, Skill.Strength, Skill.Defence -> "melee" - else -> skill.name.lowercase() - } - return getGear(style, skill) -} - -fun Bot.getGear(type: String, skill: Skill): GearDefinition? { - val setups = get().get(type) - val level = player.levels.getMax(skill) - return setups - .filter { it.levels.contains(level) } - .sortedWith(compareBy({ player.gearScore(it) }, { it.inventory.size + it.equipment.size })) - .lastOrNull() -} - -fun Bot.getSuitableItem(items: List): Item = items.first { item -> player.hasRequirements(item) && player.ownsItem(item.id, item.amount) } - -private fun Player.gearScore(definition: GearDefinition): Double { - val total = definition.inventory.size + definition.equipment.size - if (total <= 0) { - return 0.0 - } - var count = 0 - for (items in definition.inventory) { - if (items.any { item -> hasRequirements(item) && ownsItem(item.id, item.amount) }) { - count++ - } - } - for ((_, equipment) in definition.equipment) { - if (equipment.any { item -> hasRequirements(item) && ownsItem(item.id, item.amount) }) { - count++ - } - } - return count / total.toDouble() -} - -fun Bot.hasExactGear(skill: Skill): Boolean { - val gear = getGear(skill) - if (gear != null) { - return hasExactGear(gear) - } - return false -} - -fun Bot.hasExactGear(type: String, skill: Skill): Boolean { - val gear = getGear(type, skill) - if (gear != null) { - return hasExactGear(gear) - } - return false -} - -fun Bot.hasExactGear(gear: GearDefinition): Boolean = player.gearScore(gear) == 1.0 - -private suspend fun Bot.setupGearAndInv(gear: GearDefinition, buy: Boolean) { - // Pick one of each item to equip for each required slot - for ((_, equipmentList) in gear.equipment) { - val items = equipmentList - .filter { player.hasRequirements(it) || player.hasRequirementsToUse(it) || player.bank.contains(it.id, it.amount) } - if (items.isEmpty()) { - continue - } - withdrawOrBuy(items, buy) - } - await("tick") - if (!player.interfaces.contains("bank")) { - openBank() - await("tick") - } - for (items in gear.inventory) { - withdrawOrBuy(items, buy) - } - - if (player.inventory.contains("coins")) { - openBank() - depositAll("coins") - } - if (gear.type == "magic") { - setAutoCast(gear["spell"]) - } - closeBank() - - await("tick") - await("tick") - if (!hasExactGear(gear)) { - throw CancellationException("Doesn't have all the gear required.") - } -} - -suspend fun Bot.withdrawOrBuy(items: List, buy: Boolean): Boolean { - for (item in items) { - if (player.bank.contains(item.id, item.amount)) { - withdraw(item.id, amount = item.amount) - equip(item.id) - return true - } - } - if (buy) { - for (item in items) { - if (buyItem(item.id, item.amount)) { - equip(item.id) - return true - } - } - } - return false -} diff --git a/game/src/main/kotlin/content/bot/skill/combat/TrainingBot.kt b/game/src/main/kotlin/content/bot/skill/combat/TrainingBot.kt deleted file mode 100644 index dcc294db9c..0000000000 --- a/game/src/main/kotlin/content/bot/skill/combat/TrainingBot.kt +++ /dev/null @@ -1,180 +0,0 @@ -package content.bot.skill.combat - -import content.bot.* -import content.bot.interact.bank.withdrawAll -import content.bot.interact.navigation.await -import content.bot.interact.navigation.cancel -import content.bot.interact.navigation.goToArea -import content.entity.combat.attackers -import content.entity.combat.attacking -import content.entity.combat.underAttack -import content.entity.player.bank.ownsItem -import content.skill.magic.spell.spellBook -import content.skill.melee.weapon.attackRange -import net.pearx.kasechange.toLowerSpaceCase -import world.gregs.voidps.engine.Script -import world.gregs.voidps.engine.client.ui.dialogue -import world.gregs.voidps.engine.client.update.view.Viewport -import world.gregs.voidps.engine.client.variable.remaining -import world.gregs.voidps.engine.data.definition.AreaDefinition -import world.gregs.voidps.engine.data.definition.Areas -import world.gregs.voidps.engine.entity.character.mode.interact.PlayerOnObjectInteract -import world.gregs.voidps.engine.entity.character.npc.NPC -import world.gregs.voidps.engine.entity.character.npc.NPCs -import world.gregs.voidps.engine.entity.character.player.equip.has -import world.gregs.voidps.engine.entity.character.player.skill.Skill -import world.gregs.voidps.engine.entity.obj.GameObject -import world.gregs.voidps.engine.inv.inventory -import world.gregs.voidps.engine.timer.epochSeconds -import world.gregs.voidps.network.login.protocol.visual.update.player.EquipSlot - -class TrainingBot(val tasks: TaskManager) : Script { - - init { - worldSpawn { - val area = Areas.getOrNull("lumbridge_combat_tutors") ?: return@worldSpawn - val range = 1..5 - val skills = listOf(Skill.Attack, Skill.Magic, Skill.Ranged) - val melees = listOf(Skill.Attack, Skill.Strength, Skill.Defence) - for (skill in skills) { - val melee = skill == Skill.Attack - val task = Task( - name = "train ${if (melee) "melee" else skill.name} at ${area.name}".toLowerSpaceCase(), - block = { - val actualSkill = if (melee) melees.filter { levels.getMax(it) in range }.random() else skill - bot.train(area, actualSkill, range) - }, - area = area.area, - spaces = if (melee) 3 else 2, - requirements = listOf( - { if (melee) melees.any { levels.getMax(it) in range } else levels.getMax(skill) in range }, - { bot.canGetGearAndAmmo(skill) }, - ), - ) - tasks.register(task) - } - } - } - - suspend fun Bot.train(map: AreaDefinition, skill: Skill, range: IntRange) { - setupGear(map, skill) - if (skill == Skill.Magic) { - setAutoCast("wind_strike") - } else { - player.clear("autocast") - setAttackStyle(skill) - } - var target: Any? = null - while (target == null) { - await("tick") - target = if (skill == Skill.Ranged) { - getObjects { it.id == "archery_target" } - .randomOrNull() - } else { - NPCs - .filter { isAvailableTarget(map, it, skill) } - .randomOrNull() - } - } - if (target is NPC) { - if (!player.tile.within(target.tile, player.attackRange + 1)) { - player.walkTo(target.tile) - await("move") - } - } - while (player.levels.getMax(skill) < range.last + 1 && hasAmmo(skill)) { - if (target is GameObject) { - objectOption(target, "Shoot-at") - await { mode is PlayerOnObjectInteract } - await("tick") - } else if (target is NPC) { - npcOption(target, "Attack") - while (player.attacking) { - await("tick") - } - await("tick") - } - } - } - - suspend fun Bot.setupGear(area: AreaDefinition, skill: Skill) { - when (skill) { - Skill.Magic -> { - withdrawAll("air_rune", "mind_rune") - goToArea(area) - if (!player.inventory.contains("air_rune") && !player.inventory.contains("mind_rune")) { - claim("mikasi") - } - if (!player.inventory.contains("air_rune") || !player.inventory.contains("mind_rune")) { - cancel() - return - } - } - Skill.Ranged -> { - withdrawAll("training_bow", "training_arrows") - goToArea(area) - if (!player.inventory.contains("training_bow") || !player.inventory.contains("training_arrows")) { - claim("nemarti") - } - equip("training_bow") - equip("training_arrows") - } - else -> { - withdrawAll("training_sword", "training_shield") - goToArea(area) - if (!player.inventory.contains("training_sword")) { - val tutor = NPCs.first { it.tile.within(player.tile, Viewport.VIEW_RADIUS) && it.id == "harlan" } - npcOption(tutor, "Talk-to") - await { dialogue != null } - await("tick") - dialogueOption("continue") - dialogueOption("line4") - dialogueOption("continue") - dialogueOption("continue") - dialogueOption("continue") - } - equip("training_sword") - equip("training_shield") - } - } - } - - suspend fun Bot.claim(npc: String) { - val tutor = NPCs.first { it.tile.within(player.tile, Viewport.VIEW_RADIUS) && it.id == npc } - npcOption(tutor, "Talk-to") - await { dialogue != null } - await("tick") - dialogueOption("continue") - dialogueOption("line3") - dialogueOption("continue") - dialogueOption("continue") - } - - fun Bot.isAvailableTarget(map: AreaDefinition, npc: NPC, skill: Skill): Boolean { - if (!npc.tile.within(player.tile, Viewport.VIEW_RADIUS)) { - return false - } - if (npc.underAttack && !npc.attackers.contains(player)) { - return false - } - if (!npc.def.options.contains("Attack")) { - return false - } - if (!map.area.contains(npc.tile)) { - return false - } - return npc.id == if (skill == Skill.Magic) "magic_dummy" else "melee_dummy" - } - - fun Bot.canGetGearAndAmmo(skill: Skill): Boolean = when (skill) { - Skill.Magic -> (player.ownsItem("air_rune") && player.ownsItem("mind_rune")) || player.remaining("claimed_tutor_consumables", epochSeconds()) <= 0 && player.spellBook == "modern_spellbook" - Skill.Ranged -> (player.ownsItem("training_bow") && (player.ownsItem("training_arrows")) || player.remaining("claimed_tutor_consumables", epochSeconds()) <= 0) - else -> true - } - - fun Bot.hasAmmo(skill: Skill): Boolean = when (skill) { - Skill.Ranged -> player.has(EquipSlot.Ammo) - Skill.Magic -> player.inventory.contains("air_rune") && player.inventory.contains("mind_rune") - else -> true - } -} diff --git a/game/src/main/kotlin/content/bot/skill/cooking/CookingBot.kt b/game/src/main/kotlin/content/bot/skill/cooking/CookingBot.kt deleted file mode 100644 index c88898239d..0000000000 --- a/game/src/main/kotlin/content/bot/skill/cooking/CookingBot.kt +++ /dev/null @@ -1,93 +0,0 @@ -package content.bot.skill.cooking - -import content.bot.* -import content.bot.interact.navigation.await -import content.bot.interact.navigation.goToArea -import content.bot.interact.navigation.resume -import content.bot.skill.combat.getGear -import content.bot.skill.combat.getSuitableItem -import content.bot.skill.combat.hasExactGear -import content.bot.skill.combat.setupGear -import net.pearx.kasechange.toLowerSpaceCase -import world.gregs.voidps.engine.Script -import world.gregs.voidps.engine.client.ui.chat.plural -import world.gregs.voidps.engine.data.config.GearDefinition -import world.gregs.voidps.engine.data.definition.AreaDefinition -import world.gregs.voidps.engine.data.definition.Areas -import world.gregs.voidps.engine.entity.character.player.skill.Skill -import world.gregs.voidps.engine.entity.item.Item -import world.gregs.voidps.engine.entity.obj.GameObject -import world.gregs.voidps.engine.inv.inventory -import world.gregs.voidps.network.client.instruction.InteractDialogue -import world.gregs.voidps.network.client.instruction.InteractInterfaceObject - -class CookingBot(val tasks: TaskManager) : Script { - - init { - worldSpawn { - for (area in Areas.tagged("cooking")) { - val spaces: Int = area["spaces", 1] - val type: String = area.getOrNull("type") ?: "" - val task = Task( - name = "cook on ${type.plural(2)} at ${area.name}".toLowerSpaceCase(), - block = { - val gear = bot.getGear(Skill.Cooking) ?: return@Task - val item = bot.getSuitableItem(gear.inventory.first()) - while (levels.getMax(Skill.Cooking) < gear.levels.last + 1) { - bot.cook(area, item, gear) - } - }, - area = area.area, - spaces = spaces, - requirements = listOf { bot.hasExactGear(Skill.Cooking) }, - ) - tasks.register(task) - } - } - - timerStop("cooking") { - if (isBot) { - bot.resume("cooking") - } - } - } - - suspend fun Bot.cook(map: AreaDefinition, rawItem: Item, set: GearDefinition) { - setupGear(set, buy = false) - goToArea(map) - if (player.inventory.contains(rawItem.id)) { - val range = getObject { isRange(map, it) } - if (range == null) { - await("tick") - return - } - // Use item on range - player.instructions.send(InteractInterfaceObject(range.def.id, range.tile.x, range.tile.y, 149, 0, rawItem.def.id, player.inventory.indexOf(rawItem.id))) - await("tick") - await("tick") - if (rawItem.id == "raw_beef") { - player.instructions.send(InteractDialogue(228, 3, -1)) - await("tick") - } - // Select all - clickInterface(916, 8, 0) - await("tick") - // First option - player.instructions.send(InteractDialogue(905, 14, -1)) - } - var count = 0 - while (player.inventory.contains(rawItem.id)) { - await("cooking") - if (count++ > 28) { - break - } - } - } - - fun isRange(map: AreaDefinition, obj: GameObject): Boolean { - if (!map.area.contains(obj.tile)) { - return false - } - return obj.id.startsWith("cooking_range") - } -} diff --git a/game/src/main/kotlin/content/bot/skill/firemaking/FiremakingBot.kt b/game/src/main/kotlin/content/bot/skill/firemaking/FiremakingBot.kt deleted file mode 100644 index d8a665d044..0000000000 --- a/game/src/main/kotlin/content/bot/skill/firemaking/FiremakingBot.kt +++ /dev/null @@ -1,83 +0,0 @@ -package content.bot.skill.firemaking - -import content.bot.* -import content.bot.interact.navigation.await -import content.bot.interact.navigation.goToArea -import content.bot.interact.navigation.resume -import content.bot.skill.combat.getGear -import content.bot.skill.combat.getSuitableItem -import content.bot.skill.combat.hasExactGear -import content.bot.skill.combat.setupGear -import net.pearx.kasechange.toLowerSpaceCase -import world.gregs.voidps.engine.Script -import world.gregs.voidps.engine.data.definition.AreaDefinition -import world.gregs.voidps.engine.data.definition.Areas -import world.gregs.voidps.engine.entity.character.player.skill.Skill -import world.gregs.voidps.engine.entity.item.Item -import world.gregs.voidps.engine.entity.obj.GameObjects -import world.gregs.voidps.engine.entity.obj.ObjectLayer -import world.gregs.voidps.engine.inv.inventory -import world.gregs.voidps.network.client.instruction.InteractInterfaceItem - -class FiremakingBot( - val tasks: TaskManager, -) : Script { - - init { - worldSpawn { - for (area in Areas.tagged("fire_making")) { - val spaces: Int = area["spaces", 1] - val task = Task( - name = "make fires at ${area.name}".toLowerSpaceCase(), - block = { - val gear = bot.getGear(Skill.Firemaking) ?: return@Task - val lighter = bot.getSuitableItem(gear.inventory.first()) - val logs = bot.getSuitableItem(gear.inventory.last()) - while (levels.getMax(Skill.Firemaking) < gear.levels.last + 1) { - bot.light(area, lighter, logs) - } - }, - area = area.area, - spaces = spaces, - requirements = listOf { bot.hasExactGear(Skill.Firemaking) }, - ) - tasks.register(task) - } - } - - timerStop("firemaking") { - if (isBot) { - bot.resume("firemaking") - } - } - } - - suspend fun Bot.light(map: AreaDefinition, lighter: Item, logs: Item) { - setupGear(Skill.Firemaking, buy = false) - goToArea(map) - val lighterIndex = player.inventory.indexOf(lighter.id) - while (player.inventory.contains(logs.id)) { - if (GameObjects.getLayer(player.tile, ObjectLayer.GROUND) != null) { - val spot = player.tile - .toCuboid(1) - .firstOrNull { GameObjects.getLayer(it, ObjectLayer.GROUND) == null } - if (spot == null) { - await("tick") - if (player.inventory.spaces < 4) { - break - } - continue - } - player.queue.clearWeak() - player.walkTo(spot) - await("tick") - } - val logIndex = player.inventory.indexOf(logs.id) - if (logIndex == -1) { - break - } - player.instructions.send(InteractInterfaceItem(lighter.def.id, logs.def.id, lighterIndex, logIndex, 149, 0, 149, 0)) - await("firemaking") - } - } -} diff --git a/game/src/main/kotlin/content/bot/skill/fishing/FishingBot.kt b/game/src/main/kotlin/content/bot/skill/fishing/FishingBot.kt deleted file mode 100644 index 4b23c51b55..0000000000 --- a/game/src/main/kotlin/content/bot/skill/fishing/FishingBot.kt +++ /dev/null @@ -1,106 +0,0 @@ -package content.bot.skill.fishing - -import content.bot.* -import content.bot.interact.navigation.await -import content.bot.interact.navigation.goToArea -import content.bot.interact.navigation.resume -import content.bot.skill.combat.hasExactGear -import content.bot.skill.combat.setupGear -import content.entity.death.weightedSample -import net.pearx.kasechange.toLowerSpaceCase -import world.gregs.voidps.engine.Script -import world.gregs.voidps.engine.client.ui.chat.plural -import world.gregs.voidps.engine.client.update.view.Viewport -import world.gregs.voidps.engine.data.config.GearDefinition -import world.gregs.voidps.engine.data.definition.AreaDefinition -import world.gregs.voidps.engine.data.definition.Areas -import world.gregs.voidps.engine.data.definition.GearDefinitions -import world.gregs.voidps.engine.data.definition.ItemDefinitions -import world.gregs.voidps.engine.data.definition.data.Catch -import world.gregs.voidps.engine.data.definition.data.Spot -import world.gregs.voidps.engine.entity.character.npc.NPC -import world.gregs.voidps.engine.entity.character.npc.NPCs -import world.gregs.voidps.engine.entity.character.player.skill.Skill -import world.gregs.voidps.engine.entity.character.player.skill.level.Level.has -import world.gregs.voidps.engine.entity.distanceTo -import world.gregs.voidps.engine.inv.carriesItem -import world.gregs.voidps.engine.inv.inventory -import world.gregs.voidps.network.client.instruction.InteractNPC - -class FishingBot( - val tasks: TaskManager, - val gear: GearDefinitions, -) : Script { - - init { - worldSpawn { - for (area in Areas.tagged("fish")) { - val spaces: Int = area["spaces", 1] - val type: String = area.getOrNull("type") ?: continue - val sets = gear.get("fishing").filter { it["spot", ""] == type } - for (set in sets) { - val option = set["action", ""] - val bait = set.inventory.firstOrNull { it.first().amount > 1 }?.first()?.id ?: "none" - val task = Task( - name = "fish ${type.plural(2)} at ${area.name}".toLowerSpaceCase(), - block = { - while (levels.getMax(Skill.Fishing) < set.levels.last + 1) { - bot.fish(area, option, bait, set) - } - }, - area = area.area, - spaces = spaces, - requirements = listOf( - { levels.getMax(Skill.Fishing) in set.levels }, - { bot.hasExactGear(set) || bot.hasCoins(2000) }, - ), - ) - tasks.register(task) - } - } - } - - timerStop("fishing") { - if (isBot) { - bot.resume("fishing") - } - } - } - - suspend fun Bot.fish(map: AreaDefinition, option: String, bait: String, set: GearDefinition) { - setupGear(set) - goToArea(map) - while (player.inventory.spaces > 0 && (bait == "none" || player.carriesItem(bait))) { - val spots = NPCs - .filter { isAvailableSpot(map, it, option, bait) } - .map { it to tile.distanceTo(it) } - val spot = weightedSample(spots, invert = true) - if (spot == null) { - await("tick") - if (player.inventory.spaces < 4) { - break - } - continue - } - player.instructions.send(InteractNPC(spot.index, spot.def.options.indexOf(option) + 1)) - await("fishing") - } - } - - fun Bot.isAvailableSpot(map: AreaDefinition, npc: NPC, option: String, bait: String): Boolean { - if (!npc.tile.within(player.tile, Viewport.VIEW_RADIUS)) { - return false - } - if (!map.area.contains(npc.tile)) { - return false - } - if (!npc.def.options.contains(option)) { - return false - } - val spot: Spot = npc.def.getOrNull("fishing_${option.lowercase()}") ?: return false - val level = spot.bait[bait] - ?.minOf { ItemDefinitions.get(it)["fishing", Catch.EMPTY].level } - ?: return false - return player.has(Skill.Fishing, level, false) - } -} diff --git a/game/src/main/kotlin/content/bot/skill/mining/MiningBot.kt b/game/src/main/kotlin/content/bot/skill/mining/MiningBot.kt deleted file mode 100644 index 91548c53eb..0000000000 --- a/game/src/main/kotlin/content/bot/skill/mining/MiningBot.kt +++ /dev/null @@ -1,141 +0,0 @@ -package content.bot.skill.mining - -import content.bot.* -import content.bot.interact.navigation.await -import content.bot.interact.navigation.goToArea -import content.bot.interact.navigation.resume -import content.bot.skill.combat.hasExactGear -import content.bot.skill.combat.setupGear -import content.entity.death.weightedSample -import net.pearx.kasechange.toLowerSpaceCase -import world.gregs.voidps.engine.Script -import world.gregs.voidps.engine.client.instruction.handle.interactObject -import world.gregs.voidps.engine.client.ui.chat.plural -import world.gregs.voidps.engine.client.ui.chat.toIntRange -import world.gregs.voidps.engine.data.definition.AreaDefinition -import world.gregs.voidps.engine.data.definition.Areas -import world.gregs.voidps.engine.data.definition.data.Rock -import world.gregs.voidps.engine.entity.character.player.skill.Skill -import world.gregs.voidps.engine.entity.character.player.skill.level.Level.has -import world.gregs.voidps.engine.entity.distanceTo -import world.gregs.voidps.engine.entity.obj.GameObject -import world.gregs.voidps.engine.inv.inventory -import world.gregs.voidps.engine.suspend.Suspension -import kotlin.coroutines.Continuation -import kotlin.coroutines.resume -import kotlin.coroutines.suspendCoroutine - -class MiningBot(val tasks: TaskManager) : Script { - - init { - worldSpawn { - for (area in Areas.tagged("mine")) { - val spaces: Int = area["spaces", 1] - val type = area["rocks", emptyList()].firstOrNull() ?: continue - val range: IntRange = area["levels", "1-5"].toIntRange() - val task = Task( - name = "mine ${type.plural(2)} at ${area.name}".toLowerSpaceCase(), - block = { - while (levels.getMax(Skill.Mining) < range.last + 1) { - bot.mineRocks(area, type) - } - }, - area = area.area, - spaces = spaces, - requirements = listOf( - { levels.getMax(Skill.Mining) in range }, - { bot.hasExactGear(Skill.Woodcutting) || bot.hasCoins(1000) }, - ), - ) - tasks.register(task) - } - } - - timerStop("mining") { - if (isBot) { - bot.resume("mining") - } - } - } - - interface State - - object Idle : State - object Running : State - - sealed class MiningState : State { - object MissingItem : MiningState() - object Depleted : MiningState() - object Level : MiningState() - object Success : MiningState() - object InvFull : MiningState() - } - - suspend fun wait(): T { - return suspendCoroutine { - suspensions[0] = it - } as T - } - - var Bot.state: State - get() = Idle - set(value) {} - - val states = arrayOfNulls(100) - val suspensions = arrayOfNulls>(100) - - fun tick() { - for (i in states.indices) { - when (val state = states[i]) { - null, Idle, Running -> continue - else -> { - val suspension = suspensions[i] - suspensions[i] = null - suspension?.resume(state) - } - } - } - - } - - suspend fun Bot.mineRocks(map: AreaDefinition, type: String) { - setupGear(Skill.Mining) - goToArea(map) - while (player.inventory.spaces > 0) { - val rocks = getObjects { isAvailableRock(map, it, type) } - .map { rock -> rock to tile.distanceTo(rock) } - val rock = weightedSample(rocks, invert = true) - if (rock == null) { - await("tick") - if (player.inventory.spaces < 4) { - break - } - continue - } - state = Running - player.interactObject(rock, "Mine") - await("mining") - when (wait()) { - MiningState.Depleted -> continue - MiningState.MissingItem -> setupGear(Skill.Mining) - MiningState.Level -> break // TODO cancel task - MiningState.Success -> continue - MiningState.InvFull -> mineRocks(map, type) - } - } - } - - fun Bot.isAvailableRock(map: AreaDefinition, obj: GameObject, type: String): Boolean { - if (!map.area.contains(obj.tile)) { - return false - } - if (!obj.def.containsOption("Mine")) { - return false - } - if (!obj.id.contains(type)) { - return false - } - val rock: Rock = obj.def.getOrNull("mining") ?: return false - return player.has(Skill.Mining, rock.level, false) - } -} diff --git a/game/src/main/kotlin/content/bot/skill/runecrafting/RunecraftingBot.kt b/game/src/main/kotlin/content/bot/skill/runecrafting/RunecraftingBot.kt deleted file mode 100644 index 2b786a190a..0000000000 --- a/game/src/main/kotlin/content/bot/skill/runecrafting/RunecraftingBot.kt +++ /dev/null @@ -1,60 +0,0 @@ -package content.bot.skill.runecrafting - -import content.bot.* -import content.bot.interact.navigation.await -import content.bot.interact.navigation.awaitInteract -import content.bot.interact.navigation.goToArea -import content.bot.skill.combat.hasExactGear -import content.bot.skill.combat.setupGear -import world.gregs.voidps.engine.Script -import world.gregs.voidps.engine.client.ui.chat.toIntRange -import world.gregs.voidps.engine.data.definition.AreaDefinition -import world.gregs.voidps.engine.data.definition.Areas -import world.gregs.voidps.engine.entity.character.player.skill.Skill -import world.gregs.voidps.engine.entity.obj.GameObject -import world.gregs.voidps.network.client.instruction.InteractObject - -class RunecraftingBot(val tasks: TaskManager) : Script { - - init { - worldSpawn { - for (area in Areas.tagged("altar")) { - val type: String = area["type"] - val spaces: Int = area["spaces", 1] - val range: IntRange = area["levels", "1-5"].toIntRange() - val task = Task( - name = "craft $type runes at ${area.name}", - block = { - while (levels.getMax(Skill.Runecrafting) < range.last + 1) { - bot.craftRunes(area) - } - }, - area = area.area, - spaces = spaces, - requirements = listOf( - { levels.getMax(Skill.Runecrafting) in range }, - { bot.hasExactGear(Skill.Runecrafting) }, - ), - ) - tasks.register(task) - } - } - } - - suspend fun Bot.craftRunes(map: AreaDefinition) { - setupGear(Skill.Runecrafting) - goToArea(map) - await("tick") - val altar = getObjects { isAltar(map, it) } - .first() - player.instructions.send(InteractObject(altar.def.id, altar.tile.x, altar.tile.y, 1)) - awaitInteract() - } - - fun isAltar(map: AreaDefinition, obj: GameObject): Boolean { - if (!map.area.contains(obj.tile)) { - return false - } - return obj.def.containsOption("Craft-rune") - } -} diff --git a/game/src/main/kotlin/content/bot/skill/smithing/SmeltingBot.kt b/game/src/main/kotlin/content/bot/skill/smithing/SmeltingBot.kt deleted file mode 100644 index c849b78d5a..0000000000 --- a/game/src/main/kotlin/content/bot/skill/smithing/SmeltingBot.kt +++ /dev/null @@ -1,95 +0,0 @@ -package content.bot.skill.smithing - -import content.bot.* -import content.bot.interact.navigation.await -import content.bot.interact.navigation.goToArea -import content.bot.interact.navigation.resume -import content.bot.skill.combat.getGear -import content.bot.skill.combat.hasExactGear -import content.bot.skill.combat.setupGear -import content.skill.smithing.oreToBar -import net.pearx.kasechange.toLowerSpaceCase -import world.gregs.voidps.engine.Script -import world.gregs.voidps.engine.data.config.GearDefinition -import world.gregs.voidps.engine.data.definition.AreaDefinition -import world.gregs.voidps.engine.data.definition.Areas -import world.gregs.voidps.engine.data.definition.ItemDefinitions -import world.gregs.voidps.engine.entity.character.mode.interact.Interact -import world.gregs.voidps.engine.entity.character.player.skill.Skill -import world.gregs.voidps.engine.entity.obj.GameObject -import world.gregs.voidps.engine.inv.inventory -import world.gregs.voidps.network.client.instruction.InteractDialogue - -class SmeltingBot(val tasks: TaskManager) : Script { - - init { - worldSpawn { - for (area in Areas.tagged("smelting")) { - val spaces: Int = area["spaces", 1] - val task = Task( - name = "smelt bars at ${area.name}".toLowerSpaceCase(), - block = { - val gear = bot.getGear("smelting", Skill.Smithing) ?: return@Task - while (levels.getMax(Skill.Smithing) < gear.levels.last + 1) { - bot.smelt(area, gear) - } - }, - area = area.area, - spaces = spaces, - requirements = listOf { bot.hasExactGear("smelting", Skill.Smithing) }, - ) - tasks.register(task) - } - } - - timerStop("smelting") { - if (isBot) { - bot.resume("smelting") - } - } - } - - suspend fun Bot.smelt(map: AreaDefinition, set: GearDefinition) { - setupGear(set, buy = false) - goToArea(map) - val furnace = getObject { isFurnace(map, it) } - if (furnace == null) { - await("tick") - return - } - val ore = player.inventory.items.first { it.id.endsWith("_ore") } - var bar = oreToBar(ore.id) - if (bar == "iron_bar" && player.inventory.contains("coal")) { - bar = "steel_bar" - } - val barId = ItemDefinitions.get(bar).id - await("tick") - while (player.inventory.contains(ore.id)) { - itemOnObject(ore, furnace) - await("tick") - while (player.mode is Interact) { - await("tick") - } - // Select All - clickInterface(916, 8) - // Make All - var index = 0 - for (i in 0 until 10) { - val id: Int = player["skill_creation_item_$i"] ?: continue - if (id == barId) { - index = i - break - } - } - player.instructions.send(InteractDialogue(905, 14 + index, -1)) - await("smelting") - } - } - - fun isFurnace(map: AreaDefinition, obj: GameObject): Boolean { - if (!map.area.contains(obj.tile)) { - return false - } - return obj.id.startsWith("furnace") - } -} diff --git a/game/src/main/kotlin/content/bot/skill/smithing/SmithingBot.kt b/game/src/main/kotlin/content/bot/skill/smithing/SmithingBot.kt deleted file mode 100644 index 6a65ba5426..0000000000 --- a/game/src/main/kotlin/content/bot/skill/smithing/SmithingBot.kt +++ /dev/null @@ -1,92 +0,0 @@ -package content.bot.skill.smithing - -import content.bot.* -import content.bot.interact.navigation.await -import content.bot.interact.navigation.goToArea -import content.bot.interact.navigation.resume -import content.bot.skill.combat.getGear -import content.bot.skill.combat.hasExactGear -import content.bot.skill.combat.setupGear -import net.pearx.kasechange.toLowerSpaceCase -import world.gregs.voidps.cache.definition.data.InterfaceDefinition -import world.gregs.voidps.engine.Script -import world.gregs.voidps.engine.data.config.GearDefinition -import world.gregs.voidps.engine.data.definition.AreaDefinition -import world.gregs.voidps.engine.data.definition.Areas -import world.gregs.voidps.engine.data.definition.InterfaceDefinitions -import world.gregs.voidps.engine.data.definition.ItemDefinitions -import world.gregs.voidps.engine.data.definition.data.Smithing -import world.gregs.voidps.engine.entity.character.mode.interact.Interact -import world.gregs.voidps.engine.entity.character.player.skill.Skill -import world.gregs.voidps.engine.entity.character.player.skill.level.Level.has -import world.gregs.voidps.engine.entity.obj.GameObject -import world.gregs.voidps.engine.inv.inventory - -class SmithingBot( - val interfaceDefinitions: InterfaceDefinitions, - val tasks: TaskManager, -) : Script { - - init { - worldSpawn { - for (area in Areas.tagged("smithing")) { - val spaces: Int = area["spaces", 1] - val task = Task( - name = "smith on anvil at ${area.name}".toLowerSpaceCase(), - block = { - val gear = bot.getGear(Skill.Smithing) ?: return@Task - val types: List = gear.getOrNull("types") ?: return@Task - while (levels.getMax(Skill.Smithing) < gear.levels.last + 1) { - bot.smith(area, types, gear) - } - }, - area = area.area, - spaces = spaces, - requirements = listOf { bot.hasExactGear(Skill.Smithing) }, - ) - tasks.register(task) - } - } - - timerStop("smithing") { - if (isBot) { - bot.resume("smithing") - } - } - } - - suspend fun Bot.smith(map: AreaDefinition, types: List, set: GearDefinition) { - setupGear(set, buy = false) - goToArea(map) - val anvil = getObject { isAnvil(map, it) } - if (anvil == null) { - await("tick") - return - } - val bar = player.inventory.items.first { it.id.endsWith("_bar") } - val type = types.filter { player.has(Skill.Smithing, ItemDefinitions.get(bar.id.replace("_bar", "_$it")).getOrNull("smithing")?.level ?: Int.MAX_VALUE) }.random() - await("tick") - while (player.inventory.contains(bar.id)) { - itemOnObject(bar, anvil) - await("tick") - while (player.mode is Interact) { - await("tick") - } - // Make All - item - val component = interfaceDefinitions.getComponent("smithing", "${type}_all")!! - val bars = component.getOrNull("bars") ?: 1 - if (!player.inventory.contains(bar.id, bars)) { - break - } - clickInterface(300, InterfaceDefinition.componentId(component.id)) - await("smithing") - } - } - - fun isAnvil(map: AreaDefinition, obj: GameObject): Boolean { - if (!map.area.contains(obj.tile)) { - return false - } - return obj.id.startsWith("anvil") - } -} diff --git a/game/src/main/kotlin/content/bot/skill/woodcutting/WoodcuttingBot.kt b/game/src/main/kotlin/content/bot/skill/woodcutting/WoodcuttingBot.kt deleted file mode 100644 index 2227c3ce19..0000000000 --- a/game/src/main/kotlin/content/bot/skill/woodcutting/WoodcuttingBot.kt +++ /dev/null @@ -1,88 +0,0 @@ -package content.bot.skill.woodcutting - -import content.bot.* -import content.bot.interact.navigation.await -import content.bot.interact.navigation.goToArea -import content.bot.interact.navigation.resume -import content.bot.skill.combat.hasExactGear -import content.bot.skill.combat.setupGear -import content.entity.death.weightedSample -import world.gregs.voidps.engine.Script -import world.gregs.voidps.engine.client.ui.chat.plural -import world.gregs.voidps.engine.client.ui.chat.toIntRange -import world.gregs.voidps.engine.data.definition.AreaDefinition -import world.gregs.voidps.engine.data.definition.Areas -import world.gregs.voidps.engine.data.definition.data.Tree -import world.gregs.voidps.engine.entity.character.player.skill.Skill -import world.gregs.voidps.engine.entity.character.player.skill.level.Level.has -import world.gregs.voidps.engine.entity.distanceTo -import world.gregs.voidps.engine.entity.obj.GameObject -import world.gregs.voidps.engine.inv.inventory -import world.gregs.voidps.network.client.instruction.InteractObject - -class WoodcuttingBot(val tasks: TaskManager) : Script { - - init { - worldSpawn { - for (area in Areas.tagged("trees")) { - val spaces: Int = area["spaces", 1] - val range: IntRange = area["levels", "1-5"].toIntRange() - val type = area["trees", emptyList()].firstOrNull() - val task = Task( - name = "cut ${(type ?: "tree").plural(2).lowercase()} at ${area.name}", - block = { - while (levels.getMax(Skill.Woodcutting) < range.last + 1) { - bot.cutTrees(area, type) - } - }, - area = area.area, - spaces = spaces, - requirements = listOf( - { levels.getMax(Skill.Woodcutting) in range }, - { bot.hasExactGear(Skill.Woodcutting) || bot.hasCoins(1000) }, - ), - ) - tasks.register(task) - } - } - - timerStop("woodcutting") { - if (isBot) { - bot.resume("woodcutting") - } - } - } - - suspend fun Bot.cutTrees(map: AreaDefinition, type: String? = null) { - setupGear(Skill.Woodcutting) - goToArea(map) - while (player.inventory.spaces > 0) { - val trees = getObjects { isAvailableTree(map, it, type) } - .map { tree -> tree to tile.distanceTo(tree) } - val tree = weightedSample(trees, invert = true) - if (tree == null) { - await("tick") - if (player.inventory.spaces < 4) { - break - } - continue - } - player.instructions.send(InteractObject(tree.def.id, tree.tile.x, tree.tile.y, 1)) - await("woodcutting") - } - } - - fun Bot.isAvailableTree(map: AreaDefinition, obj: GameObject, type: String?): Boolean { - if (!map.area.contains(obj.tile)) { - return false - } - if (!obj.def.containsOption("Chop down")) { - return false - } - if (type != null && !obj.id.contains(type)) { - return false - } - val tree: Tree = obj.def.getOrNull("woodcutting") ?: return false - return player.has(Skill.Woodcutting, tree.level, false) - } -} diff --git a/game/src/main/kotlin/content/entity/player/command/PathFindingCommands.kt b/game/src/main/kotlin/content/entity/player/command/PathFindingCommands.kt index 999b0bd010..3948d84fa0 100644 --- a/game/src/main/kotlin/content/entity/player/command/PathFindingCommands.kt +++ b/game/src/main/kotlin/content/entity/player/command/PathFindingCommands.kt @@ -6,9 +6,7 @@ import content.bot.action.BehaviourFrame import content.bot.action.BotAction import content.bot.action.Resolver import content.bot.bot -import content.bot.interact.path.Dijkstra -import content.bot.interact.path.EdgeTraversal -import content.bot.interact.path.NodeTargetStrategy +import content.bot.interact.path.Graph import content.bot.isBot import content.entity.gfx.areaGfx import org.rsmod.game.pathfinder.PathFinder @@ -138,36 +136,15 @@ class PathFindingCommands(val patrols: PatrolDefinitions) : Script { } adminCommand("walk_to_bank") { - val east = Tile(3179, 3433).toCuboid(15, 14) - val west = Tile(3250, 3417).toCuboid(7, 8) - val dijkstra: Dijkstra = get() - val strategy = object : NodeTargetStrategy() { - override fun reached(node: Any): Boolean = if (node is Tile) east.contains(node) || west.contains(node) else false - } + val graph: Graph = get() + val output = mutableListOf() println( "Path took ${ measureNanoTime { - dijkstra.find(this, strategy, EdgeTraversal()) + graph.findNearest(this, output, "bank") } }ns", ) - /*action { FIXME - var first = true - while (waypoints.isNotEmpty()) { - val next = waypoints.poll() - suspendCoroutine { cont -> - val tile = if (first && !tile.within(next.end as Tile, 20)) { - next.start - } else { - next.end - } as Tile - first = false - scheduler.add { - walkTo(tile) - } - } - } - }*/ } timerTick("show_path") { diff --git a/game/src/main/kotlin/content/entity/player/command/ServerCommands.kt b/game/src/main/kotlin/content/entity/player/command/ServerCommands.kt index 6202b60782..fd683e9fa4 100644 --- a/game/src/main/kotlin/content/entity/player/command/ServerCommands.kt +++ b/game/src/main/kotlin/content/entity/player/command/ServerCommands.kt @@ -1,7 +1,6 @@ package content.entity.player.command import content.bot.BotManager -import content.bot.interact.navigation.graph.NavigationGraph import content.entity.obj.ObjectTeleports import content.entity.obj.ship.CharterShips import content.entity.player.modal.book.Books @@ -65,7 +64,7 @@ class ServerCommands(val accountLoader: PlayerAccountLoader) : Script { handler = ::update, ) val configs = setOf( - "books", "teleports", "music_tracks", "fairy_rings", "ships", "objects", "items", "nav_graph", "npcs", "areas", "emotes", "anims", "containers", "graphics", + "books", "teleports", "music_tracks", "fairy_rings", "ships", "objects", "items", "bots", "npcs", "areas", "emotes", "anims", "containers", "graphics", "item_on_item", "sounds", "quests", "midis", "variables", "music", "interfaces", "spells", "patrols", "prayers", "drops", "client_scripts", "settings", ) adminCommand( @@ -95,7 +94,6 @@ class ServerCommands(val accountLoader: PlayerAccountLoader) : Script { ItemDefinitions.load(files.list(Settings["definitions.items"])) loadItemSpawns(itemSpawns, files.list(Settings["spawns.items"])) } - "nav_graph", "ai_graph" -> get().load(files.find(Settings["map.navGraph"])) "npcs" -> { NPCDefinitions.load(files.list(Settings["definitions.npcs"])) loadNpcSpawns(files, reload = true) diff --git a/game/src/main/resources/game.properties b/game/src/main/resources/game.properties index 52ce62e6ca..807e360609 100644 --- a/game/src/main/resources/game.properties +++ b/game/src/main/resources/game.properties @@ -241,9 +241,6 @@ bots.count=10 # Frequency between spawning bots on startup bots.spawnSeconds=60 -# What tasks to give AI-controlled bots with no tasks (options: nothing, randomWalk) -bots.idle=randomWalk - # Use bot names instead of randomised bots.numberedNames=false @@ -380,9 +377,6 @@ map.teleports=teles.toml # Path to the music track location data map.music=music_tracks.toml -# Path to the navigation graph data -map.navGraph=nav_graph.toml - # Canoe station information map.canoes=canoe_stations.toml diff --git a/game/src/test/kotlin/content/bot/path/DijkstraTest.kt b/game/src/test/kotlin/content/bot/path/DijkstraTest.kt deleted file mode 100644 index 91843f252f..0000000000 --- a/game/src/test/kotlin/content/bot/path/DijkstraTest.kt +++ /dev/null @@ -1,189 +0,0 @@ -package content.bot.path - -import content.bot.interact.navigation.graph.Condition -import content.bot.interact.navigation.graph.Edge -import content.bot.interact.navigation.graph.NavigationGraph -import content.bot.interact.navigation.graph.waypoints -import content.bot.interact.path.Dijkstra -import content.bot.interact.path.DijkstraFrontier -import content.bot.interact.path.EdgeTraversal -import content.bot.interact.path.NodeTargetStrategy -import io.mockk.every -import io.mockk.mockk -import io.mockk.mockkStatic -import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet -import kotlinx.io.pool.DefaultPool -import kotlinx.io.pool.ObjectPool -import org.junit.jupiter.api.Assertions.* -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import world.gregs.voidps.engine.entity.character.player.Player -import world.gregs.voidps.type.Tile -import java.util.* -import kotlin.test.assertNotNull - -internal class DijkstraTest { - - private lateinit var graph: NavigationGraph - private lateinit var dij: Dijkstra - private lateinit var pool: ObjectPool - - @BeforeEach - fun setup() { - graph = NavigationGraph() - pool = object : DefaultPool(1) { - override fun produceInstance() = DijkstraFrontier(3) - } - mockkStatic("content.bot.interact.navigation.graph.EdgeKt") - dij = Dijkstra(graph, pool) - } - - /** - * P -- A -- B -- C -- D - */ - @Test - fun `Find path`() { - val player: Player = mockk() - val a = Tile(5, 10) - val b = Tile(15, 0) - val c = Tile(20, 0) - val d = Tile(25, 0) - - val e1 = Edge("", player, a, 0) - val e2 = Edge("", a, b, 0) - val e3 = Edge("", b, c, 0) - val e4 = Edge("", c, d, 0) - graph.add(player, ObjectOpenHashSet.of(e1)) - graph.add(a, ObjectOpenHashSet.of(e2)) - graph.add(b, ObjectOpenHashSet.of(e3)) - graph.add(c, ObjectOpenHashSet.of(e4)) - - val waypoints = LinkedList() - every { player.waypoints } returns waypoints - - val strategy: NodeTargetStrategy = object : NodeTargetStrategy() { - override fun reached(node: Any): Boolean = node == c - } - val traversal = EdgeTraversal() - // When - val result = dij.find(player, strategy, traversal) - // Then - assertNotNull(result) - assertEquals(c, result) - assertEquals(e1, waypoints.poll()) - assertEquals(e2, waypoints.poll()) - assertEquals(e3, waypoints.poll()) - assertTrue(waypoints.isEmpty()) - } - - /** -- A - * 10 / - * P -- - * 9 \ - * -- B - */ - @Test - fun `Find lowest cost path`() { - val player: Player = mockk() - val a = Tile(5, 10) - val b = Tile(15, 0) - - val edge = Edge("", player, b, 9) - graph.add(player, ObjectOpenHashSet.of(Edge("", player, a, 10), edge)) - - val waypoints = LinkedList() - every { player.waypoints } returns waypoints - - val strategy: NodeTargetStrategy = object : NodeTargetStrategy() { - override fun reached(node: Any): Boolean = node != player - } - val traversal = EdgeTraversal() - // When - val result = dij.find(player, strategy, traversal) - // Then - assertNotNull(result) - assertEquals(edge, waypoints.poll()) - assertTrue(waypoints.isEmpty()) - } - - /** B - * / \ - * P - A < C - * - */ - @Test - fun `Find directional twice visited node`() { - val player: Player = mockk() - val a = Tile(5, 10) - val b = Tile(15, 0) - val c = Tile(0, 0) - - val e1 = Edge("", player, a, 0) - val e2 = Edge("", a, b, 0) - val e3 = Edge("", b, c, 0) - val e4 = Edge("", c, a, 0) - graph.add(player, ObjectOpenHashSet.of(e1)) - graph.add(a, ObjectOpenHashSet.of(e2)) - graph.add(b, ObjectOpenHashSet.of(e3)) - graph.add(c, ObjectOpenHashSet.of(e4)) - - val waypoints = LinkedList() - every { player.waypoints } returns waypoints - - var first = true - val strategy: NodeTargetStrategy = object : NodeTargetStrategy() { - override fun reached(node: Any): Boolean = if (node == a) { - val answer = !first - first = false - answer - } else { - false - } - } - val traversal = EdgeTraversal() - // When - val result = dij.find(player, strategy, traversal) - // Then - assertNotNull(result) - assertEquals(e1, waypoints.poll()) - assertEquals(e2, waypoints.poll()) - assertEquals(e3, waypoints.poll()) - assertEquals(e4, waypoints.poll()) - assertTrue(waypoints.isEmpty()) - } - - /** - * P -/- A - */ - @Test - fun `Paths can be blocked`() { - val p: Player = mockk() - val a = Tile(5, 10) - - val edge = Edge( - "", - p, - a, - 9, - requirements = listOf( - object : Condition { - override fun has(player: Player): Boolean = player != p - }, - ), - ) - graph.add(p, ObjectOpenHashSet.of(edge)) - - val waypoints = LinkedList() - every { p.waypoints } returns waypoints - - val strategy: NodeTargetStrategy = object : NodeTargetStrategy() { - override fun reached(node: Any): Boolean = node == a - } - val traversal = EdgeTraversal() - // When - val result = dij.find(p, strategy, traversal) - // Then - assertNull(result) - assertTrue(waypoints.isEmpty()) - } -} diff --git a/tools/src/main/kotlin/world/gregs/voidps/tools/map/view/MapViewer.kt b/tools/src/main/kotlin/world/gregs/voidps/tools/map/view/MapViewer.kt index 76abfa81c3..6a94d47594 100644 --- a/tools/src/main/kotlin/world/gregs/voidps/tools/map/view/MapViewer.kt +++ b/tools/src/main/kotlin/world/gregs/voidps/tools/map/view/MapViewer.kt @@ -2,8 +2,6 @@ package world.gregs.voidps.tools.map.view import com.github.weisj.darklaf.LafManager import com.github.weisj.darklaf.LafManager.getPreferredThemeStyle -import content.bot.interact.navigation.graph.NavigationGraph -import content.bot.interact.path.Graph import world.gregs.voidps.cache.CacheDelegate import world.gregs.voidps.cache.definition.decoder.ObjectDecoder import world.gregs.voidps.engine.data.Settings From 8ecc11d1a958747dd22a7ee9311244b2caf7a58f Mon Sep 17 00:00:00 2001 From: GregHib Date: Sun, 8 Feb 2026 14:01:53 +0000 Subject: [PATCH 072/101] Fix --- .../content/entity/player/command/PathFindingCommands.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/game/src/main/kotlin/content/entity/player/command/PathFindingCommands.kt b/game/src/main/kotlin/content/entity/player/command/PathFindingCommands.kt index 3948d84fa0..c985307986 100644 --- a/game/src/main/kotlin/content/entity/player/command/PathFindingCommands.kt +++ b/game/src/main/kotlin/content/entity/player/command/PathFindingCommands.kt @@ -136,7 +136,8 @@ class PathFindingCommands(val patrols: PatrolDefinitions) : Script { } adminCommand("walk_to_bank") { - val graph: Graph = get() + val manager: BotManager = get() + val graph = manager.graph val output = mutableListOf() println( "Path took ${ From 2f52c6d769ddf4d5d9626debbf5ff27908ab1610 Mon Sep 17 00:00:00 2001 From: GregHib Date: Sun, 8 Feb 2026 14:07:02 +0000 Subject: [PATCH 073/101] Store tags by tile --- game/src/main/kotlin/GameModules.kt | 1 + .../main/kotlin/content/bot/interact/path/Graph.kt | 12 ++++++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/game/src/main/kotlin/GameModules.kt b/game/src/main/kotlin/GameModules.kt index e2f4df9bab..4c0ffc6009 100644 --- a/game/src/main/kotlin/GameModules.kt +++ b/game/src/main/kotlin/GameModules.kt @@ -12,6 +12,7 @@ import world.gregs.voidps.engine.client.instruction.InterfaceHandler import world.gregs.voidps.engine.data.ConfigFiles import world.gregs.voidps.engine.data.Settings import world.gregs.voidps.engine.data.Storage +import world.gregs.voidps.engine.data.definition.Areas import world.gregs.voidps.engine.data.file.FileStorage import world.gregs.voidps.engine.entity.item.floor.ItemSpawns import java.io.File diff --git a/game/src/main/kotlin/content/bot/interact/path/Graph.kt b/game/src/main/kotlin/content/bot/interact/path/Graph.kt index 1c9a23a2cb..2df105e846 100644 --- a/game/src/main/kotlin/content/bot/interact/path/Graph.kt +++ b/game/src/main/kotlin/content/bot/interact/path/Graph.kt @@ -25,6 +25,7 @@ class Graph( val actions: Array?> = emptyArray(), val adjacentEdges: Array = emptyArray(), val tiles: IntArray = intArrayOf(), + val tags: Array?> = emptyArray(), val shortcuts: Map = emptyMap(), var nodeCount: Int = 0, ) { @@ -41,8 +42,7 @@ class Graph( fun findNearest(player: Player, output: MutableList, tag: String): Boolean { val start = startingPoints(player) return find(player, output, start, target = { - val tile = Tile(tiles[it]) - Areas.tagged(tag).any { a -> tile in a.area } // TODO store tags and/or areas per node + tags[it]?.contains(tag) ?: false }) } @@ -145,6 +145,7 @@ class Graph( // Nodes val tiles = LinkedHashSet() val nodes = mutableSetOf() + val tags = mutableListOf?>() // Edges val endNodes = mutableListOf() @@ -188,6 +189,12 @@ class Graph( fun add(tile: Tile): Int { if (tiles.add(tile)) { + val tags = Areas.get(tile.zone).filter { it.area.contains(tile) }.flatMap { it.tags } + if (tags.isNotEmpty()) { + this.tags.add(tags.toSet()) + } else { + this.tags.add(null) + } return tiles.size - 1 } return tiles.indexOf(tile) @@ -213,6 +220,7 @@ class Graph( adjacentEdges = Array(nodes.size) { edges[it]?.toIntArray() }, nodeCount = nodes.size, tiles = tiles.map { it.id }.toIntArray(), + tags = tags.toTypedArray(), shortcuts = shortcuts, ) From 314ebe30f54567db210b8de5824f2872c947029a Mon Sep 17 00:00:00 2001 From: GregHib Date: Sun, 8 Feb 2026 17:35:11 +0000 Subject: [PATCH 074/101] Sort classes --- game/src/main/kotlin/content/bot/Bot.kt | 9 ++--- .../src/main/kotlin/content/bot/BotManager.kt | 16 ++++++-- .../src/main/kotlin/content/bot/BotUpdates.kt | 7 ++++ .../kotlin/content/bot/action/ActionParser.kt | 2 +- .../kotlin/content/bot/action/BotAction.kt | 9 ++++- .../bot/{action => behaviour}/Behaviour.kt | 17 +++++--- .../{action => behaviour}/BehaviourFrame.kt | 3 +- .../{action => behaviour}/BehaviourState.kt | 2 +- .../bot/{action => behaviour}/Reason.kt | 4 +- .../{ => behaviour/activity}/ActivitySlots.kt | 4 +- .../activity}/BotActivity.kt | 15 +++---- .../path => behaviour/navigation}/Graph.kt | 39 +++++++++++++------ .../navigation}/NavigationShortcut.kt | 6 ++- .../bot/{fact => behaviour/setup}/Deficit.kt | 6 ++- .../{action => behaviour/setup}/Resolver.kt | 6 ++- .../navigation/graph/NavigationGraph.kt | 19 --------- .../content/bot/{fact => req}/Requirement.kt | 8 +++- .../bot/{fact => req}/RequirementEvaluator.kt | 11 ++++-- .../kotlin/content/bot/{ => req}/fact/Fact.kt | 6 +-- .../content/bot/{ => req}/fact/FactParser.kt | 4 +- .../content/bot/{ => req}/fact/ItemView.kt | 2 +- .../bot/{fact => req/predicate}/Predicate.kt | 4 +- .../main/kotlin/content/bot/skill/SkillBot.kt | 15 ------- .../content/entity/obj/ObjectTeleports.kt | 2 +- .../player/command/PathFindingCommands.kt | 5 +-- .../fairy_ring/FairyRingCodes.kt | 2 +- .../kotlin/content/bot/ActivitySlotsTest.kt | 3 +- .../test/kotlin/content/bot/BotManagerTest.kt | 12 ++++-- .../content/bot/interact/path/GraphTest.kt | 9 +++-- .../voidps/tools/map/view/draw/GraphDrawer.kt | 2 +- .../voidps/tools/map/view/draw/MapView.kt | 2 +- 31 files changed, 139 insertions(+), 112 deletions(-) rename game/src/main/kotlin/content/bot/{action => behaviour}/Behaviour.kt (95%) rename game/src/main/kotlin/content/bot/{action => behaviour}/BehaviourFrame.kt (93%) rename game/src/main/kotlin/content/bot/{action => behaviour}/BehaviourState.kt (90%) rename game/src/main/kotlin/content/bot/{action => behaviour}/Reason.kt (72%) rename game/src/main/kotlin/content/bot/{ => behaviour/activity}/ActivitySlots.kt (89%) rename game/src/main/kotlin/content/bot/{action => behaviour/activity}/BotActivity.kt (55%) rename game/src/main/kotlin/content/bot/{interact/path => behaviour/navigation}/Graph.kt (91%) rename game/src/main/kotlin/content/bot/{action => behaviour/navigation}/NavigationShortcut.kt (69%) rename game/src/main/kotlin/content/bot/{fact => behaviour/setup}/Deficit.kt (97%) rename game/src/main/kotlin/content/bot/{action => behaviour/setup}/Resolver.kt (79%) delete mode 100644 game/src/main/kotlin/content/bot/interact/navigation/graph/NavigationGraph.kt rename game/src/main/kotlin/content/bot/{fact => req}/Requirement.kt (81%) rename game/src/main/kotlin/content/bot/{fact => req}/RequirementEvaluator.kt (88%) rename game/src/main/kotlin/content/bot/{ => req}/fact/Fact.kt (97%) rename game/src/main/kotlin/content/bot/{ => req}/fact/FactParser.kt (98%) rename game/src/main/kotlin/content/bot/{ => req}/fact/ItemView.kt (94%) rename game/src/main/kotlin/content/bot/{fact => req/predicate}/Predicate.kt (98%) delete mode 100644 game/src/main/kotlin/content/bot/skill/SkillBot.kt diff --git a/game/src/main/kotlin/content/bot/Bot.kt b/game/src/main/kotlin/content/bot/Bot.kt index a57a144e4b..7103a4234a 100644 --- a/game/src/main/kotlin/content/bot/Bot.kt +++ b/game/src/main/kotlin/content/bot/Bot.kt @@ -1,13 +1,12 @@ package content.bot -import content.bot.action.BehaviourFrame -import content.bot.action.BehaviourState import content.bot.action.BotAction -import content.bot.action.BotActivity -import content.bot.action.Reason +import content.bot.behaviour.activity.BotActivity +import content.bot.behaviour.BehaviourFrame +import content.bot.behaviour.BehaviourState +import content.bot.behaviour.Reason import world.gregs.voidps.engine.entity.character.Character import world.gregs.voidps.engine.entity.character.player.Player -import world.gregs.voidps.network.client.Instruction import java.util.Stack data class Bot(val player: Player) : Character by player { diff --git a/game/src/main/kotlin/content/bot/BotManager.kt b/game/src/main/kotlin/content/bot/BotManager.kt index c72ee3407a..c67fdbebac 100644 --- a/game/src/main/kotlin/content/bot/BotManager.kt +++ b/game/src/main/kotlin/content/bot/BotManager.kt @@ -2,9 +2,19 @@ package content.bot import com.github.michaelbull.logging.InlineLogger import content.bot.action.* -import content.bot.fact.Requirement -import content.bot.interact.path.Graph -import content.bot.interact.path.Graph.Companion.loadGraph +import content.bot.behaviour.activity.ActivitySlots +import content.bot.behaviour.activity.BotActivity +import content.bot.behaviour.Behaviour +import content.bot.behaviour.BehaviourFrame +import content.bot.behaviour.BehaviourState +import content.bot.behaviour.HardReason +import content.bot.behaviour.Reason +import content.bot.behaviour.loadBehaviours +import content.bot.req.Requirement +import content.bot.behaviour.navigation.Graph +import content.bot.behaviour.navigation.Graph.Companion.loadGraph +import content.bot.behaviour.navigation.NavigationShortcut +import content.bot.behaviour.setup.Resolver import world.gregs.voidps.engine.data.ConfigFiles import world.gregs.voidps.engine.data.Settings import world.gregs.voidps.engine.event.AuditLog diff --git a/game/src/main/kotlin/content/bot/BotUpdates.kt b/game/src/main/kotlin/content/bot/BotUpdates.kt index d7f0776b9c..d000688ee8 100644 --- a/game/src/main/kotlin/content/bot/BotUpdates.kt +++ b/game/src/main/kotlin/content/bot/BotUpdates.kt @@ -1,6 +1,7 @@ package content.bot import world.gregs.voidps.engine.Script +import world.gregs.voidps.network.client.instruction.InteractDialogue /** * Listen for state changes which would change which activities are available to a bot @@ -36,5 +37,11 @@ class BotUpdates(val manager: BotManager) : Script { manager.update(bot, "enter:${it.name}") } } + + interfaceOpened("dialogue_level_up") { + if (isBot) { + instructions.trySend(InteractDialogue(interfaceId = 740, componentId = 3, option = -1)) + } + } } } \ No newline at end of file diff --git a/game/src/main/kotlin/content/bot/action/ActionParser.kt b/game/src/main/kotlin/content/bot/action/ActionParser.kt index f661854b6e..6a7266ac38 100644 --- a/game/src/main/kotlin/content/bot/action/ActionParser.kt +++ b/game/src/main/kotlin/content/bot/action/ActionParser.kt @@ -1,6 +1,6 @@ package content.bot.action -import content.bot.fact.Requirement +import content.bot.req.Requirement sealed class ActionParser { open val required = emptySet() diff --git a/game/src/main/kotlin/content/bot/action/BotAction.kt b/game/src/main/kotlin/content/bot/action/BotAction.kt index b37d209bf7..226e3ca054 100644 --- a/game/src/main/kotlin/content/bot/action/BotAction.kt +++ b/game/src/main/kotlin/content/bot/action/BotAction.kt @@ -1,9 +1,14 @@ package content.bot.action +import content.bot.behaviour.BehaviourFrame +import content.bot.behaviour.BehaviourState import content.bot.Bot import content.bot.BotManager -import content.bot.fact.Requirement -import content.bot.interact.path.Graph +import content.bot.behaviour.Reason +import content.bot.req.Requirement +import content.bot.behaviour.navigation.Graph +import content.bot.behaviour.navigation.NavigationShortcut +import content.bot.behaviour.setup.Resolver import content.entity.combat.attackers import content.entity.combat.dead import world.gregs.voidps.engine.GameLoop diff --git a/game/src/main/kotlin/content/bot/action/Behaviour.kt b/game/src/main/kotlin/content/bot/behaviour/Behaviour.kt similarity index 95% rename from game/src/main/kotlin/content/bot/action/Behaviour.kt rename to game/src/main/kotlin/content/bot/behaviour/Behaviour.kt index 44e8fd9c15..f05948853b 100644 --- a/game/src/main/kotlin/content/bot/action/Behaviour.kt +++ b/game/src/main/kotlin/content/bot/behaviour/Behaviour.kt @@ -1,6 +1,11 @@ -package content.bot.action +package content.bot.behaviour -import content.bot.fact.Requirement +import content.bot.action.ActionParser +import content.bot.action.BotAction +import content.bot.behaviour.activity.BotActivity +import content.bot.behaviour.navigation.NavigationShortcut +import content.bot.behaviour.setup.Resolver +import content.bot.req.Requirement import it.unimi.dsi.fastutil.objects.ObjectArrayList import world.gregs.config.Config import world.gregs.config.ConfigReader @@ -53,7 +58,7 @@ private fun loadActivities(activities: MutableMap, template capacity = capacity, requires = Requirement.parse(requires, debug), setup = Requirement.parse(setup, debug), - actions = ActionParser.parse(actions, debug), + actions = ActionParser.Companion.parse(actions, debug), produces = Requirement.parse(produces, debug, requirePredicates = false).toSet() ) } @@ -81,7 +86,7 @@ private fun loadSetups(resolvers: MutableMap>, tem weight = weight, requires = Requirement.parse(requires, debug), setup = Requirement.parse(setup, debug), - actions = ActionParser.parse(actions, debug), + actions = ActionParser.Companion.parse(actions, debug), produces = products.toSet() ) for (product in products) { @@ -114,7 +119,7 @@ private fun loadShortcuts(shortcuts: MutableList, templates: weight = weight, requires = Requirement.parse(requires, debug), setup = Requirement.parse(setup, debug), - actions = ActionParser.parse(actions, debug), + actions = ActionParser.Companion.parse(actions, debug), produces = Requirement.parse(produces, debug, requirePredicates = false).toSet() ) ) @@ -263,7 +268,7 @@ private data class Fragment( if (combinedList.isEmpty()) { return emptyList() } - return ActionParser.parse(combinedList, "$id template $template") + return ActionParser.Companion.parse(combinedList, "$id template $template") } @Suppress("UNCHECKED_CAST") diff --git a/game/src/main/kotlin/content/bot/action/BehaviourFrame.kt b/game/src/main/kotlin/content/bot/behaviour/BehaviourFrame.kt similarity index 93% rename from game/src/main/kotlin/content/bot/action/BehaviourFrame.kt rename to game/src/main/kotlin/content/bot/behaviour/BehaviourFrame.kt index 22fd4ae170..b984567c30 100644 --- a/game/src/main/kotlin/content/bot/action/BehaviourFrame.kt +++ b/game/src/main/kotlin/content/bot/behaviour/BehaviourFrame.kt @@ -1,6 +1,7 @@ -package content.bot.action +package content.bot.behaviour import content.bot.Bot +import content.bot.action.BotAction data class BehaviourFrame( val behaviour: Behaviour, diff --git a/game/src/main/kotlin/content/bot/action/BehaviourState.kt b/game/src/main/kotlin/content/bot/behaviour/BehaviourState.kt similarity index 90% rename from game/src/main/kotlin/content/bot/action/BehaviourState.kt rename to game/src/main/kotlin/content/bot/behaviour/BehaviourState.kt index ab82e399d6..a71dbd2545 100644 --- a/game/src/main/kotlin/content/bot/action/BehaviourState.kt +++ b/game/src/main/kotlin/content/bot/behaviour/BehaviourState.kt @@ -1,4 +1,4 @@ -package content.bot.action +package content.bot.behaviour sealed interface BehaviourState { object Pending : BehaviourState diff --git a/game/src/main/kotlin/content/bot/action/Reason.kt b/game/src/main/kotlin/content/bot/behaviour/Reason.kt similarity index 72% rename from game/src/main/kotlin/content/bot/action/Reason.kt rename to game/src/main/kotlin/content/bot/behaviour/Reason.kt index 04caba4855..057c90bad2 100644 --- a/game/src/main/kotlin/content/bot/action/Reason.kt +++ b/game/src/main/kotlin/content/bot/behaviour/Reason.kt @@ -1,4 +1,4 @@ -package content.bot.action +package content.bot.behaviour interface Reason { data class Invalid(val message: String) : HardReason @@ -7,7 +7,7 @@ interface Reason { object Timeout : HardReason object Stuck : SoftReason object NoTarget : SoftReason - data class Requirement(val fact: content.bot.fact.Requirement<*>) : HardReason + data class Requirement(val fact: content.bot.req.Requirement<*>) : HardReason } interface SoftReason : Reason interface HardReason : Reason diff --git a/game/src/main/kotlin/content/bot/ActivitySlots.kt b/game/src/main/kotlin/content/bot/behaviour/activity/ActivitySlots.kt similarity index 89% rename from game/src/main/kotlin/content/bot/ActivitySlots.kt rename to game/src/main/kotlin/content/bot/behaviour/activity/ActivitySlots.kt index 7cafa7ca67..0ae20b19ba 100644 --- a/game/src/main/kotlin/content/bot/ActivitySlots.kt +++ b/game/src/main/kotlin/content/bot/behaviour/activity/ActivitySlots.kt @@ -1,6 +1,4 @@ -package content.bot - -import content.bot.action.BotActivity +package content.bot.behaviour.activity class ActivitySlots { private val occupied = mutableMapOf() diff --git a/game/src/main/kotlin/content/bot/action/BotActivity.kt b/game/src/main/kotlin/content/bot/behaviour/activity/BotActivity.kt similarity index 55% rename from game/src/main/kotlin/content/bot/action/BotActivity.kt rename to game/src/main/kotlin/content/bot/behaviour/activity/BotActivity.kt index 7e18ebd3bf..e07e2e8b53 100644 --- a/game/src/main/kotlin/content/bot/action/BotActivity.kt +++ b/game/src/main/kotlin/content/bot/behaviour/activity/BotActivity.kt @@ -1,13 +1,8 @@ -package content.bot.action +package content.bot.behaviour.activity -import content.bot.fact.Requirement -import it.unimi.dsi.fastutil.objects.ObjectArrayList -import world.gregs.config.Config -import world.gregs.config.ConfigReader -import world.gregs.voidps.engine.data.ConfigFiles -import world.gregs.voidps.engine.data.Settings -import world.gregs.voidps.engine.timedLoad -import kotlin.collections.set +import content.bot.behaviour.Behaviour +import content.bot.action.BotAction +import content.bot.req.Requirement /** * An activity with a limited number of slots that bots can perform @@ -20,4 +15,4 @@ data class BotActivity( override val setup: List> = emptyList(), override val actions: List = emptyList(), override val produces: Set> = emptySet(), -) : Behaviour +) : Behaviour \ No newline at end of file diff --git a/game/src/main/kotlin/content/bot/interact/path/Graph.kt b/game/src/main/kotlin/content/bot/behaviour/navigation/Graph.kt similarity index 91% rename from game/src/main/kotlin/content/bot/interact/path/Graph.kt rename to game/src/main/kotlin/content/bot/behaviour/navigation/Graph.kt index 2df105e846..8492d4b890 100644 --- a/game/src/main/kotlin/content/bot/interact/path/Graph.kt +++ b/game/src/main/kotlin/content/bot/behaviour/navigation/Graph.kt @@ -1,16 +1,15 @@ -package content.bot.interact.path +package content.bot.behaviour.navigation import content.bot.action.ActionParser import content.bot.action.BotAction -import content.bot.action.NavigationShortcut -import content.bot.action.actions -import content.bot.action.requirements +import content.bot.behaviour.actions import content.bot.bot -import content.bot.fact.Predicate -import content.bot.fact.Requirement -import content.bot.interact.navigation.graph.readTile +import content.bot.req.predicate.Predicate +import content.bot.req.Requirement import content.bot.isBot +import content.bot.behaviour.requirements import world.gregs.config.Config +import world.gregs.config.ConfigReader import world.gregs.voidps.engine.data.definition.Areas import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.timedLoad @@ -158,7 +157,7 @@ class Graph( val shortcuts = mutableMapOf() init { - tiles.add(Tile.EMPTY) // Virtual + tiles.add(Tile.Companion.EMPTY) // Virtual nodes.add(0) } @@ -247,8 +246,8 @@ class Graph( val list = key() assert(list == "edges") { "Expected edges list, got: $list ${exception()}" } while (nextElement()) { - var from = Tile.EMPTY - var to = Tile.EMPTY + var from = Tile.Companion.EMPTY + var to = Tile.Companion.EMPTY var cost = 0 val actions: MutableList>> = mutableListOf() val requirements = mutableListOf>>>() @@ -268,8 +267,8 @@ class Graph( builder.addEdge(Tile(from.x, from.y, from.level), Tile(to.x, to.y, to.level), cost, listOf(BotAction.WalkTo(to.x, to.y)), null) builder.addEdge(Tile(to.x, to.y, to.level), Tile(from.x, from.y, from.level), cost, listOf(BotAction.WalkTo(from.x, from.y)), null) } - requirements.isEmpty() -> builder.addEdge(Tile(from.x, from.y, from.level), Tile(to.x, to.y, to.level), cost, ActionParser.parse(actions, exception()), null) - else -> builder.addEdge(Tile(from.x, from.y, from.level), Tile(to.x, to.y, to.level), cost, ActionParser.parse(actions, exception()), Requirement.parse(requirements, exception())) + requirements.isEmpty() -> builder.addEdge(Tile(from.x, from.y, from.level), Tile(to.x, to.y, to.level), cost, ActionParser.Companion.parse(actions, exception()), null) + else -> builder.addEdge(Tile(from.x, from.y, from.level), Tile(to.x, to.y, to.level), cost, ActionParser.Companion.parse(actions, exception()), Requirement.Companion.parse(requirements, exception())) } } } @@ -283,5 +282,21 @@ class Graph( // builder.print() return builder.build() } + + fun ConfigReader.readTile(): Tile { + var x = 0 + var y = 0 + var level = 0 + while (nextEntry()) { + when (val key = key()) { + "x" -> x = int() + "y" -> y = int() + "level" -> level = int() + else -> throw IllegalArgumentException("Unexpected key: '$key' ${exception()}") + } + } + return Tile(x, y, level) + } + } } \ No newline at end of file diff --git a/game/src/main/kotlin/content/bot/action/NavigationShortcut.kt b/game/src/main/kotlin/content/bot/behaviour/navigation/NavigationShortcut.kt similarity index 69% rename from game/src/main/kotlin/content/bot/action/NavigationShortcut.kt rename to game/src/main/kotlin/content/bot/behaviour/navigation/NavigationShortcut.kt index 1193ee0547..88f3f6bf6f 100644 --- a/game/src/main/kotlin/content/bot/action/NavigationShortcut.kt +++ b/game/src/main/kotlin/content/bot/behaviour/navigation/NavigationShortcut.kt @@ -1,6 +1,8 @@ -package content.bot.action +package content.bot.behaviour.navigation -import content.bot.fact.Requirement +import content.bot.behaviour.Behaviour +import content.bot.action.BotAction +import content.bot.req.Requirement data class NavigationShortcut( override val id: String, diff --git a/game/src/main/kotlin/content/bot/fact/Deficit.kt b/game/src/main/kotlin/content/bot/behaviour/setup/Deficit.kt similarity index 97% rename from game/src/main/kotlin/content/bot/fact/Deficit.kt rename to game/src/main/kotlin/content/bot/behaviour/setup/Deficit.kt index 2042507999..b389cf25bf 100644 --- a/game/src/main/kotlin/content/bot/fact/Deficit.kt +++ b/game/src/main/kotlin/content/bot/behaviour/setup/Deficit.kt @@ -1,7 +1,9 @@ -package content.bot.fact +package content.bot.behaviour.setup import content.bot.action.BotAction -import content.bot.action.Resolver +import content.bot.req.Requirement +import content.bot.req.fact.Fact +import content.bot.req.predicate.Predicate import content.entity.player.bank.bank import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.entity.item.Item diff --git a/game/src/main/kotlin/content/bot/action/Resolver.kt b/game/src/main/kotlin/content/bot/behaviour/setup/Resolver.kt similarity index 79% rename from game/src/main/kotlin/content/bot/action/Resolver.kt rename to game/src/main/kotlin/content/bot/behaviour/setup/Resolver.kt index 8aea05c4d1..dd5f6b675d 100644 --- a/game/src/main/kotlin/content/bot/action/Resolver.kt +++ b/game/src/main/kotlin/content/bot/behaviour/setup/Resolver.kt @@ -1,6 +1,8 @@ -package content.bot.action +package content.bot.behaviour.setup -import content.bot.fact.Requirement +import content.bot.behaviour.Behaviour +import content.bot.action.BotAction +import content.bot.req.Requirement /** * An activity that can be performed to resolve a requirement diff --git a/game/src/main/kotlin/content/bot/interact/navigation/graph/NavigationGraph.kt b/game/src/main/kotlin/content/bot/interact/navigation/graph/NavigationGraph.kt deleted file mode 100644 index bf8df95c24..0000000000 --- a/game/src/main/kotlin/content/bot/interact/navigation/graph/NavigationGraph.kt +++ /dev/null @@ -1,19 +0,0 @@ -package content.bot.interact.navigation.graph - -import world.gregs.config.ConfigReader -import world.gregs.voidps.type.Tile - -fun ConfigReader.readTile(): Tile { - var x = 0 - var y = 0 - var level = 0 - while (nextEntry()) { - when (val key = key()) { - "x" -> x = int() - "y" -> y = int() - "level" -> level = int() - else -> throw IllegalArgumentException("Unexpected key: '$key' ${exception()}") - } - } - return Tile(x, y, level) -} diff --git a/game/src/main/kotlin/content/bot/fact/Requirement.kt b/game/src/main/kotlin/content/bot/req/Requirement.kt similarity index 81% rename from game/src/main/kotlin/content/bot/fact/Requirement.kt rename to game/src/main/kotlin/content/bot/req/Requirement.kt index 31f986d552..af04da7a92 100644 --- a/game/src/main/kotlin/content/bot/fact/Requirement.kt +++ b/game/src/main/kotlin/content/bot/req/Requirement.kt @@ -1,5 +1,9 @@ -package content.bot.fact +package content.bot.req +import content.bot.behaviour.setup.Deficit +import content.bot.req.fact.Fact +import content.bot.req.fact.FactParser +import content.bot.req.predicate.Predicate import world.gregs.voidps.engine.entity.character.player.Player data class Requirement(val fact: Fact, val predicate: Predicate? = null) { @@ -16,7 +20,7 @@ data class Requirement(val fact: Fact, val predicate: Predicate? = null fun parse(list: List>>>, name: String, requirePredicates: Boolean = true): List> { val requirements = mutableListOf>() for ((type, value) in list) { - val parser = FactParser.parsers[type] ?: error("No fact parser for '$type' in ${name}.") + val parser = FactParser.Companion.parsers[type] ?: error("No fact parser for '$type' in ${name}.") for (map in value) { val error = parser.check(map) if (error != null) { diff --git a/game/src/main/kotlin/content/bot/fact/RequirementEvaluator.kt b/game/src/main/kotlin/content/bot/req/RequirementEvaluator.kt similarity index 88% rename from game/src/main/kotlin/content/bot/fact/RequirementEvaluator.kt rename to game/src/main/kotlin/content/bot/req/RequirementEvaluator.kt index 4c41c8d265..6e146a8c26 100644 --- a/game/src/main/kotlin/content/bot/fact/RequirementEvaluator.kt +++ b/game/src/main/kotlin/content/bot/req/RequirementEvaluator.kt @@ -1,10 +1,15 @@ -package content.bot.fact +package content.bot.req -import content.bot.fact.Deficit.MissingInventory -import content.bot.fact.Predicate.IntEquals +import content.bot.behaviour.setup.Deficit +import content.bot.behaviour.setup.Deficit.MissingInventory +import content.bot.req.fact.Fact +import content.bot.req.fact.ItemView +import content.bot.req.predicate.Predicate +import content.bot.req.predicate.Predicate.IntEquals import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.entity.item.Item import world.gregs.voidps.type.Tile +import kotlin.collections.plusAssign /** * Evaluates [Requirement]'s to produce known [Deficit]'s diff --git a/game/src/main/kotlin/content/bot/fact/Fact.kt b/game/src/main/kotlin/content/bot/req/fact/Fact.kt similarity index 97% rename from game/src/main/kotlin/content/bot/fact/Fact.kt rename to game/src/main/kotlin/content/bot/req/fact/Fact.kt index 2f1efda816..ccf143ac61 100644 --- a/game/src/main/kotlin/content/bot/fact/Fact.kt +++ b/game/src/main/kotlin/content/bot/req/fact/Fact.kt @@ -1,12 +1,12 @@ -package content.bot.fact +package content.bot.req.fact +import content.bot.req.predicate.Predicate import content.entity.player.bank.bank import world.gregs.voidps.engine.GameLoop import world.gregs.voidps.engine.client.variable.remaining import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.entity.character.player.combatLevel import world.gregs.voidps.engine.entity.character.player.skill.Skill -import world.gregs.voidps.engine.entity.obj.GameObject import world.gregs.voidps.engine.entity.obj.GameObjects import world.gregs.voidps.engine.inv.equipment import world.gregs.voidps.engine.inv.inventory @@ -14,7 +14,7 @@ import world.gregs.voidps.engine.timer.epochSeconds import world.gregs.voidps.type.Tile /** - * A bots state which can be a [Requirement] for, or a product of performing a [content.bot.action.Behaviour] + * A bots state which can be a [content.bot.req.Requirement] for, or a product of performing a [content.bot.behaviour.Behaviour] * @param priority Ensure bots aren't walking to locations before getting items etc... lower values are prioritised first. */ sealed class Fact(val priority: Int) { diff --git a/game/src/main/kotlin/content/bot/fact/FactParser.kt b/game/src/main/kotlin/content/bot/req/fact/FactParser.kt similarity index 98% rename from game/src/main/kotlin/content/bot/fact/FactParser.kt rename to game/src/main/kotlin/content/bot/req/fact/FactParser.kt index 77fbeb95cf..9ea924ce93 100644 --- a/game/src/main/kotlin/content/bot/fact/FactParser.kt +++ b/game/src/main/kotlin/content/bot/req/fact/FactParser.kt @@ -1,5 +1,7 @@ -package content.bot.fact +package content.bot.req.fact +import content.bot.req.Requirement +import content.bot.req.predicate.Predicate import world.gregs.voidps.type.Tile sealed class FactParser { diff --git a/game/src/main/kotlin/content/bot/fact/ItemView.kt b/game/src/main/kotlin/content/bot/req/fact/ItemView.kt similarity index 94% rename from game/src/main/kotlin/content/bot/fact/ItemView.kt rename to game/src/main/kotlin/content/bot/req/fact/ItemView.kt index c1c727bc26..8f3fe5368c 100644 --- a/game/src/main/kotlin/content/bot/fact/ItemView.kt +++ b/game/src/main/kotlin/content/bot/req/fact/ItemView.kt @@ -1,4 +1,4 @@ -package content.bot.fact +package content.bot.req.fact import world.gregs.voidps.engine.entity.item.Item import world.gregs.voidps.engine.inv.Inventory diff --git a/game/src/main/kotlin/content/bot/fact/Predicate.kt b/game/src/main/kotlin/content/bot/req/predicate/Predicate.kt similarity index 98% rename from game/src/main/kotlin/content/bot/fact/Predicate.kt rename to game/src/main/kotlin/content/bot/req/predicate/Predicate.kt index 4165ed928a..f30e368a47 100644 --- a/game/src/main/kotlin/content/bot/fact/Predicate.kt +++ b/game/src/main/kotlin/content/bot/req/predicate/Predicate.kt @@ -1,5 +1,7 @@ -package content.bot.fact +package content.bot.req.predicate +import content.bot.req.fact.ItemView +import content.bot.req.RequirementEvaluator import world.gregs.voidps.engine.data.definition.Areas import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.entity.character.player.skill.level.Level.hasRequirements diff --git a/game/src/main/kotlin/content/bot/skill/SkillBot.kt b/game/src/main/kotlin/content/bot/skill/SkillBot.kt deleted file mode 100644 index 9e33f1a55a..0000000000 --- a/game/src/main/kotlin/content/bot/skill/SkillBot.kt +++ /dev/null @@ -1,15 +0,0 @@ -package content.bot.skill - -import content.bot.isBot -import world.gregs.voidps.engine.Script -import world.gregs.voidps.network.client.instruction.InteractDialogue - -class SkillBot : Script { - init { - interfaceOpened("dialogue_level_up") { - if (isBot) { - instructions.trySend(InteractDialogue(interfaceId = 740, componentId = 3, option = -1)) - } - } - } -} diff --git a/game/src/main/kotlin/content/entity/obj/ObjectTeleports.kt b/game/src/main/kotlin/content/entity/obj/ObjectTeleports.kt index eebdab7326..4627ff7686 100644 --- a/game/src/main/kotlin/content/entity/obj/ObjectTeleports.kt +++ b/game/src/main/kotlin/content/entity/obj/ObjectTeleports.kt @@ -1,6 +1,6 @@ package content.entity.obj -import content.bot.interact.navigation.graph.readTile +import content.bot.behaviour.navigation.Graph.Companion.readTile import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap import world.gregs.config.Config diff --git a/game/src/main/kotlin/content/entity/player/command/PathFindingCommands.kt b/game/src/main/kotlin/content/entity/player/command/PathFindingCommands.kt index c985307986..f01df6227b 100644 --- a/game/src/main/kotlin/content/entity/player/command/PathFindingCommands.kt +++ b/game/src/main/kotlin/content/entity/player/command/PathFindingCommands.kt @@ -2,11 +2,10 @@ package content.entity.player.command import content.bot.Bot import content.bot.BotManager -import content.bot.action.BehaviourFrame +import content.bot.behaviour.BehaviourFrame import content.bot.action.BotAction -import content.bot.action.Resolver +import content.bot.behaviour.setup.Resolver import content.bot.bot -import content.bot.interact.path.Graph import content.bot.isBot import content.entity.gfx.areaGfx import org.rsmod.game.pathfinder.PathFinder diff --git a/game/src/main/kotlin/content/quest/member/fairy_tale_part_2/fairy_ring/FairyRingCodes.kt b/game/src/main/kotlin/content/quest/member/fairy_tale_part_2/fairy_ring/FairyRingCodes.kt index 2abf1a1f79..4f88157c2c 100644 --- a/game/src/main/kotlin/content/quest/member/fairy_tale_part_2/fairy_ring/FairyRingCodes.kt +++ b/game/src/main/kotlin/content/quest/member/fairy_tale_part_2/fairy_ring/FairyRingCodes.kt @@ -1,6 +1,6 @@ package content.quest.member.fairy_tale_part_2.fairy_ring -import content.bot.interact.navigation.graph.readTile +import content.bot.behaviour.navigation.Graph.Companion.readTile import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap import world.gregs.config.Config import world.gregs.voidps.engine.timedLoad diff --git a/game/src/test/kotlin/content/bot/ActivitySlotsTest.kt b/game/src/test/kotlin/content/bot/ActivitySlotsTest.kt index 83e778244b..a5f7639103 100644 --- a/game/src/test/kotlin/content/bot/ActivitySlotsTest.kt +++ b/game/src/test/kotlin/content/bot/ActivitySlotsTest.kt @@ -1,6 +1,7 @@ package content.bot -import content.bot.action.BotActivity +import content.bot.behaviour.activity.BotActivity +import content.bot.behaviour.activity.ActivitySlots import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test diff --git a/game/src/test/kotlin/content/bot/BotManagerTest.kt b/game/src/test/kotlin/content/bot/BotManagerTest.kt index 9c03bd0cab..c3ef8b6cda 100644 --- a/game/src/test/kotlin/content/bot/BotManagerTest.kt +++ b/game/src/test/kotlin/content/bot/BotManagerTest.kt @@ -1,9 +1,15 @@ package content.bot import content.bot.action.* -import content.bot.fact.Fact -import content.bot.fact.Predicate -import content.bot.fact.Requirement +import content.bot.behaviour.activity.BotActivity +import content.bot.behaviour.BehaviourFrame +import content.bot.behaviour.BehaviourState +import content.bot.behaviour.Reason +import content.bot.behaviour.SoftReason +import content.bot.req.fact.Fact +import content.bot.req.predicate.Predicate +import content.bot.req.Requirement +import content.bot.behaviour.setup.Resolver import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.Test import world.gregs.voidps.engine.entity.character.player.Player diff --git a/game/src/test/kotlin/content/bot/interact/path/GraphTest.kt b/game/src/test/kotlin/content/bot/interact/path/GraphTest.kt index 594e5df832..3afa58f977 100644 --- a/game/src/test/kotlin/content/bot/interact/path/GraphTest.kt +++ b/game/src/test/kotlin/content/bot/interact/path/GraphTest.kt @@ -1,9 +1,10 @@ package content.bot.interact.path -import content.bot.action.NavigationShortcut -import content.bot.fact.Fact -import content.bot.fact.Predicate -import content.bot.fact.Requirement +import content.bot.behaviour.navigation.NavigationShortcut +import content.bot.req.fact.Fact +import content.bot.req.predicate.Predicate +import content.bot.req.Requirement +import content.bot.behaviour.navigation.Graph import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Test import world.gregs.voidps.engine.data.definition.AreaDefinition diff --git a/tools/src/main/kotlin/world/gregs/voidps/tools/map/view/draw/GraphDrawer.kt b/tools/src/main/kotlin/world/gregs/voidps/tools/map/view/draw/GraphDrawer.kt index f89f8e9b33..1d8fe0278a 100644 --- a/tools/src/main/kotlin/world/gregs/voidps/tools/map/view/draw/GraphDrawer.kt +++ b/tools/src/main/kotlin/world/gregs/voidps/tools/map/view/draw/GraphDrawer.kt @@ -1,6 +1,6 @@ package world.gregs.voidps.tools.map.view.draw -import content.bot.interact.path.Graph +import content.bot.behaviour.navigation.Graph import org.rsmod.game.pathfinder.StepValidator import org.rsmod.game.pathfinder.collision.CollisionStrategies import org.rsmod.game.pathfinder.collision.CollisionStrategy diff --git a/tools/src/main/kotlin/world/gregs/voidps/tools/map/view/draw/MapView.kt b/tools/src/main/kotlin/world/gregs/voidps/tools/map/view/draw/MapView.kt index 6f41932a8a..09ba3db25b 100644 --- a/tools/src/main/kotlin/world/gregs/voidps/tools/map/view/draw/MapView.kt +++ b/tools/src/main/kotlin/world/gregs/voidps/tools/map/view/draw/MapView.kt @@ -1,6 +1,6 @@ package world.gregs.voidps.tools.map.view.draw -import content.bot.interact.path.Graph +import content.bot.behaviour.navigation.Graph import kotlinx.coroutines.* import world.gregs.voidps.engine.data.ConfigFiles import world.gregs.voidps.engine.data.Settings From 7b800d3f14444937ec64bf6885b77c33c3de23d9 Mon Sep 17 00:00:00 2001 From: GregHib Date: Sun, 8 Feb 2026 17:36:57 +0000 Subject: [PATCH 075/101] Formatting --- game/src/main/kotlin/GameModules.kt | 1 - .../area/asgarnia/falador/SquireAsrol.kt | 2 +- .../area/asgarnia/port_sarim/Thurgo.kt | 2 +- .../content/area/misthalin/BorderGuard.kt | 1 - .../area/misthalin/edgeville/Jeffery.kt | 2 +- .../god_wars_dungeon/ArmadylPillar.kt | 2 +- game/src/main/kotlin/content/bot/Bot.kt | 6 +- .../src/main/kotlin/content/bot/BotManager.kt | 17 +- game/src/main/kotlin/content/bot/BotSpawns.kt | 2 +- .../src/main/kotlin/content/bot/BotUpdates.kt | 2 +- .../kotlin/content/bot/action/ActionParser.kt | 8 +- .../kotlin/content/bot/action/BotAction.kt | 156 +++++++++--------- .../kotlin/content/bot/behaviour/Behaviour.kt | 10 +- .../content/bot/behaviour/BehaviourFrame.kt | 2 +- .../content/bot/behaviour/BehaviourState.kt | 4 +- .../kotlin/content/bot/behaviour/Reason.kt | 1 - .../bot/behaviour/activity/ActivitySlots.kt | 6 +- .../bot/behaviour/activity/BotActivity.kt | 4 +- .../content/bot/behaviour/navigation/Graph.kt | 11 +- .../navigation/NavigationShortcut.kt | 4 +- .../content/bot/behaviour/setup/Deficit.kt | 24 +-- .../content/bot/behaviour/setup/Resolver.kt | 4 +- .../kotlin/content/bot/req/Requirement.kt | 14 +- .../content/bot/req/RequirementEvaluator.kt | 2 +- .../main/kotlin/content/bot/req/fact/Fact.kt | 9 +- .../kotlin/content/bot/req/fact/FactParser.kt | 38 ++--- .../content/bot/req/predicate/Predicate.kt | 20 ++- .../player/command/PathFindingCommands.kt | 2 +- .../kotlin/content/bot/ActivitySlotsTest.kt | 4 +- .../test/kotlin/content/bot/BotManagerTest.kt | 72 ++++---- .../content/bot/interact/path/GraphTest.kt | 10 +- 31 files changed, 208 insertions(+), 234 deletions(-) diff --git a/game/src/main/kotlin/GameModules.kt b/game/src/main/kotlin/GameModules.kt index 4c0ffc6009..e2f4df9bab 100644 --- a/game/src/main/kotlin/GameModules.kt +++ b/game/src/main/kotlin/GameModules.kt @@ -12,7 +12,6 @@ import world.gregs.voidps.engine.client.instruction.InterfaceHandler import world.gregs.voidps.engine.data.ConfigFiles import world.gregs.voidps.engine.data.Settings import world.gregs.voidps.engine.data.Storage -import world.gregs.voidps.engine.data.definition.Areas import world.gregs.voidps.engine.data.file.FileStorage import world.gregs.voidps.engine.entity.item.floor.ItemSpawns import java.io.File diff --git a/game/src/main/kotlin/content/area/asgarnia/falador/SquireAsrol.kt b/game/src/main/kotlin/content/area/asgarnia/falador/SquireAsrol.kt index 5f5d14aa3a..d4be43bf21 100644 --- a/game/src/main/kotlin/content/area/asgarnia/falador/SquireAsrol.kt +++ b/game/src/main/kotlin/content/area/asgarnia/falador/SquireAsrol.kt @@ -13,8 +13,8 @@ import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.entity.character.player.combatLevel import world.gregs.voidps.engine.entity.character.player.skill.Skill import world.gregs.voidps.engine.event.AuditLog -import world.gregs.voidps.engine.inv.equipment import world.gregs.voidps.engine.inv.carriesItem +import world.gregs.voidps.engine.inv.equipment import world.gregs.voidps.engine.inv.inventory import world.gregs.voidps.engine.inv.remove import world.gregs.voidps.engine.queue.softQueue diff --git a/game/src/main/kotlin/content/area/asgarnia/port_sarim/Thurgo.kt b/game/src/main/kotlin/content/area/asgarnia/port_sarim/Thurgo.kt index 3560238b44..d930c2fa08 100644 --- a/game/src/main/kotlin/content/area/asgarnia/port_sarim/Thurgo.kt +++ b/game/src/main/kotlin/content/area/asgarnia/port_sarim/Thurgo.kt @@ -6,8 +6,8 @@ import content.quest.quest import world.gregs.voidps.engine.Script import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.entity.item.Item -import world.gregs.voidps.engine.inv.contains import world.gregs.voidps.engine.inv.carriesItem +import world.gregs.voidps.engine.inv.contains import world.gregs.voidps.engine.inv.inventory import world.gregs.voidps.engine.inv.remove import world.gregs.voidps.engine.inv.transact.operation.AddItem.add diff --git a/game/src/main/kotlin/content/area/misthalin/BorderGuard.kt b/game/src/main/kotlin/content/area/misthalin/BorderGuard.kt index 06db37df70..d1d1b55b0c 100644 --- a/game/src/main/kotlin/content/area/misthalin/BorderGuard.kt +++ b/game/src/main/kotlin/content/area/misthalin/BorderGuard.kt @@ -7,7 +7,6 @@ import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.entity.obj.GameObject import world.gregs.voidps.engine.entity.obj.GameObjects import world.gregs.voidps.engine.entity.obj.ObjectLayer -import world.gregs.voidps.type.Area import world.gregs.voidps.type.Distance.nearestTo import world.gregs.voidps.type.area.Rectangle import kotlin.collections.set diff --git a/game/src/main/kotlin/content/area/misthalin/edgeville/Jeffery.kt b/game/src/main/kotlin/content/area/misthalin/edgeville/Jeffery.kt index 683883cc04..e93434d50c 100644 --- a/game/src/main/kotlin/content/area/misthalin/edgeville/Jeffery.kt +++ b/game/src/main/kotlin/content/area/misthalin/edgeville/Jeffery.kt @@ -10,8 +10,8 @@ import content.quest.quest import world.gregs.voidps.engine.Script import world.gregs.voidps.engine.entity.character.npc.NPC import world.gregs.voidps.engine.entity.character.player.Player -import world.gregs.voidps.engine.inv.equipment import world.gregs.voidps.engine.inv.carriesItem +import world.gregs.voidps.engine.inv.equipment import world.gregs.voidps.engine.inv.inventory import world.gregs.voidps.engine.inv.replace diff --git a/game/src/main/kotlin/content/area/troll_country/god_wars_dungeon/ArmadylPillar.kt b/game/src/main/kotlin/content/area/troll_country/god_wars_dungeon/ArmadylPillar.kt index fa69e6386e..32b6cec048 100644 --- a/game/src/main/kotlin/content/area/troll_country/god_wars_dungeon/ArmadylPillar.kt +++ b/game/src/main/kotlin/content/area/troll_country/god_wars_dungeon/ArmadylPillar.kt @@ -11,8 +11,8 @@ import world.gregs.voidps.engine.entity.character.player.skill.level.Level.has import world.gregs.voidps.engine.entity.character.sound import world.gregs.voidps.engine.entity.obj.GameObjects import world.gregs.voidps.engine.inv.add -import world.gregs.voidps.engine.inv.equipment import world.gregs.voidps.engine.inv.carriesItem +import world.gregs.voidps.engine.inv.equipment import world.gregs.voidps.engine.inv.inventory import world.gregs.voidps.type.Direction import world.gregs.voidps.type.Tile diff --git a/game/src/main/kotlin/content/bot/Bot.kt b/game/src/main/kotlin/content/bot/Bot.kt index 7103a4234a..8ca9be58bf 100644 --- a/game/src/main/kotlin/content/bot/Bot.kt +++ b/game/src/main/kotlin/content/bot/Bot.kt @@ -1,10 +1,10 @@ package content.bot import content.bot.action.BotAction -import content.bot.behaviour.activity.BotActivity import content.bot.behaviour.BehaviourFrame import content.bot.behaviour.BehaviourState import content.bot.behaviour.Reason +import content.bot.behaviour.activity.BotActivity import world.gregs.voidps.engine.entity.character.Character import world.gregs.voidps.engine.entity.character.player.Player import java.util.Stack @@ -36,9 +36,7 @@ data class Bot(val player: Player) : Character by player { frame().state = BehaviourState.Failed(Reason.Cancelled) } - override fun toString(): String { - return "BOT ${player.accountName}" - } + override fun toString(): String = "BOT ${player.accountName}" } val Player.isBot: Boolean diff --git a/game/src/main/kotlin/content/bot/BotManager.kt b/game/src/main/kotlin/content/bot/BotManager.kt index c67fdbebac..cc0f29c728 100644 --- a/game/src/main/kotlin/content/bot/BotManager.kt +++ b/game/src/main/kotlin/content/bot/BotManager.kt @@ -2,19 +2,19 @@ package content.bot import com.github.michaelbull.logging.InlineLogger import content.bot.action.* -import content.bot.behaviour.activity.ActivitySlots -import content.bot.behaviour.activity.BotActivity import content.bot.behaviour.Behaviour import content.bot.behaviour.BehaviourFrame import content.bot.behaviour.BehaviourState import content.bot.behaviour.HardReason import content.bot.behaviour.Reason +import content.bot.behaviour.activity.ActivitySlots +import content.bot.behaviour.activity.BotActivity import content.bot.behaviour.loadBehaviours -import content.bot.req.Requirement import content.bot.behaviour.navigation.Graph import content.bot.behaviour.navigation.Graph.Companion.loadGraph import content.bot.behaviour.navigation.NavigationShortcut import content.bot.behaviour.setup.Resolver +import content.bot.req.Requirement import world.gregs.voidps.engine.data.ConfigFiles import world.gregs.voidps.engine.data.Settings import world.gregs.voidps.engine.event.AuditLog @@ -103,9 +103,7 @@ class BotManager( } } - private fun hasRequirements(bot: Bot, activity: BotActivity): Boolean { - return slots.hasFree(activity) && !bot.blocked.contains(activity.id) && activity.requires.all { it.check(bot.player) } - } + private fun hasRequirements(bot: Bot, activity: BotActivity): Boolean = slots.hasFree(activity) && !bot.blocked.contains(activity.id) && activity.requires.all { it.check(bot.player) } fun assign(bot: Bot, id: String): Boolean { val activity = activities[id] ?: return false @@ -176,7 +174,7 @@ class BotManager( // Attempt resolution AuditLog.event(bot, "start_resolver", resolver.id, behaviour.id) if (bot.player["debug", false]) { - logger.info { "Starting resolution: ${resolver.id} for ${behaviour.id} requirement: ${requirement}." } + logger.info { "Starting resolution: ${resolver.id} for ${behaviour.id} requirement: $requirement." } } frame.blocked.add(resolver.id) val resolverFrame = BehaviourFrame(resolver) @@ -268,7 +266,7 @@ class BotManager( } private fun debugResolvers(behaviour: Behaviour, requirement: Requirement<*>, resolvers: MutableList, frame: BehaviourFrame, bot: Bot) { - logger.info { "No resolver found for ${behaviour.id} keys: ${requirement.fact.keys()} requirement: ${requirement}." } + logger.info { "No resolver found for ${behaviour.id} keys: ${requirement.fact.keys()} requirement: $requirement." } for (resolver in resolvers) { if (frame.blocked.contains(resolver.id)) { logger.debug { "Resolver: ${resolver.id} - Blocked by frame behaviour: ${frame.behaviour.id}." } @@ -301,5 +299,4 @@ class BotManager( } } } - -} \ No newline at end of file +} diff --git a/game/src/main/kotlin/content/bot/BotSpawns.kt b/game/src/main/kotlin/content/bot/BotSpawns.kt index 133b9f9e01..c69fd4352e 100644 --- a/game/src/main/kotlin/content/bot/BotSpawns.kt +++ b/game/src/main/kotlin/content/bot/BotSpawns.kt @@ -159,7 +159,7 @@ class BotSpawns( } else { names.remove(selected) } - "${prefix}${selected}" + "${prefix}$selected" } val player = Player(tile = Areas["lumbridge_teleport"].random(), accountName = name) val bot = player.initBot() diff --git a/game/src/main/kotlin/content/bot/BotUpdates.kt b/game/src/main/kotlin/content/bot/BotUpdates.kt index d000688ee8..18176ab0e5 100644 --- a/game/src/main/kotlin/content/bot/BotUpdates.kt +++ b/game/src/main/kotlin/content/bot/BotUpdates.kt @@ -44,4 +44,4 @@ class BotUpdates(val manager: BotManager) : Script { } } } -} \ No newline at end of file +} diff --git a/game/src/main/kotlin/content/bot/action/ActionParser.kt b/game/src/main/kotlin/content/bot/action/ActionParser.kt index 6a7266ac38..7dae51ff51 100644 --- a/game/src/main/kotlin/content/bot/action/ActionParser.kt +++ b/game/src/main/kotlin/content/bot/action/ActionParser.kt @@ -156,16 +156,16 @@ sealed class ActionParser { is List<*> -> value as List> else -> return listOf() } - return Requirement.parse(listOf(key to list), "ActionParser.${key}") + return Requirement.parse(listOf(key to list), "ActionParser.$key") } fun parse(list: List>>, name: String): List { val actions = mutableListOf() for ((type, map) in list) { - val parser = parsers[type] ?: error("No action parser for '$type' in ${name}.") + val parser = parsers[type] ?: error("No action parser for '$type' in $name.") val error = parser.check(map) if (error != null) { - error("Action '$type' $error in ${name}.") + error("Action '$type' $error in $name.") } val action = parser.parse(map) actions.add(action) @@ -187,4 +187,4 @@ sealed class ActionParser { "enter" to EnterParser, ) } -} \ No newline at end of file +} diff --git a/game/src/main/kotlin/content/bot/action/BotAction.kt b/game/src/main/kotlin/content/bot/action/BotAction.kt index 226e3ca054..2cdcf4a39c 100644 --- a/game/src/main/kotlin/content/bot/action/BotAction.kt +++ b/game/src/main/kotlin/content/bot/action/BotAction.kt @@ -1,14 +1,14 @@ package content.bot.action -import content.bot.behaviour.BehaviourFrame -import content.bot.behaviour.BehaviourState import content.bot.Bot import content.bot.BotManager +import content.bot.behaviour.BehaviourFrame +import content.bot.behaviour.BehaviourState import content.bot.behaviour.Reason -import content.bot.req.Requirement import content.bot.behaviour.navigation.Graph import content.bot.behaviour.navigation.NavigationShortcut import content.bot.behaviour.setup.Resolver +import content.bot.req.Requirement import content.entity.combat.attackers import content.entity.combat.dead import world.gregs.voidps.engine.GameLoop @@ -95,7 +95,7 @@ sealed interface BotAction { } } if (actions.isNotEmpty()) { - bot.queue(BehaviourFrame(Resolver("go_to_${target}", 0, actions = actions))) + bot.queue(BehaviourFrame(Resolver("go_to_$target", 0, actions = actions))) } if (nav != null) { bot.queue(BehaviourFrame(nav)) @@ -358,7 +358,7 @@ sealed interface BotAction { val to = inventory[toSlot] val valid = get().handle(bot.player, InteractInterfaceItem(from.def.id, to.def.id, fromSlot, toSlot, 149, 0, 149, 0)) return when { - !valid -> BehaviourState.Failed(Reason.Invalid("Invalid item on item: ${from.def.id}:${fromSlot} -> ${to.def.id}:${toSlot}.")) + !valid -> BehaviourState.Failed(Reason.Invalid("Invalid item on item: ${from.def.id}:$fromSlot -> ${to.def.id}:$toSlot.")) success == null -> BehaviourState.Wait(1, BehaviourState.Success) success.check(bot.player) -> BehaviourState.Success else -> BehaviourState.Running @@ -390,7 +390,7 @@ sealed interface BotAction { } val valid = get().handle(bot.player, InteractInterfaceObject(obj.intId, obj.x, obj.y, 149, 0, item.def.id, slot)) if (!valid) { - return BehaviourState.Failed(Reason.Invalid("Invalid item on object: ${item.def.id}:${slot} -> ${obj}.")) + return BehaviourState.Failed(Reason.Invalid("Invalid item on object: ${item.def.id}:$slot -> $obj.")) } return BehaviourState.Running } @@ -417,16 +417,16 @@ sealed interface BotAction { } val (id, component) = split val item = split.getOrNull(2) - val def = definitions.getOrNull(id) ?: return BehaviourState.Failed(Reason.Invalid("Invalid interface id $id:${component}:${item}.")) - val componentId = definitions.getComponentId(id, component) ?: return BehaviourState.Failed(Reason.Invalid("Invalid interface component $id:${component}:${item}.")) - val componentDef = definitions.getComponent(id, component) ?: return BehaviourState.Failed(Reason.Invalid("Invalid interface component definition $id:${component}:${item}.")) + val def = definitions.getOrNull(id) ?: return BehaviourState.Failed(Reason.Invalid("Invalid interface id $id:$component:$item.")) + val componentId = definitions.getComponentId(id, component) ?: return BehaviourState.Failed(Reason.Invalid("Invalid interface component $id:$component:$item.")) + val componentDef = definitions.getComponent(id, component) ?: return BehaviourState.Failed(Reason.Invalid("Invalid interface component definition $id:$component:$item.")) var options = componentDef.options if (options == null) { options = componentDef.getOrNull("options") ?: emptyArray() } val index = options.indexOf(option) if (index == -1) { - return BehaviourState.Failed(Reason.Invalid("No interface option $option for $id:$component:${item} options=${options.contentToString()}.")) + return BehaviourState.Failed(Reason.Invalid("No interface option $option for $id:$component:$item options=${options.contentToString()}.")) } val itemDef = if (item != null) ItemDefinitions.getOrNull(item) else null @@ -436,16 +436,17 @@ sealed interface BotAction { itemSlot *= 6 } val valid = get().handle( - bot.player, InteractInterface( + bot.player, + InteractInterface( interfaceId = def.id, componentId = componentId, itemId = itemDef?.id ?: -1, itemSlot = itemSlot, - option = index - ) + option = index, + ), ) return when { - !valid -> BehaviourState.Failed(Reason.Invalid("Invalid interaction: ${def.id}:${componentId}:${itemDef?.id} slot $itemSlot option ${index}.")) + !valid -> BehaviourState.Failed(Reason.Invalid("Invalid interaction: ${def.id}:$componentId:${itemDef?.id} slot $itemSlot option $index.")) success == null -> BehaviourState.Wait(1, BehaviourState.Success) success.check(bot.player) -> BehaviourState.Success else -> BehaviourState.Running @@ -465,23 +466,24 @@ sealed interface BotAction { } val (id, component) = split val item = split.getOrNull(2) - val def = definitions.getOrNull(id) ?: return BehaviourState.Failed(Reason.Invalid("Invalid interface id $id:${component}:${item}.")) - val componentId = definitions.getComponentId(id, component) ?: return BehaviourState.Failed(Reason.Invalid("Invalid interface component $id:${component}:${item}.")) - val componentDef = definitions.getComponent(id, component) ?: return BehaviourState.Failed(Reason.Invalid("Invalid interface component definition $id:${component}:${item}.")) + val def = definitions.getOrNull(id) ?: return BehaviourState.Failed(Reason.Invalid("Invalid interface id $id:$component:$item.")) + val componentId = definitions.getComponentId(id, component) ?: return BehaviourState.Failed(Reason.Invalid("Invalid interface component $id:$component:$item.")) + val componentDef = definitions.getComponent(id, component) ?: return BehaviourState.Failed(Reason.Invalid("Invalid interface component definition $id:$component:$item.")) var options = componentDef.options if (options == null) { options = componentDef.getOrNull("options") ?: emptyArray() } val index = options.indexOf(option) val valid = get().handle( - bot.player, InteractDialogue( + bot.player, + InteractDialogue( interfaceId = def.id, componentId = componentId, - option = index - ) + option = index, + ), ) if (!valid) { - return BehaviourState.Failed(Reason.Invalid("Invalid interaction: ${def.id}:${componentId} option=${index}.")) + return BehaviourState.Failed(Reason.Invalid("Invalid interaction: ${def.id}:$componentId option=$index.")) } return BehaviourState.Wait(1, BehaviourState.Success) } @@ -568,62 +570,62 @@ sealed interface BotAction { * * TODO behaviour loop detection * - more resolvers like bank all, drop cheap items - how to handle combat, one task or multiple? - One Fight action - frames should have tick(): State methods - Combat should be an action which has a state machine for eating, retargeting, looting etc.. - GatheringActivity - TravelActivity - how to handle navigation in a non-hacky way - navigation behaviours - make nav-graph points only? - combine nav-graph requirements with facts - Goal generators - Rather than check all req for all activities do it reactively - Received an item recently? Add relevant activities to that item to the list of posibilities - Been too long since you picked up an item, now remove that goal from the list - No possibilities? Now expand search wider - - Open questions: - - Complex activities like minigames, quests - Minigames: - They are closed mechanical systems so they are actions. - JoinMinigameLobby - Success, Timeout, Kicked etc.. - PlayMinigame - Roll selection, objectives, movement, combat, scoring. Fails on game end or leaving/disconnect - Trading with players: - Outcomes are non-deterministic, waiting on player timing - SellingAction - TradeAction - inits trade - trade rules - max wait - accepted items - price bounds - reacts to - offer chances - cancellation - terminates with success/failure - Quests: - Some complex quest mechanics might need custom actions - Activities: - TalkToCook - GetBucket - GetMilk - GetEgg - ReturnToCook - - navigation + actions - Virtual nodes - Create a temp node - link the current tile as a weight = 0 - link any applicable teleports - run traversal from temp source node - - targeting - policy - - activity generators - reactive loading - Separate mandatory and resolvable requirements - mandatory requirements become gates for if activities are in the current pool - Listeners wait for state changes on specific mandatory requirements (level up, skill changes) and revaluate adding to activity pool - These listeners also check current activity requirements and fail it if no longer gated + more resolvers like bank all, drop cheap items + how to handle combat, one task or multiple? - One Fight action + frames should have tick(): State methods + Combat should be an action which has a state machine for eating, retargeting, looting etc.. + GatheringActivity + TravelActivity + how to handle navigation in a non-hacky way + navigation behaviours + make nav-graph points only? + combine nav-graph requirements with facts + Goal generators + Rather than check all req for all activities do it reactively + Received an item recently? Add relevant activities to that item to the list of posibilities + Been too long since you picked up an item, now remove that goal from the list + No possibilities? Now expand search wider + + Open questions: + - Complex activities like minigames, quests + Minigames: + They are closed mechanical systems so they are actions. + JoinMinigameLobby - Success, Timeout, Kicked etc.. + PlayMinigame - Roll selection, objectives, movement, combat, scoring. Fails on game end or leaving/disconnect + Trading with players: + Outcomes are non-deterministic, waiting on player timing + SellingAction + TradeAction + inits trade + trade rules + max wait + accepted items + price bounds + reacts to + offer chances + cancellation + terminates with success/failure + Quests: + Some complex quest mechanics might need custom actions + Activities: + TalkToCook + GetBucket + GetMilk + GetEgg + ReturnToCook + - navigation + actions + Virtual nodes + Create a temp node + link the current tile as a weight = 0 + link any applicable teleports + run traversal from temp source node + - targeting + policy + - activity generators - reactive loading + Separate mandatory and resolvable requirements + mandatory requirements become gates for if activities are in the current pool + Listeners wait for state changes on specific mandatory requirements (level up, skill changes) and revaluate adding to activity pool + These listeners also check current activity requirements and fail it if no longer gated */ } diff --git a/game/src/main/kotlin/content/bot/behaviour/Behaviour.kt b/game/src/main/kotlin/content/bot/behaviour/Behaviour.kt index f05948853b..646c076c49 100644 --- a/game/src/main/kotlin/content/bot/behaviour/Behaviour.kt +++ b/game/src/main/kotlin/content/bot/behaviour/Behaviour.kt @@ -59,7 +59,7 @@ private fun loadActivities(activities: MutableMap, template requires = Requirement.parse(requires, debug), setup = Requirement.parse(setup, debug), actions = ActionParser.Companion.parse(actions, debug), - produces = Requirement.parse(produces, debug, requirePredicates = false).toSet() + produces = Requirement.parse(produces, debug, requirePredicates = false).toSet(), ) } } @@ -87,7 +87,7 @@ private fun loadSetups(resolvers: MutableMap>, tem requires = Requirement.parse(requires, debug), setup = Requirement.parse(setup, debug), actions = ActionParser.Companion.parse(actions, debug), - produces = products.toSet() + produces = products.toSet(), ) for (product in products) { for (key in product.fact.keys()) { @@ -120,8 +120,8 @@ private fun loadShortcuts(shortcuts: MutableList, templates: requires = Requirement.parse(requires, debug), setup = Requirement.parse(setup, debug), actions = ActionParser.Companion.parse(actions, debug), - produces = Requirement.parse(produces, debug, requirePredicates = false).toSet() - ) + produces = Requirement.parse(produces, debug, requirePredicates = false).toSet(), + ), ) } } @@ -276,7 +276,7 @@ private data class Fragment( if (value is String && value.contains('$')) { val ref = value.reference() val name = ref.trim('$', '{', '}') - val replacement = fields[name] ?: error("No field found for behaviour=$id type=${type} key=$key ref=$ref") + val replacement = fields[name] ?: error("No field found for behaviour=$id type=$type key=$key ref=$ref") if (replacement is String) value.replace(ref, replacement) else replacement } else if (value is Map<*, *>) { resolve(value as Map, type) diff --git a/game/src/main/kotlin/content/bot/behaviour/BehaviourFrame.kt b/game/src/main/kotlin/content/bot/behaviour/BehaviourFrame.kt index b984567c30..c2af41e94a 100644 --- a/game/src/main/kotlin/content/bot/behaviour/BehaviourFrame.kt +++ b/game/src/main/kotlin/content/bot/behaviour/BehaviourFrame.kt @@ -44,4 +44,4 @@ data class BehaviourFrame( } state = BehaviourState.Success } -} \ No newline at end of file +} diff --git a/game/src/main/kotlin/content/bot/behaviour/BehaviourState.kt b/game/src/main/kotlin/content/bot/behaviour/BehaviourState.kt index a71dbd2545..77123599fc 100644 --- a/game/src/main/kotlin/content/bot/behaviour/BehaviourState.kt +++ b/game/src/main/kotlin/content/bot/behaviour/BehaviourState.kt @@ -1,9 +1,9 @@ package content.bot.behaviour -sealed interface BehaviourState { +sealed interface BehaviourState { object Pending : BehaviourState object Running : BehaviourState object Success : BehaviourState data class Failed(val reason: Reason) : BehaviourState data class Wait(var ticks: Int, val next: BehaviourState) : BehaviourState -} \ No newline at end of file +} diff --git a/game/src/main/kotlin/content/bot/behaviour/Reason.kt b/game/src/main/kotlin/content/bot/behaviour/Reason.kt index 057c90bad2..bfa3709242 100644 --- a/game/src/main/kotlin/content/bot/behaviour/Reason.kt +++ b/game/src/main/kotlin/content/bot/behaviour/Reason.kt @@ -11,4 +11,3 @@ interface Reason { } interface SoftReason : Reason interface HardReason : Reason - diff --git a/game/src/main/kotlin/content/bot/behaviour/activity/ActivitySlots.kt b/game/src/main/kotlin/content/bot/behaviour/activity/ActivitySlots.kt index 0ae20b19ba..efb366c4ef 100644 --- a/game/src/main/kotlin/content/bot/behaviour/activity/ActivitySlots.kt +++ b/game/src/main/kotlin/content/bot/behaviour/activity/ActivitySlots.kt @@ -3,9 +3,7 @@ package content.bot.behaviour.activity class ActivitySlots { private val occupied = mutableMapOf() - fun hasFree(activity: BotActivity): Boolean { - return occupied.getOrDefault(activity.id, 0) < activity.capacity - } + fun hasFree(activity: BotActivity): Boolean = occupied.getOrDefault(activity.id, 0) < activity.capacity fun occupy(activity: BotActivity) { occupied[activity.id] = (occupied.getOrDefault(activity.id, 0) + 1).coerceAtMost(activity.capacity) @@ -14,4 +12,4 @@ class ActivitySlots { fun release(activity: BotActivity) { occupied[activity.id] = ((occupied[activity.id] ?: 1) - 1).coerceAtLeast(0) } -} \ No newline at end of file +} diff --git a/game/src/main/kotlin/content/bot/behaviour/activity/BotActivity.kt b/game/src/main/kotlin/content/bot/behaviour/activity/BotActivity.kt index e07e2e8b53..890d06c5b3 100644 --- a/game/src/main/kotlin/content/bot/behaviour/activity/BotActivity.kt +++ b/game/src/main/kotlin/content/bot/behaviour/activity/BotActivity.kt @@ -1,7 +1,7 @@ package content.bot.behaviour.activity -import content.bot.behaviour.Behaviour import content.bot.action.BotAction +import content.bot.behaviour.Behaviour import content.bot.req.Requirement /** @@ -15,4 +15,4 @@ data class BotActivity( override val setup: List> = emptyList(), override val actions: List = emptyList(), override val produces: Set> = emptySet(), -) : Behaviour \ No newline at end of file +) : Behaviour diff --git a/game/src/main/kotlin/content/bot/behaviour/navigation/Graph.kt b/game/src/main/kotlin/content/bot/behaviour/navigation/Graph.kt index 8492d4b890..d3c2ccb388 100644 --- a/game/src/main/kotlin/content/bot/behaviour/navigation/Graph.kt +++ b/game/src/main/kotlin/content/bot/behaviour/navigation/Graph.kt @@ -3,11 +3,11 @@ package content.bot.behaviour.navigation import content.bot.action.ActionParser import content.bot.action.BotAction import content.bot.behaviour.actions +import content.bot.behaviour.requirements import content.bot.bot -import content.bot.req.predicate.Predicate -import content.bot.req.Requirement import content.bot.isBot -import content.bot.behaviour.requirements +import content.bot.req.Requirement +import content.bot.req.predicate.Predicate import world.gregs.config.Config import world.gregs.config.ConfigReader import world.gregs.voidps.engine.data.definition.Areas @@ -229,7 +229,7 @@ class Graph( for (edge in adj.sorted()) { val end = endNodes[edge] val weight = weights[edge] - println("Edge ${edge}: $start -> $end ($weight)") + println("Edge $edge: $start -> $end ($weight)") } } println("Nodes: ${nodes.size} edges: $edgeCount") @@ -297,6 +297,5 @@ class Graph( } return Tile(x, y, level) } - } -} \ No newline at end of file +} diff --git a/game/src/main/kotlin/content/bot/behaviour/navigation/NavigationShortcut.kt b/game/src/main/kotlin/content/bot/behaviour/navigation/NavigationShortcut.kt index 88f3f6bf6f..2d9ad747ca 100644 --- a/game/src/main/kotlin/content/bot/behaviour/navigation/NavigationShortcut.kt +++ b/game/src/main/kotlin/content/bot/behaviour/navigation/NavigationShortcut.kt @@ -1,7 +1,7 @@ package content.bot.behaviour.navigation -import content.bot.behaviour.Behaviour import content.bot.action.BotAction +import content.bot.behaviour.Behaviour import content.bot.req.Requirement data class NavigationShortcut( @@ -11,4 +11,4 @@ data class NavigationShortcut( override val setup: List> = emptyList(), override val actions: List = emptyList(), override val produces: Set> = emptySet(), -) : Behaviour \ No newline at end of file +) : Behaviour diff --git a/game/src/main/kotlin/content/bot/behaviour/setup/Deficit.kt b/game/src/main/kotlin/content/bot/behaviour/setup/Deficit.kt index b389cf25bf..f41b8019eb 100644 --- a/game/src/main/kotlin/content/bot/behaviour/setup/Deficit.kt +++ b/game/src/main/kotlin/content/bot/behaviour/setup/Deficit.kt @@ -16,9 +16,7 @@ sealed interface Deficit { fun resolve(player: Player): Resolver? data class NotInArea(val area: String) : Deficit { - override fun resolve(player: Player): Resolver { - return Resolver("go_to_${area}", -1, actions = listOf(BotAction.GoTo(area))) - } + override fun resolve(player: Player): Resolver = Resolver("go_to_$area", -1, actions = listOf(BotAction.GoTo(area))) } data class MissingEquipment(val entries: List>) : Deficit { @@ -44,11 +42,12 @@ sealed interface Deficit { val spaceNeeded = withdraw(actions, player, entries, uniqueName) if (actions.isNotEmpty()) { return Resolver( - "withdraw$uniqueName", weight = 20, + "withdraw$uniqueName", + weight = 20, setup = listOf( - Requirement(Fact.InventorySpace, Predicate.IntRange(spaceNeeded)) + Requirement(Fact.InventorySpace, Predicate.IntRange(spaceNeeded)), ), - actions = actions + actions = actions, ) } return null @@ -94,7 +93,7 @@ sealed interface Deficit { var spaceNeeded = 0 val actions = mutableListOf( BotAction.GoToNearest("bank"), - BotAction.InteractObject("Use-quickly", "bank_booth*", success = Requirement(Fact.InterfaceOpen("bank"), Predicate.BooleanTrue)) + BotAction.InteractObject("Use-quickly", "bank_booth*", success = Requirement(Fact.InterfaceOpen("bank"), Predicate.BooleanTrue)), ) val uniqueName = StringBuilder() for (item in player.bank.items) { @@ -109,7 +108,7 @@ sealed interface Deficit { spaceNeeded += needed uniqueName.append("_${item.id}") if (needed == 1 || needed == 5 || needed == 10) { - actions.add(BotAction.InterfaceOption("Withdraw-${needed}", "bank:inventory:${item.id}")) + actions.add(BotAction.InterfaceOption("Withdraw-$needed", "bank:inventory:${item.id}")) } else { BotAction.InterfaceOption("Withdraw-X", "bank:inventory:${item.id}") BotAction.IntEntry(needed) @@ -122,14 +121,15 @@ sealed interface Deficit { } actions.add(BotAction.CloseInterface) return Resolver( - "withdraw_$uniqueName", weight = 20, + "withdraw_$uniqueName", + weight = 20, setup = listOf( - Requirement(Fact.InventorySpace, Predicate.IntRange(spaceNeeded)) + Requirement(Fact.InventorySpace, Predicate.IntRange(spaceNeeded)), ), - actions = actions + actions = actions, ) } return null } } -} \ No newline at end of file +} diff --git a/game/src/main/kotlin/content/bot/behaviour/setup/Resolver.kt b/game/src/main/kotlin/content/bot/behaviour/setup/Resolver.kt index dd5f6b675d..cb1d956b04 100644 --- a/game/src/main/kotlin/content/bot/behaviour/setup/Resolver.kt +++ b/game/src/main/kotlin/content/bot/behaviour/setup/Resolver.kt @@ -1,7 +1,7 @@ package content.bot.behaviour.setup -import content.bot.behaviour.Behaviour import content.bot.action.BotAction +import content.bot.behaviour.Behaviour import content.bot.req.Requirement /** @@ -16,4 +16,4 @@ data class Resolver( override val setup: List> = emptyList(), override val actions: List = emptyList(), override val produces: Set> = emptySet(), -) : Behaviour \ No newline at end of file +) : Behaviour diff --git a/game/src/main/kotlin/content/bot/req/Requirement.kt b/game/src/main/kotlin/content/bot/req/Requirement.kt index af04da7a92..b897585994 100644 --- a/game/src/main/kotlin/content/bot/req/Requirement.kt +++ b/game/src/main/kotlin/content/bot/req/Requirement.kt @@ -8,28 +8,24 @@ import world.gregs.voidps.engine.entity.character.player.Player data class Requirement(val fact: Fact, val predicate: Predicate? = null) { - fun check(player: Player): Boolean { - return predicate?.test(player, fact.getValue(player)) ?: false - } + fun check(player: Player): Boolean = predicate?.test(player, fact.getValue(player)) ?: false - fun deficits(player: Player): List { - return predicate?.evaluator?.evaluate(player, fact, predicate) ?: emptyList() - } + fun deficits(player: Player): List = predicate?.evaluator?.evaluate(player, fact, predicate) ?: emptyList() companion object { fun parse(list: List>>>, name: String, requirePredicates: Boolean = true): List> { val requirements = mutableListOf>() for ((type, value) in list) { - val parser = FactParser.Companion.parsers[type] ?: error("No fact parser for '$type' in ${name}.") + val parser = FactParser.Companion.parsers[type] ?: error("No fact parser for '$type' in $name.") for (map in value) { val error = parser.check(map) if (error != null) { - error("Fact '$type' $error in ${name}.") + error("Fact '$type' $error in $name.") } } val requirement = parser.requirement(value) if (requirePredicates && requirement.predicate == null) { - error("No predicates found for requirement $type map $value in ${name}.") + error("No predicates found for requirement $type map $value in $name.") } requirements.add(requirement) } diff --git a/game/src/main/kotlin/content/bot/req/RequirementEvaluator.kt b/game/src/main/kotlin/content/bot/req/RequirementEvaluator.kt index 6e146a8c26..938d9fe80a 100644 --- a/game/src/main/kotlin/content/bot/req/RequirementEvaluator.kt +++ b/game/src/main/kotlin/content/bot/req/RequirementEvaluator.kt @@ -61,4 +61,4 @@ sealed class RequirementEvaluator { } } } -} \ No newline at end of file +} diff --git a/game/src/main/kotlin/content/bot/req/fact/Fact.kt b/game/src/main/kotlin/content/bot/req/fact/Fact.kt index ccf143ac61..1cb6678426 100644 --- a/game/src/main/kotlin/content/bot/req/fact/Fact.kt +++ b/game/src/main/kotlin/content/bot/req/fact/Fact.kt @@ -56,22 +56,22 @@ sealed class Fact(val priority: Int) { } data class IntVariable(val id: String, val default: Int) : Fact(1) { - override fun keys() = setOf("var:${id}") + override fun keys() = setOf("var:$id") override fun getValue(player: Player) = player.variables.get(id) ?: default } data class BoolVariable(val id: String, val default: Boolean?) : Fact(1) { - override fun keys() = setOf("var:${id}") + override fun keys() = setOf("var:$id") override fun getValue(player: Player) = player.variables.get(id) ?: default } data class StringVariable(val id: String, val default: String?) : Fact(1) { - override fun keys() = setOf("var:${id}") + override fun keys() = setOf("var:$id") override fun getValue(player: Player) = player.variables.get(id) ?: default } data class DoubleVariable(val id: String, val default: Double?) : Fact(1) { - override fun keys() = setOf("var:${id}") + override fun keys() = setOf("var:$id") override fun getValue(player: Player) = player.variables.get(id) ?: default } @@ -186,5 +186,4 @@ sealed class Fact(val priority: Int) { } } } - } diff --git a/game/src/main/kotlin/content/bot/req/fact/FactParser.kt b/game/src/main/kotlin/content/bot/req/fact/FactParser.kt index 9ea924ce93..3597961fea 100644 --- a/game/src/main/kotlin/content/bot/req/fact/FactParser.kt +++ b/game/src/main/kotlin/content/bot/req/fact/FactParser.kt @@ -34,33 +34,25 @@ sealed class FactParser { object InventoryItems : FactParser() { override fun parse(map: Map) = Fact.InventoryItems override fun predicate(map: Map) = null - override fun requirement(list: List>): Requirement { - return Requirement(Fact.InventoryItems, Predicate.parseItems(list)) - } + override fun requirement(list: List>): Requirement = Requirement(Fact.InventoryItems, Predicate.parseItems(list)) } object EquipmentItems : FactParser() { override fun parse(map: Map) = Fact.EquipmentItems override fun predicate(map: Map) = null - override fun requirement(list: List>): Requirement { - return Requirement(Fact.EquipmentItems, Predicate.parseItems(list)) - } + override fun requirement(list: List>): Requirement = Requirement(Fact.EquipmentItems, Predicate.parseItems(list)) } object BankedItems : FactParser() { override fun parse(map: Map) = Fact.BankItems override fun predicate(map: Map) = null - override fun requirement(list: List>): Requirement { - return Requirement(Fact.BankItems, Predicate.parseItems(list)) - } + override fun requirement(list: List>): Requirement = Requirement(Fact.BankItems, Predicate.parseItems(list)) } object AllItems : FactParser() { override fun parse(map: Map) = Fact.AllItems override fun predicate(map: Map) = null - override fun requirement(list: List>): Requirement { - return Requirement(Fact.AllItems, Predicate.parseItems(list)) - } + override fun requirement(list: List>): Requirement = Requirement(Fact.AllItems, Predicate.parseItems(list)) } object Variable : FactParser() { @@ -76,22 +68,18 @@ sealed class FactParser { } as Fact } - override fun predicate(map: Map): Predicate? { - return when (val default = map["default"]) { - is Int -> Predicate.parseInt(map) - is String -> Predicate.parseString(map) - is Double -> Predicate.parseDouble(map) - is Boolean -> Predicate.parseBool(map) - else -> error("Invalid default value $default") - } as? Predicate - } + override fun predicate(map: Map): Predicate? = when (val default = map["default"]) { + is Int -> Predicate.parseInt(map) + is String -> Predicate.parseString(map) + is Double -> Predicate.parseDouble(map) + is Boolean -> Predicate.parseBool(map) + else -> error("Invalid default value $default") + } as? Predicate } object Clock : FactParser() { override val required = setOf("id") - override fun parse(map: Map): Fact { - return Fact.ClockRemaining(map["id"] as String, map["seconds"] as? Boolean ?: false) - } + override fun parse(map: Map): Fact = Fact.ClockRemaining(map["id"] as String, map["seconds"] as? Boolean ?: false) override fun predicate(map: Map) = Predicate.parseInt(map) } @@ -126,7 +114,7 @@ sealed class FactParser { object ObjectExists : FactParser() { override val required = setOf("id", "x", "y") - override fun parse(map: Map): Fact.ObjectExists{ + override fun parse(map: Map): Fact.ObjectExists { val tile = Tile(map["x"] as Int, map["y"] as Int) val id = map["id"] as String return Fact.ObjectExists(Predicate.StringEquals(id), tile) diff --git a/game/src/main/kotlin/content/bot/req/predicate/Predicate.kt b/game/src/main/kotlin/content/bot/req/predicate/Predicate.kt index f30e368a47..cf7a4a5aa4 100644 --- a/game/src/main/kotlin/content/bot/req/predicate/Predicate.kt +++ b/game/src/main/kotlin/content/bot/req/predicate/Predicate.kt @@ -1,7 +1,7 @@ package content.bot.req.predicate -import content.bot.req.fact.ItemView import content.bot.req.RequirementEvaluator +import content.bot.req.fact.ItemView import world.gregs.voidps.engine.data.definition.Areas import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.entity.character.player.skill.level.Level.hasRequirements @@ -175,13 +175,15 @@ sealed class Predicate { val id = item["id"] as String var filter = if (id.contains(",")) { val ids = id.split(",") - AnyItem(ids.flatMap { id -> - if (id.any { char -> char == '*' || char == '#' }) { - Wildcards.get(id, Wildcard.Item) - } else { - setOf(id) - } - }.toSet()) + AnyItem( + ids.flatMap { id -> + if (id.any { char -> char == '*' || char == '#' }) { + Wildcards.get(id, Wildcard.Item) + } else { + setOf(id) + } + }.toSet(), + ) } else if (id.any { it == '*' || it == '#' }) { AnyItem(Wildcards.get(id, Wildcard.Item)) } else { @@ -196,4 +198,4 @@ sealed class Predicate { return filter } } -} \ No newline at end of file +} diff --git a/game/src/main/kotlin/content/entity/player/command/PathFindingCommands.kt b/game/src/main/kotlin/content/entity/player/command/PathFindingCommands.kt index f01df6227b..4d8c413df6 100644 --- a/game/src/main/kotlin/content/entity/player/command/PathFindingCommands.kt +++ b/game/src/main/kotlin/content/entity/player/command/PathFindingCommands.kt @@ -2,8 +2,8 @@ package content.entity.player.command import content.bot.Bot import content.bot.BotManager -import content.bot.behaviour.BehaviourFrame import content.bot.action.BotAction +import content.bot.behaviour.BehaviourFrame import content.bot.behaviour.setup.Resolver import content.bot.bot import content.bot.isBot diff --git a/game/src/test/kotlin/content/bot/ActivitySlotsTest.kt b/game/src/test/kotlin/content/bot/ActivitySlotsTest.kt index a5f7639103..f544d2a4ec 100644 --- a/game/src/test/kotlin/content/bot/ActivitySlotsTest.kt +++ b/game/src/test/kotlin/content/bot/ActivitySlotsTest.kt @@ -1,7 +1,7 @@ package content.bot -import content.bot.behaviour.activity.BotActivity import content.bot.behaviour.activity.ActivitySlots +import content.bot.behaviour.activity.BotActivity import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -40,4 +40,4 @@ class ActivitySlotsTest { assertTrue(slots.hasFree(activity)) } -} \ No newline at end of file +} diff --git a/game/src/test/kotlin/content/bot/BotManagerTest.kt b/game/src/test/kotlin/content/bot/BotManagerTest.kt index c3ef8b6cda..bd6b40e845 100644 --- a/game/src/test/kotlin/content/bot/BotManagerTest.kt +++ b/game/src/test/kotlin/content/bot/BotManagerTest.kt @@ -1,15 +1,15 @@ package content.bot import content.bot.action.* -import content.bot.behaviour.activity.BotActivity import content.bot.behaviour.BehaviourFrame import content.bot.behaviour.BehaviourState import content.bot.behaviour.Reason import content.bot.behaviour.SoftReason +import content.bot.behaviour.activity.BotActivity +import content.bot.behaviour.setup.Resolver +import content.bot.req.Requirement import content.bot.req.fact.Fact import content.bot.req.predicate.Predicate -import content.bot.req.Requirement -import content.bot.behaviour.setup.Resolver import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.Test import world.gregs.voidps.engine.entity.character.player.Player @@ -22,7 +22,7 @@ class BotManagerTest { fun `Taskless bot gets assigned an activity`() { val activity = testActivity( id = "woodcutting", - plan = listOf(BotAction.Wait(1)) + plan = listOf(BotAction.Wait(1)), ) val manager = BotManager(mutableMapOf(activity.id to activity)) val bot = testBot(activity) @@ -37,7 +37,7 @@ class BotManagerTest { fun `Activity capacity is respected`() { val activity = testActivity( id = "mine", - plan = listOf(BotAction.Wait(1)) + plan = listOf(BotAction.Wait(1)), ) val manager = BotManager(mutableMapOf(activity.id to activity)) @@ -55,7 +55,7 @@ class BotManagerTest { fun `Pending frame starts running`() { val activity = testActivity( id = "walk", - plan = listOf(BotAction.Wait(1, BehaviourState.Running)) + plan = listOf(BotAction.Wait(1, BehaviourState.Running)), ) val manager = BotManager(mutableMapOf(activity.id to activity)) val bot = testBot(activity) @@ -73,17 +73,15 @@ class BotManagerTest { id = "task", plan = listOf( BotAction.Clone("1"), - BotAction.Clone("1") - ) + BotAction.Clone("1"), + ), ) val frame = BehaviourFrame(activity) frame.start(testBot(activity)) frame.success() - val advanced = frame.next() - assertTrue(advanced) assertEquals(1, frame.index) assertEquals(BehaviourState.Running, frame.state) @@ -93,7 +91,7 @@ class BotManagerTest { fun `Activity slot released on success`() { val activity = testActivity( id = "cook", - plan = listOf(BotAction.Wait(1)) + plan = listOf(BotAction.Wait(1)), ) val manager = BotManager(mutableMapOf(activity.id to activity)) @@ -112,11 +110,11 @@ class BotManagerTest { fun `Completed activity most likely to be reassigned on success`() { val activity = testActivity( id = "cook", - plan = listOf(BotAction.Wait(1)) + plan = listOf(BotAction.Wait(1)), ) val test = testActivity( id = "test", - plan = listOf(BotAction.Wait(1)) + plan = listOf(BotAction.Wait(1)), ) val activities = mutableMapOf(activity.id to activity, test.id to test) @@ -137,7 +135,7 @@ class BotManagerTest { fun `Activity slot released on failure`() { val activity = testActivity( id = "smith", - plan = listOf(BotAction.Wait(1)) + plan = listOf(BotAction.Wait(1)), ) val manager = BotManager(mutableMapOf(activity.id to activity)) @@ -156,7 +154,7 @@ class BotManagerTest { fun `Failed activity is blocked`() { val activity = testActivity( id = "fish", - plan = listOf(BotAction.Clone("")) + plan = listOf(BotAction.Clone("")), ) val manager = BotManager(mutableMapOf(activity.id to activity)) @@ -177,9 +175,9 @@ class BotManagerTest { val activity = testActivity( id = "test", requires = listOf( - Requirement(Fact.AttackLevel, Predicate.IntRange(99, 99)) + Requirement(Fact.AttackLevel, Predicate.IntRange(99, 99)), ), - plan = listOf(BotAction.Wait(4)) + plan = listOf(BotAction.Wait(4)), ) val manager = BotManager(mutableMapOf(activity.id to activity)) @@ -201,16 +199,16 @@ class BotManagerTest { id = "go_to_area", weight = 1, actions = listOf(BotAction.Wait(1)), - produces = setOf(condition) + produces = setOf(condition), ) val activity = testActivity( id = "woodcut", resolves = listOf(condition), - plan = listOf(BotAction.Wait(1)) + plan = listOf(BotAction.Wait(1)), ) val manager = BotManager( mutableMapOf(activity.id to activity), - mutableMapOf(condition.fact.keys().first() to mutableListOf(resolver)) + mutableMapOf(condition.fact.keys().first() to mutableListOf(resolver)), ) val bot = testBot(activity) @@ -231,12 +229,12 @@ class BotManagerTest { val activity = testActivity( id = "mine", resolves = listOf(condition), - plan = listOf(BotAction.Clone("")) + plan = listOf(BotAction.Clone("")), ) val manager = BotManager( mutableMapOf(activity.id to activity), - mutableMapOf(condition.fact.keys().first() to mutableListOf(bad, good)) + mutableMapOf(condition.fact.keys().first() to mutableListOf(bad, good)), ) val bot = testBot(activity) @@ -253,11 +251,11 @@ class BotManagerTest { val activity = testActivity( id = "open_door", resolves = listOf(condition), - plan = listOf(BotAction.Wait(1)) + plan = listOf(BotAction.Wait(1)), ) val manager = BotManager( mutableMapOf(activity.id to activity), - mutableMapOf(condition.fact.keys().first() to mutableListOf(resolver)) + mutableMapOf(condition.fact.keys().first() to mutableListOf(resolver)), ) val bot = testBot(activity) @@ -278,16 +276,16 @@ class BotManagerTest { val resolver = Resolver( id = "walk", weight = 1, - actions = listOf(BotAction.Wait(1)) + actions = listOf(BotAction.Wait(1)), ) val activity = testActivity( id = "enter_zone", resolves = listOf(condition), - plan = listOf(BotAction.Wait(1)) + plan = listOf(BotAction.Wait(1)), ) val manager = BotManager( mutableMapOf(activity.id to activity), - mutableMapOf(condition.fact.keys().first() to mutableListOf(resolver)) + mutableMapOf(condition.fact.keys().first() to mutableListOf(resolver)), ) val bot = testBot(activity) @@ -306,16 +304,16 @@ class BotManagerTest { val resolver = Resolver( id = "test", weight = 1, - actions = listOf(BotAction.Wait(1)) + actions = listOf(BotAction.Wait(1)), ) val activity = testActivity( id = "smelt", resolves = listOf(condition), - plan = listOf(BotAction.Wait(1)) + plan = listOf(BotAction.Wait(1)), ) val manager = BotManager( mutableMapOf(activity.id to activity), - mutableMapOf(condition.fact.keys().first() to mutableListOf(resolver)) + mutableMapOf(condition.fact.keys().first() to mutableListOf(resolver)), ) val bot = testBot(activity) @@ -336,16 +334,16 @@ class BotManagerTest { id = "mine_gem", weight = 1, actions = listOf(BotAction.Clone("")), - requires = listOf(Requirement(Fact.MiningLevel, Predicate.IntRange(99, 99))) + requires = listOf(Requirement(Fact.MiningLevel, Predicate.IntRange(99, 99))), ) val activity = testActivity( id = "craft", resolves = listOf(condition), - plan = listOf(BotAction.Wait(1)) + plan = listOf(BotAction.Wait(1)), ) val manager = BotManager( mutableMapOf(activity.id to activity), - mutableMapOf(condition.fact.keys().first() to mutableListOf(resolver)) + mutableMapOf(condition.fact.keys().first() to mutableListOf(resolver)), ) val bot = testBot(activity) @@ -363,16 +361,16 @@ class BotManagerTest { val resolver = Resolver( id = "get_tool", weight = 1, - actions = listOf(BotAction.Wait(1)) + actions = listOf(BotAction.Wait(1)), ) val activity = testActivity( id = "work", resolves = listOf(condition), - plan = listOf(BotAction.Wait(1)) + plan = listOf(BotAction.Wait(1)), ) val manager = BotManager( mutableMapOf(activity.id to activity), - mutableMapOf(condition.fact.keys().first() to mutableListOf(resolver)) + mutableMapOf(condition.fact.keys().first() to mutableListOf(resolver)), ) val bot = testBot(activity) @@ -388,4 +386,4 @@ class BotManagerTest { resolves: List> = emptyList(), plan: List, ) = BotActivity(id, 1, requires, resolves, plan) -} \ No newline at end of file +} diff --git a/game/src/test/kotlin/content/bot/interact/path/GraphTest.kt b/game/src/test/kotlin/content/bot/interact/path/GraphTest.kt index 3afa58f977..d1424930f3 100644 --- a/game/src/test/kotlin/content/bot/interact/path/GraphTest.kt +++ b/game/src/test/kotlin/content/bot/interact/path/GraphTest.kt @@ -1,10 +1,10 @@ package content.bot.interact.path +import content.bot.behaviour.navigation.Graph import content.bot.behaviour.navigation.NavigationShortcut +import content.bot.req.Requirement import content.bot.req.fact.Fact import content.bot.req.predicate.Predicate -import content.bot.req.Requirement -import content.bot.behaviour.navigation.Graph import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Test import world.gregs.voidps.engine.data.definition.AreaDefinition @@ -258,7 +258,7 @@ class GraphTest { to = b, weight = 1, actions = emptyList(), - conditions = listOf(Requirement(Fact.PlayerTile, Predicate.TileEquals(100))) + conditions = listOf(Requirement(Fact.PlayerTile, Predicate.TileEquals(100))), ) val graph = builder.build() @@ -303,7 +303,7 @@ class GraphTest { id = "teleport", weight = 1, requires = listOf(Requirement(Fact.PlayerTile, Predicate.TileEquals(50, 50))), - produces = setOf(Requirement(Fact.PlayerTile, Predicate.InArea("town"))) + produces = setOf(Requirement(Fact.PlayerTile, Predicate.InArea("town"))), ) val builder = Graph.Builder() @@ -377,4 +377,4 @@ class GraphTest { assertTrue(success) assertEquals(listOf(aj, jk, kl, lm, mh), output) } -} \ No newline at end of file +} From de4f96543921e66d79064755e557a97ca1c1666f Mon Sep 17 00:00:00 2001 From: GregHib Date: Sun, 8 Feb 2026 20:26:48 +0000 Subject: [PATCH 076/101] Fix tags off-by-one error --- .../content/bot/behaviour/navigation/Graph.kt | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/game/src/main/kotlin/content/bot/behaviour/navigation/Graph.kt b/game/src/main/kotlin/content/bot/behaviour/navigation/Graph.kt index d3c2ccb388..1c3fed9aa9 100644 --- a/game/src/main/kotlin/content/bot/behaviour/navigation/Graph.kt +++ b/game/src/main/kotlin/content/bot/behaviour/navigation/Graph.kt @@ -157,7 +157,8 @@ class Graph( val shortcuts = mutableMapOf() init { - tiles.add(Tile.Companion.EMPTY) // Virtual + tiles.add(Tile.EMPTY) // Virtual + tags.add(null) nodes.add(0) } @@ -189,11 +190,7 @@ class Graph( fun add(tile: Tile): Int { if (tiles.add(tile)) { val tags = Areas.get(tile.zone).filter { it.area.contains(tile) }.flatMap { it.tags } - if (tags.isNotEmpty()) { - this.tags.add(tags.toSet()) - } else { - this.tags.add(null) - } + this.tags.add(if (tags.isNotEmpty()) tags.toSet() else null) return tiles.size - 1 } return tiles.indexOf(tile) @@ -246,8 +243,8 @@ class Graph( val list = key() assert(list == "edges") { "Expected edges list, got: $list ${exception()}" } while (nextElement()) { - var from = Tile.Companion.EMPTY - var to = Tile.Companion.EMPTY + var from = Tile.EMPTY + var to = Tile.EMPTY var cost = 0 val actions: MutableList>> = mutableListOf() val requirements = mutableListOf>>>() From 00fd3357eac0c282bf35fff06a0ceda30662c11e Mon Sep 17 00:00:00 2001 From: GregHib Date: Sun, 8 Feb 2026 20:27:53 +0000 Subject: [PATCH 077/101] Add interface closer parser --- game/src/main/kotlin/content/bot/action/ActionParser.kt | 8 ++++++++ .../main/kotlin/content/bot/req/RequirementEvaluator.kt | 8 ++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/game/src/main/kotlin/content/bot/action/ActionParser.kt b/game/src/main/kotlin/content/bot/action/ActionParser.kt index 7dae51ff51..9e1536dabc 100644 --- a/game/src/main/kotlin/content/bot/action/ActionParser.kt +++ b/game/src/main/kotlin/content/bot/action/ActionParser.kt @@ -54,6 +54,13 @@ sealed class ActionParser { } } + object CloseInterfaceParser : ActionParser() { + override val required = setOf("id") + override fun parse(map: Map): BotAction { + return BotAction.CloseInterface + } + } + object DialogueParser : ActionParser() { override val required = setOf("id") override val optional = setOf("option", "success") @@ -183,6 +190,7 @@ sealed class ActionParser { "wait" to WaitParser, "restart" to RestartParser, "interface" to InterfaceParser, + "interface_close" to CloseInterfaceParser, "continue" to DialogueParser, "enter" to EnterParser, ) diff --git a/game/src/main/kotlin/content/bot/req/RequirementEvaluator.kt b/game/src/main/kotlin/content/bot/req/RequirementEvaluator.kt index 938d9fe80a..2bf037efc3 100644 --- a/game/src/main/kotlin/content/bot/req/RequirementEvaluator.kt +++ b/game/src/main/kotlin/content/bot/req/RequirementEvaluator.kt @@ -32,12 +32,12 @@ sealed class RequirementEvaluator { object InventoryEval : RequirementEvaluator() { override fun evaluate(player: Player, fact: Fact, predicate: Predicate): List { if (fact is Fact.InventoryItems && predicate is Predicate.InventoryItems) { - val entries = mutableListOf() - collect(player, fact, predicate) { filter, needed -> entries += MissingInventory.Entry(filter, needed) } + val entries = mutableListOf() + collect(player, fact, predicate) { filter, needed -> entries += Deficit.Entry(filter, needed) } return listOf(MissingInventory(entries)) } else if (fact is Fact.EquipmentItems && predicate is Predicate.InventoryItems) { - val entries = mutableListOf>() - collect(player, fact, predicate) { filter, _ -> entries += filter } + val entries = mutableListOf() + collect(player, fact, predicate) { filter, needed -> entries += Deficit.Entry(filter, needed) } return listOf(Deficit.MissingEquipment(entries)) } return emptyList() From 45c3b4d72fd94d47a91f5dffaa8288417d1f603d Mon Sep 17 00:00:00 2001 From: GregHib Date: Sun, 8 Feb 2026 20:28:12 +0000 Subject: [PATCH 078/101] Tweak deficits --- .../kotlin/content/bot/action/BotAction.kt | 68 ++----------------- .../content/bot/behaviour/setup/Deficit.kt | 18 ++--- 2 files changed, 15 insertions(+), 71 deletions(-) diff --git a/game/src/main/kotlin/content/bot/action/BotAction.kt b/game/src/main/kotlin/content/bot/action/BotAction.kt index 2cdcf4a39c..1434a93d90 100644 --- a/game/src/main/kotlin/content/bot/action/BotAction.kt +++ b/game/src/main/kotlin/content/bot/action/BotAction.kt @@ -64,7 +64,7 @@ sealed interface BotAction { } override fun update(bot: Bot, frame: BehaviourFrame): BehaviourState { - if (bot.steps.isNotEmpty()) { + if (bot.steps.isNotEmpty() || bot.mode != EmptyMode) { return BehaviourState.Running } val def = Areas.getOrNull(target) ?: return BehaviourState.Failed(Reason.Invalid("No areas found with id '$target'.")) @@ -121,7 +121,7 @@ sealed interface BotAction { } override fun update(bot: Bot, frame: BehaviourFrame): BehaviourState { - if (bot.steps.isNotEmpty()) { + if (bot.steps.isNotEmpty() || bot.mode != EmptyMode) { return BehaviourState.Running } val set = Areas.tagged(tag) @@ -553,14 +553,14 @@ sealed interface BotAction { * TODO * combat training dummy bots * firemaking bot + * fishing bot * rune mysteries quest bot + * bot spawning in other locations + * track timeouts by comparing previous to current produce for progress + * remove misc old bot data from areas * bot saving? * bot setups - * bot spawning in other locations - * tidy up old bot code - * move tags into edges not areas * item tags? - * track timeouts by comparing previous to current produce for progress * * Idea: Reactions? * A separate queue that runs "reactions" e.g. @@ -570,62 +570,6 @@ sealed interface BotAction { * * TODO behaviour loop detection * - more resolvers like bank all, drop cheap items - how to handle combat, one task or multiple? - One Fight action - frames should have tick(): State methods - Combat should be an action which has a state machine for eating, retargeting, looting etc.. - GatheringActivity - TravelActivity - how to handle navigation in a non-hacky way - navigation behaviours - make nav-graph points only? - combine nav-graph requirements with facts - Goal generators - Rather than check all req for all activities do it reactively - Received an item recently? Add relevant activities to that item to the list of posibilities - Been too long since you picked up an item, now remove that goal from the list - No possibilities? Now expand search wider - - Open questions: - - Complex activities like minigames, quests - Minigames: - They are closed mechanical systems so they are actions. - JoinMinigameLobby - Success, Timeout, Kicked etc.. - PlayMinigame - Roll selection, objectives, movement, combat, scoring. Fails on game end or leaving/disconnect - Trading with players: - Outcomes are non-deterministic, waiting on player timing - SellingAction - TradeAction - inits trade - trade rules - max wait - accepted items - price bounds - reacts to - offer chances - cancellation - terminates with success/failure - Quests: - Some complex quest mechanics might need custom actions - Activities: - TalkToCook - GetBucket - GetMilk - GetEgg - ReturnToCook - - navigation + actions - Virtual nodes - Create a temp node - link the current tile as a weight = 0 - link any applicable teleports - run traversal from temp source node - - targeting - policy - - activity generators - reactive loading - Separate mandatory and resolvable requirements - mandatory requirements become gates for if activities are in the current pool - Listeners wait for state changes on specific mandatory requirements (level up, skill changes) and revaluate adding to activity pool - These listeners also check current activity requirements and fail it if no longer gated */ } diff --git a/game/src/main/kotlin/content/bot/behaviour/setup/Deficit.kt b/game/src/main/kotlin/content/bot/behaviour/setup/Deficit.kt index f41b8019eb..3f2b70aa62 100644 --- a/game/src/main/kotlin/content/bot/behaviour/setup/Deficit.kt +++ b/game/src/main/kotlin/content/bot/behaviour/setup/Deficit.kt @@ -19,7 +19,9 @@ sealed interface Deficit { override fun resolve(player: Player): Resolver = Resolver("go_to_$area", -1, actions = listOf(BotAction.GoTo(area))) } - data class MissingEquipment(val entries: List>) : Deficit { + data class Entry(val filter: Predicate, val needed: Int) + + data class MissingEquipment(val entries: List) : Deficit { override fun resolve(player: Player): Resolver? { val entries = entries.toMutableList() val actions = mutableListOf() @@ -30,7 +32,7 @@ sealed interface Deficit { } val iterator = entries.iterator() while (iterator.hasNext()) { - val entry = iterator.next() + val (entry, needed) = iterator.next() if (!entry.test(player, item)) { continue } @@ -53,7 +55,7 @@ sealed interface Deficit { return null } - private fun withdraw(actions: MutableList, player: Player, entries: MutableList>, uniqueName: StringBuilder): Int { + private fun withdraw(actions: MutableList, player: Player, entries: MutableList, uniqueName: StringBuilder): Int { if (entries.isEmpty()) { return 0 } @@ -66,12 +68,12 @@ sealed interface Deficit { } val iterator = entries.iterator() while (iterator.hasNext()) { - val entry = iterator.next() + val (entry, needed) = iterator.next() if (!entry.test(player, item)) { continue } iterator.remove() - spaceNeeded++ + spaceNeeded += if (player.bank.stackable(item.id)) 1 else needed uniqueName.append("_${item.id}") actions.add(BotAction.InterfaceOption("Withdraw-1", "bank:inventory:${item.id}")) } @@ -87,8 +89,6 @@ sealed interface Deficit { } data class MissingInventory(val entries: List) : Deficit { - data class Entry(val filter: Predicate, val needed: Int) - override fun resolve(player: Player): Resolver? { var spaceNeeded = 0 val actions = mutableListOf( @@ -105,7 +105,7 @@ sealed interface Deficit { continue } val needed = entry.needed - spaceNeeded += needed + spaceNeeded += if (player.bank.stackable(item.id)) 1 else needed uniqueName.append("_${item.id}") if (needed == 1 || needed == 5 || needed == 10) { actions.add(BotAction.InterfaceOption("Withdraw-$needed", "bank:inventory:${item.id}")) @@ -115,7 +115,7 @@ sealed interface Deficit { } } } - if (spaceNeeded > 0) { + if (actions.size > 2) { if (player.inventory.spaces < spaceNeeded) { actions.add(2, BotAction.InteractObject("Deposit carried items", "bank:carried", success = Requirement(Fact.InventorySpace, Predicate.IntEquals(28)))) } From 8f581c3a32b620b2b27c7a251e45a68225623905 Mon Sep 17 00:00:00 2001 From: GregHib Date: Sun, 8 Feb 2026 23:03:31 +0000 Subject: [PATCH 079/101] Rethinking evaluation, started adding fishing --- .../misthalin/lumbridge/lumbridge.areas.toml | 1 + .../misthalin/lumbridge/lumbridge.bots.toml | 9 +++ .../misthalin/lumbridge/lumbridge.setups.toml | 47 +++++++++++++ data/bot/bank.setups.toml | 2 +- data/bot/fishing.templates.toml | 43 ++++++++++++ data/bot/shop.templates.toml | 66 ++++++++++++++++++- .../src/main/kotlin/content/bot/BotManager.kt | 10 ++- .../kotlin/content/bot/behaviour/Behaviour.kt | 15 +++-- .../content/bot/behaviour/setup/Deficit.kt | 11 ++++ .../kotlin/content/bot/req/Requirement.kt | 4 +- .../main/kotlin/content/bot/req/fact/Fact.kt | 22 +++++-- .../content/bot/req/predicate/Predicate.kt | 7 ++ 12 files changed, 221 insertions(+), 16 deletions(-) create mode 100644 data/bot/fishing.templates.toml diff --git a/data/area/misthalin/lumbridge/lumbridge.areas.toml b/data/area/misthalin/lumbridge/lumbridge.areas.toml index 191cb6b6ab..e00b04d63f 100644 --- a/data/area/misthalin/lumbridge/lumbridge.areas.toml +++ b/data/area/misthalin/lumbridge/lumbridge.areas.toml @@ -185,6 +185,7 @@ tags = ["fire_making", "spaces_2"] [lumbridge_kitchen] x = [3205, 3212] y = [3212, 3217] +level = 0 type = "range" tags = ["cooking", "spaces_2"] diff --git a/data/area/misthalin/lumbridge/lumbridge.bots.toml b/data/area/misthalin/lumbridge/lumbridge.bots.toml index 032359923e..9cbf5c80cf 100644 --- a/data/area/misthalin/lumbridge/lumbridge.bots.toml +++ b/data/area/misthalin/lumbridge/lumbridge.bots.toml @@ -52,6 +52,15 @@ requires = [ { skill = { id = "thieving", min = 1 } } ] +# Fishing +[lumbridge_crayfish] +template = "fish_crayfish" +capacity = 4 +fields = { location = "lumbridge_south_river_fishing_spot", spot = "fishing_spot_crayfish_lumbridge" } +requires = [ + { skill = { id = "fishing", min = 1, max = 5 } } +] + # Fletching [lumbridge_fletch_arrow_shafts] template = "fletching_arrow_shafts_template" diff --git a/data/area/misthalin/lumbridge/lumbridge.setups.toml b/data/area/misthalin/lumbridge/lumbridge.setups.toml index 2794888c43..7e058dfff2 100644 --- a/data/area/misthalin/lumbridge/lumbridge.setups.toml +++ b/data/area/misthalin/lumbridge/lumbridge.setups.toml @@ -1,3 +1,50 @@ +# Hanks fishing shop +[take_small_fishing_net_hank] +template = "take_shop_sample" +weight = 30 +fields = { shop_location = "hanks_fishing_shop", shopkeeper = "hank", item = "small_fishing_net" } + +[take_crayfish_cage_hank] +template = "take_shop_sample" +weight = 30 +fields = { shop_location = "hanks_fishing_shop", shopkeeper = "hank", item = "crayfish_cage" } + +[buy_small_fishing_net_hank] +template = "buy_from_shop" +weight = 35 +fields = { cost = 40, shop_location = "hanks_fishing_shop", shopkeeper = "hank", item = "small_fishing_net" } + +[buy_crayfish_cage_hank] +template = "buy_from_shop" +weight = 35 +fields = { cost = 20, shop_location = "hanks_fishing_shop", shopkeeper = "hank", item = "crayfish_cage" } + +[buy_fishing_bait_hank] +template = "buy_from_shop" +weight = 35 +fields = { cost = 3, shop_location = "hanks_fishing_shop", shopkeeper = "hank", item = "fishing_bait" } + +[buy_feather_hank] +template = "buy_from_shop" +weight = 35 +fields = { cost = 6, shop_location = "hanks_fishing_shop", shopkeeper = "hank", item = "feather" } + +[buy_fishing_rod_hank] +template = "buy_from_shop" +weight = 35 +fields = { cost = 5, shop_location = "hanks_fishing_shop", shopkeeper = "hank", item = "fishing_rod" } +requires = [ + { skill = { id = "fishing", min = 5 } }, +] + +[buy_fly_fishing_rod_hank] +template = "buy_from_shop" +weight = 35 +fields = { cost = 5, shop_location = "hanks_fishing_shop", shopkeeper = "hank", item = "fishing_rod" } +requires = [ + { skill = { id = "fishing", min = 20 } }, +] + # Bobs Brilliant Axes [take_bronze_hatchet_bobs] template = "take_shop_sample" diff --git a/data/bot/bank.setups.toml b/data/bot/bank.setups.toml index 6d9c753a4b..2f0ba3501d 100644 --- a/data/bot/bank.setups.toml +++ b/data/bot/bank.setups.toml @@ -5,7 +5,7 @@ actions = [ { interface = { option = "Deposit carried items", id = "bank:carried", success = { inventory_space = { min = 28 } } } }, ] produces = [ - { inventory_space = { min = 28 } } + { inventory_space = { equals = 28 } } ] #[deposit_worn_items] diff --git a/data/bot/fishing.templates.toml b/data/bot/fishing.templates.toml new file mode 100644 index 0000000000..0c0d0ac469 --- /dev/null +++ b/data/bot/fishing.templates.toml @@ -0,0 +1,43 @@ +[fish_crayfish] +requires = [ + { skill = { id = "fishing", min = 1 } }, +] +setup = [ + { carries = { id = "crayfish_cage" } }, + { area = { id = "$location" } }, + { inventory_space = { min = 27 } }, +] +actions = [ + { npc = { option = "Cage", id = "$spot", delay = 5, success = { inventory_space = { max = 0 } } } }, +] +produces = [ + { carries = { id = "raw_crayfish" } }, + { skill = { id = "fishing" } } +] + +[fish_small_net] +setup = [ + { carries = { id = "small_fishing_net" } }, + { area = { id = "$location" } }, + { inventory_space = { min = 27 } }, +] +actions = [ + { npc = { option = "Net", id = "$spot", delay = 5, success = { inventory_space = { max = 0 } } } }, +] +produces = [ + { carries = { id = "raw_shrimp" } }, + { skill = { id = "fishing" } } +] + +[fish_bait] +setup = [ + { carries = { id = "small_fishing_net" } }, + { area = { id = "$location" } }, + { inventory_space = { min = 27 } }, +] +actions = [ + { npc = { option = "Net", id = "$spot", delay = 5, success = { inventory_space = { max = 0 } } } }, +] +produces = [ + { skill = { id = "fishing" } } +] diff --git a/data/bot/shop.templates.toml b/data/bot/shop.templates.toml index 0313e4a7c6..b6dce9e270 100644 --- a/data/bot/shop.templates.toml +++ b/data/bot/shop.templates.toml @@ -9,7 +9,7 @@ actions = [ { interface = { option = "Buy-1", id = "shop:stock:$item", success = { carries = [{ id = "$item" }] } } }, ] produces = [ - { carries = [{ id = "$item" }] } + { carries = [{ id = "$item", equals = 1 }] } ] [take_shop_sample] @@ -22,5 +22,65 @@ actions = [ { interface = { option = "Take-1", id = "shop:sample:$item", success = { carries = [{ id = "$item" }] } } }, ] produces = [ - { carries = [{ id = "$item" }] } -] \ No newline at end of file + { carries = [{ id = "$item", equals = 1 }] } +] + +[sell_50_to_shop] +setup = [ + { carries = [{ id = "$item", min = "50" }] }, + { area = { id = "$shop_location" } }, + { inventory_space = { min = 1 } }, +] +actions = [ + { npc = { option = "Trade", id = "$shopkeeper", success = { interface_open = { id = "shop" } } } }, + { interface = { option = "Sell-50", id = "shop_side:inventory:$item", success = { carries = [{ id = "coins" }] } } }, + { interface_close = { id = "shop" } } +] +produces = [ + { carries = { id = "coins" } } +] + +[sell_10_to_shop] +setup = [ + { carries = [{ id = "$item", min = "10" }] }, + { area = { id = "$shop_location" } }, + { inventory_space = { min = 1 } }, +] +actions = [ + { npc = { option = "Trade", id = "$shopkeeper", success = { interface_open = { id = "shop" } } } }, + { interface = { option = "Sell-10", id = "shop_side:inventory:$item", success = { carries = [{ id = "coins" }] } } }, + { interface_close = { id = "shop" } } +] +produces = [ + { carries = { id = "coins" } } +] + +[sell_5_to_shop] +setup = [ + { carries = [{ id = "$item", min = "5" }] }, + { area = { id = "$shop_location" } }, + { inventory_space = { min = 1 } }, +] +actions = [ + { npc = { option = "Trade", id = "$shopkeeper", success = { interface_open = { id = "shop" } } } }, + { interface = { option = "Sell-5", id = "shop_side:inventory:$item", success = { carries = [{ id = "coins" }] } } }, + { interface_close = { id = "shop" } } +] +produces = [ + { carries = { id = "coins" } } +] + +[sell_to_shop] +setup = [ + { carries = [{ id = "$item", min = "1" }] }, + { area = { id = "$shop_location" } }, + { inventory_space = { min = 1 } }, +] +actions = [ + { npc = { option = "Trade", id = "$shopkeeper", success = { interface_open = { id = "shop" } } } }, + { interface = { option = "Sell-1", id = "shop_side:inventory:$item", success = { carries = [{ id = "coins" }] } } }, + { interface_close = { id = "shop" } } +] +produces = [ + { carries = { id = "coins" } } +] diff --git a/game/src/main/kotlin/content/bot/BotManager.kt b/game/src/main/kotlin/content/bot/BotManager.kt index cc0f29c728..b73e84d984 100644 --- a/game/src/main/kotlin/content/bot/BotManager.kt +++ b/game/src/main/kotlin/content/bot/BotManager.kt @@ -191,11 +191,17 @@ class BotManager( private fun availableResolvers(bot: Bot, requirement: Requirement<*>): MutableList { val options = mutableListOf() + println("Resolvers for $requirement") +// FIXME because InventoryItems no longer produces individual keys, there's no way to match resolvers to required items for (deficit in requirement.deficits(bot.player)) { options.add(deficit.resolve(bot.player) ?: continue) } - for (key in requirement.fact.keys()) { - options.addAll(resolvers[key] ?: continue) + println("Check ${requirement.fact} ${requirement.predicate}") + for (key in requirement.keys()) { + println("Resolvers for $key : ${resolvers[key]}") + for (resolver in resolvers[key] ?: continue) { + options.add(resolver) + } } return options } diff --git a/game/src/main/kotlin/content/bot/behaviour/Behaviour.kt b/game/src/main/kotlin/content/bot/behaviour/Behaviour.kt index 646c076c49..f0117305f4 100644 --- a/game/src/main/kotlin/content/bot/behaviour/Behaviour.kt +++ b/game/src/main/kotlin/content/bot/behaviour/Behaviour.kt @@ -58,7 +58,7 @@ private fun loadActivities(activities: MutableMap, template capacity = capacity, requires = Requirement.parse(requires, debug), setup = Requirement.parse(setup, debug), - actions = ActionParser.Companion.parse(actions, debug), + actions = ActionParser.parse(actions, debug), produces = Requirement.parse(produces, debug, requirePredicates = false).toSet(), ) } @@ -86,11 +86,11 @@ private fun loadSetups(resolvers: MutableMap>, tem weight = weight, requires = Requirement.parse(requires, debug), setup = Requirement.parse(setup, debug), - actions = ActionParser.Companion.parse(actions, debug), + actions = ActionParser.parse(actions, debug), produces = products.toSet(), ) for (product in products) { - for (key in product.fact.keys()) { + for (key in product.keys()) { resolvers.getOrPut(key) { mutableListOf() }.add(resolver) } } @@ -98,7 +98,12 @@ private fun loadSetups(resolvers: MutableMap>, tem } for (fragment in fragments) { val template = templates[fragment.template] ?: error("Unable to find template '${fragment.template}' for ${fragment.id}.") - resolvers.getOrPut(fragment.id) { mutableListOf() }.add(fragment.resolver(template)) + val resolver = fragment.resolver(template) + for (product in resolver.produces) { + for (key in product.keys()) { + resolvers.getOrPut(key) { mutableListOf() }.add(resolver) + } + } } resolvers.size } @@ -119,7 +124,7 @@ private fun loadShortcuts(shortcuts: MutableList, templates: weight = weight, requires = Requirement.parse(requires, debug), setup = Requirement.parse(setup, debug), - actions = ActionParser.Companion.parse(actions, debug), + actions = ActionParser.parse(actions, debug), produces = Requirement.parse(produces, debug, requirePredicates = false).toSet(), ), ) diff --git a/game/src/main/kotlin/content/bot/behaviour/setup/Deficit.kt b/game/src/main/kotlin/content/bot/behaviour/setup/Deficit.kt index 3f2b70aa62..29ccacbe5f 100644 --- a/game/src/main/kotlin/content/bot/behaviour/setup/Deficit.kt +++ b/game/src/main/kotlin/content/bot/behaviour/setup/Deficit.kt @@ -19,6 +19,17 @@ sealed interface Deficit { override fun resolve(player: Player): Resolver = Resolver("go_to_$area", -1, actions = listOf(BotAction.GoTo(area))) } + /* + TODO setup resolution order + if equipment + deposit all inventory items + withdraw needed items + equip all needed items + deposit all inv items + if inventory + deposit all items + withdraw needed items + */ data class Entry(val filter: Predicate, val needed: Int) data class MissingEquipment(val entries: List) : Deficit { diff --git a/game/src/main/kotlin/content/bot/req/Requirement.kt b/game/src/main/kotlin/content/bot/req/Requirement.kt index b897585994..01bd492810 100644 --- a/game/src/main/kotlin/content/bot/req/Requirement.kt +++ b/game/src/main/kotlin/content/bot/req/Requirement.kt @@ -8,6 +8,8 @@ import world.gregs.voidps.engine.entity.character.player.Player data class Requirement(val fact: Fact, val predicate: Predicate? = null) { + fun keys(): Set = (predicate?.keys() ?: emptySet()) + fact.groups() + fun check(player: Player): Boolean = predicate?.test(player, fact.getValue(player)) ?: false fun deficits(player: Player): List = predicate?.evaluator?.evaluate(player, fact, predicate) ?: emptyList() @@ -16,7 +18,7 @@ data class Requirement(val fact: Fact, val predicate: Predicate? = null fun parse(list: List>>>, name: String, requirePredicates: Boolean = true): List> { val requirements = mutableListOf>() for ((type, value) in list) { - val parser = FactParser.Companion.parsers[type] ?: error("No fact parser for '$type' in $name.") + val parser = FactParser.parsers[type] ?: error("No fact parser for '$type' in $name.") for (map in value) { val error = parser.check(map) if (error != null) { diff --git a/game/src/main/kotlin/content/bot/req/fact/Fact.kt b/game/src/main/kotlin/content/bot/req/fact/Fact.kt index 1cb6678426..f3e336335c 100644 --- a/game/src/main/kotlin/content/bot/req/fact/Fact.kt +++ b/game/src/main/kotlin/content/bot/req/fact/Fact.kt @@ -14,6 +14,19 @@ import world.gregs.voidps.engine.timer.epochSeconds import world.gregs.voidps.type.Tile /** + * TODO what is the purpose of a fact? + * 1. provides current state of a player to Requirement/predicate + * 2. Allows matching resolvers by specific produces keys + * 3. Allows grouping requirements to listen for updates + * 4. Determines order of requirements to be checked in + * + * + * TODO you want to define configuration coarse level + * but evaluates at a fine level + * execution is at a course level + * + * Coarse desired state expands into fine facts + * * A bots state which can be a [content.bot.req.Requirement] for, or a product of performing a [content.bot.behaviour.Behaviour] * @param priority Ensure bots aren't walking to locations before getting items etc... lower values are prioritised first. */ @@ -56,22 +69,22 @@ sealed class Fact(val priority: Int) { } data class IntVariable(val id: String, val default: Int) : Fact(1) { - override fun keys() = setOf("var:$id") + override fun keys() = setOf("var:${id}") override fun getValue(player: Player) = player.variables.get(id) ?: default } data class BoolVariable(val id: String, val default: Boolean?) : Fact(1) { - override fun keys() = setOf("var:$id") + override fun keys() = setOf("var:${id}") override fun getValue(player: Player) = player.variables.get(id) ?: default } data class StringVariable(val id: String, val default: String?) : Fact(1) { - override fun keys() = setOf("var:$id") + override fun keys() = setOf("var:${id}") override fun getValue(player: Player) = player.variables.get(id) ?: default } data class DoubleVariable(val id: String, val default: Double?) : Fact(1) { - override fun keys() = setOf("var:$id") + override fun keys() = setOf("var:${id}") override fun getValue(player: Player) = player.variables.get(id) ?: default } @@ -186,4 +199,5 @@ sealed class Fact(val priority: Int) { } } } + } diff --git a/game/src/main/kotlin/content/bot/req/predicate/Predicate.kt b/game/src/main/kotlin/content/bot/req/predicate/Predicate.kt index cf7a4a5aa4..372cef4334 100644 --- a/game/src/main/kotlin/content/bot/req/predicate/Predicate.kt +++ b/game/src/main/kotlin/content/bot/req/predicate/Predicate.kt @@ -15,6 +15,7 @@ sealed class Predicate { abstract fun test(player: Player, value: T): Boolean open val children: Set> = emptySet() open val evaluator: RequirementEvaluator? = null + open fun keys(): Set = emptySet() data class IntRange(val min: Int? = null, val max: Int? = null) : Predicate() { override fun test(player: Player, value: Int): Boolean { @@ -43,6 +44,7 @@ sealed class Predicate { data class InArea(val name: String) : Predicate() { override val evaluator = RequirementEvaluator.TileEval override fun test(player: Player, value: Tile) = value in Areas[name] + override fun keys() = setOf("in:${name}") } object BooleanTrue : Predicate() { @@ -89,10 +91,13 @@ sealed class Predicate { } return true } + + override fun keys() = entries.flatMap { it.filter.keys() }.toSet() } data class AnyItem(private val ids: Set) : Predicate() { override fun test(player: Player, value: Item) = value.id in ids + override fun keys() = ids } object EquipableItem : Predicate() { @@ -109,10 +114,12 @@ sealed class Predicate { data class AllOf(override val children: Set>) : Predicate() { override fun test(player: Player, value: T) = children.all { it.test(player, value) } + override fun keys() = children.flatMap { it.keys() }.toSet() } data class AnyOf(override val children: Set>) : Predicate() { override fun test(player: Player, value: T) = children.any { it.test(player, value) } + override fun keys() = children.flatMap { it.keys() }.toSet() } companion object { From 3d0840ec0329ceba6f9ce5b25c3a70441dd40d71 Mon Sep 17 00:00:00 2001 From: GregHib Date: Mon, 9 Feb 2026 21:10:30 +0000 Subject: [PATCH 080/101] Change produces to keys only --- .../misthalin/lumbridge/lumbridge.bots.toml | 4 +- data/bot/bank.setups.toml | 2 +- data/bot/combat.templates.toml | 16 +++--- data/bot/cooking.templates.toml | 8 +-- data/bot/fishing.templates.toml | 39 ++++++++++++-- data/bot/fletching.templates.toml | 8 +-- data/bot/mining.templates.toml | 32 +++++------ data/bot/prayer.bots.toml | 2 +- data/bot/shop.templates.toml | 12 ++--- data/bot/teleport.shortcuts.toml | 6 +-- data/bot/thieving.templates.toml | 4 +- data/bot/walking.setups.toml | 2 +- data/bot/woodcutting.templates.toml | 16 +++--- .../kotlin/content/bot/behaviour/Behaviour.kt | 53 +++++++++++-------- .../bot/behaviour/activity/BotActivity.kt | 2 +- .../content/bot/behaviour/navigation/Graph.kt | 7 ++- .../navigation/NavigationShortcut.kt | 2 +- .../content/bot/behaviour/setup/Resolver.kt | 2 +- 18 files changed, 126 insertions(+), 91 deletions(-) diff --git a/data/area/misthalin/lumbridge/lumbridge.bots.toml b/data/area/misthalin/lumbridge/lumbridge.bots.toml index 9cbf5c80cf..2466a87515 100644 --- a/data/area/misthalin/lumbridge/lumbridge.bots.toml +++ b/data/area/misthalin/lumbridge/lumbridge.bots.toml @@ -78,7 +78,7 @@ requires = [ { skill = { id = "fletching", min = 5 } } ] produces = [ - { carries = [{ id = "shortbow_u" }] } + { item = "shortbow_u" } ] [lumbridge_fletch_longbows] @@ -88,7 +88,7 @@ requires = [ { skill = { id = "fletching", min = 10 } } ] produces = [ - { carries = [{ id = "longbow_u" }] } + { item = "longbow_u" } ] # Woodcutting diff --git a/data/bot/bank.setups.toml b/data/bot/bank.setups.toml index 2f0ba3501d..8baaba81dc 100644 --- a/data/bot/bank.setups.toml +++ b/data/bot/bank.setups.toml @@ -5,7 +5,7 @@ actions = [ { interface = { option = "Deposit carried items", id = "bank:carried", success = { inventory_space = { min = 28 } } } }, ] produces = [ - { inventory_space = { equals = 28 } } + { item = "" } ] #[deposit_worn_items] diff --git a/data/bot/combat.templates.toml b/data/bot/combat.templates.toml index 0da866a4dd..1dd8f4f3e9 100644 --- a/data/bot/combat.templates.toml +++ b/data/bot/combat.templates.toml @@ -12,10 +12,10 @@ actions = [ { npc = { option = "Attack", id = "chicken*", delay = 5, success = { inventory_space = { max = 0 } } } }, ] produces = [ - { carries = [{ id = "feather" }] }, - { carries = [{ id = "bones" }] }, - { carries = [{ id = "raw_chicken" }] }, - { skill = { id = "$skill" } } + { item = "feather" }, + { item = "bones" }, + { item = "raw_chicken" }, + { skill = "$skill" } ] [kill_cows] @@ -33,8 +33,8 @@ actions = [ { npc = { option = "Attack", id = "cow*", delay = 5, success = { inventory_space = { max = 0 } } } }, ] produces = [ - { carries = [{ id = "bones" }] }, - { carries = [{ id = "cowhide" }] }, - { carries = [{ id = "raw_beef" }] }, - { skill = { id = "$skill" } } + { item = "bones" }, + { item = "cowhide" }, + { item = "raw_beef" }, + { skill = "$skill" } ] \ No newline at end of file diff --git a/data/bot/cooking.templates.toml b/data/bot/cooking.templates.toml index cdc69dfbe0..6f8e66cb65 100644 --- a/data/bot/cooking.templates.toml +++ b/data/bot/cooking.templates.toml @@ -14,8 +14,8 @@ actions = [ { restart = { wait_if = { has_queue = { id = "cooking" } }, success = { carries = { id = "$raw", max = 0 } } } } ] produces = [ - { skill = { id = "cooking" } }, - { carries = { id = "$cooked" } } + { skill = "cooking" }, + { item = "$cooked" } ] # Beef has a different popup so can't use normal template @@ -36,6 +36,6 @@ actions = [ { restart = { wait_if = { has_queue = { id = "cooking" } }, success = { carries = { id = "raw_beef", max = 0 } } } } ] produces = [ - { skill = { id = "cooking" } }, - { carries = { id = "beef" } } + { skill = "cooking" }, + { item = "beef" } ] diff --git a/data/bot/fishing.templates.toml b/data/bot/fishing.templates.toml index 0c0d0ac469..ce82c81fe9 100644 --- a/data/bot/fishing.templates.toml +++ b/data/bot/fishing.templates.toml @@ -11,8 +11,8 @@ actions = [ { npc = { option = "Cage", id = "$spot", delay = 5, success = { inventory_space = { max = 0 } } } }, ] produces = [ - { carries = { id = "raw_crayfish" } }, - { skill = { id = "fishing" } } + { inventory = "raw_crayfish" }, + { skill = "fishing" } ] [fish_small_net] @@ -25,8 +25,8 @@ actions = [ { npc = { option = "Net", id = "$spot", delay = 5, success = { inventory_space = { max = 0 } } } }, ] produces = [ - { carries = { id = "raw_shrimp" } }, - { skill = { id = "fishing" } } + { inventory = "raw_shrimp" }, + { skill = "fishing" } ] [fish_bait] @@ -39,5 +39,34 @@ actions = [ { npc = { option = "Net", id = "$spot", delay = 5, success = { inventory_space = { max = 0 } } } }, ] produces = [ - { skill = { id = "fishing" } } + { skill = "fishing" } ] + +# Requires +# Skill range +# variable = id, equals, default +# clock = ticks + +# Setup +# area = id +# inventory = {} + +# Actions +# inventory space max = 0 +# interface open id + +# Wait_if +#variable, id, min, default +# clock id, min + +# Success +# tile = x, y/level +# carries coins/amount +#has_queue = id + +# Produces +# skill = "name" +# area = "name" +# item = "name" +# combine into Set skill:name +# variable = "value" \ No newline at end of file diff --git a/data/bot/fletching.templates.toml b/data/bot/fletching.templates.toml index 191f742e58..8399d3721a 100644 --- a/data/bot/fletching.templates.toml +++ b/data/bot/fletching.templates.toml @@ -13,8 +13,8 @@ actions = [ { restart = { wait_if = [{ has_queue = { id = "fletching" } }], success = { inventory_space = { min = 26 } } } } ] produces = [ - { carries = { id = "arrow_shafts" } }, - { skill = { id = "fletching" } } + { item = "arrow_shafts" }, + { skill = "fletching" } ] [fletching_shortbow_template] @@ -32,7 +32,7 @@ actions = [ { restart = { success = { inventory_space = { min = 26 } } } } ] produces = [ - { skill = { id = "fletching" } } + { skill = "fletching" } ] [fletching_longbow_template] @@ -50,5 +50,5 @@ actions = [ { restart = { success = { inventory_space = { min = 26 } } } } ] produces = [ - { skill = { id = "fletching" } } + { skill = "fletching" } ] diff --git a/data/bot/mining.templates.toml b/data/bot/mining.templates.toml index 147b9c9b49..a0761bb2ff 100644 --- a/data/bot/mining.templates.toml +++ b/data/bot/mining.templates.toml @@ -11,8 +11,8 @@ actions = [ { object = { option = "Mine", id = "copper_rocks*", delay = 5, success = { inventory_space = { max = 0 } } } }, ] produces = [ - { carries = [{ id = "copper_ore" }] }, - { skill = { id = "mining" } } + { item = "copper_ore" }, + { skill = "mining" } ] [tin_ore_template] @@ -28,8 +28,8 @@ actions = [ { object = { option = "Mine", id = "tin_rocks*", delay = 5, success = { inventory_space = { max = 0 } } } }, ] produces = [ - { carries = [{ id = "tin_ore" }] }, - { skill = { id = "mining" } } + { item = "tin_ore" }, + { skill = "mining" } ] [clay_template] @@ -45,8 +45,8 @@ actions = [ { object = { option = "Mine", id = "clay_rocks*", delay = 5, success = { inventory_space = { max = 0 } } } }, ] produces = [ - { carries = [{ id = "clay" }] }, - { skill = { id = "mining" } } + { item = "clay" }, + { skill = "mining" } ] [iron_ore_template] @@ -62,8 +62,8 @@ actions = [ { object = { option = "Mine", id = "iron_rocks*", delay = 5, success = { inventory_space = { max = 0 } } } }, ] produces = [ - { carries = [{ id = "iron_ore" }] }, - { skill = { id = "mining" } } + { item = "iron_ore" }, + { skill = "mining" } ] [silver_ore_template] @@ -79,8 +79,8 @@ actions = [ { object = { option = "Mine", id = "silver_rocks*", delay = 5, success = { inventory_space = { max = 0 } } } }, ] produces = [ - { carries = [{ id = "silver_ore" }] }, - { skill = { id = "mining" } } + { item = "silver_ore" }, + { skill = "mining" } ] [coal_template] @@ -96,8 +96,8 @@ actions = [ { object = { option = "Mine", id = "coal_rocks*", delay = 10, success = { inventory_space = { max = 0 } } } }, ] produces = [ - { carries = [{ id = "coal" }] }, - { skill = { id = "mining" } } + { item = "coal" }, + { skill = "mining" } ] [gold_ore_template] @@ -113,8 +113,8 @@ actions = [ { object = { option = "Mine", id = "gold_rocks*", delay = 15, success = { inventory_space = { max = 0 } } } }, ] produces = [ - { carries = [{ id = "gold_ore" }] }, - { skill = { id = "mining" } } + { item = "gold_ore" }, + { skill = "mining" } ] [mithril_ore_template] @@ -130,6 +130,6 @@ actions = [ { object = { option = "Mine", id = "mithril_rocks*", delay = 15, success = { inventory_space = { max = 0 } } } }, ] produces = [ - { carries = [{ id = "mithril_ore" }] }, - { skill = { id = "mining" } } + { item = "mithril_ore" }, + { skill = "mining" } ] diff --git a/data/bot/prayer.bots.toml b/data/bot/prayer.bots.toml index afe06d0463..c8ed980066 100644 --- a/data/bot/prayer.bots.toml +++ b/data/bot/prayer.bots.toml @@ -11,5 +11,5 @@ actions = [ { restart = { wait_if = [{ variable = { id = "bone_delay", min = 0, default = -1 } }], success = { inventory_space = { min = 28 } } } } ] produces = [ - { skill = { id = "prayer" } } + { skill = "prayer" } ] diff --git a/data/bot/shop.templates.toml b/data/bot/shop.templates.toml index b6dce9e270..50152b5518 100644 --- a/data/bot/shop.templates.toml +++ b/data/bot/shop.templates.toml @@ -9,7 +9,7 @@ actions = [ { interface = { option = "Buy-1", id = "shop:stock:$item", success = { carries = [{ id = "$item" }] } } }, ] produces = [ - { carries = [{ id = "$item", equals = 1 }] } + { item = "$item" } ] [take_shop_sample] @@ -22,7 +22,7 @@ actions = [ { interface = { option = "Take-1", id = "shop:sample:$item", success = { carries = [{ id = "$item" }] } } }, ] produces = [ - { carries = [{ id = "$item", equals = 1 }] } + { item = "$item" } ] [sell_50_to_shop] @@ -37,7 +37,7 @@ actions = [ { interface_close = { id = "shop" } } ] produces = [ - { carries = { id = "coins" } } + { item = "coins" } ] [sell_10_to_shop] @@ -52,7 +52,7 @@ actions = [ { interface_close = { id = "shop" } } ] produces = [ - { carries = { id = "coins" } } + { item = "coins" } ] [sell_5_to_shop] @@ -67,7 +67,7 @@ actions = [ { interface_close = { id = "shop" } } ] produces = [ - { carries = { id = "coins" } } + { item = "coins" } ] [sell_to_shop] @@ -82,5 +82,5 @@ actions = [ { interface_close = { id = "shop" } } ] produces = [ - { carries = { id = "coins" } } + { item = "coins" } ] diff --git a/data/bot/teleport.shortcuts.toml b/data/bot/teleport.shortcuts.toml index 43e2838afd..b66962bd7d 100644 --- a/data/bot/teleport.shortcuts.toml +++ b/data/bot/teleport.shortcuts.toml @@ -36,7 +36,7 @@ actions = [ { wait = { ticks = 5 } }, ] produces = [ - { area = { id = "varrock_teleport" } } + { area = "varrock_teleport" } ] [teleport_varrock_via_bank] @@ -59,7 +59,7 @@ actions = [ # { wait = { location = "varrock_teleport" } }, ] produces = [ - { area = { id = "varrock_teleport" } } + { area = "varrock_teleport" } ] [teleport_lumbridge] @@ -73,5 +73,5 @@ actions = [ { wait = { ticks = 25 } }, ] produces = [ - { area = { id = "lumbridge_teleport" } } + { area = "lumbridge_teleport" } ] diff --git a/data/bot/thieving.templates.toml b/data/bot/thieving.templates.toml index ab17df3bf8..cf177f0b4b 100644 --- a/data/bot/thieving.templates.toml +++ b/data/bot/thieving.templates.toml @@ -15,6 +15,6 @@ actions = [ ], success = { skill = { id = "constitution", max = 20 } } } } ] produces = [ - { carries = [{ id = "coins" }] }, - { skill = { id = "thieving" } } + { item = "coins" }, + { skill = "thieving" } ] \ No newline at end of file diff --git a/data/bot/walking.setups.toml b/data/bot/walking.setups.toml index e1a12f746e..8b8be2acb0 100644 --- a/data/bot/walking.setups.toml +++ b/data/bot/walking.setups.toml @@ -19,5 +19,5 @@ actions = [ { interface = { option = "Turn Run mode on", id = "energy_orb:run_background" } } ] produces = [ - { variable = { id = "movement", equals = "run", default = "walk" } } + { variable = "movement" } ] diff --git a/data/bot/woodcutting.templates.toml b/data/bot/woodcutting.templates.toml index 742c1283a8..657768d692 100644 --- a/data/bot/woodcutting.templates.toml +++ b/data/bot/woodcutting.templates.toml @@ -11,8 +11,8 @@ actions = [ { object = { option = "Chop down", id = "tree*", delay = 5, success = { inventory_space = { max = 0 } } } }, ] produces = [ - { carries = [{ id = "logs" }] }, - { skill = { id = "woodcutting" } } + { item = "logs" }, + { skill = "woodcutting" } ] [oak_tree_template] @@ -28,8 +28,8 @@ actions = [ { object = { option = "Chop down", id = "oak*", delay = 5, success = { inventory_space = { max = 0 } } } }, ] produces = [ - { carries = [{ id = "oak_logs" }] }, - { skill = { id = "woodcutting" } } + { item = "oak_logs" }, + { skill = "woodcutting" } ] [willow_tree_template] @@ -45,8 +45,8 @@ actions = [ { object = { option = "Chop down", id = "willow*", delay = 5, success = { inventory_space = { max = 0 } } } }, ] produces = [ - { carries = [{ id = "willow_logs" }] }, - { skill = { id = "woodcutting" } } + { item = "willow_logs" }, + { skill = "woodcutting" } ] [yew_tree_template] @@ -62,6 +62,6 @@ actions = [ { object = { option = "Chop down", id = "yew*", delay = 5, success = { inventory_space = { max = 0 } } } }, ] produces = [ - { carries = [{ id = "yew_logs" }] }, - { skill = { id = "woodcutting" } } + { item = "yew_logs" }, + { skill = "woodcutting" } ] diff --git a/game/src/main/kotlin/content/bot/behaviour/Behaviour.kt b/game/src/main/kotlin/content/bot/behaviour/Behaviour.kt index f0117305f4..f38c18ac82 100644 --- a/game/src/main/kotlin/content/bot/behaviour/Behaviour.kt +++ b/game/src/main/kotlin/content/bot/behaviour/Behaviour.kt @@ -18,7 +18,7 @@ interface Behaviour { val requires: List> val setup: List> val actions: List - val produces: Set> + val produces: Set } fun loadBehaviours( @@ -59,7 +59,7 @@ private fun loadActivities(activities: MutableMap, template requires = Requirement.parse(requires, debug), setup = Requirement.parse(setup, debug), actions = ActionParser.parse(actions, debug), - produces = Requirement.parse(produces, debug, requirePredicates = false).toSet(), + produces = produces, ) } } @@ -80,29 +80,24 @@ private fun loadSetups(resolvers: MutableMap>, tem fragments.add(Fragment(id, template, fields, weight, requires, setup, actions, produces)) } else { val debug = "$id ${exception()}" - val products = Requirement.parse(produces, debug) val resolver = Resolver( id = id, weight = weight, requires = Requirement.parse(requires, debug), setup = Requirement.parse(setup, debug), actions = ActionParser.parse(actions, debug), - produces = products.toSet(), + produces = produces, ) - for (product in products) { - for (key in product.keys()) { - resolvers.getOrPut(key) { mutableListOf() }.add(resolver) - } + for (key in produces) { + resolvers.getOrPut(key) { mutableListOf() }.add(resolver) } } } for (fragment in fragments) { val template = templates[fragment.template] ?: error("Unable to find template '${fragment.template}' for ${fragment.id}.") val resolver = fragment.resolver(template) - for (product in resolver.produces) { - for (key in product.keys()) { - resolvers.getOrPut(key) { mutableListOf() }.add(resolver) - } + for (key in resolver.produces) { + resolvers.getOrPut(key) { mutableListOf() }.add(resolver) } } resolvers.size @@ -125,7 +120,7 @@ private fun loadShortcuts(shortcuts: MutableList, templates: requires = Requirement.parse(requires, debug), setup = Requirement.parse(setup, debug), actions = ActionParser.parse(actions, debug), - produces = Requirement.parse(produces, debug, requirePredicates = false).toSet(), + produces = produces, ), ) } @@ -138,7 +133,7 @@ private fun loadShortcuts(shortcuts: MutableList, templates: } } -private fun load(paths: List, block: ConfigReader.(String, String?, Map?, Int, List>>>, List>>>, List>>, List>>>) -> Unit) { +private fun load(paths: List, block: ConfigReader.(String, String?, Map?, Int, List>>>, List>>>, List>>, Set) -> Unit) { for (path in paths) { Config.fileReader(path) { while (nextSection()) { @@ -149,14 +144,14 @@ private fun load(paths: List, block: ConfigReader.(String, String?, Map< val requires = mutableListOf>>>() val setup = mutableListOf>>>() val actions = mutableListOf>>() - val produces = mutableListOf>>>() + val produces = mutableSetOf() while (nextPair()) { when (val key = key()) { "template" -> template = string() "requires" -> requirements(requires) "setup" -> requirements(setup) "actions" -> actions(actions) - "produces" -> requirements(produces) + "produces" -> produces(produces) "weight", "capacity" -> value = int() "fields" -> fields = map() else -> throw IllegalArgumentException("Unexpected key: '$key' ${exception()}") @@ -181,13 +176,13 @@ private fun loadTemplates(paths: List): Map { val requires = mutableListOf>>>() val setup = mutableListOf>>>() val actions = mutableListOf>>() - val produces = mutableListOf>>>() + val produces = mutableSetOf() while (nextPair()) { when (val key = key()) { "requires" -> requirements(requires) "setup" -> requirements(setup) "actions" -> actions(actions) - "produces" -> requirements(produces) + "produces" -> produces(produces) else -> throw IllegalArgumentException("Unexpected key: '$key' ${exception()}") } } @@ -217,6 +212,16 @@ internal fun ConfigReader.requirements(requires: MutableList) { + while (nextElement()) { + while (nextEntry()) { + val key = key() + val value = string() + requires.add("$key:$value") + } + } +} + internal fun ConfigReader.actions(list: MutableList>>) { while (nextElement()) { while (nextEntry()) { @@ -235,7 +240,7 @@ private data class Fragment( val requires: List>>>, val setup: List>>>, val actions: List>>, - val produces: List>>>, + val produces: Set, ) { fun activity(template: Template) = BotActivity( id = id, @@ -243,9 +248,11 @@ private data class Fragment( requires = resolveRequirements(template.requires, requires), setup = resolveRequirements(template.setup, setup), actions = resolveActions(template.actions, actions), - produces = resolveRequirements(template.produces, produces, requirePredicates = false).toSet(), + produces = resolve(template.produces) + produces, ) + private fun resolve(set: Set): Set = set + private fun resolveRequirements(templated: List>>>, original: List>>>, requirePredicates: Boolean = true): List> { val combinedList = mutableListOf>>>() combinedList.addAll(original) @@ -319,7 +326,7 @@ private data class Fragment( requires = resolveRequirements(template.requires, requires), setup = resolveRequirements(template.setup, setup), actions = resolveActions(template.actions, actions), - produces = resolveRequirements(template.produces, produces, requirePredicates = false).toSet(), + produces = resolve(template.produces) + produces, ) fun shortcut(template: Template) = NavigationShortcut( @@ -328,7 +335,7 @@ private data class Fragment( requires = resolveRequirements(template.requires, requires), setup = resolveRequirements(template.setup, setup), actions = resolveActions(template.actions, actions), - produces = resolveRequirements(template.produces, produces, requirePredicates = false).toSet(), + produces = resolve(template.produces) + produces, ) } @@ -336,5 +343,5 @@ private data class Template( val requires: List>>>, val setup: List>>>, val actions: List>>, - val produces: List>>>, + val produces: Set, ) diff --git a/game/src/main/kotlin/content/bot/behaviour/activity/BotActivity.kt b/game/src/main/kotlin/content/bot/behaviour/activity/BotActivity.kt index 890d06c5b3..f4f444be0b 100644 --- a/game/src/main/kotlin/content/bot/behaviour/activity/BotActivity.kt +++ b/game/src/main/kotlin/content/bot/behaviour/activity/BotActivity.kt @@ -14,5 +14,5 @@ data class BotActivity( override val requires: List> = emptyList(), override val setup: List> = emptyList(), override val actions: List = emptyList(), - override val produces: Set> = emptySet(), + override val produces: Set = emptySet(), ) : Behaviour diff --git a/game/src/main/kotlin/content/bot/behaviour/navigation/Graph.kt b/game/src/main/kotlin/content/bot/behaviour/navigation/Graph.kt index 1c3fed9aa9..817cd3d581 100644 --- a/game/src/main/kotlin/content/bot/behaviour/navigation/Graph.kt +++ b/game/src/main/kotlin/content/bot/behaviour/navigation/Graph.kt @@ -7,7 +7,6 @@ import content.bot.behaviour.requirements import content.bot.bot import content.bot.isBot import content.bot.req.Requirement -import content.bot.req.predicate.Predicate import world.gregs.config.Config import world.gregs.config.ConfigReader import world.gregs.voidps.engine.data.definition.Areas @@ -163,11 +162,11 @@ class Graph( } fun add(shortcut: NavigationShortcut): Int { - val first = shortcut.produces.map { it.predicate }.filterIsInstance().firstOrNull() ?: throw IllegalArgumentException("Shortcut requires location product ${shortcut.id}") - val area = Areas[first.name] + val name = shortcut.produces.firstOrNull { it.startsWith("area:") }?.removePrefix("area:") ?: throw IllegalArgumentException("Shortcut requires location product ${shortcut.id}") + val area = Areas[name] val end = tiles.indexOfFirst { it in area } if (end == -1) { - throw IllegalArgumentException("Unable to find nav graph tile in shortcut area '${first.name}'.") + throw IllegalArgumentException("Unable to find nav graph tile in shortcut area '${name}'.") } val index = addEdge(0, end, shortcut.weight, shortcut.actions, shortcut.requires) shortcuts[index] = shortcut diff --git a/game/src/main/kotlin/content/bot/behaviour/navigation/NavigationShortcut.kt b/game/src/main/kotlin/content/bot/behaviour/navigation/NavigationShortcut.kt index 2d9ad747ca..cd787590b9 100644 --- a/game/src/main/kotlin/content/bot/behaviour/navigation/NavigationShortcut.kt +++ b/game/src/main/kotlin/content/bot/behaviour/navigation/NavigationShortcut.kt @@ -10,5 +10,5 @@ data class NavigationShortcut( override val requires: List> = emptyList(), override val setup: List> = emptyList(), override val actions: List = emptyList(), - override val produces: Set> = emptySet(), + override val produces: Set = emptySet(), ) : Behaviour diff --git a/game/src/main/kotlin/content/bot/behaviour/setup/Resolver.kt b/game/src/main/kotlin/content/bot/behaviour/setup/Resolver.kt index cb1d956b04..3ef23fc7f4 100644 --- a/game/src/main/kotlin/content/bot/behaviour/setup/Resolver.kt +++ b/game/src/main/kotlin/content/bot/behaviour/setup/Resolver.kt @@ -15,5 +15,5 @@ data class Resolver( override val requires: List> = emptyList(), override val setup: List> = emptyList(), override val actions: List = emptyList(), - override val produces: Set> = emptySet(), + override val produces: Set = emptySet(), ) : Behaviour From 862a9457aad82ebde4352383b86a01a3a3b953ee Mon Sep 17 00:00:00 2001 From: GregHib Date: Mon, 9 Feb 2026 22:48:40 +0000 Subject: [PATCH 081/101] Rename carries to inventory --- data/bot/al_kharid.nav-edges.toml | 8 ++--- data/bot/cooking.templates.toml | 8 ++--- data/bot/fishing.templates.toml | 18 ++++++++---- data/bot/fletching.templates.toml | 6 ++-- data/bot/mining.templates.toml | 24 +++++---------- data/bot/prayer.bots.toml | 2 +- data/bot/shop.templates.toml | 29 ++++++++----------- data/bot/teleport.shortcuts.toml | 2 +- data/bot/woodcutting.templates.toml | 12 +++----- .../kotlin/content/bot/req/fact/FactParser.kt | 2 +- 10 files changed, 50 insertions(+), 61 deletions(-) diff --git a/data/bot/al_kharid.nav-edges.toml b/data/bot/al_kharid.nav-edges.toml index 7bf93942dd..88d8b8aa61 100644 --- a/data/bot/al_kharid.nav-edges.toml +++ b/data/bot/al_kharid.nav-edges.toml @@ -10,10 +10,10 @@ edges = [ { from = { x = 3278, y = 3228 }, to = { x = 3268, y = 3228 } }, # al_kharid_crossroad_to_tollgate_north { from = { x = 3278, y = 3228 }, to = { x = 3268, y = 3227 } }, # al_kharid_crossroad_to_tollgate { from = { x = 3278, y = 3228 }, to = { x = 3280, y = 3216 } }, # al_kharid_crossroad_to_glider - { from = { x = 3267, y = 3228 }, to = { x = 3268, y = 3228 }, cost = 1, actions = [{ object = { option = "Pay-toll(10gp)", id = "toll_gate_al_kharid_north", x = 3268, y = 3228, success = { tile = { x = 3268, y = 3228 } } } }], requires = [{ carries = [{ id = "coins", amount = 10 }] }] }, # lumbridge_tollgate_north_to_al_kharid_tollgate - { from = { x = 3268, y = 3228 }, to = { x = 3267, y = 3228 }, cost = 1, actions = [{ object = { option = "Pay-toll(10gp)", id = "toll_gate_al_kharid_north", x = 3268, y = 3228, success = { tile = { x = 3267, y = 3228 } } } }], requires = [{ carries = [{ id = "coins", amount = 10 }] }] }, # al_kharid_tollgate_north_to_lumbridge_tollgate - { from = { x = 3267, y = 3227 }, to = { x = 3268, y = 3227 }, cost = 1, actions = [{ object = { option = "Pay-toll(10gp)", id = "toll_gate_al_kharid_south", x = 3268, y = 3227, success = { tile = { x = 3268, y = 3227 } } } }], requires = [{ carries = [{ id = "coins", amount = 10 }] }] }, # lumbridge_tollgate_to_al_kharid - { from = { x = 3268, y = 3227 }, to = { x = 3267, y = 3227 }, cost = 1, actions = [{ object = { option = "Pay-toll(10gp)", id = "toll_gate_al_kharid_south", x = 3268, y = 3227, success = { tile = { x = 3267, y = 3227 } } } }], requires = [{ carries = [{ id = "coins", amount = 10 }] }] }, # al_kharid_tollgate_to_lumbridge + { from = { x = 3267, y = 3228 }, to = { x = 3268, y = 3228 }, cost = 1, actions = [{ object = { option = "Pay-toll(10gp)", id = "toll_gate_al_kharid_north", x = 3268, y = 3228, success = { tile = { x = 3268, y = 3228 } } } }], requires = [{ inventory = [{ id = "coins", amount = 10 }] }] }, # lumbridge_tollgate_north_to_al_kharid_tollgate + { from = { x = 3268, y = 3228 }, to = { x = 3267, y = 3228 }, cost = 1, actions = [{ object = { option = "Pay-toll(10gp)", id = "toll_gate_al_kharid_north", x = 3268, y = 3228, success = { tile = { x = 3267, y = 3228 } } } }], requires = [{ inventory = [{ id = "coins", amount = 10 }] }] }, # al_kharid_tollgate_north_to_lumbridge_tollgate + { from = { x = 3267, y = 3227 }, to = { x = 3268, y = 3227 }, cost = 1, actions = [{ object = { option = "Pay-toll(10gp)", id = "toll_gate_al_kharid_south", x = 3268, y = 3227, success = { tile = { x = 3268, y = 3227 } } } }], requires = [{ inventory = [{ id = "coins", amount = 10 }] }] }, # lumbridge_tollgate_to_al_kharid + { from = { x = 3268, y = 3227 }, to = { x = 3267, y = 3227 }, cost = 1, actions = [{ object = { option = "Pay-toll(10gp)", id = "toll_gate_al_kharid_south", x = 3268, y = 3227, success = { tile = { x = 3267, y = 3227 } } } }], requires = [{ inventory = [{ id = "coins", amount = 10 }] }] }, # al_kharid_tollgate_to_lumbridge { from = { x = 3268, y = 3227 }, to = { x = 3280, y = 3216 } }, # al_kharid_tollgate_to_glider { from = { x = 3268, y = 3228 }, to = { x = 3280, y = 3216 } }, # al_kharid_tollgate_north_to_glider { from = { x = 3280, y = 3216 }, to = { x = 3292, y = 3215 } }, # al_kharid_glider_to_musician diff --git a/data/bot/cooking.templates.toml b/data/bot/cooking.templates.toml index 6f8e66cb65..a69b0e39e5 100644 --- a/data/bot/cooking.templates.toml +++ b/data/bot/cooking.templates.toml @@ -4,14 +4,14 @@ requires = [ { skill = { id = "cooking", min = "$level" } }, ] setup = [ - { carries = { id = "$raw", amount = 28 } }, + { inventory = { id = "$raw", min = 28 } }, { area = { id = "$location" } }, ] actions = [ { item_on_object = { id = "$raw", object = "$obj", success = { interface_open = { id = "dialogue_skill_creation" } } } }, { interface = { option = "All", id = "skill_creation_amount:all" } }, { continue = { id = "dialogue_skill_creation:choice1" } }, - { restart = { wait_if = { has_queue = { id = "cooking" } }, success = { carries = { id = "$raw", max = 0 } } } } + { restart = { wait_if = { has_queue = { id = "cooking" } }, success = { inventory = { id = "$raw", max = 0 } } } } ] produces = [ { skill = "cooking" }, @@ -25,7 +25,7 @@ requires = [ { skill = { id = "cooking", min = 1 } }, ] setup = [ - { carries = { id = "raw_beef", amount = 28 } }, + { inventory = { id = "raw_beef", min = 28 } }, { area = { id = "$location" } }, ] actions = [ @@ -33,7 +33,7 @@ actions = [ { continue = { option = "Continue", id = "dialogue_multi2:line2", success = { interface_open = { id = "dialogue_skill_creation" } } } }, { interface = { option = "All", id = "skill_creation_amount:all" } }, { continue = { id = "dialogue_skill_creation:choice1" } }, - { restart = { wait_if = { has_queue = { id = "cooking" } }, success = { carries = { id = "raw_beef", max = 0 } } } } + { restart = { wait_if = { has_queue = { id = "cooking" } }, success = { inventory = { id = "raw_beef", max = 0 } } } } ] produces = [ { skill = "cooking" }, diff --git a/data/bot/fishing.templates.toml b/data/bot/fishing.templates.toml index ce82c81fe9..6dc8118549 100644 --- a/data/bot/fishing.templates.toml +++ b/data/bot/fishing.templates.toml @@ -3,9 +3,11 @@ requires = [ { skill = { id = "fishing", min = 1 } }, ] setup = [ - { carries = { id = "crayfish_cage" } }, + { inventory = [ + { id = "crayfish_cage" }, + { id = "", min = 27 } + ] }, { area = { id = "$location" } }, - { inventory_space = { min = 27 } }, ] actions = [ { npc = { option = "Cage", id = "$spot", delay = 5, success = { inventory_space = { max = 0 } } } }, @@ -17,9 +19,11 @@ produces = [ [fish_small_net] setup = [ - { carries = { id = "small_fishing_net" } }, + { inventory = [ + { id = "small_fishing_net" }, + { id = "", min = 27 } + ] }, { area = { id = "$location" } }, - { inventory_space = { min = 27 } }, ] actions = [ { npc = { option = "Net", id = "$spot", delay = 5, success = { inventory_space = { max = 0 } } } }, @@ -31,9 +35,11 @@ produces = [ [fish_bait] setup = [ - { carries = { id = "small_fishing_net" } }, + { inventory = [ + { id = "small_fishing_net" }, + { id = "", min = 27 } + ] }, { area = { id = "$location" } }, - { inventory_space = { min = 27 } }, ] actions = [ { npc = { option = "Net", id = "$spot", delay = 5, success = { inventory_space = { max = 0 } } } }, diff --git a/data/bot/fletching.templates.toml b/data/bot/fletching.templates.toml index 8399d3721a..edae56d7e8 100644 --- a/data/bot/fletching.templates.toml +++ b/data/bot/fletching.templates.toml @@ -4,7 +4,7 @@ requires = [ ] setup = [ { area = { id = "$location" } }, - { carries = [{ id = "knife" }, { id = "$logs", min = 27 }] }, + { inventory = [{ id = "knife" }, { id = "$logs", min = 27 }] }, ] actions = [ { item_on_item = { id = "knife", on = "$logs", success = { interface_open = { id = "dialogue_skill_creation" } } } }, @@ -23,7 +23,7 @@ requires = [ ] setup = [ { area = { id = "$location" } }, - { carries = [{ id = "knife" }, { id = "$logs", min = 27 }] }, + { inventory = [{ id = "knife" }, { id = "$logs", min = 27 }] }, ] actions = [ { item_on_item = { id = "knife", on = "$logs" } }, @@ -41,7 +41,7 @@ requires = [ ] setup = [ { area = { id = "$location" } }, - { carries = [{ id = "knife" }, { id = "$logs", min = 27 }] }, + { inventory = [{ id = "knife" }, { id = "$logs", min = 27 }] }, ] actions = [ { item_on_item = { id = "knife", on = "$logs", success = { has_queue = { id = "fletching_make_dialog" } } } }, diff --git a/data/bot/mining.templates.toml b/data/bot/mining.templates.toml index a0761bb2ff..8cfe01f417 100644 --- a/data/bot/mining.templates.toml +++ b/data/bot/mining.templates.toml @@ -3,9 +3,8 @@ requires = [ { skill = { id = "mining", min = 1, max = 15 } }, ] setup = [ - { carries = [{ id = "steel_pickaxe,iron_pickaxe,bronze_pickaxe", usable = true }] }, + { inventory = [{ id = "steel_pickaxe,iron_pickaxe,bronze_pickaxe", usable = true }, { id = "", min = 27 }] }, { area = { id = "$location" } }, - { inventory_space = { min = 27 } }, ] actions = [ { object = { option = "Mine", id = "copper_rocks*", delay = 5, success = { inventory_space = { max = 0 } } } }, @@ -20,9 +19,8 @@ requires = [ { skill = { id = "mining", min = 1, max = 15 } }, ] setup = [ - { carries = [{ id = "steel_pickaxe,iron_pickaxe,bronze_pickaxe", usable = true }] }, + { inventory = [{ id = "steel_pickaxe,iron_pickaxe,bronze_pickaxe", usable = true }, { id = "", min = 27 }] }, { area = { id = "$location" } }, - { inventory_space = { min = 27 } }, ] actions = [ { object = { option = "Mine", id = "tin_rocks*", delay = 5, success = { inventory_space = { max = 0 } } } }, @@ -37,9 +35,8 @@ requires = [ { skill = { id = "mining", min = 1, max = 15 } }, ] setup = [ - { carries = [{ id = "steel_pickaxe,iron_pickaxe,bronze_pickaxe", usable = true }] }, + { inventory = [{ id = "steel_pickaxe,iron_pickaxe,bronze_pickaxe", usable = true }, { id = "", min = 27 }] }, { area = { id = "$location" } }, - { inventory_space = { min = 27 } }, ] actions = [ { object = { option = "Mine", id = "clay_rocks*", delay = 5, success = { inventory_space = { max = 0 } } } }, @@ -54,9 +51,8 @@ requires = [ { skill = { id = "mining", min = 15, max = 40 } }, ] setup = [ - { carries = [{ id = "adamant_pickaxe,mithril_pickaxe,steel_pickaxe", usable = true }] }, + { inventory = [{ id = "adamant_pickaxe,mithril_pickaxe,steel_pickaxe", usable = true }, { id = "", min = 27 }] }, { area = { id = "$location" } }, - { inventory_space = { min = 27 } }, ] actions = [ { object = { option = "Mine", id = "iron_rocks*", delay = 5, success = { inventory_space = { max = 0 } } } }, @@ -71,9 +67,8 @@ requires = [ { skill = { id = "mining", min = 20, max = 40 } }, ] setup = [ - { carries = [{ id = "adamant_pickaxe,mithril_pickaxe,steel_pickaxe", usable = true }] }, + { inventory = [{ id = "adamant_pickaxe,mithril_pickaxe,steel_pickaxe", usable = true }, { id = "", min = 27 }] }, { area = { id = "$location" } }, - { inventory_space = { min = 27 } }, ] actions = [ { object = { option = "Mine", id = "silver_rocks*", delay = 5, success = { inventory_space = { max = 0 } } } }, @@ -88,9 +83,8 @@ requires = [ { skill = { id = "mining", min = 30, max = 55 } }, ] setup = [ - { carries = [{ id = "rune_pickaxe,adamant_pickaxe,mithril_pickaxe", usable = true }] }, + { inventory = [{ id = "rune_pickaxe,adamant_pickaxe,mithril_pickaxe", usable = true }, { id = "", min = 27 }] }, { area = { id = "$location" } }, - { inventory_space = { min = 27 } }, ] actions = [ { object = { option = "Mine", id = "coal_rocks*", delay = 10, success = { inventory_space = { max = 0 } } } }, @@ -105,9 +99,8 @@ requires = [ { skill = { id = "mining", min = 40, max = 60 } }, ] setup = [ - { carries = [{ id = "rune_pickaxe,adamant_pickaxe,mithril_pickaxe", usable = true }] }, + { inventory = [{ id = "rune_pickaxe,adamant_pickaxe,mithril_pickaxe", usable = true }, { id = "", min = 27 }] }, { area = { id = "$location" } }, - { inventory_space = { min = 27 } }, ] actions = [ { object = { option = "Mine", id = "gold_rocks*", delay = 15, success = { inventory_space = { max = 0 } } } }, @@ -122,9 +115,8 @@ requires = [ { skill = { id = "mining", min = 55, max = 70 } }, ] setup = [ - { carries = [{ id = "dragon_pickaxe,rune_pickaxe,adamant_pickaxe", usable = true }] }, + { inventory = [{ id = "dragon_pickaxe,rune_pickaxe,adamant_pickaxe", usable = true }, { id = "", min = 27 }] }, { area = { id = "$location" } }, - { inventory_space = { min = 27 } }, ] actions = [ { object = { option = "Mine", id = "mithril_rocks*", delay = 15, success = { inventory_space = { max = 0 } } } }, diff --git a/data/bot/prayer.bots.toml b/data/bot/prayer.bots.toml index c8ed980066..c55cd14e43 100644 --- a/data/bot/prayer.bots.toml +++ b/data/bot/prayer.bots.toml @@ -4,7 +4,7 @@ requires = [ { owns = { id = "bones", min = 28 } }, ] setup = [ - { carries = [{ id = "bones", min = 28 }] }, + { inventory = [{ id = "bones", min = 28 }] }, ] actions = [ { interface = { option = "Bury", id = "inventory:inventory:bones" } }, diff --git a/data/bot/shop.templates.toml b/data/bot/shop.templates.toml index 50152b5518..14a1783982 100644 --- a/data/bot/shop.templates.toml +++ b/data/bot/shop.templates.toml @@ -1,12 +1,11 @@ [buy_from_shop] setup = [ - { carries = [{ id = "coins", min = "$cost" }] }, + { inventory = [{ id = "coins", min = "$cost" }, { id = "", min = 1 }] }, { area = { id = "$shop_location" } }, - { inventory_space = { min = 1 } }, ] actions = [ { npc = { option = "Trade", id = "$shopkeeper", success = { interface_open = { id = "shop" } } } }, - { interface = { option = "Buy-1", id = "shop:stock:$item", success = { carries = [{ id = "$item" }] } } }, + { interface = { option = "Buy-1", id = "shop:stock:$item", success = { inventory = [{ id = "$item" }] } } }, ] produces = [ { item = "$item" } @@ -14,12 +13,12 @@ produces = [ [take_shop_sample] setup = [ + { inventory = [{ id = "", min = 1 }] }, { area = { id = "$shop_location" } }, - { inventory_space = { min = 1 } }, ] actions = [ { npc = { option = "Trade", id = "$shopkeeper", success = { interface_open = { id = "shop" } } } }, - { interface = { option = "Take-1", id = "shop:sample:$item", success = { carries = [{ id = "$item" }] } } }, + { interface = { option = "Take-1", id = "shop:sample:$item", success = { inventory = [{ id = "$item" }] } } }, ] produces = [ { item = "$item" } @@ -27,13 +26,12 @@ produces = [ [sell_50_to_shop] setup = [ - { carries = [{ id = "$item", min = "50" }] }, + { inventory = [{ id = "$item", min = "50" }, { id = "", min = 1 }] }, { area = { id = "$shop_location" } }, - { inventory_space = { min = 1 } }, ] actions = [ { npc = { option = "Trade", id = "$shopkeeper", success = { interface_open = { id = "shop" } } } }, - { interface = { option = "Sell-50", id = "shop_side:inventory:$item", success = { carries = [{ id = "coins" }] } } }, + { interface = { option = "Sell-50", id = "shop_side:inventory:$item", success = { inventory = [{ id = "coins" }] } } }, { interface_close = { id = "shop" } } ] produces = [ @@ -42,13 +40,12 @@ produces = [ [sell_10_to_shop] setup = [ - { carries = [{ id = "$item", min = "10" }] }, + { inventory = [{ id = "$item", min = "10" }, { id = "", min = 1 }] }, { area = { id = "$shop_location" } }, - { inventory_space = { min = 1 } }, ] actions = [ { npc = { option = "Trade", id = "$shopkeeper", success = { interface_open = { id = "shop" } } } }, - { interface = { option = "Sell-10", id = "shop_side:inventory:$item", success = { carries = [{ id = "coins" }] } } }, + { interface = { option = "Sell-10", id = "shop_side:inventory:$item", success = { inventory = [{ id = "coins" }] } } }, { interface_close = { id = "shop" } } ] produces = [ @@ -57,13 +54,12 @@ produces = [ [sell_5_to_shop] setup = [ - { carries = [{ id = "$item", min = "5" }] }, + { inventory = [{ id = "$item", min = "5" }, { id = "", min = 1 }] }, { area = { id = "$shop_location" } }, - { inventory_space = { min = 1 } }, ] actions = [ { npc = { option = "Trade", id = "$shopkeeper", success = { interface_open = { id = "shop" } } } }, - { interface = { option = "Sell-5", id = "shop_side:inventory:$item", success = { carries = [{ id = "coins" }] } } }, + { interface = { option = "Sell-5", id = "shop_side:inventory:$item", success = { inventory = [{ id = "coins" }] } } }, { interface_close = { id = "shop" } } ] produces = [ @@ -72,13 +68,12 @@ produces = [ [sell_to_shop] setup = [ - { carries = [{ id = "$item", min = "1" }] }, + { inventory = [{ id = "$item", min = "1" }, { id = "", min = 1 }] }, { area = { id = "$shop_location" } }, - { inventory_space = { min = 1 } }, ] actions = [ { npc = { option = "Trade", id = "$shopkeeper", success = { interface_open = { id = "shop" } } } }, - { interface = { option = "Sell-1", id = "shop_side:inventory:$item", success = { carries = [{ id = "coins" }] } } }, + { interface = { option = "Sell-1", id = "shop_side:inventory:$item", success = { inventory = [{ id = "coins" }] } } }, { interface_close = { id = "shop" } } ] produces = [ diff --git a/data/bot/teleport.shortcuts.toml b/data/bot/teleport.shortcuts.toml index b66962bd7d..5812c9f6d2 100644 --- a/data/bot/teleport.shortcuts.toml +++ b/data/bot/teleport.shortcuts.toml @@ -28,8 +28,8 @@ [teleport_varrock] weight = 125 requires = [ + { inventory = [{ id = "fire_rune", min = 1 }, { id = "air_rune", min = 3 }, { id = "law_rune", min = 1 }] }, { variable = { id = "spellbook_config", equals = 0, default = 0 } }, - { carries = [{ id = "fire_rune", min = 1 }, { id = "air_rune", min = 3 }, { id = "law_rune", min = 1 }] }, ] actions = [ { interface = { option = "Cast", id = "modern_spellbook:varrock_teleport" } }, diff --git a/data/bot/woodcutting.templates.toml b/data/bot/woodcutting.templates.toml index 657768d692..71a129f557 100644 --- a/data/bot/woodcutting.templates.toml +++ b/data/bot/woodcutting.templates.toml @@ -3,9 +3,8 @@ requires = [ { skill = { id = "woodcutting", min = 1, max = 15 } }, ] setup = [ - { carries = [{ id = "steel_hatchet,iron_hatchet,bronze_hatchet" }] }, + { inventory = [{ id = "steel_hatchet,iron_hatchet,bronze_hatchet" }, { id = "", min = 27 }] }, { area = { id = "$location" } }, - { inventory_space = { min = 27 } }, ] actions = [ { object = { option = "Chop down", id = "tree*", delay = 5, success = { inventory_space = { max = 0 } } } }, @@ -20,9 +19,8 @@ requires = [ { skill = { id = "woodcutting", min = 15, max = 30 } }, ] setup = [ - { carries = [{ id = "mithril_hatchet,steel_hatchet" }] }, + { inventory = [{ id = "mithril_hatchet,steel_hatchet" }, { id = "", min = 27 }] }, { area = { id = "$location" } }, - { inventory_space = { min = 27 } }, ] actions = [ { object = { option = "Chop down", id = "oak*", delay = 5, success = { inventory_space = { max = 0 } } } }, @@ -37,9 +35,8 @@ requires = [ { skill = { id = "woodcutting", min = 30, max = 60 } }, ] setup = [ - { carries = [{ id = "rune_hatchet,adamant_hatchet,mithril_hatchet,steel_hatchet" }] }, + { inventory = [{ id = "rune_hatchet,adamant_hatchet,mithril_hatchet,steel_hatchet" }, { id = "", min = 27 }] }, { area = { id = "$location" } }, - { inventory_space = { min = 27 } }, ] actions = [ { object = { option = "Chop down", id = "willow*", delay = 5, success = { inventory_space = { max = 0 } } } }, @@ -54,9 +51,8 @@ requires = [ { skill = { id = "woodcutting", min = 60, max = 75 } }, ] setup = [ - { carries = [{ id = "dragon_hatchet,rune_hatchet,adamant_hatchet" }] }, + { inventory = [{ id = "dragon_hatchet,rune_hatchet,adamant_hatchet" }, { id = "", min = 27 }] }, { area = { id = "$location" } }, - { inventory_space = { min = 27 } }, ] actions = [ { object = { option = "Chop down", id = "yew*", delay = 5, success = { inventory_space = { max = 0 } } } }, diff --git a/game/src/main/kotlin/content/bot/req/fact/FactParser.kt b/game/src/main/kotlin/content/bot/req/fact/FactParser.kt index 3597961fea..a8f24a5c4b 100644 --- a/game/src/main/kotlin/content/bot/req/fact/FactParser.kt +++ b/game/src/main/kotlin/content/bot/req/fact/FactParser.kt @@ -131,7 +131,7 @@ sealed class FactParser { companion object { val parsers = mapOf( "inventory_space" to InventorySpace, - "carries" to InventoryItems, + "inventory" to InventoryItems, "owns" to AllItems, "banked" to BankedItems, "equips" to EquipmentItems, From 407f8a5acfc545f1ba21b1aa1bed360e204f669f Mon Sep 17 00:00:00 2001 From: GregHib Date: Mon, 9 Feb 2026 22:50:37 +0000 Subject: [PATCH 082/101] Add start new condition system --- .../src/main/kotlin/content/bot/BotManager.kt | 1 + .../main/kotlin/content/bot/req/Condition.kt | 124 ++++++++++++++++++ .../kotlin/content/bot/req/fact/FactParser.kt | 8 ++ 3 files changed, 133 insertions(+) create mode 100644 game/src/main/kotlin/content/bot/req/Condition.kt diff --git a/game/src/main/kotlin/content/bot/BotManager.kt b/game/src/main/kotlin/content/bot/BotManager.kt index b73e84d984..f19ffdcf91 100644 --- a/game/src/main/kotlin/content/bot/BotManager.kt +++ b/game/src/main/kotlin/content/bot/BotManager.kt @@ -52,6 +52,7 @@ class BotManager( } fun update(bot: Bot, group: String) { + // TODO if product produced reset timeout val iterator = bot.available.iterator() while (iterator.hasNext()) { val id = iterator.next() diff --git a/game/src/main/kotlin/content/bot/req/Condition.kt b/game/src/main/kotlin/content/bot/req/Condition.kt new file mode 100644 index 0000000000..45bee5b4da --- /dev/null +++ b/game/src/main/kotlin/content/bot/req/Condition.kt @@ -0,0 +1,124 @@ +package content.bot.req + +import world.gregs.voidps.engine.entity.character.player.Player +import world.gregs.voidps.engine.entity.character.player.equip.equipped +import world.gregs.voidps.engine.inv.inventory +import world.gregs.voidps.network.login.protocol.visual.update.player.EquipSlot + +sealed class Condition { + abstract fun keys(): Set + abstract fun events(): Set + abstract fun check(player: Player): Boolean + + data class Entry(val id: String, val min: Int?, val max: Int?) + + data class Inventory(val items: List) : Condition() { + override fun keys() = items.map { "item:${it.id}" }.toSet() + override fun events() = setOf("inventory") + override fun check(player: Player): Boolean { + for (item in items) { + val amount = player.inventory.count(item.id) + if (item.min != null && amount < item.min) { + return false + } + if (item.max != null && amount > item.max) { + return false + } + } + return true + } + + fun parse(list: List>): Inventory { + val items = mutableListOf() + for (map in list) { + val id = map["id"] as String + var min = map["min"] as? Int + val max = map["max"] as? Int + if (min == null && max == null) { + min = 0 + } + items.add(Entry(id, min, max)) + } + return Inventory(items) + } + } + + data class Equipment(val items: Map) : Condition() { + override fun keys() = items.values.map { "item:${it.id}" }.toSet() + override fun events() = setOf("worn_equipment") + + override fun check(player: Player): Boolean { + for ((slot, entry) in items) { + val item = player.equipped(slot) + if (item.id != entry.id) { + return false + } + if (entry.min != null && item.amount < entry.min) { + return false + } + if (entry.max != null && item.amount > entry.max) { + return false + } + } + return true + } + + @Suppress("UNCHECKED_CAST") + fun parse(list: List>): Equipment { + val items = mutableMapOf() + for (map in list) { + for ((key, value) in map) { + value as Map + items[EquipSlot.by(key)] = Entry( + id = map["id"] as String, + min = map["min"] as? Int ?: 0, + max = map["max"] as? Int + ) + } + } + return Equipment(items) + } + } + + data class Variable(val id: String, val equals: Any, val default: Any) : Condition() { + override fun keys() = setOf("var:$id") + override fun events() = setOf("variable") + override fun check(player: Player) = player.variables.get(id, default) == equals + + @Suppress("UNCHECKED_CAST") + fun parse(list: List>): Condition? { + val map = list.single() + if (map.containsKey("equals")) { + return Variable( + id = map["id"] as String, + equals = map["equals"]!!, + default = map["default"]!!, + ) + } else if (map.containsKey("min")) { + return VariableIn( + id = map["id"] as String, + default = map["default"] as Int, + min = map["min"] as? Int ?: 0, + max = map["max"] as? Int, + ) + } + return null + } + } + + data class VariableIn(val id: String, val default: Int, val min: Int?, val max: Int?) : Condition() { + override fun keys() = setOf("var:$id") + override fun events() = setOf("variable") + override fun check(player: Player): Boolean { + val value = player.variables.get(id, default) + if (min != null && value < min) { + return false + } + if (max != null && value > max) { + return false + } + return true + } + } + +} \ No newline at end of file diff --git a/game/src/main/kotlin/content/bot/req/fact/FactParser.kt b/game/src/main/kotlin/content/bot/req/fact/FactParser.kt index a8f24a5c4b..26b569ebbd 100644 --- a/game/src/main/kotlin/content/bot/req/fact/FactParser.kt +++ b/game/src/main/kotlin/content/bot/req/fact/FactParser.kt @@ -2,6 +2,10 @@ package content.bot.req.fact import content.bot.req.Requirement import content.bot.req.predicate.Predicate +import world.gregs.voidps.engine.entity.character.player.Player +import world.gregs.voidps.engine.entity.character.player.equip.equipped +import world.gregs.voidps.engine.inv.inventory +import world.gregs.voidps.network.login.protocol.visual.update.player.EquipSlot import world.gregs.voidps.type.Tile sealed class FactParser { @@ -119,6 +123,7 @@ sealed class FactParser { val id = map["id"] as String return Fact.ObjectExists(Predicate.StringEquals(id), tile) } + override fun predicate(map: Map) = Predicate.BooleanTrue } @@ -128,7 +133,9 @@ sealed class FactParser { override fun predicate(map: Map) = Predicate.parseInt(map) } + companion object { + // Parser creates requirement sealed class which has it all in one val parsers = mapOf( "inventory_space" to InventorySpace, "inventory" to InventoryItems, @@ -148,3 +155,4 @@ sealed class FactParser { ) } } + From 35d6de4dd73e2e6fa67fac24068639c5b80e96a9 Mon Sep 17 00:00:00 2001 From: GregHib Date: Tue, 10 Feb 2026 13:24:40 +0000 Subject: [PATCH 083/101] Add new condition system with dynamic resolvers in place of facts, predicates and deficits --- data/bot/bank.setups.toml | 4 +- data/bot/combat.templates.toml | 12 +- data/bot/cooking.templates.toml | 16 +- data/bot/fishing.templates.toml | 14 +- data/bot/fletching.templates.toml | 18 +- data/bot/mining.templates.toml | 32 +- data/bot/prayer.bots.toml | 20 +- data/bot/shop.templates.toml | 12 +- data/bot/teleport.shortcuts.toml | 14 +- data/bot/thieving.templates.toml | 2 +- data/bot/woodcutting.templates.toml | 16 +- .../src/main/kotlin/content/bot/BotManager.kt | 18 +- .../kotlin/content/bot/action/ActionParser.kt | 6 +- .../kotlin/content/bot/action/BotAction.kt | 20 +- .../kotlin/content/bot/behaviour/Behaviour.kt | 24 +- .../kotlin/content/bot/behaviour/Reason.kt | 4 +- .../bot/behaviour/activity/BotActivity.kt | 6 +- .../content/bot/behaviour/navigation/Graph.kt | 16 +- .../navigation/NavigationShortcut.kt | 6 +- .../content/bot/behaviour/setup/Deficit.kt | 30 +- .../bot/behaviour/setup/DynamicResolvers.kt | 156 ++++++++ .../content/bot/behaviour/setup/Resolver.kt | 6 +- .../main/kotlin/content/bot/req/Condition.kt | 356 +++++++++++++++--- .../kotlin/content/bot/req/Requirement.kt | 38 -- .../content/bot/req/RequirementEvaluator.kt | 64 ---- .../main/kotlin/content/bot/req/fact/Fact.kt | 203 ---------- .../kotlin/content/bot/req/fact/FactParser.kt | 158 -------- .../kotlin/content/bot/req/fact/ItemView.kt | 12 - .../content/bot/req/predicate/Predicate.kt | 208 ---------- .../test/kotlin/content/bot/BotManagerTest.kt | 45 ++- .../content/bot/interact/path/GraphTest.kt | 12 +- .../gregs/voidps/tools/ItemDefinitions.kt | 5 +- 32 files changed, 644 insertions(+), 909 deletions(-) create mode 100644 game/src/main/kotlin/content/bot/behaviour/setup/DynamicResolvers.kt delete mode 100644 game/src/main/kotlin/content/bot/req/Requirement.kt delete mode 100644 game/src/main/kotlin/content/bot/req/RequirementEvaluator.kt delete mode 100644 game/src/main/kotlin/content/bot/req/fact/Fact.kt delete mode 100644 game/src/main/kotlin/content/bot/req/fact/FactParser.kt delete mode 100644 game/src/main/kotlin/content/bot/req/fact/ItemView.kt delete mode 100644 game/src/main/kotlin/content/bot/req/predicate/Predicate.kt diff --git a/data/bot/bank.setups.toml b/data/bot/bank.setups.toml index 8baaba81dc..f5fe437e8d 100644 --- a/data/bot/bank.setups.toml +++ b/data/bot/bank.setups.toml @@ -2,10 +2,10 @@ actions = [ { go_to = { nearest = "bank" } }, { object = { option = "Use-quickly", id = "bank_booth*", success = { interface_open = { id = "bank" } } } }, - { interface = { option = "Deposit carried items", id = "bank:carried", success = { inventory_space = { min = 28 } } } }, + { interface = { option = "Deposit carried items", id = "bank:carried", success = { inventory = { id = "empty", min = 28 } } } }, ] produces = [ - { item = "" } + { item = "empty" } ] #[deposit_worn_items] diff --git a/data/bot/combat.templates.toml b/data/bot/combat.templates.toml index 1dd8f4f3e9..8506fee067 100644 --- a/data/bot/combat.templates.toml +++ b/data/bot/combat.templates.toml @@ -3,13 +3,13 @@ requires = [ { skill = { id = "$skill", min = 1, max = 5 } }, ] setup = [ - { equips = { id = "bronze_sword,bronze_dagger,bronze_scimitar" } }, + { equipment = { weapon = { id = "bronze_sword,bronze_dagger,bronze_scimitar" } } }, { area = { id = "$location" } }, - { inventory_space = { min = 27 } }, + { inventory = { id = "empty", min = 27 } }, ] actions = [ { interface = { option = "Select", id = "combat_styles:$style" } }, - { npc = { option = "Attack", id = "chicken*", delay = 5, success = { inventory_space = { max = 0 } } } }, + { npc = { option = "Attack", id = "chicken*", delay = 5, success = { inventory = { id = "empty", max = 0 } } } }, ] produces = [ { item = "feather" }, @@ -24,13 +24,13 @@ requires = [ { skill = { id = "$skill", min = 5, max = 10 } }, ] setup = [ - { equips = { id = "iron_sword,iron_dagger,iron_scimitar" } }, + { equipment = { weapon = { id = "iron_sword,iron_dagger,iron_scimitar" } } }, { area = { id = "$location" } }, - { inventory_space = { min = 27 } }, + { inventory = { id = "empty", min = 27 } }, ] actions = [ { interface = { option = "Select", id = "combat_styles:style1" } }, - { npc = { option = "Attack", id = "cow*", delay = 5, success = { inventory_space = { max = 0 } } } }, + { npc = { option = "Attack", id = "cow*", delay = 5, success = { inventory = { id = "empty", max = 0 } } } }, ] produces = [ { item = "bones" }, diff --git a/data/bot/cooking.templates.toml b/data/bot/cooking.templates.toml index a69b0e39e5..9f18e36655 100644 --- a/data/bot/cooking.templates.toml +++ b/data/bot/cooking.templates.toml @@ -1,6 +1,6 @@ [cooking_template] requires = [ - { owns = { id = "$raw", amount = 28 } }, + { bank = { id = "$raw", amount = 28 } }, { skill = { id = "cooking", min = "$level" } }, ] setup = [ @@ -11,17 +11,18 @@ actions = [ { item_on_object = { id = "$raw", object = "$obj", success = { interface_open = { id = "dialogue_skill_creation" } } } }, { interface = { option = "All", id = "skill_creation_amount:all" } }, { continue = { id = "dialogue_skill_creation:choice1" } }, - { restart = { wait_if = { has_queue = { id = "cooking" } }, success = { inventory = { id = "$raw", max = 0 } } } } + { restart = { wait_if = { queue = { id = "cooking" } }, success = { inventory = { id = "$raw", max = 0 } } } } ] produces = [ { skill = "cooking" }, - { item = "$cooked" } + { item = "$cooked" }, + { item = "$burnt" } ] -# Beef has a different popup so can't use normal template +# Beef has a different popup so can't use the normal template [beef_template] requires = [ - { owns = { id = "raw_beef", amount = 28 } }, + { bank = { id = "raw_beef", amount = 28 } }, { skill = { id = "cooking", min = 1 } }, ] setup = [ @@ -33,9 +34,10 @@ actions = [ { continue = { option = "Continue", id = "dialogue_multi2:line2", success = { interface_open = { id = "dialogue_skill_creation" } } } }, { interface = { option = "All", id = "skill_creation_amount:all" } }, { continue = { id = "dialogue_skill_creation:choice1" } }, - { restart = { wait_if = { has_queue = { id = "cooking" } }, success = { inventory = { id = "raw_beef", max = 0 } } } } + { restart = { wait_if = { queue = { id = "cooking" } }, success = { inventory = { id = "raw_beef", max = 0 } } } } ] produces = [ { skill = "cooking" }, - { item = "beef" } + { item = "beef" }, + { item = "burnt_meat" } ] diff --git a/data/bot/fishing.templates.toml b/data/bot/fishing.templates.toml index 6dc8118549..ff6b1437de 100644 --- a/data/bot/fishing.templates.toml +++ b/data/bot/fishing.templates.toml @@ -5,12 +5,12 @@ requires = [ setup = [ { inventory = [ { id = "crayfish_cage" }, - { id = "", min = 27 } + { id = "empty", min = 27 } ] }, { area = { id = "$location" } }, ] actions = [ - { npc = { option = "Cage", id = "$spot", delay = 5, success = { inventory_space = { max = 0 } } } }, + { npc = { option = "Cage", id = "$spot", delay = 5, success = { inventory = { id = "empty", max = 0 } } } }, ] produces = [ { inventory = "raw_crayfish" }, @@ -21,12 +21,12 @@ produces = [ setup = [ { inventory = [ { id = "small_fishing_net" }, - { id = "", min = 27 } + { id = "empty", min = 27 } ] }, { area = { id = "$location" } }, ] actions = [ - { npc = { option = "Net", id = "$spot", delay = 5, success = { inventory_space = { max = 0 } } } }, + { npc = { option = "Net", id = "$spot", delay = 5, success = { inventory = { id = "empty", max = 0 } } } }, ] produces = [ { inventory = "raw_shrimp" }, @@ -37,12 +37,12 @@ produces = [ setup = [ { inventory = [ { id = "small_fishing_net" }, - { id = "", min = 27 } + { id = "empty", min = 27 } ] }, { area = { id = "$location" } }, ] actions = [ - { npc = { option = "Net", id = "$spot", delay = 5, success = { inventory_space = { max = 0 } } } }, + { npc = { option = "Net", id = "$spot", delay = 5, success = { inventory = { id = "empty", max = 0 } } } }, ] produces = [ { skill = "fishing" } @@ -68,7 +68,7 @@ produces = [ # Success # tile = x, y/level # carries coins/amount -#has_queue = id +#queue = id # Produces # skill = "name" diff --git a/data/bot/fletching.templates.toml b/data/bot/fletching.templates.toml index edae56d7e8..039e0c19b6 100644 --- a/data/bot/fletching.templates.toml +++ b/data/bot/fletching.templates.toml @@ -1,6 +1,6 @@ [fletching_arrow_shafts_template] requires = [ - { owns = { id = "$logs", min = 27 } } + { bank = { id = "$logs", min = 27 } } ] setup = [ { area = { id = "$location" } }, @@ -10,7 +10,7 @@ actions = [ { item_on_item = { id = "knife", on = "$logs", success = { interface_open = { id = "dialogue_skill_creation" } } } }, { interface = { option = "All", id = "skill_creation_amount:all" } }, { continue = { id = "dialogue_skill_creation:choice1" } }, - { restart = { wait_if = [{ has_queue = { id = "fletching" } }], success = { inventory_space = { min = 26 } } } } + { restart = { wait_if = [{ queue = { id = "fletching" } }], success = { inventory = { id = "empty", min = 26 } } } } ] produces = [ { item = "arrow_shafts" }, @@ -19,7 +19,7 @@ produces = [ [fletching_shortbow_template] requires = [ - { owns = { id = "$logs", min = 27 } } + { bank = { id = "$logs", min = 27 } } ] setup = [ { area = { id = "$location" } }, @@ -28,8 +28,8 @@ setup = [ actions = [ { item_on_item = { id = "knife", on = "$logs" } }, { interface = { option = "All", id = "skill_creation_amount:all" } }, - { continue = { id = "dialogue_skill_creation:choice2", success = { has_queue = { id = "fletching" } } } }, - { restart = { success = { inventory_space = { min = 26 } } } } + { continue = { id = "dialogue_skill_creation:choice2", success = { queue = { id = "fletching" } } } }, + { restart = { success = { inventory = { id = "empty", min = 26 } } } } ] produces = [ { skill = "fletching" } @@ -37,17 +37,17 @@ produces = [ [fletching_longbow_template] requires = [ - { owns = { id = "$logs", min = 27 } } + { bank = { id = "$logs", min = 27 } } ] setup = [ { area = { id = "$location" } }, { inventory = [{ id = "knife" }, { id = "$logs", min = 27 }] }, ] actions = [ - { item_on_item = { id = "knife", on = "$logs", success = { has_queue = { id = "fletching_make_dialog" } } } }, + { item_on_item = { id = "knife", on = "$logs", success = { queue = { id = "fletching_make_dialog" } } } }, { interface = { option = "All", id = "skill_creation_amount:all" } }, - { continue = { id = "dialogue_skill_creation:choice3", success = { has_queue = { id = "fletching" } } } }, - { restart = { success = { inventory_space = { min = 26 } } } } + { continue = { id = "dialogue_skill_creation:choice3", success = { queue = { id = "fletching" } } } }, + { restart = { success = { inventory = { id = "empty", min = 26 } } } } ] produces = [ { skill = "fletching" } diff --git a/data/bot/mining.templates.toml b/data/bot/mining.templates.toml index 8cfe01f417..8512fa59d6 100644 --- a/data/bot/mining.templates.toml +++ b/data/bot/mining.templates.toml @@ -3,11 +3,11 @@ requires = [ { skill = { id = "mining", min = 1, max = 15 } }, ] setup = [ - { inventory = [{ id = "steel_pickaxe,iron_pickaxe,bronze_pickaxe", usable = true }, { id = "", min = 27 }] }, + { inventory = [{ id = "steel_pickaxe,iron_pickaxe,bronze_pickaxe", usable = true }, { id = "empty", min = 27 }] }, { area = { id = "$location" } }, ] actions = [ - { object = { option = "Mine", id = "copper_rocks*", delay = 5, success = { inventory_space = { max = 0 } } } }, + { object = { option = "Mine", id = "copper_rocks*", delay = 5, success = { inventory = { id = "empty", max = 0 } } } }, ] produces = [ { item = "copper_ore" }, @@ -19,11 +19,11 @@ requires = [ { skill = { id = "mining", min = 1, max = 15 } }, ] setup = [ - { inventory = [{ id = "steel_pickaxe,iron_pickaxe,bronze_pickaxe", usable = true }, { id = "", min = 27 }] }, + { inventory = [{ id = "steel_pickaxe,iron_pickaxe,bronze_pickaxe", usable = true }, { id = "empty", min = 27 }] }, { area = { id = "$location" } }, ] actions = [ - { object = { option = "Mine", id = "tin_rocks*", delay = 5, success = { inventory_space = { max = 0 } } } }, + { object = { option = "Mine", id = "tin_rocks*", delay = 5, success = { inventory = { id = "empty", max = 0 } } } }, ] produces = [ { item = "tin_ore" }, @@ -35,11 +35,11 @@ requires = [ { skill = { id = "mining", min = 1, max = 15 } }, ] setup = [ - { inventory = [{ id = "steel_pickaxe,iron_pickaxe,bronze_pickaxe", usable = true }, { id = "", min = 27 }] }, + { inventory = [{ id = "steel_pickaxe,iron_pickaxe,bronze_pickaxe", usable = true }, { id = "empty", min = 27 }] }, { area = { id = "$location" } }, ] actions = [ - { object = { option = "Mine", id = "clay_rocks*", delay = 5, success = { inventory_space = { max = 0 } } } }, + { object = { option = "Mine", id = "clay_rocks*", delay = 5, success = { inventory = { id = "empty", max = 0 } } } }, ] produces = [ { item = "clay" }, @@ -51,11 +51,11 @@ requires = [ { skill = { id = "mining", min = 15, max = 40 } }, ] setup = [ - { inventory = [{ id = "adamant_pickaxe,mithril_pickaxe,steel_pickaxe", usable = true }, { id = "", min = 27 }] }, + { inventory = [{ id = "adamant_pickaxe,mithril_pickaxe,steel_pickaxe", usable = true }, { id = "empty", min = 27 }] }, { area = { id = "$location" } }, ] actions = [ - { object = { option = "Mine", id = "iron_rocks*", delay = 5, success = { inventory_space = { max = 0 } } } }, + { object = { option = "Mine", id = "iron_rocks*", delay = 5, success = { inventory = { id = "empty", max = 0 } } } }, ] produces = [ { item = "iron_ore" }, @@ -67,11 +67,11 @@ requires = [ { skill = { id = "mining", min = 20, max = 40 } }, ] setup = [ - { inventory = [{ id = "adamant_pickaxe,mithril_pickaxe,steel_pickaxe", usable = true }, { id = "", min = 27 }] }, + { inventory = [{ id = "adamant_pickaxe,mithril_pickaxe,steel_pickaxe", usable = true }, { id = "empty", min = 27 }] }, { area = { id = "$location" } }, ] actions = [ - { object = { option = "Mine", id = "silver_rocks*", delay = 5, success = { inventory_space = { max = 0 } } } }, + { object = { option = "Mine", id = "silver_rocks*", delay = 5, success = { inventory = { id = "empty", max = 0 } } } }, ] produces = [ { item = "silver_ore" }, @@ -83,11 +83,11 @@ requires = [ { skill = { id = "mining", min = 30, max = 55 } }, ] setup = [ - { inventory = [{ id = "rune_pickaxe,adamant_pickaxe,mithril_pickaxe", usable = true }, { id = "", min = 27 }] }, + { inventory = [{ id = "rune_pickaxe,adamant_pickaxe,mithril_pickaxe", usable = true }, { id = "empty", min = 27 }] }, { area = { id = "$location" } }, ] actions = [ - { object = { option = "Mine", id = "coal_rocks*", delay = 10, success = { inventory_space = { max = 0 } } } }, + { object = { option = "Mine", id = "coal_rocks*", delay = 10, success = { inventory = { id = "empty", max = 0 } } } }, ] produces = [ { item = "coal" }, @@ -99,11 +99,11 @@ requires = [ { skill = { id = "mining", min = 40, max = 60 } }, ] setup = [ - { inventory = [{ id = "rune_pickaxe,adamant_pickaxe,mithril_pickaxe", usable = true }, { id = "", min = 27 }] }, + { inventory = [{ id = "rune_pickaxe,adamant_pickaxe,mithril_pickaxe", usable = true }, { id = "empty", min = 27 }] }, { area = { id = "$location" } }, ] actions = [ - { object = { option = "Mine", id = "gold_rocks*", delay = 15, success = { inventory_space = { max = 0 } } } }, + { object = { option = "Mine", id = "gold_rocks*", delay = 15, success = { inventory = { id = "empty", max = 0 } } } }, ] produces = [ { item = "gold_ore" }, @@ -115,11 +115,11 @@ requires = [ { skill = { id = "mining", min = 55, max = 70 } }, ] setup = [ - { inventory = [{ id = "dragon_pickaxe,rune_pickaxe,adamant_pickaxe", usable = true }, { id = "", min = 27 }] }, + { inventory = [{ id = "dragon_pickaxe,rune_pickaxe,adamant_pickaxe", usable = true }, { id = "empty", min = 27 }] }, { area = { id = "$location" } }, ] actions = [ - { object = { option = "Mine", id = "mithril_rocks*", delay = 15, success = { inventory_space = { max = 0 } } } }, + { object = { option = "Mine", id = "mithril_rocks*", delay = 15, success = { inventory = { id = "empty", max = 0 } } } }, ] produces = [ { item = "mithril_ore" }, diff --git a/data/bot/prayer.bots.toml b/data/bot/prayer.bots.toml index c55cd14e43..d0aff6244f 100644 --- a/data/bot/prayer.bots.toml +++ b/data/bot/prayer.bots.toml @@ -1,14 +1,30 @@ +[bury_bones_in_bank] +capacity = 5 +requires = [ + { bank = { id = "bones", min = 28 } }, +] +setup = [ + { inventory = [{ id = "bones", min = 28 }] }, +] +actions = [ + { interface = { option = "Bury", id = "inventory:inventory:bones" } }, + { restart = { wait_if = [{ variable = { id = "bone_delay", min = 0, default = -1 } }], success = { inventory = { id = "empty", min = 28 } } } } +] +produces = [ + { skill = "prayer" } +] + [bury_bones] capacity = 5 requires = [ - { owns = { id = "bones", min = 28 } }, + { inventory = [{ id = "bones", min = 28 }] }, ] setup = [ { inventory = [{ id = "bones", min = 28 }] }, ] actions = [ { interface = { option = "Bury", id = "inventory:inventory:bones" } }, - { restart = { wait_if = [{ variable = { id = "bone_delay", min = 0, default = -1 } }], success = { inventory_space = { min = 28 } } } } + { restart = { wait_if = [{ variable = { id = "bone_delay", min = 0, default = -1 } }], success = { inventory = { id = "empty", min = 28 } } } } ] produces = [ { skill = "prayer" } diff --git a/data/bot/shop.templates.toml b/data/bot/shop.templates.toml index 14a1783982..9dac5516bc 100644 --- a/data/bot/shop.templates.toml +++ b/data/bot/shop.templates.toml @@ -1,6 +1,6 @@ [buy_from_shop] setup = [ - { inventory = [{ id = "coins", min = "$cost" }, { id = "", min = 1 }] }, + { inventory = [{ id = "coins", min = "$cost" }, { id = "empty", min = 1 }] }, { area = { id = "$shop_location" } }, ] actions = [ @@ -13,7 +13,7 @@ produces = [ [take_shop_sample] setup = [ - { inventory = [{ id = "", min = 1 }] }, + { inventory = [{ id = "empty", min = 1 }] }, { area = { id = "$shop_location" } }, ] actions = [ @@ -26,7 +26,7 @@ produces = [ [sell_50_to_shop] setup = [ - { inventory = [{ id = "$item", min = "50" }, { id = "", min = 1 }] }, + { inventory = [{ id = "$item", min = "50" }, { id = "empty", min = 1 }] }, { area = { id = "$shop_location" } }, ] actions = [ @@ -40,7 +40,7 @@ produces = [ [sell_10_to_shop] setup = [ - { inventory = [{ id = "$item", min = "10" }, { id = "", min = 1 }] }, + { inventory = [{ id = "$item", min = "10" }, { id = "empty", min = 1 }] }, { area = { id = "$shop_location" } }, ] actions = [ @@ -54,7 +54,7 @@ produces = [ [sell_5_to_shop] setup = [ - { inventory = [{ id = "$item", min = "5" }, { id = "", min = 1 }] }, + { inventory = [{ id = "$item", min = "5" }, { id = "empty", min = 1 }] }, { area = { id = "$shop_location" } }, ] actions = [ @@ -68,7 +68,7 @@ produces = [ [sell_to_shop] setup = [ - { inventory = [{ id = "$item", min = "1" }, { id = "", min = 1 }] }, + { inventory = [{ id = "$item", min = "1" }, { id = "empty", min = 1 }] }, { area = { id = "$shop_location" } }, ] actions = [ diff --git a/data/bot/teleport.shortcuts.toml b/data/bot/teleport.shortcuts.toml index 5812c9f6d2..bf0be6cff8 100644 --- a/data/bot/teleport.shortcuts.toml +++ b/data/bot/teleport.shortcuts.toml @@ -28,7 +28,11 @@ [teleport_varrock] weight = 125 requires = [ - { inventory = [{ id = "fire_rune", min = 1 }, { id = "air_rune", min = 3 }, { id = "law_rune", min = 1 }] }, + { inventory = [ + { id = "fire_rune", min = 1 }, + { id = "air_rune", min = 3 }, + { id = "law_rune", min = 1 } + ] }, { variable = { id = "spellbook_config", equals = 0, default = 0 } }, ] actions = [ @@ -43,9 +47,11 @@ produces = [ weight = 150 requires = [ { variable = { id = "spellbook_config", equals = 0, default = 0 }}, - { owns = { id = "fire_rune", min = 1 }}, - { owns = { id = "air_rune", min = 3 }}, - { owns = { id = "law_rune", min = 1 }}, + { bank = [ + { id = "fire_rune", min = 1 }, + { id = "air_rune", min = 3 }, + { id = "law_rune", min = 1 }, + ]}, ] actions = [ { go_to = { nearest = "bank" } }, diff --git a/data/bot/thieving.templates.toml b/data/bot/thieving.templates.toml index cf177f0b4b..88c3644e73 100644 --- a/data/bot/thieving.templates.toml +++ b/data/bot/thieving.templates.toml @@ -4,7 +4,7 @@ requires = [ { skill = { id = "thieving", min = 1, max = 10 } }, ] setup = [ - { inventory_space = { min = 1 } }, + { inventory = { id = "empty", min = 1 } }, { area = { id = "$location" } }, ] actions = [ diff --git a/data/bot/woodcutting.templates.toml b/data/bot/woodcutting.templates.toml index 71a129f557..d67587e148 100644 --- a/data/bot/woodcutting.templates.toml +++ b/data/bot/woodcutting.templates.toml @@ -3,11 +3,11 @@ requires = [ { skill = { id = "woodcutting", min = 1, max = 15 } }, ] setup = [ - { inventory = [{ id = "steel_hatchet,iron_hatchet,bronze_hatchet" }, { id = "", min = 27 }] }, + { inventory = [{ id = "steel_hatchet,iron_hatchet,bronze_hatchet" }, { id = "empty", min = 27 }] }, { area = { id = "$location" } }, ] actions = [ - { object = { option = "Chop down", id = "tree*", delay = 5, success = { inventory_space = { max = 0 } } } }, + { object = { option = "Chop down", id = "tree*", delay = 5, success = { inventory = { id = "empty", max = 0 } } } }, ] produces = [ { item = "logs" }, @@ -19,11 +19,11 @@ requires = [ { skill = { id = "woodcutting", min = 15, max = 30 } }, ] setup = [ - { inventory = [{ id = "mithril_hatchet,steel_hatchet" }, { id = "", min = 27 }] }, + { inventory = [{ id = "mithril_hatchet,steel_hatchet" }, { id = "empty", min = 27 }] }, { area = { id = "$location" } }, ] actions = [ - { object = { option = "Chop down", id = "oak*", delay = 5, success = { inventory_space = { max = 0 } } } }, + { object = { option = "Chop down", id = "oak*", delay = 5, success = { inventory = { id = "empty", max = 0 } } } }, ] produces = [ { item = "oak_logs" }, @@ -35,11 +35,11 @@ requires = [ { skill = { id = "woodcutting", min = 30, max = 60 } }, ] setup = [ - { inventory = [{ id = "rune_hatchet,adamant_hatchet,mithril_hatchet,steel_hatchet" }, { id = "", min = 27 }] }, + { inventory = [{ id = "rune_hatchet,adamant_hatchet,mithril_hatchet,steel_hatchet" }, { id = "empty", min = 27 }] }, { area = { id = "$location" } }, ] actions = [ - { object = { option = "Chop down", id = "willow*", delay = 5, success = { inventory_space = { max = 0 } } } }, + { object = { option = "Chop down", id = "willow*", delay = 5, success = { inventory = { id = "empty", max = 0 } } } }, ] produces = [ { item = "willow_logs" }, @@ -51,11 +51,11 @@ requires = [ { skill = { id = "woodcutting", min = 60, max = 75 } }, ] setup = [ - { inventory = [{ id = "dragon_hatchet,rune_hatchet,adamant_hatchet" }, { id = "", min = 27 }] }, + { inventory = [{ id = "dragon_hatchet,rune_hatchet,adamant_hatchet" }, { id = "empty", min = 27 }] }, { area = { id = "$location" } }, ] actions = [ - { object = { option = "Chop down", id = "yew*", delay = 5, success = { inventory_space = { max = 0 } } } }, + { object = { option = "Chop down", id = "yew*", delay = 5, success = { inventory = { id = "empty", max = 0 } } } }, ] produces = [ { item = "yew_logs" }, diff --git a/game/src/main/kotlin/content/bot/BotManager.kt b/game/src/main/kotlin/content/bot/BotManager.kt index f19ffdcf91..50f19e036e 100644 --- a/game/src/main/kotlin/content/bot/BotManager.kt +++ b/game/src/main/kotlin/content/bot/BotManager.kt @@ -13,8 +13,9 @@ import content.bot.behaviour.loadBehaviours import content.bot.behaviour.navigation.Graph import content.bot.behaviour.navigation.Graph.Companion.loadGraph import content.bot.behaviour.navigation.NavigationShortcut +import content.bot.behaviour.setup.DynamicResolvers import content.bot.behaviour.setup.Resolver -import content.bot.req.Requirement +import content.bot.req.Condition import world.gregs.voidps.engine.data.ConfigFiles import world.gregs.voidps.engine.data.Settings import world.gregs.voidps.engine.event.AuditLog @@ -190,16 +191,13 @@ class BotManager( frame.start(bot) } - private fun availableResolvers(bot: Bot, requirement: Requirement<*>): MutableList { + private fun availableResolvers(bot: Bot, requirement: Condition): MutableList { val options = mutableListOf() - println("Resolvers for $requirement") -// FIXME because InventoryItems no longer produces individual keys, there's no way to match resolvers to required items - for (deficit in requirement.deficits(bot.player)) { - options.add(deficit.resolve(bot.player) ?: continue) + val dynamic = DynamicResolvers.resolver(bot.player, requirement) + if (dynamic != null) { + options.add(dynamic) } - println("Check ${requirement.fact} ${requirement.predicate}") for (key in requirement.keys()) { - println("Resolvers for $key : ${resolvers[key]}") for (resolver in resolvers[key] ?: continue) { options.add(resolver) } @@ -272,8 +270,8 @@ class BotManager( bot.reset() } - private fun debugResolvers(behaviour: Behaviour, requirement: Requirement<*>, resolvers: MutableList, frame: BehaviourFrame, bot: Bot) { - logger.info { "No resolver found for ${behaviour.id} keys: ${requirement.fact.keys()} requirement: $requirement." } + private fun debugResolvers(behaviour: Behaviour, requirement: Condition, resolvers: MutableList, frame: BehaviourFrame, bot: Bot) { + logger.info { "No resolver found for ${behaviour.id} keys: ${requirement.keys()} requirement: $requirement." } for (resolver in resolvers) { if (frame.blocked.contains(resolver.id)) { logger.debug { "Resolver: ${resolver.id} - Blocked by frame behaviour: ${frame.behaviour.id}." } diff --git a/game/src/main/kotlin/content/bot/action/ActionParser.kt b/game/src/main/kotlin/content/bot/action/ActionParser.kt index 9e1536dabc..f93511e1d7 100644 --- a/game/src/main/kotlin/content/bot/action/ActionParser.kt +++ b/game/src/main/kotlin/content/bot/action/ActionParser.kt @@ -1,6 +1,6 @@ package content.bot.action -import content.bot.req.Requirement +import content.bot.req.Condition sealed class ActionParser { open val required = emptySet() @@ -154,7 +154,7 @@ sealed class ActionParser { companion object { @Suppress("UNCHECKED_CAST") - private fun requirement(map: Map, key: String): List> { + private fun requirement(map: Map, key: String): List { val parent = map[key] as? Map ?: return listOf() val key = parent.keys.singleOrNull() ?: error("Collection $map has more than one element.") val value = parent[key] ?: return listOf() @@ -163,7 +163,7 @@ sealed class ActionParser { is List<*> -> value as List> else -> return listOf() } - return Requirement.parse(listOf(key to list), "ActionParser.$key") + return Condition.parse(listOf(key to list), "ActionParser.$key in $map") } fun parse(list: List>>, name: String): List { diff --git a/game/src/main/kotlin/content/bot/action/BotAction.kt b/game/src/main/kotlin/content/bot/action/BotAction.kt index 1434a93d90..601ff05f4d 100644 --- a/game/src/main/kotlin/content/bot/action/BotAction.kt +++ b/game/src/main/kotlin/content/bot/action/BotAction.kt @@ -8,7 +8,7 @@ import content.bot.behaviour.Reason import content.bot.behaviour.navigation.Graph import content.bot.behaviour.navigation.NavigationShortcut import content.bot.behaviour.setup.Resolver -import content.bot.req.Requirement +import content.bot.req.Condition import content.entity.combat.attackers import content.entity.combat.dead import world.gregs.voidps.engine.GameLoop @@ -143,7 +143,7 @@ sealed interface BotAction { val option: String, val id: String, val delay: Int = 0, - val success: Requirement<*>? = null, + val success: Condition? = null, val radius: Int = 10, ) : BotAction { @@ -195,7 +195,7 @@ sealed interface BotAction { data class FightNpc( val id: String, val delay: Int = 0, - val success: Requirement<*>? = null, + val success: Condition? = null, val radius: Int = 10, val healPercentage: Int = 20, val lootOverValue: Int = 0, @@ -285,7 +285,7 @@ sealed interface BotAction { val option: String, val id: String, val delay: Int = 0, - val success: Requirement<*>? = null, + val success: Condition? = null, val radius: Int = 10, val x: Int? = null, val y: Int? = null, @@ -340,7 +340,7 @@ sealed interface BotAction { } } - data class ItemOnItem(val item: String, val on: String, val success: Requirement<*>? = null) : BotAction { + data class ItemOnItem(val item: String, val on: String, val success: Condition? = null) : BotAction { override fun update(bot: Bot, frame: BehaviourFrame): BehaviourState? { if (success != null && success.check(bot.player)) { return BehaviourState.Success @@ -366,7 +366,7 @@ sealed interface BotAction { } } - data class ItemOnObject(val item: String, val id: String, val success: Requirement<*>? = null) : BotAction { + data class ItemOnObject(val item: String, val id: String, val success: Condition? = null) : BotAction { override fun update(bot: Bot, frame: BehaviourFrame): BehaviourState { if (success != null && success.check(bot.player)) { return BehaviourState.Success @@ -405,7 +405,7 @@ sealed interface BotAction { } } - data class InterfaceOption(val option: String, val id: String, val success: Requirement<*>? = null) : BotAction { + data class InterfaceOption(val option: String, val id: String, val success: Condition? = null) : BotAction { override fun update(bot: Bot, frame: BehaviourFrame): BehaviourState? { if (success != null && success.check(bot.player)) { return BehaviourState.Success @@ -454,7 +454,7 @@ sealed interface BotAction { } } - data class DialogueContinue(val option: String, val id: String, val success: Requirement<*>? = null) : BotAction { + data class DialogueContinue(val option: String, val id: String, val success: Condition? = null) : BotAction { override fun update(bot: Bot, frame: BehaviourFrame): BehaviourState { if (success != null && success.check(bot.player)) { return BehaviourState.Success @@ -512,8 +512,8 @@ sealed interface BotAction { * Restarts the current action when [check] doesn't hold true (or bot has no mode) and success state isn't matched. */ data class Restart( - val wait: List>, - val success: Requirement<*>, + val wait: List, + val success: Condition, ) : BotAction { override fun update(bot: Bot, frame: BehaviourFrame): BehaviourState { if (success.check(bot.player)) { diff --git a/game/src/main/kotlin/content/bot/behaviour/Behaviour.kt b/game/src/main/kotlin/content/bot/behaviour/Behaviour.kt index f38c18ac82..20607c8e6f 100644 --- a/game/src/main/kotlin/content/bot/behaviour/Behaviour.kt +++ b/game/src/main/kotlin/content/bot/behaviour/Behaviour.kt @@ -5,7 +5,7 @@ import content.bot.action.BotAction import content.bot.behaviour.activity.BotActivity import content.bot.behaviour.navigation.NavigationShortcut import content.bot.behaviour.setup.Resolver -import content.bot.req.Requirement +import content.bot.req.Condition import it.unimi.dsi.fastutil.objects.ObjectArrayList import world.gregs.config.Config import world.gregs.config.ConfigReader @@ -15,8 +15,8 @@ import world.gregs.voidps.engine.timedLoad interface Behaviour { val id: String - val requires: List> - val setup: List> + val requires: List + val setup: List val actions: List val produces: Set } @@ -34,7 +34,7 @@ fun loadBehaviours( var total = 0 for (activity in activities.values) { for (req in activity.requires) { - for (key in req.fact.groups()) { + for (key in req.events()) { groups.getOrPut(key) { mutableListOf() }.add(activity.id) } } @@ -56,8 +56,8 @@ private fun loadActivities(activities: MutableMap, template activities[id] = BotActivity( id = id, capacity = capacity, - requires = Requirement.parse(requires, debug), - setup = Requirement.parse(setup, debug), + requires = Condition.parse(requires, debug), + setup = Condition.parse(setup, debug), actions = ActionParser.parse(actions, debug), produces = produces, ) @@ -83,8 +83,8 @@ private fun loadSetups(resolvers: MutableMap>, tem val resolver = Resolver( id = id, weight = weight, - requires = Requirement.parse(requires, debug), - setup = Requirement.parse(setup, debug), + requires = Condition.parse(requires, debug), + setup = Condition.parse(setup, debug), actions = ActionParser.parse(actions, debug), produces = produces, ) @@ -117,8 +117,8 @@ private fun loadShortcuts(shortcuts: MutableList, templates: NavigationShortcut( id = id, weight = weight, - requires = Requirement.parse(requires, debug), - setup = Requirement.parse(setup, debug), + requires = Condition.parse(requires, debug), + setup = Condition.parse(setup, debug), actions = ActionParser.parse(actions, debug), produces = produces, ), @@ -253,7 +253,7 @@ private data class Fragment( private fun resolve(set: Set): Set = set - private fun resolveRequirements(templated: List>>>, original: List>>>, requirePredicates: Boolean = true): List> { + private fun resolveRequirements(templated: List>>>, original: List>>>, requirePredicates: Boolean = true): List { val combinedList = mutableListOf>>>() combinedList.addAll(original) for ((type, list) in templated) { @@ -265,7 +265,7 @@ private data class Fragment( if (combinedList.isEmpty()) { return emptyList() } - return Requirement.parse(combinedList, "$id template $template", requirePredicates) + return Condition.parse(combinedList, "$id template $template") } private fun resolveActions(templated: List>>, original: List>>, requirePredicates: Boolean = true): List { diff --git a/game/src/main/kotlin/content/bot/behaviour/Reason.kt b/game/src/main/kotlin/content/bot/behaviour/Reason.kt index bfa3709242..ae9657ea99 100644 --- a/game/src/main/kotlin/content/bot/behaviour/Reason.kt +++ b/game/src/main/kotlin/content/bot/behaviour/Reason.kt @@ -1,5 +1,7 @@ package content.bot.behaviour +import content.bot.req.Condition + interface Reason { data class Invalid(val message: String) : HardReason object Cancelled : HardReason @@ -7,7 +9,7 @@ interface Reason { object Timeout : HardReason object Stuck : SoftReason object NoTarget : SoftReason - data class Requirement(val fact: content.bot.req.Requirement<*>) : HardReason + data class Requirement(val condition: Condition) : HardReason } interface SoftReason : Reason interface HardReason : Reason diff --git a/game/src/main/kotlin/content/bot/behaviour/activity/BotActivity.kt b/game/src/main/kotlin/content/bot/behaviour/activity/BotActivity.kt index f4f444be0b..9605de0989 100644 --- a/game/src/main/kotlin/content/bot/behaviour/activity/BotActivity.kt +++ b/game/src/main/kotlin/content/bot/behaviour/activity/BotActivity.kt @@ -2,7 +2,7 @@ package content.bot.behaviour.activity import content.bot.action.BotAction import content.bot.behaviour.Behaviour -import content.bot.req.Requirement +import content.bot.req.Condition /** * An activity with a limited number of slots that bots can perform @@ -11,8 +11,8 @@ import content.bot.req.Requirement data class BotActivity( override val id: String, val capacity: Int, - override val requires: List> = emptyList(), - override val setup: List> = emptyList(), + override val requires: List = emptyList(), + override val setup: List = emptyList(), override val actions: List = emptyList(), override val produces: Set = emptySet(), ) : Behaviour diff --git a/game/src/main/kotlin/content/bot/behaviour/navigation/Graph.kt b/game/src/main/kotlin/content/bot/behaviour/navigation/Graph.kt index 817cd3d581..b501ff94cd 100644 --- a/game/src/main/kotlin/content/bot/behaviour/navigation/Graph.kt +++ b/game/src/main/kotlin/content/bot/behaviour/navigation/Graph.kt @@ -6,7 +6,7 @@ import content.bot.behaviour.actions import content.bot.behaviour.requirements import content.bot.bot import content.bot.isBot -import content.bot.req.Requirement +import content.bot.req.Condition import world.gregs.config.Config import world.gregs.config.ConfigReader import world.gregs.voidps.engine.data.definition.Areas @@ -19,7 +19,7 @@ import java.util.PriorityQueue class Graph( val endNodes: IntArray = intArrayOf(), val edgeWeights: IntArray = intArrayOf(), - val edgeConditions: Array>?> = emptyArray(), + val edgeConditions: Array?> = emptyArray(), val actions: Array?> = emptyArray(), val adjacentEdges: Array = emptyArray(), val tiles: IntArray = intArrayOf(), @@ -30,7 +30,7 @@ class Graph( fun actions(edge: Int): List? = actions[edge] - fun conditions(edge: Int): List>? = edgeConditions[edge] + fun conditions(edge: Int): List? = edgeConditions[edge] fun tile(edge: Int): Tile { val nodeIndex = endNodes[edge] @@ -148,7 +148,7 @@ class Graph( // Edges val endNodes = mutableListOf() val weights = mutableListOf() - val conditions = mutableListOf>?>() + val conditions = mutableListOf?>() val actions = mutableListOf?>() val edges = mutableMapOf>() var edgeCount = 0 @@ -180,7 +180,7 @@ class Graph( addEdge(end, start, weight, actions) } - fun addEdge(from: Tile, to: Tile, weight: Int, actions: List, conditions: List>?) { + fun addEdge(from: Tile, to: Tile, weight: Int, actions: List, conditions: List?) { val start = add(from) val end = add(to) addEdge(start, end, weight, actions, conditions) @@ -195,7 +195,7 @@ class Graph( return tiles.indexOf(tile) } - fun addEdge(start: Int, end: Int, weight: Int, actions: List? = null, conditions: List>? = null): Int { + fun addEdge(start: Int, end: Int, weight: Int, actions: List? = null, conditions: List? = null): Int { val edgeIndex = edgeCount++ nodes.add(start) nodes.add(end) @@ -263,8 +263,8 @@ class Graph( builder.addEdge(Tile(from.x, from.y, from.level), Tile(to.x, to.y, to.level), cost, listOf(BotAction.WalkTo(to.x, to.y)), null) builder.addEdge(Tile(to.x, to.y, to.level), Tile(from.x, from.y, from.level), cost, listOf(BotAction.WalkTo(from.x, from.y)), null) } - requirements.isEmpty() -> builder.addEdge(Tile(from.x, from.y, from.level), Tile(to.x, to.y, to.level), cost, ActionParser.Companion.parse(actions, exception()), null) - else -> builder.addEdge(Tile(from.x, from.y, from.level), Tile(to.x, to.y, to.level), cost, ActionParser.Companion.parse(actions, exception()), Requirement.Companion.parse(requirements, exception())) + requirements.isEmpty() -> builder.addEdge(Tile(from.x, from.y, from.level), Tile(to.x, to.y, to.level), cost, ActionParser.parse(actions, exception()), null) + else -> builder.addEdge(Tile(from.x, from.y, from.level), Tile(to.x, to.y, to.level), cost, ActionParser.parse(actions, exception()), Condition.parse(requirements, exception())) } } } diff --git a/game/src/main/kotlin/content/bot/behaviour/navigation/NavigationShortcut.kt b/game/src/main/kotlin/content/bot/behaviour/navigation/NavigationShortcut.kt index cd787590b9..5887ddf0a6 100644 --- a/game/src/main/kotlin/content/bot/behaviour/navigation/NavigationShortcut.kt +++ b/game/src/main/kotlin/content/bot/behaviour/navigation/NavigationShortcut.kt @@ -2,13 +2,13 @@ package content.bot.behaviour.navigation import content.bot.action.BotAction import content.bot.behaviour.Behaviour -import content.bot.req.Requirement +import content.bot.req.Condition data class NavigationShortcut( override val id: String, val weight: Int, - override val requires: List> = emptyList(), - override val setup: List> = emptyList(), + override val requires: List = emptyList(), + override val setup: List = emptyList(), override val actions: List = emptyList(), override val produces: Set = emptySet(), ) : Behaviour diff --git a/game/src/main/kotlin/content/bot/behaviour/setup/Deficit.kt b/game/src/main/kotlin/content/bot/behaviour/setup/Deficit.kt index 29ccacbe5f..04469d412e 100644 --- a/game/src/main/kotlin/content/bot/behaviour/setup/Deficit.kt +++ b/game/src/main/kotlin/content/bot/behaviour/setup/Deficit.kt @@ -1,17 +1,10 @@ package content.bot.behaviour.setup -import content.bot.action.BotAction -import content.bot.req.Requirement -import content.bot.req.fact.Fact -import content.bot.req.predicate.Predicate -import content.entity.player.bank.bank -import world.gregs.voidps.engine.entity.character.player.Player -import world.gregs.voidps.engine.entity.item.Item -import world.gregs.voidps.engine.inv.inventory /** * Missing setup [Requirement]'s which can be produce dynamic [Resolver]'s */ +/* sealed interface Deficit { fun resolve(player: Player): Resolver? @@ -19,7 +12,8 @@ sealed interface Deficit { override fun resolve(player: Player): Resolver = Resolver("go_to_$area", -1, actions = listOf(BotAction.GoTo(area))) } - /* + */ +/* TODO setup resolution order if equipment deposit all inventory items @@ -29,7 +23,8 @@ sealed interface Deficit { if inventory deposit all items withdraw needed items - */ + *//* + data class Entry(val filter: Predicate, val needed: Int) data class MissingEquipment(val entries: List) : Deficit { @@ -58,7 +53,7 @@ sealed interface Deficit { "withdraw$uniqueName", weight = 20, setup = listOf( - Requirement(Fact.InventorySpace, Predicate.IntRange(spaceNeeded)), + Condition.Inventory(listOf(Condition.Entry("empty", min = spaceNeeded))), ), actions = actions, ) @@ -72,7 +67,7 @@ sealed interface Deficit { } var spaceNeeded = 0 actions.add(BotAction.GoToNearest("bank")) - actions.add(BotAction.InteractObject("Use-quickly", "bank_booth*", success = Requirement(Fact.InterfaceOpen("bank"), Predicate.BooleanTrue))) + actions.add(BotAction.InteractObject("Use-quickly", "bank_booth*", success = Condition.InterfaceOpen("bank"))) for (item in player.bank.items) { if (item.isEmpty()) { continue @@ -91,7 +86,7 @@ sealed interface Deficit { } if (spaceNeeded > 0) { if (player.inventory.spaces < spaceNeeded) { - actions.add(2, BotAction.InteractObject("Deposit carried items", "bank:carried", success = Requirement(Fact.InventorySpace, Predicate.IntEquals(28)))) + actions.add(2, BotAction.InteractObject("Deposit carried items", "bank:carried", success = Condition.Inventory(listOf(Condition.Entry("empty", min = 28))))) } actions.add(BotAction.CloseInterface) } @@ -104,7 +99,7 @@ sealed interface Deficit { var spaceNeeded = 0 val actions = mutableListOf( BotAction.GoToNearest("bank"), - BotAction.InteractObject("Use-quickly", "bank_booth*", success = Requirement(Fact.InterfaceOpen("bank"), Predicate.BooleanTrue)), + BotAction.InteractObject("Use-quickly", "bank_booth*", success = Condition.InterfaceOpen("bank")), ) val uniqueName = StringBuilder() for (item in player.bank.items) { @@ -128,15 +123,13 @@ sealed interface Deficit { } if (actions.size > 2) { if (player.inventory.spaces < spaceNeeded) { - actions.add(2, BotAction.InteractObject("Deposit carried items", "bank:carried", success = Requirement(Fact.InventorySpace, Predicate.IntEquals(28)))) + actions.add(2, BotAction.InteractObject("Deposit carried items", "bank:carried", success = Condition.Inventory(listOf(Condition.Entry("empty", min = 28))))) } actions.add(BotAction.CloseInterface) return Resolver( "withdraw_$uniqueName", weight = 20, - setup = listOf( - Requirement(Fact.InventorySpace, Predicate.IntRange(spaceNeeded)), - ), + setup = listOf(Condition.Inventory(listOf(Condition.Entry("empty", min = spaceNeeded)))), actions = actions, ) } @@ -144,3 +137,4 @@ sealed interface Deficit { } } } +*/ diff --git a/game/src/main/kotlin/content/bot/behaviour/setup/DynamicResolvers.kt b/game/src/main/kotlin/content/bot/behaviour/setup/DynamicResolvers.kt new file mode 100644 index 0000000000..76e977459f --- /dev/null +++ b/game/src/main/kotlin/content/bot/behaviour/setup/DynamicResolvers.kt @@ -0,0 +1,156 @@ +package content.bot.behaviour.setup + +import content.bot.action.BotAction +import content.bot.req.Condition +import content.bot.req.Condition.Entry +import content.entity.player.bank.bank +import world.gregs.voidps.engine.entity.character.player.Player +import world.gregs.voidps.engine.entity.character.player.equip.equipped +import world.gregs.voidps.engine.entity.character.player.skill.level.Level.hasRequirements +import world.gregs.voidps.engine.entity.character.player.skill.level.Level.hasRequirementsToUse +import world.gregs.voidps.engine.entity.item.Item +import world.gregs.voidps.engine.entity.item.slot +import world.gregs.voidps.engine.inv.inventory +import world.gregs.voidps.network.login.protocol.visual.update.player.EquipSlot + +object DynamicResolvers { + + fun resolver(player: Player, condition: Condition) = when (condition) { + is Condition.InArea -> Resolver("go_to_${condition.id}", -1, actions = listOf(BotAction.GoTo(condition.id))) + is Condition.Equipment -> resolveEquipment(player, condition.items) + is Condition.Inventory -> resolveInventory(player, condition.items) + else -> null + } + + private fun resolveInventory(player: Player, items: List): Resolver? { + val actions = mutableListOf() + val items = items.toMutableList() + actions.add(BotAction.GoToNearest("bank")) + actions.add(BotAction.InteractObject("Use-quickly", "bank_booth*", success = Condition.InterfaceOpen("bank"))) + // Free up inventory space + actions.add(BotAction.InterfaceOption("Deposit carried items", "bank:carried", success = Condition.Inventory(listOf(Entry(setOf("empty"), min = 28))))) + var found = false + for (entry in items) { + if (entry.ids.contains("empty")) { + continue + } + val index = player.bank.items.indexOfFirst { valid(player, it, entry) } + if (index == -1) { + // No valid items in the bank + return null + } + val item = player.bank.items[index] + // Withdraw the necessary item + withdraw(actions, entry, item) + found = true + } + actions.add(BotAction.CloseInterface) + if (found) { + return Resolver("withdraw_from_bank", weight = 20, actions = actions) + } + return null + } + + private fun valid(player: Player, item: Item, entry: Entry): Boolean { + if (item.isEmpty()) { + return false + } + if (!entry.ids.contains(item.id)) { + return false + } + if (entry.usable && !player.hasRequirementsToUse(item)) { + return false + } + if (entry.equippable && !player.hasRequirements(item)) { + return false + } + return true + } + + private fun resolveEquipment(player: Player, equipment: Map): Resolver? { + val actions = mutableListOf() + val equipment = equipment.toMutableMap() + + // Unequip items + for ((slot, entry) in equipment) { + if (!entry.ids.contains("empty")) { + continue + } + val item = player.equipped(slot) + if (item.isNotEmpty()) { + actions.add(BotAction.InterfaceOption("Remove", "worn_equipment:${slot.name.lowercase()}_slot:${item}")) + } + equipment.remove(slot) + } + + // Equip any held items + for (item in player.inventory.items) { + val slot = item.slot + if (slot == EquipSlot.None) { + continue + } + val entry = equipment[slot] ?: continue + if (!entry.ids.contains(item.id)) { + continue + } + actions.add(BotAction.InterfaceOption("Equip", "inventory:inventory:${item.id}")) + equipment.remove(slot) + } + + // Grab anything else from the bank + if (equipment.isNotEmpty()) { + actions.add(BotAction.GoToNearest("bank")) + actions.add(BotAction.InteractObject("Use-quickly", "bank_booth*", success = Condition.InterfaceOpen("bank"))) + // Free up inventory space if needed + if (player.inventory.spaces < equipment.size) { + actions.add(BotAction.InterfaceOption("Deposit carried items", "bank:carried", success = Condition.Inventory(listOf(Condition.Entry(setOf("empty"), min = 28))))) + } + val toEquip = mutableSetOf() + for (item in player.bank.items) { + if (item.isEmpty()) { + continue + } + val slot = item.slot + if (slot == EquipSlot.None) { + continue + } + if (equipment.isEmpty()) { + break + } + // Check if item meets requirement + val entry = equipment[slot] ?: continue + if (!entry.ids.contains(item.id)) { + continue + } + if (entry.usable && !player.hasRequirementsToUse(item)) { + continue + } + if (entry.equippable && !player.hasRequirements(item)) { + continue + } + // Withdraw the necessary item + withdraw(actions, entry, item) + toEquip.add(item.id) + equipment.remove(slot) + } + actions.add(BotAction.CloseInterface) + // Equip all items + for (id in toEquip) { + actions.add(BotAction.InterfaceOption("Equip", "inventory:inventory:${id}")) + } + } + if (actions.isNotEmpty()) { + return Resolver("equip_from_bank", weight = 20, actions = actions) + } + return null + } + + private fun withdraw(actions: MutableList, entry: Entry, item: Item) { + if (entry.min != null && entry.min > 1) { + actions.add(BotAction.InterfaceOption("Withdraw-X", "bank:inventory:${item.id}")) + actions.add(BotAction.IntEntry(entry.min)) + } else { + actions.add(BotAction.InterfaceOption("Withdraw-1", "bank:inventory:${item.id}")) + } + } +} \ No newline at end of file diff --git a/game/src/main/kotlin/content/bot/behaviour/setup/Resolver.kt b/game/src/main/kotlin/content/bot/behaviour/setup/Resolver.kt index 3ef23fc7f4..7221e12d93 100644 --- a/game/src/main/kotlin/content/bot/behaviour/setup/Resolver.kt +++ b/game/src/main/kotlin/content/bot/behaviour/setup/Resolver.kt @@ -2,7 +2,7 @@ package content.bot.behaviour.setup import content.bot.action.BotAction import content.bot.behaviour.Behaviour -import content.bot.req.Requirement +import content.bot.req.Condition /** * An activity that can be performed to resolve a requirement @@ -12,8 +12,8 @@ import content.bot.req.Requirement data class Resolver( override val id: String, val weight: Int, - override val requires: List> = emptyList(), - override val setup: List> = emptyList(), + override val requires: List = emptyList(), + override val setup: List = emptyList(), override val actions: List = emptyList(), override val produces: Set = emptySet(), ) : Behaviour diff --git a/game/src/main/kotlin/content/bot/req/Condition.kt b/game/src/main/kotlin/content/bot/req/Condition.kt index 45bee5b4da..ad4fc0f469 100644 --- a/game/src/main/kotlin/content/bot/req/Condition.kt +++ b/game/src/main/kotlin/content/bot/req/Condition.kt @@ -1,92 +1,269 @@ package content.bot.req +import content.entity.player.bank.bank +import net.pearx.kasechange.toPascalCase +import world.gregs.voidps.engine.client.variable.hasClock +import world.gregs.voidps.engine.data.definition.Areas import world.gregs.voidps.engine.entity.character.player.Player +import world.gregs.voidps.engine.entity.character.player.combatLevel import world.gregs.voidps.engine.entity.character.player.equip.equipped +import world.gregs.voidps.engine.entity.character.player.skill.Skill +import world.gregs.voidps.engine.entity.character.player.skill.level.Level.hasRequirements +import world.gregs.voidps.engine.entity.character.player.skill.level.Level.hasRequirementsToUse +import world.gregs.voidps.engine.entity.obj.GameObjects +import world.gregs.voidps.engine.event.Wildcard +import world.gregs.voidps.engine.event.Wildcards +import world.gregs.voidps.engine.inv.equipment import world.gregs.voidps.engine.inv.inventory import world.gregs.voidps.network.login.protocol.visual.update.player.EquipSlot -sealed class Condition { +sealed class Condition(val priority: Int) { abstract fun keys(): Set abstract fun events(): Set abstract fun check(player: Player): Boolean - data class Entry(val id: String, val min: Int?, val max: Int?) + data class InArea(val id: String) : Condition(1000) { + override fun keys() = setOf("area:$id") + override fun events() = setOf("area") + override fun check(player: Player) = player.tile in Areas[id] + } - data class Inventory(val items: List) : Condition() { - override fun keys() = items.map { "item:${it.id}" }.toSet() - override fun events() = setOf("inventory") + data class AtTile(val x: Int? = null, val y: Int? = null, val level: Int? = null) : Condition(1000) { + override fun keys() = setOf("tile") + override fun events() = setOf("tile") override fun check(player: Player): Boolean { - for (item in items) { - val amount = player.inventory.count(item.id) - if (item.min != null && amount < item.min) { - return false - } - if (item.max != null && amount > item.max) { - return false - } + if (x != null && player.tile.x != x) { + return false + } + if (y != null && player.tile.y != y) { + return false + } + if (level != null && player.tile.level != level) { + return false } return true } + } - fun parse(list: List>): Inventory { - val items = mutableListOf() - for (map in list) { - val id = map["id"] as String - var min = map["min"] as? Int - val max = map["max"] as? Int - if (min == null && max == null) { - min = 0 - } - items.add(Entry(id, min, max)) - } - return Inventory(items) - } + data class Queue(val id: String) : Condition(1) { + override fun keys() = setOf("queue:$id") + override fun events() = setOf("queue") + override fun check(player: Player) = player.queue.contains(id) + } + + data class Timer(val id: String) : Condition(1) { + override fun keys() = setOf("timer:$id") + override fun events() = setOf("timer") + override fun check(player: Player) = player.timers.contains(id) || player.softTimers.contains(id) } - data class Equipment(val items: Map) : Condition() { - override fun keys() = items.values.map { "item:${it.id}" }.toSet() + data class Clock(val id: String) : Condition(1) { + override fun keys() = setOf("clock:$id") + override fun events() = setOf("clock") + override fun check(player: Player) = player.hasClock(id) + } + + data class Entry(val ids: Set, val min: Int? = null, val max: Int? = null, val usable: Boolean = false, val equippable: Boolean = false) + + data class Inventory(val items: List) : Condition(100) { + override fun keys() = items.flatMap { entry -> entry.ids.map { "item:${it}" } }.toSet() + override fun events() = setOf("inventory") + override fun check(player: Player) = contains(player, player.inventory, items) + } + + data class Equipment(val items: Map) : Condition(90) { + override fun keys() = items.values.flatMap { entry -> entry.ids.map { "item:${it}" } }.toSet() override fun events() = setOf("worn_equipment") override fun check(player: Player): Boolean { for ((slot, entry) in items) { val item = player.equipped(slot) - if (item.id != entry.id) { + if (!entry.ids.contains(item.id)) { return false } - if (entry.min != null && item.amount < entry.min) { + if (!inRange(item.amount, entry.min, entry.max)) { return false } - if (entry.max != null && item.amount > entry.max) { + if (entry.usable && item.amount > 0 && !player.hasRequirementsToUse(item)) { return false } } return true } + } + + data class Bank(val items: List) : Condition(80) { + override fun keys() = items.flatMap { entry -> entry.ids.map { "bank:${it}" } }.toSet() + override fun events() = setOf("bank") + override fun check(player: Player) = contains(player, player.bank, items) + } + + data class Variable(val id: String, val equals: Any, val default: Any) : Condition(1) { + override fun keys() = setOf("var:$id") + override fun events() = setOf("variable") + override fun check(player: Player) = player.variables.get(id, default) == equals + } + + data class VariableIn(val id: String, val default: Int, val min: Int?, val max: Int?) : Condition(1) { + override fun keys() = setOf("var:$id") + override fun events() = setOf("variable") + override fun check(player: Player): Boolean { + val value = player.variables.get(id, default) + return inRange(value, min, max) + } + } + + data class ObjectExists(val id: String, val x: Int, val y: Int) : Condition(900) { + override fun keys() = setOf("obj:$id") + override fun events() = setOf("object") + override fun check(player: Player) = GameObjects.findOrNull(player.tile.copy(x, y), id) != null + } + + data class CombatLevel(val min: Int? = null, val max: Int? = null) : Condition(1) { + override fun keys() = setOf("skill:combat") + override fun events() = setOf("skill") + override fun check(player: Player) = inRange(player.combatLevel, min, max) + } + + data class InterfaceOpen(val id: String) : Condition(1) { + override fun keys() = setOf("iface:$id") + override fun events() = setOf("interface") + override fun check(player: Player) = player.interfaces.contains(id) + } + + data class SkillLevel(val skill: Skill, val min: Int? = null, val max: Int? = null) : Condition(1) { + override fun keys() = setOf("skill:${skill.name}") + override fun events() = setOf("skill:${skill.name}") + override fun check(player: Player) = inRange(player.levels.get(skill), min, max) + } + + companion object { + private fun inRange(value: Int, min: Int?, max: Int?): Boolean { + if (min != null && value < min) { + return false + } + if (max != null && value > max) { + return false + } + return true + } + + private fun contains(player: Player, inventory: world.gregs.voidps.engine.inv.Inventory, items: List): Boolean { + for (item in items) { + var found = false + for (id in item.ids) { + val amount = if (id == "empty") { + inventory.spaces + } else { + inventory.count(id) + } + if (!inRange(amount, item.min, item.max)) { + continue + } + if (item.usable && amount > 0 && id != "empty") { + val index = inventory.indexOf(id) + if (index == -1) { + error("Unable to find item $id in inventory.") + } + val item = inventory[index] + if (!player.hasRequirementsToUse(item)) { + continue + } + } + if (item.equippable && amount > 0 && id != "empty") { + val index = inventory.indexOf(id) + if (index == -1) { + error("Unable to find item $id in inventory.") + } + val item = inventory[index] + if (!player.hasRequirements(item)) { + continue + } + } + found = true + } + if (!found) { + return false + } + } + return true + } + + fun parse(list: List>>>, name: String): List { + val requirements = mutableListOf() + for ((type, value) in list) { + val condition = parse(type, value) ?: error("No condition parser for '$type' in $name.") + requirements.add(condition) + } + requirements.sortBy { it.priority } + return requirements + } + + fun parse(type: String, list: List>): Condition? { + return when (type) { + "inventory" -> parseInventory(list) + "equipment" -> parseEquipment(list) + "bank" -> parseBank(list) + "variable" -> parseVariable(list) + "clock" -> parseClock(list) + "timer" -> parseTimer(list) + "queue" -> parseQueue(list) + "area" -> parseArea(list) + "tile" -> parseTile(list) + "object" -> parseObject(list) + "combat_level" -> parseCombat(list) + "interface_open" -> parseInterface(list) + "skill" -> parseSkills(list) + else -> null + } + } + + private fun parseInventory(list: List>): Inventory { + return Inventory(parseItems(list)) + } + + private fun parseItems(list: List>): MutableList { + val items = mutableListOf() + for (map in list) { + val id = map["id"] as? String ?: error("Missing item id in $list") + var min = map["min"] as? Int + val max = map["max"] as? Int + if (min == null && max == null) { + min = 0 + } + val ids = toIds(id) + items.add(Entry(ids, min, max)) + } + return items + } + + private fun toIds(id: String): Set = id.split(",").flatMap { if (it.any { char -> char == '*' || char == '#' }) Wildcards.get(it, Wildcard.Item) else setOf(it) }.toSet() @Suppress("UNCHECKED_CAST") - fun parse(list: List>): Equipment { + private fun parseEquipment(list: List>): Equipment { val items = mutableMapOf() for (map in list) { for ((key, value) in map) { - value as Map - items[EquipSlot.by(key)] = Entry( - id = map["id"] as String, - min = map["min"] as? Int ?: 0, - max = map["max"] as? Int + val slot = EquipSlot.by(key) + require(slot != EquipSlot.None) { "Invalid equipment slot: $key in $list" } + value as? Map ?: error("Equipment $key expecting map, found: $value") + val id = value["id"] as? String ?: error("Missing item id in $list") + items[slot] = Entry( + ids = toIds(id), + min = value["min"] as? Int ?: 0, + max = value["max"] as? Int ) } } return Equipment(items) } - } - data class Variable(val id: String, val equals: Any, val default: Any) : Condition() { - override fun keys() = setOf("var:$id") - override fun events() = setOf("variable") - override fun check(player: Player) = player.variables.get(id, default) == equals + private fun parseBank(list: List>): Bank { + return Bank(parseItems(list)) + } @Suppress("UNCHECKED_CAST") - fun parse(list: List>): Condition? { + private fun parseVariable(list: List>): Condition? { val map = list.single() if (map.containsKey("equals")) { return Variable( @@ -104,21 +281,90 @@ sealed class Condition { } return null } - } - data class VariableIn(val id: String, val default: Int, val min: Int?, val max: Int?) : Condition() { - override fun keys() = setOf("var:$id") - override fun events() = setOf("variable") - override fun check(player: Player): Boolean { - val value = player.variables.get(id, default) - if (min != null && value < min) { - return false + private fun parseClock(list: List>): Condition? { + val map = list.single() + if (map.containsKey("id")) { + return Clock(id = map["id"] as String) } - if (max != null && value > max) { - return false + return null + } + + private fun parseTimer(list: List>): Condition? { + val map = list.single() + if (map.containsKey("id")) { + return Timer(id = map["id"] as String) } - return true + return null + } + + private fun parseQueue(list: List>): Condition? { + val map = list.single() + if (map.containsKey("id")) { + return Queue(id = map["id"] as String) + } + return null + } + + private fun parseObject(list: List>): Condition? { + val map = list.single() + if (map.containsKey("id")) { + return ObjectExists( + id = map["id"] as String, + x = map["x"] as Int, + y = map["y"] as Int + ) + } + return null + } + + private fun parseTile(list: List>): Condition? { + val map = list.single() + if (map.containsKey("x") || map.containsKey("y") || map.containsKey("level")) { + return AtTile( + x = map["x"] as? Int, + y = map["y"] as? Int, + level = map["level"] as? Int, + ) + } + return null + } + + private fun parseArea(list: List>): Condition? { + val map = list.single() + if (map.containsKey("id")) { + return InArea(id = map["id"] as String) + } + return null + } + + private fun parseCombat(list: List>): Condition? { + val map = list.single() + if (map.containsKey("min") || map.containsKey("max")) { + return CombatLevel(min = map["min"] as? Int, max = map["max"] as? Int) + } + return null + } + + private fun parseInterface(list: List>): Condition? { + val map = list.single() + if (map.containsKey("id")) { + return InterfaceOpen(id = map["id"] as String) + } + return null + } + + private fun parseSkills(list: List>): Condition? { + val map = list.single() + if (map.containsKey("id")) { + return SkillLevel( + skill = Skill.of((map["id"] as String).toPascalCase()) ?: error("Unknown skill: '${map["id"]}'"), + min = map["min"] as? Int, + max = map["max"] as? Int + ) + } + return null } } -} \ No newline at end of file +} diff --git a/game/src/main/kotlin/content/bot/req/Requirement.kt b/game/src/main/kotlin/content/bot/req/Requirement.kt deleted file mode 100644 index 01bd492810..0000000000 --- a/game/src/main/kotlin/content/bot/req/Requirement.kt +++ /dev/null @@ -1,38 +0,0 @@ -package content.bot.req - -import content.bot.behaviour.setup.Deficit -import content.bot.req.fact.Fact -import content.bot.req.fact.FactParser -import content.bot.req.predicate.Predicate -import world.gregs.voidps.engine.entity.character.player.Player - -data class Requirement(val fact: Fact, val predicate: Predicate? = null) { - - fun keys(): Set = (predicate?.keys() ?: emptySet()) + fact.groups() - - fun check(player: Player): Boolean = predicate?.test(player, fact.getValue(player)) ?: false - - fun deficits(player: Player): List = predicate?.evaluator?.evaluate(player, fact, predicate) ?: emptyList() - - companion object { - fun parse(list: List>>>, name: String, requirePredicates: Boolean = true): List> { - val requirements = mutableListOf>() - for ((type, value) in list) { - val parser = FactParser.parsers[type] ?: error("No fact parser for '$type' in $name.") - for (map in value) { - val error = parser.check(map) - if (error != null) { - error("Fact '$type' $error in $name.") - } - } - val requirement = parser.requirement(value) - if (requirePredicates && requirement.predicate == null) { - error("No predicates found for requirement $type map $value in $name.") - } - requirements.add(requirement) - } - requirements.sortBy { it.fact.priority } - return requirements - } - } -} diff --git a/game/src/main/kotlin/content/bot/req/RequirementEvaluator.kt b/game/src/main/kotlin/content/bot/req/RequirementEvaluator.kt deleted file mode 100644 index 2bf037efc3..0000000000 --- a/game/src/main/kotlin/content/bot/req/RequirementEvaluator.kt +++ /dev/null @@ -1,64 +0,0 @@ -package content.bot.req - -import content.bot.behaviour.setup.Deficit -import content.bot.behaviour.setup.Deficit.MissingInventory -import content.bot.req.fact.Fact -import content.bot.req.fact.ItemView -import content.bot.req.predicate.Predicate -import content.bot.req.predicate.Predicate.IntEquals -import world.gregs.voidps.engine.entity.character.player.Player -import world.gregs.voidps.engine.entity.item.Item -import world.gregs.voidps.type.Tile -import kotlin.collections.plusAssign - -/** - * Evaluates [Requirement]'s to produce known [Deficit]'s - */ -sealed class RequirementEvaluator { - abstract fun evaluate(player: Player, fact: Fact, predicate: Predicate): List - - object TileEval : RequirementEvaluator() { - override fun evaluate(player: Player, fact: Fact, predicate: Predicate): List { - if (fact is Fact.PlayerTile && predicate is Predicate.InArea) { - val value = fact.getValue(player) - if (!predicate.test(player, value)) { - return listOf(Deficit.NotInArea(predicate.name)) - } - } - return emptyList() - } - } - - object InventoryEval : RequirementEvaluator() { - override fun evaluate(player: Player, fact: Fact, predicate: Predicate): List { - if (fact is Fact.InventoryItems && predicate is Predicate.InventoryItems) { - val entries = mutableListOf() - collect(player, fact, predicate) { filter, needed -> entries += Deficit.Entry(filter, needed) } - return listOf(MissingInventory(entries)) - } else if (fact is Fact.EquipmentItems && predicate is Predicate.InventoryItems) { - val entries = mutableListOf() - collect(player, fact, predicate) { filter, needed -> entries += Deficit.Entry(filter, needed) } - return listOf(Deficit.MissingEquipment(entries)) - } - return emptyList() - } - - private fun collect(player: Player, fact: Fact, predicate: Predicate.InventoryItems, block: (Predicate, Int) -> Unit) { - val value = fact.getValue(player) - for (entry in predicate.entries) { - val have = value.count { entry.filter.test(player, it) } - if (entry.amount.test(player, have)) { - continue - } - val needed = when (entry.amount) { - is Predicate.IntRange -> entry.amount.min!! - have - is IntEquals -> entry.amount.value - have - else -> continue - } - if (needed > 0) { - block.invoke(entry.filter, needed) - } - } - } - } -} diff --git a/game/src/main/kotlin/content/bot/req/fact/Fact.kt b/game/src/main/kotlin/content/bot/req/fact/Fact.kt deleted file mode 100644 index f3e336335c..0000000000 --- a/game/src/main/kotlin/content/bot/req/fact/Fact.kt +++ /dev/null @@ -1,203 +0,0 @@ -package content.bot.req.fact - -import content.bot.req.predicate.Predicate -import content.entity.player.bank.bank -import world.gregs.voidps.engine.GameLoop -import world.gregs.voidps.engine.client.variable.remaining -import world.gregs.voidps.engine.entity.character.player.Player -import world.gregs.voidps.engine.entity.character.player.combatLevel -import world.gregs.voidps.engine.entity.character.player.skill.Skill -import world.gregs.voidps.engine.entity.obj.GameObjects -import world.gregs.voidps.engine.inv.equipment -import world.gregs.voidps.engine.inv.inventory -import world.gregs.voidps.engine.timer.epochSeconds -import world.gregs.voidps.type.Tile - -/** - * TODO what is the purpose of a fact? - * 1. provides current state of a player to Requirement/predicate - * 2. Allows matching resolvers by specific produces keys - * 3. Allows grouping requirements to listen for updates - * 4. Determines order of requirements to be checked in - * - * - * TODO you want to define configuration coarse level - * but evaluates at a fine level - * execution is at a course level - * - * Coarse desired state expands into fine facts - * - * A bots state which can be a [content.bot.req.Requirement] for, or a product of performing a [content.bot.behaviour.Behaviour] - * @param priority Ensure bots aren't walking to locations before getting items etc... lower values are prioritised first. - */ -sealed class Fact(val priority: Int) { - abstract fun getValue(player: Player): T - - /** - * Fact specific identifiers for finding resolvers e.g. inv:bronze_hatchet, bank:coins etc... - */ - open fun keys(): Set = emptySet() - - /** - * Group types to listen for types of updates e.g. inv:bank, enter:area etc... - */ - open fun groups(): Set = keys() - - object InventorySpace : Fact(10) { - override fun keys() = setOf("inventory_space") - override fun getValue(player: Player) = player.inventory.spaces - } - - object InventoryItems : Fact(100) { - override fun keys() = setOf("inv:inventory") - override fun getValue(player: Player) = ItemView(player.inventory) - } - - object EquipmentItems : Fact(90) { - override fun keys() = setOf("inv:worn_equipment") - override fun getValue(player: Player) = ItemView(player.equipment) - } - - object BankItems : Fact(100) { - override fun keys() = setOf("inv:bank") - override fun getValue(player: Player) = ItemView(player.bank) - } - - object AllItems : Fact(100) { - override fun keys() = setOf("inv:inventory", "inv:bank", "inv:worn_equipment") - override fun getValue(player: Player) = ItemView(player.inventory, player.bank, player.equipment) - } - - data class IntVariable(val id: String, val default: Int) : Fact(1) { - override fun keys() = setOf("var:${id}") - override fun getValue(player: Player) = player.variables.get(id) ?: default - } - - data class BoolVariable(val id: String, val default: Boolean?) : Fact(1) { - override fun keys() = setOf("var:${id}") - override fun getValue(player: Player) = player.variables.get(id) ?: default - } - - data class StringVariable(val id: String, val default: String?) : Fact(1) { - override fun keys() = setOf("var:${id}") - override fun getValue(player: Player) = player.variables.get(id) ?: default - } - - data class DoubleVariable(val id: String, val default: Double?) : Fact(1) { - override fun keys() = setOf("var:${id}") - override fun getValue(player: Player) = player.variables.get(id) ?: default - } - - data class ClockRemaining(val clock: String, val seconds: Boolean = false) : Fact(1) { - override fun keys() = setOf("var:$clock") - override fun getValue(player: Player) = player.remaining(clock, if (seconds) epochSeconds() else GameLoop.tick) - } - - data class HasTimer(val timer: String) : Fact(1) { - override fun keys() = setOf("timer:$timer") - override fun getValue(player: Player) = player.timers.contains(timer) - } - - data class HasQueue(val queue: String) : Fact(1) { - override fun keys() = setOf("queue:$queue") - override fun getValue(player: Player) = player.queue.contains(queue) - } - - data class InterfaceOpen(val id: String) : Fact(1) { - override fun keys() = setOf("iface:$id") - override fun getValue(player: Player) = player.interfaces.contains(id) - } - - object PlayerTile : Fact(1000) { - override fun keys() = setOf("tile") - override fun getValue(player: Player) = player.tile - } - - object PlayerLevel : Fact(1000) { - override fun keys() = setOf("level") - override fun getValue(player: Player) = player.tile.level - } - - object CombatLevel : Fact(1) { - override fun keys() = setOf("combat") - override fun getValue(player: Player) = player.combatLevel - } - - data class ObjectExists(val filter: Predicate, val tile: Tile) : Fact(1) { - override fun keys() = setOf("object") - override fun getValue(player: Player): Boolean { - for (obj in GameObjects.at(tile)) { - if (filter.test(player, obj.id)) { - return true - } - } - return false - } - } - - object AttackLevel : SkillLevel(Skill.Attack) - object DefenceLevel : SkillLevel(Skill.Defence) - object StrengthLevel : SkillLevel(Skill.Strength) - object ConstitutionLevel : SkillLevel(Skill.Constitution) - object RangedLevel : SkillLevel(Skill.Ranged) - object PrayerLevel : SkillLevel(Skill.Prayer) - object MagicLevel : SkillLevel(Skill.Magic) - object CookingLevel : SkillLevel(Skill.Cooking) - object WoodcuttingLevel : SkillLevel(Skill.Woodcutting) - object FletchingLevel : SkillLevel(Skill.Fletching) - object FishingLevel : SkillLevel(Skill.Fishing) - object FiremakingLevel : SkillLevel(Skill.Firemaking) - object CraftingLevel : SkillLevel(Skill.Crafting) - object SmithingLevel : SkillLevel(Skill.Smithing) - object MiningLevel : SkillLevel(Skill.Mining) - object HerbloreLevel : SkillLevel(Skill.Herblore) - object AgilityLevel : SkillLevel(Skill.Agility) - object ThievingLevel : SkillLevel(Skill.Thieving) - object SlayerLevel : SkillLevel(Skill.Slayer) - object FarmingLevel : SkillLevel(Skill.Farming) - object RunecraftingLevel : SkillLevel(Skill.Runecrafting) - object HunterLevel : SkillLevel(Skill.Hunter) - object ConstructionLevel : SkillLevel(Skill.Construction) - object SummoningLevel : SkillLevel(Skill.Summoning) - object DungeoneeringLevel : SkillLevel(Skill.Dungeoneering) - - abstract class SkillLevel( - val skill: Skill?, - ) : Fact(0) { - override fun keys() = setOf("skill:${skill!!.name.lowercase()}") - override fun getValue(player: Player) = player.levels.get(skill!!) - - companion object { - - fun of(skill: String): SkillLevel = when (skill.lowercase()) { - "attack" -> AttackLevel - "defence" -> DefenceLevel - "strength" -> StrengthLevel - "constitution" -> ConstitutionLevel - "ranged" -> RangedLevel - "prayer" -> PrayerLevel - "magic" -> MagicLevel - "cooking" -> CookingLevel - "woodcutting" -> WoodcuttingLevel - "fletching" -> FletchingLevel - "fishing" -> FishingLevel - "firemaking" -> FiremakingLevel - "crafting" -> CraftingLevel - "smithing" -> SmithingLevel - "mining" -> MiningLevel - "herblore" -> HerbloreLevel - "agility" -> AgilityLevel - "thieving" -> ThievingLevel - "slayer" -> SlayerLevel - "farming" -> FarmingLevel - "runecrafting" -> RunecraftingLevel - "hunter" -> HunterLevel - "construction" -> ConstructionLevel - "summoning" -> SummoningLevel - "dungeoneering" -> DungeoneeringLevel - else -> throw IllegalArgumentException("Unknown skill: $skill") - } - } - } - -} diff --git a/game/src/main/kotlin/content/bot/req/fact/FactParser.kt b/game/src/main/kotlin/content/bot/req/fact/FactParser.kt deleted file mode 100644 index 26b569ebbd..0000000000 --- a/game/src/main/kotlin/content/bot/req/fact/FactParser.kt +++ /dev/null @@ -1,158 +0,0 @@ -package content.bot.req.fact - -import content.bot.req.Requirement -import content.bot.req.predicate.Predicate -import world.gregs.voidps.engine.entity.character.player.Player -import world.gregs.voidps.engine.entity.character.player.equip.equipped -import world.gregs.voidps.engine.inv.inventory -import world.gregs.voidps.network.login.protocol.visual.update.player.EquipSlot -import world.gregs.voidps.type.Tile - -sealed class FactParser { - open val required: Set = emptySet() - abstract fun parse(map: Map): Fact - abstract fun predicate(map: Map): Predicate? - - open fun requirement(list: List>): Requirement { - val map = list.singleOrNull() - if (map != null) { - return Requirement(parse(map), predicate(map)) - } - throw IllegalStateException("No list requirement implemented for ${this::class.simpleName} fact type") - } - - fun check(map: Map): String? { - for (key in required) { - if (!map.containsKey(key)) { - return "missing key '$key' in map $map" - } - } - return null - } - - object InventorySpace : FactParser() { - override fun parse(map: Map) = Fact.InventorySpace - override fun predicate(map: Map) = Predicate.parseInt(map) - } - - object InventoryItems : FactParser() { - override fun parse(map: Map) = Fact.InventoryItems - override fun predicate(map: Map) = null - override fun requirement(list: List>): Requirement = Requirement(Fact.InventoryItems, Predicate.parseItems(list)) - } - - object EquipmentItems : FactParser() { - override fun parse(map: Map) = Fact.EquipmentItems - override fun predicate(map: Map) = null - override fun requirement(list: List>): Requirement = Requirement(Fact.EquipmentItems, Predicate.parseItems(list)) - } - - object BankedItems : FactParser() { - override fun parse(map: Map) = Fact.BankItems - override fun predicate(map: Map) = null - override fun requirement(list: List>): Requirement = Requirement(Fact.BankItems, Predicate.parseItems(list)) - } - - object AllItems : FactParser() { - override fun parse(map: Map) = Fact.AllItems - override fun predicate(map: Map) = null - override fun requirement(list: List>): Requirement = Requirement(Fact.AllItems, Predicate.parseItems(list)) - } - - object Variable : FactParser() { - override val required = setOf("id", "default") - override fun parse(map: Map): Fact { - val id = map["id"] as String - return when (val default = map["default"]) { - is Int -> Fact.IntVariable(id, default) - is String -> Fact.StringVariable(id, default) - is Double -> Fact.DoubleVariable(id, default) - is Boolean -> Fact.BoolVariable(id, default) - else -> error("Invalid default value $default") - } as Fact - } - - override fun predicate(map: Map): Predicate? = when (val default = map["default"]) { - is Int -> Predicate.parseInt(map) - is String -> Predicate.parseString(map) - is Double -> Predicate.parseDouble(map) - is Boolean -> Predicate.parseBool(map) - else -> error("Invalid default value $default") - } as? Predicate - } - - object Clock : FactParser() { - override val required = setOf("id") - override fun parse(map: Map): Fact = Fact.ClockRemaining(map["id"] as String, map["seconds"] as? Boolean ?: false) - - override fun predicate(map: Map) = Predicate.parseInt(map) - } - - object Timer : FactParser() { - override val required = setOf("id") - override fun parse(map: Map) = Fact.HasTimer(map["id"] as String) - override fun predicate(map: Map) = Predicate.BooleanTrue - } - - object Queue : FactParser() { - override val required = setOf("id") - override fun parse(map: Map) = Fact.HasQueue(map["id"] as String) - override fun predicate(map: Map) = Predicate.BooleanTrue - } - - object Interface : FactParser() { - override val required = setOf("id") - override fun parse(map: Map) = Fact.InterfaceOpen(map["id"] as String) - override fun predicate(map: Map) = Predicate.BooleanTrue - } - - object PlayerTile : FactParser() { - override fun parse(map: Map) = Fact.PlayerTile - override fun predicate(map: Map) = Predicate.parseTile(map) - } - - object CombatLevel : FactParser() { - override fun parse(map: Map) = Fact.CombatLevel - override fun predicate(map: Map) = Predicate.parseInt(map) - } - - object ObjectExists : FactParser() { - override val required = setOf("id", "x", "y") - override fun parse(map: Map): Fact.ObjectExists { - val tile = Tile(map["x"] as Int, map["y"] as Int) - val id = map["id"] as String - return Fact.ObjectExists(Predicate.StringEquals(id), tile) - } - - override fun predicate(map: Map) = Predicate.BooleanTrue - } - - object Skill : FactParser() { - override val required = setOf("id") - override fun parse(map: Map) = Fact.SkillLevel.of(map["id"] as String) - override fun predicate(map: Map) = Predicate.parseInt(map) - } - - - companion object { - // Parser creates requirement sealed class which has it all in one - val parsers = mapOf( - "inventory_space" to InventorySpace, - "inventory" to InventoryItems, - "owns" to AllItems, - "banked" to BankedItems, - "equips" to EquipmentItems, - "variable" to Variable, - "clock" to Clock, - "has_timer" to Timer, - "interface_open" to Interface, - "has_queue" to Queue, - "tile" to PlayerTile, - "area" to PlayerTile, - "combat_level" to CombatLevel, - "skill" to Skill, - "object" to ObjectExists, - ) - } -} - diff --git a/game/src/main/kotlin/content/bot/req/fact/ItemView.kt b/game/src/main/kotlin/content/bot/req/fact/ItemView.kt deleted file mode 100644 index 8f3fe5368c..0000000000 --- a/game/src/main/kotlin/content/bot/req/fact/ItemView.kt +++ /dev/null @@ -1,12 +0,0 @@ -package content.bot.req.fact - -import world.gregs.voidps.engine.entity.item.Item -import world.gregs.voidps.engine.inv.Inventory - -class ItemView(vararg val inventories: Inventory) { - fun contains(item: String) = inventories.any { it.contains(item) } - fun contains(item: String, amount: Int) = inventories.any { it.contains(item, amount) } - fun count(item: String) = inventories.sumOf { it.count(item) } - fun count(block: (Item) -> Boolean) = inventories.sumOf { it.items.count(block) } - fun size() = inventories.sumOf { it.size } -} diff --git a/game/src/main/kotlin/content/bot/req/predicate/Predicate.kt b/game/src/main/kotlin/content/bot/req/predicate/Predicate.kt deleted file mode 100644 index 372cef4334..0000000000 --- a/game/src/main/kotlin/content/bot/req/predicate/Predicate.kt +++ /dev/null @@ -1,208 +0,0 @@ -package content.bot.req.predicate - -import content.bot.req.RequirementEvaluator -import content.bot.req.fact.ItemView -import world.gregs.voidps.engine.data.definition.Areas -import world.gregs.voidps.engine.entity.character.player.Player -import world.gregs.voidps.engine.entity.character.player.skill.level.Level.hasRequirements -import world.gregs.voidps.engine.entity.character.player.skill.level.Level.hasRequirementsToUse -import world.gregs.voidps.engine.entity.item.Item -import world.gregs.voidps.engine.event.Wildcard -import world.gregs.voidps.engine.event.Wildcards -import world.gregs.voidps.type.Tile - -sealed class Predicate { - abstract fun test(player: Player, value: T): Boolean - open val children: Set> = emptySet() - open val evaluator: RequirementEvaluator? = null - open fun keys(): Set = emptySet() - - data class IntRange(val min: Int? = null, val max: Int? = null) : Predicate() { - override fun test(player: Player, value: Int): Boolean { - if (min != null && value < min) return false - if (max != null && value > max) return false - return true - } - } - - data class IntEquals(val value: Int) : Predicate() { - override fun test(player: Player, value: Int) = value == this.value - } - - data class DoubleRange(val min: Double? = null, val max: Double? = null) : Predicate() { - override fun test(player: Player, value: Double): Boolean { - if (min != null && value < min) return false - if (max != null && value > max) return false - return true - } - } - - data class DoubleEquals(val value: Double) : Predicate() { - override fun test(player: Player, value: Double) = value == this.value - } - - data class InArea(val name: String) : Predicate() { - override val evaluator = RequirementEvaluator.TileEval - override fun test(player: Player, value: Tile) = value in Areas[name] - override fun keys() = setOf("in:${name}") - } - - object BooleanTrue : Predicate() { - override fun test(player: Player, value: Boolean) = value - } - - object BooleanFalse : Predicate() { - override fun test(player: Player, value: Boolean) = !value - } - - data class StringEquals(val value: String) : Predicate() { - override fun test(player: Player, value: String) = value == this.value - } - - data class TileEquals(val x: Int? = null, val y: Int? = null, val level: Int? = null) : Predicate() { - override val evaluator = RequirementEvaluator.TileEval - override fun test(player: Player, value: Tile): Boolean { - if (x != null && value.x != x) return false - if (y != null && value.y != y) return false - if (level != null && value.level != level) return false - return true - } - } - - data class Within(val x: Int, val y: Int, val level: Int, val radius: Int) : Predicate() { - override val evaluator = RequirementEvaluator.TileEval - override fun test(player: Player, value: Tile) = value.within(x, y, level, radius) - } - - data class InventoryItems(val entries: List) : Predicate() { - data class Entry( - val filter: Predicate, - val amount: Predicate, - ) - override val evaluator = RequirementEvaluator.InventoryEval - override val children = entries.map { it.amount }.toSet() + entries.map { it.filter }.toSet() - - override fun test(player: Player, value: ItemView): Boolean { - for ((filter, amount) in entries) { - val count = value.count { item -> filter.test(player, item) } - if (!amount.test(player, count)) { - return false - } - } - return true - } - - override fun keys() = entries.flatMap { it.filter.keys() }.toSet() - } - - data class AnyItem(private val ids: Set) : Predicate() { - override fun test(player: Player, value: Item) = value.id in ids - override fun keys() = ids - } - - object EquipableItem : Predicate() { - override fun test(player: Player, value: Item) = player.hasRequirements(value) - } - - object UsableItem : Predicate() { - override fun test(player: Player, value: Item) = player.hasRequirementsToUse(value) - } - - data class EqualsItem(private val id: String) : Predicate() { - override fun test(player: Player, value: Item) = value.id == id - } - - data class AllOf(override val children: Set>) : Predicate() { - override fun test(player: Player, value: T) = children.all { it.test(player, value) } - override fun keys() = children.flatMap { it.keys() }.toSet() - } - - data class AnyOf(override val children: Set>) : Predicate() { - override fun test(player: Player, value: T) = children.any { it.test(player, value) } - override fun keys() = children.flatMap { it.keys() }.toSet() - } - - companion object { - fun parseInt(map: Map): Predicate? = when { - map.containsKey("min") || map.containsKey("max") -> IntRange(map["min"] as? Int, map["max"] as? Int) - map.containsKey("equals") -> { - when (val value = map["equals"]) { - is Int -> IntEquals(value) - else -> error("Unsupported equals type: '${value?.let { it::class.simpleName }}'") - } - } - else -> null - } - - fun parseDouble(map: Map): Predicate? = when { - map.containsKey("min") || map.containsKey("max") -> DoubleRange(map["min"] as? Double, map["max"] as? Double) - map.containsKey("equals") -> { - when (val value = map["equals"]) { - is Double -> DoubleEquals(value) - else -> error("Unsupported equals type: '${value?.let { it::class.simpleName }}'") - } - } - else -> null - } - - fun parseBool(map: Map): Predicate? { - val equals = map["equals"] ?: return null - if (equals !is Boolean) { - error("Unsupported equals type: '${equals.let { it::class.simpleName }}'") - } - return if (equals) BooleanTrue else BooleanFalse - } - - fun parseString(map: Map): Predicate? { - val equals = map["equals"] ?: return null - if (equals !is String) { - error("Unsupported equals type: '${equals.let { it::class.simpleName }}'") - } - return StringEquals(equals) - } - - fun parseTile(map: Map): Predicate? = when { - map.containsKey("id") -> InArea(map["id"] as String) - map.containsKey("x") || map.containsKey("y") || map.containsKey("level") -> TileEquals(map["x"] as? Int, map["y"] as? Int, map["level"] as? Int) - else -> null - } - - fun parseItems(items: List>): InventoryItems { - val entries = mutableListOf() - for (item in items) { - val filter = itemFilter(item) - val counter = parseInt(item) ?: IntRange(min = 1) - entries.add(InventoryItems.Entry(filter, counter)) - } - return InventoryItems(entries) - } - - private fun itemFilter(item: Map): Predicate { - require(item.containsKey("id")) { "Item must have field 'id' in map $item" } - val id = item["id"] as String - var filter = if (id.contains(",")) { - val ids = id.split(",") - AnyItem( - ids.flatMap { id -> - if (id.any { char -> char == '*' || char == '#' }) { - Wildcards.get(id, Wildcard.Item) - } else { - setOf(id) - } - }.toSet(), - ) - } else if (id.any { it == '*' || it == '#' }) { - AnyItem(Wildcards.get(id, Wildcard.Item)) - } else { - EqualsItem(id) - } - if (item.containsKey("usable") && item["usable"] as Boolean) { - // TODO lookup values from custom configs e.g. firemaking.level - filter = AllOf(setOf(filter, UsableItem)) - } else if (item.containsKey("equipable") && item["equipable"] as Boolean) { - filter = AllOf(setOf(filter, EquipableItem)) - } - return filter - } - } -} diff --git a/game/src/test/kotlin/content/bot/BotManagerTest.kt b/game/src/test/kotlin/content/bot/BotManagerTest.kt index bd6b40e845..b44b96843c 100644 --- a/game/src/test/kotlin/content/bot/BotManagerTest.kt +++ b/game/src/test/kotlin/content/bot/BotManagerTest.kt @@ -7,12 +7,11 @@ import content.bot.behaviour.Reason import content.bot.behaviour.SoftReason import content.bot.behaviour.activity.BotActivity import content.bot.behaviour.setup.Resolver -import content.bot.req.Requirement -import content.bot.req.fact.Fact -import content.bot.req.predicate.Predicate +import content.bot.req.Condition import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.Test import world.gregs.voidps.engine.entity.character.player.Player +import world.gregs.voidps.engine.entity.character.player.skill.Skill class BotManagerTest { @@ -162,7 +161,7 @@ class BotManagerTest { manager.tick(bot) manager.tick(bot) - bot.frame().fail(Reason.Requirement(Requirement(Fact.PlayerTile))) + bot.frame().fail(Reason.Requirement(Condition.AtTile())) manager.tick(bot) manager.tick(bot) @@ -175,7 +174,7 @@ class BotManagerTest { val activity = testActivity( id = "test", requires = listOf( - Requirement(Fact.AttackLevel, Predicate.IntRange(99, 99)), + Condition.SkillLevel(Skill.Attack, 99) ), plan = listOf(BotAction.Wait(4)), ) @@ -194,12 +193,12 @@ class BotManagerTest { @Test fun `Resolvable requirement queues resolver before activity starts`() { - val condition = Requirement(Fact.PlayerTile, Predicate.Within(100, 100, 2, 2)) + val condition = Condition.AtTile(100, 100, 2) val resolver = Resolver( id = "go_to_area", weight = 1, actions = listOf(BotAction.Wait(1)), - produces = setOf(condition), + produces = setOf("tile"), ) val activity = testActivity( id = "woodcut", @@ -208,7 +207,7 @@ class BotManagerTest { ) val manager = BotManager( mutableMapOf(activity.id to activity), - mutableMapOf(condition.fact.keys().first() to mutableListOf(resolver)), + mutableMapOf(condition.keys().first() to mutableListOf(resolver)), ) val bot = testBot(activity) @@ -221,7 +220,7 @@ class BotManagerTest { @Test fun `Lowest weight resolver is selected`() { - val condition = Requirement(Fact.PlayerTile, Predicate.Within(100, 100, 2, 2)) + val condition = Condition.AtTile(100, 100, 2) val bad = Resolver("bad", weight = 10, actions = listOf(BotAction.Clone(""))) val good = Resolver("good", weight = 1, actions = listOf(BotAction.Clone(""))) @@ -234,7 +233,7 @@ class BotManagerTest { val manager = BotManager( mutableMapOf(activity.id to activity), - mutableMapOf(condition.fact.keys().first() to mutableListOf(bad, good)), + mutableMapOf(condition.keys().first() to mutableListOf(bad, good)), ) val bot = testBot(activity) @@ -246,7 +245,7 @@ class BotManagerTest { @Test fun `Blocked resolver is not reselected`() { - val condition = Requirement(Fact.PlayerTile, Predicate.Within(100, 100, 2, 2)) + val condition = Condition.AtTile(100, 100, 2) val resolver = Resolver(id = "get_key", weight = 1, actions = listOf(BotAction.Clone(""))) val activity = testActivity( id = "open_door", @@ -255,7 +254,7 @@ class BotManagerTest { ) val manager = BotManager( mutableMapOf(activity.id to activity), - mutableMapOf(condition.fact.keys().first() to mutableListOf(resolver)), + mutableMapOf(condition.keys().first() to mutableListOf(resolver)), ) val bot = testBot(activity) @@ -272,7 +271,7 @@ class BotManagerTest { @Test fun `Hard failure in resolver stops bot`() { - val condition = Requirement(Fact.PlayerTile, Predicate.Within(100, 100, 2, 2)) + val condition = Condition.AtTile(100, 100, 2) val resolver = Resolver( id = "walk", weight = 1, @@ -285,7 +284,7 @@ class BotManagerTest { ) val manager = BotManager( mutableMapOf(activity.id to activity), - mutableMapOf(condition.fact.keys().first() to mutableListOf(resolver)), + mutableMapOf(condition.keys().first() to mutableListOf(resolver)), ) val bot = testBot(activity) @@ -300,7 +299,7 @@ class BotManagerTest { @Test fun `Soft failure in resolver only pops resolver`() { - val condition = Requirement(Fact.PlayerTile, Predicate.Within(100, 100, 2, 2)) + val condition = Condition.AtTile(100, 100, 2) val resolver = Resolver( id = "test", weight = 1, @@ -313,7 +312,7 @@ class BotManagerTest { ) val manager = BotManager( mutableMapOf(activity.id to activity), - mutableMapOf(condition.fact.keys().first() to mutableListOf(resolver)), + mutableMapOf(condition.keys().first() to mutableListOf(resolver)), ) val bot = testBot(activity) @@ -329,12 +328,12 @@ class BotManagerTest { @Test fun `Resolver with unmet mandatory requirements is skipped`() { - val condition = Requirement(Fact.PlayerTile, Predicate.Within(100, 100, 2, 2)) + val condition = Condition.AtTile(100, 100, 2) val resolver = Resolver( id = "mine_gem", weight = 1, actions = listOf(BotAction.Clone("")), - requires = listOf(Requirement(Fact.MiningLevel, Predicate.IntRange(99, 99))), + requires = listOf(Condition.SkillLevel(Skill.Mining, 99)), ) val activity = testActivity( id = "craft", @@ -343,7 +342,7 @@ class BotManagerTest { ) val manager = BotManager( mutableMapOf(activity.id to activity), - mutableMapOf(condition.fact.keys().first() to mutableListOf(resolver)), + mutableMapOf(condition.keys().first() to mutableListOf(resolver)), ) val bot = testBot(activity) @@ -357,7 +356,7 @@ class BotManagerTest { @Test fun `Activity are occupied while resolver is running`() { - val condition = Requirement(Fact.PlayerTile, Predicate.Within(100, 100, 2, 2)) + val condition = Condition.AtTile(100, 100, 2) val resolver = Resolver( id = "get_tool", weight = 1, @@ -370,7 +369,7 @@ class BotManagerTest { ) val manager = BotManager( mutableMapOf(activity.id to activity), - mutableMapOf(condition.fact.keys().first() to mutableListOf(resolver)), + mutableMapOf(condition.keys().first() to mutableListOf(resolver)), ) val bot = testBot(activity) @@ -382,8 +381,8 @@ class BotManagerTest { fun testActivity( id: String, - requires: List> = emptyList(), - resolves: List> = emptyList(), + requires: List = emptyList(), + resolves: List = emptyList(), plan: List, ) = BotActivity(id, 1, requires, resolves, plan) } diff --git a/game/src/test/kotlin/content/bot/interact/path/GraphTest.kt b/game/src/test/kotlin/content/bot/interact/path/GraphTest.kt index d1424930f3..7e3e966d14 100644 --- a/game/src/test/kotlin/content/bot/interact/path/GraphTest.kt +++ b/game/src/test/kotlin/content/bot/interact/path/GraphTest.kt @@ -2,9 +2,7 @@ package content.bot.interact.path import content.bot.behaviour.navigation.Graph import content.bot.behaviour.navigation.NavigationShortcut -import content.bot.req.Requirement -import content.bot.req.fact.Fact -import content.bot.req.predicate.Predicate +import content.bot.req.Condition import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Test import world.gregs.voidps.engine.data.definition.AreaDefinition @@ -139,7 +137,7 @@ class GraphTest { builder.addEdge(b, c, 3) val cd = builder.addEdge(c, d, 6) val ec = builder.addEdge(e, c, 2) - builder.addEdge(e, d, 7, conditions = listOf(Requirement(Fact.PlayerTile, Predicate.TileEquals(100)))) + builder.addEdge(e, d, 7, conditions = listOf(Condition.AtTile(100))) // builder.print() val output = mutableListOf() @@ -258,7 +256,7 @@ class GraphTest { to = b, weight = 1, actions = emptyList(), - conditions = listOf(Requirement(Fact.PlayerTile, Predicate.TileEquals(100))), + conditions = listOf(Condition.AtTile(100)), ) val graph = builder.build() @@ -302,8 +300,8 @@ class GraphTest { val shortcut = NavigationShortcut( id = "teleport", weight = 1, - requires = listOf(Requirement(Fact.PlayerTile, Predicate.TileEquals(50, 50))), - produces = setOf(Requirement(Fact.PlayerTile, Predicate.InArea("town"))), + requires = listOf(Condition.AtTile(50, 50)), + produces = setOf("area:town"), ) val builder = Graph.Builder() diff --git a/tools/src/main/kotlin/world/gregs/voidps/tools/ItemDefinitions.kt b/tools/src/main/kotlin/world/gregs/voidps/tools/ItemDefinitions.kt index 84ceb2f9ab..f690390084 100644 --- a/tools/src/main/kotlin/world/gregs/voidps/tools/ItemDefinitions.kt +++ b/tools/src/main/kotlin/world/gregs/voidps/tools/ItemDefinitions.kt @@ -23,10 +23,11 @@ object ItemDefinitions { val decoder = ItemDefinitions.init(ItemDecoder(parameters).load(cache)).load(files.list(Settings["definitions.items"])) for (i in decoder.definitions.indices) { val def = decoder.getOrNull(i) ?: continue - if (def.stringId.contains("dragon_plate")) { + if (def.stringId.contains("rune_plate")) { // if (def.get("category", "") != "") // if (/*def.get("category", "") == "throwable" &&*/ def.contains("secondary_use_level")) - println("${def.stringId} ${def.extras}") +// println("${def.stringId} ${def.extras}") + println(def) } // if (def.contains("ammo_group")) { // println("${def.stringId} ${def.extras}") From 42e9e3ed4981380f94b459f740f4e78fba77c77a Mon Sep 17 00:00:00 2001 From: GregHib Date: Tue, 10 Feb 2026 15:33:14 +0000 Subject: [PATCH 084/101] Add timeout when an activity doesn't produce anything for a given time --- data/bot/fishing.templates.toml | 35 +-- game/src/main/kotlin/content/bot/Bot.kt | 3 +- .../bot/{BotSpawns.kt => BotCommands.kt} | 2 +- .../src/main/kotlin/content/bot/BotManager.kt | 269 ++++++++++-------- .../src/main/kotlin/content/bot/BotUpdates.kt | 55 ++-- .../kotlin/content/bot/behaviour/Behaviour.kt | 33 ++- .../content/bot/behaviour/BehaviourFrame.kt | 6 +- .../bot/{req => behaviour}/Condition.kt | 10 +- .../kotlin/content/bot/behaviour/Reason.kt | 2 - .../{ => behaviour}/action/ActionParser.kt | 4 +- .../bot/{ => behaviour}/action/BotAction.kt | 14 +- .../bot/behaviour/activity/ActivitySlots.kt | 3 + .../bot/behaviour/activity/BotActivity.kt | 5 +- .../content/bot/behaviour/navigation/Graph.kt | 6 +- .../navigation/NavigationShortcut.kt | 5 +- .../content/bot/behaviour/setup/Deficit.kt | 140 --------- .../bot/behaviour/setup/DynamicResolvers.kt | 10 +- .../content/bot/behaviour/setup/Resolver.kt | 7 +- .../player/command/PathFindingCommands.kt | 2 +- .../test/kotlin/content/bot/BotManagerTest.kt | 6 +- .../activity}/ActivitySlotsTest.kt | 14 +- .../navigation}/GraphTest.kt | 17 +- 22 files changed, 276 insertions(+), 372 deletions(-) rename game/src/main/kotlin/content/bot/{BotSpawns.kt => BotCommands.kt} (99%) rename game/src/main/kotlin/content/bot/{req => behaviour}/Condition.kt (98%) rename game/src/main/kotlin/content/bot/{ => behaviour}/action/ActionParser.kt (99%) rename game/src/main/kotlin/content/bot/{ => behaviour}/action/BotAction.kt (98%) delete mode 100644 game/src/main/kotlin/content/bot/behaviour/setup/Deficit.kt rename game/src/test/kotlin/content/bot/{ => behaviour/activity}/ActivitySlotsTest.kt (67%) rename game/src/test/kotlin/content/bot/{interact/path => behaviour/navigation}/GraphTest.kt (96%) diff --git a/data/bot/fishing.templates.toml b/data/bot/fishing.templates.toml index ff6b1437de..5540d09e8b 100644 --- a/data/bot/fishing.templates.toml +++ b/data/bot/fishing.templates.toml @@ -13,7 +13,7 @@ actions = [ { npc = { option = "Cage", id = "$spot", delay = 5, success = { inventory = { id = "empty", max = 0 } } } }, ] produces = [ - { inventory = "raw_crayfish" }, + { item = "raw_crayfish" }, { skill = "fishing" } ] @@ -29,7 +29,7 @@ actions = [ { npc = { option = "Net", id = "$spot", delay = 5, success = { inventory = { id = "empty", max = 0 } } } }, ] produces = [ - { inventory = "raw_shrimp" }, + { item = "raw_shrimp" }, { skill = "fishing" } ] @@ -45,34 +45,5 @@ actions = [ { npc = { option = "Net", id = "$spot", delay = 5, success = { inventory = { id = "empty", max = 0 } } } }, ] produces = [ - { skill = "fishing" } + { skill = "fishing" }, ] - -# Requires -# Skill range -# variable = id, equals, default -# clock = ticks - -# Setup -# area = id -# inventory = {} - -# Actions -# inventory space max = 0 -# interface open id - -# Wait_if -#variable, id, min, default -# clock id, min - -# Success -# tile = x, y/level -# carries coins/amount -#queue = id - -# Produces -# skill = "name" -# area = "name" -# item = "name" -# combine into Set skill:name -# variable = "value" \ No newline at end of file diff --git a/game/src/main/kotlin/content/bot/Bot.kt b/game/src/main/kotlin/content/bot/Bot.kt index 8ca9be58bf..4cd3d5ac08 100644 --- a/game/src/main/kotlin/content/bot/Bot.kt +++ b/game/src/main/kotlin/content/bot/Bot.kt @@ -1,6 +1,6 @@ package content.bot -import content.bot.action.BotAction +import content.bot.behaviour.action.BotAction import content.bot.behaviour.BehaviourFrame import content.bot.behaviour.BehaviourState import content.bot.behaviour.Reason @@ -14,6 +14,7 @@ data class Bot(val player: Player) : Character by player { var previous: BotActivity? = null val frames = Stack() val available = mutableSetOf() + var evaluate = mutableSetOf() fun noTask() = frames.isEmpty() diff --git a/game/src/main/kotlin/content/bot/BotSpawns.kt b/game/src/main/kotlin/content/bot/BotCommands.kt similarity index 99% rename from game/src/main/kotlin/content/bot/BotSpawns.kt rename to game/src/main/kotlin/content/bot/BotCommands.kt index c69fd4352e..4fdee5aaee 100644 --- a/game/src/main/kotlin/content/bot/BotSpawns.kt +++ b/game/src/main/kotlin/content/bot/BotCommands.kt @@ -32,7 +32,7 @@ import java.util.concurrent.TimeUnit import kotlin.coroutines.resume import kotlin.text.toIntOrNull -class BotSpawns( +class BotCommands( val enums: EnumDefinitions, val structs: StructDefinitions, val loader: PlayerAccountLoader, diff --git a/game/src/main/kotlin/content/bot/BotManager.kt b/game/src/main/kotlin/content/bot/BotManager.kt index 50f19e036e..9c86472621 100644 --- a/game/src/main/kotlin/content/bot/BotManager.kt +++ b/game/src/main/kotlin/content/bot/BotManager.kt @@ -1,7 +1,6 @@ package content.bot import com.github.michaelbull.logging.InlineLogger -import content.bot.action.* import content.bot.behaviour.Behaviour import content.bot.behaviour.BehaviourFrame import content.bot.behaviour.BehaviourState @@ -15,7 +14,8 @@ import content.bot.behaviour.navigation.Graph.Companion.loadGraph import content.bot.behaviour.navigation.NavigationShortcut import content.bot.behaviour.setup.DynamicResolvers import content.bot.behaviour.setup.Resolver -import content.bot.req.Condition +import content.bot.behaviour.Condition +import content.bot.behaviour.action.BotAction import world.gregs.voidps.engine.data.ConfigFiles import world.gregs.voidps.engine.data.Settings import world.gregs.voidps.engine.event.AuditLog @@ -35,15 +35,24 @@ class BotManager( private val groups: MutableMap> = mutableMapOf(), ) : Runnable { lateinit var graph: Graph - val slots = ActivitySlots() + internal val slots = ActivitySlots() val bots = mutableListOf() private val logger = InlineLogger("BotManager") val activityNames: Set get() = activities.keys + fun load(files: ConfigFiles): BotManager { + val shortcuts = mutableListOf() + loadBehaviours(files, activities, groups, resolvers, shortcuts) + graph = loadGraph(files.list(Settings["bots.nav.definitions"]), shortcuts) + return this + } + fun add(bot: Bot) { bots.add(bot) + // Update all available activities + bot.available.clear() for (activity in activities.values) { if (activity.requires.any { !it.check(bot.player) }) { continue @@ -52,26 +61,6 @@ class BotManager( } } - fun update(bot: Bot, group: String) { - // TODO if product produced reset timeout - val iterator = bot.available.iterator() - while (iterator.hasNext()) { - val id = iterator.next() - val activity = activities[id] ?: continue - // TODO could filter by keys - if (activity.requires.any { !it.check(bot.player) }) { - iterator.remove() - } - } - for (id in groups[group] ?: return) { - val activity = activities[id] ?: continue - if (activity.requires.any { !it.check(bot.player) }) { - continue - } - bot.available.add(activity.id) - } - } - fun remove(bot: Bot): Boolean { if (bots.remove(bot)) { stop(bot) @@ -80,13 +69,6 @@ class BotManager( return false } - fun load(files: ConfigFiles): BotManager { - val shortcuts = mutableListOf() - loadBehaviours(files, activities, groups, resolvers, shortcuts) - graph = loadGraph(files.list(Settings["bots.nav.definitions"]), shortcuts) - return this - } - override fun run() { for (bot in bots) { tick(bot) @@ -94,52 +76,85 @@ class BotManager( } fun tick(bot: Bot) { - if (bot.noTask()) { - assignRandom(bot) - return - } try { + if (bot.noTask()) { + assignRandom(bot) + return + } execute(bot) } catch (exception: Exception) { logger.error(exception) { "Error in bot '${bot.player.accountName}' tick ${bot.frames.map { it.behaviour.id }}." } } } - private fun hasRequirements(bot: Bot, activity: BotActivity): Boolean = slots.hasFree(activity) && !bot.blocked.contains(activity.id) && activity.requires.all { it.check(bot.player) } - + /** + * Assign activity [id] to [bot] + * Useful for debugging + */ fun assign(bot: Bot, id: String): Boolean { val activity = activities[id] ?: return false assign(bot, activity) return true } - private val idle = BotActivity("idle", 2048, actions = listOf(BotAction.Wait(TimeUnit.SECONDS.toTicks(30)))) - + /** + * Assign a random activity that is available to the [bot]. + */ private fun assignRandom(bot: Bot) { - val activity = if (bot.previous != null && hasRequirements(bot, bot.previous!!)) { - bot.previous + if (bot.evaluate.isNotEmpty()) { + updateAvailable(bot) + } + if (bot.player["debug", false]) { + logger.trace { "Picking bot ${bot.player.accountName} new task from available: ${bot.available}." } + } + var activity = if (hasRequirements(bot, bot.previous)) { + bot.previous!! } else { - if (bot.player["debug", false]) { - logger.trace { "Picking bot ${bot.player.accountName} new task from available: ${bot.available}." } - } - val id = bot.available.filter { - val activity = activities[it] - activity != null && hasRequirements(bot, activity) - }.randomOrNull(random) // TODO weight by distance? - if (id == null && bot.player["debug", false]) { - debugActivities(bot) - } - activities[id] ?: idle + bot.available + .filter { hasRequirements(bot, activities[it]) } + .randomOrNull(random) + ?.let { activities[it] } } if (activity == null) { if (bot.player["debug", false]) { logger.info { "No activities with requirements met for bot: ${bot.player.accountName}." } + debugActivities(bot) } - return + activity = idle } assign(bot, activity) } + /** + * Remove invalid activities, check for new valid activities based on recent state changes ready to [Bot.evaluate]. + */ + private fun updateAvailable(bot: Bot) { + // Remove activities which are no longer available + val iterator = bot.available.iterator() + while (iterator.hasNext()) { + val id = iterator.next() + val activity = activities[id] ?: continue + if (activity.requires.any { !it.check(bot.player) }) { + iterator.remove() + } + } + // Add activities which have become available + for (group in bot.evaluate) { + for (id in groups[group] ?: return) { + val activity = activities[id] ?: continue + if (activity.requires.any { !it.check(bot.player) }) { + continue + } + bot.available.add(activity.id) + } + } + bot.evaluate.clear() + } + + private val idle = BotActivity("idle", 2048, timeout = TimeUnit.HOURS.toTicks(1), actions = listOf(BotAction.Wait(TimeUnit.SECONDS.toTicks(30)))) + + private fun hasRequirements(bot: Bot, activity: BotActivity?): Boolean = activity != null && slots.hasFree(activity) && !bot.blocked.contains(activity.id) && activity.requires.all { it.check(bot.player) } + private fun assign(bot: Bot, activity: BotActivity) { AuditLog.event(bot, "assigned", activity.id) if (bot.player["debug", false]) { @@ -150,7 +165,35 @@ class BotManager( bot.queue(BehaviourFrame(activity)) } - private fun start(bot: Bot, behaviour: Behaviour, frame: BehaviourFrame) { + /** + * Check and update [bot]'s current activity and action state + */ + private fun execute(bot: Bot) { + val frame = bot.frame() + when (val state = frame.state) { + BehaviourState.Running -> frame.update(bot) + BehaviourState.Pending -> start(bot, frame) + BehaviourState.Success -> nextAction(bot, frame) + is BehaviourState.Failed -> handleFail(bot, frame, state) + is BehaviourState.Wait -> { + val behaviour = frame.behaviour + if (bot.player["debug", false]) { + logger.trace { "Bot wait: ${behaviour.id} state: ${frame.state} action: ${frame.action()}." } + bot["previous_state"] = frame.state + } + if (--state.ticks <= 0) { + frame.state = state.next + } + } + } + } + + /** + * Check [bot] has requirements to start the current activity + * Find [resolvers] if activity has set up [Condition]s + */ + private fun start(bot: Bot, frame: BehaviourFrame) { + val behaviour = frame.behaviour for (requirement in behaviour.requires) { if (requirement.check(bot.player)) { continue @@ -158,16 +201,23 @@ class BotManager( frame.fail(Reason.Requirement(requirement)) return } + val debug = bot.player["debug", false] for (requirement in behaviour.setup) { if (requirement.check(bot.player)) { continue } - val resolvers = availableResolvers(bot, requirement) + frame.blocked.removeAll(DynamicResolvers.ids()) + val resolvers = buildList { + DynamicResolvers.resolver(bot.player, requirement)?.let { add(it) } + requirement.keys() + .flatMap { resolvers[it].orEmpty() } + .forEach(::add) + } val resolver = resolvers .filter { !frame.blocked.contains(it.id) && it.requires.none { fact -> !fact.check(bot.player) } } .minByOrNull { it.weight } if (resolver == null) { - if (bot.player["debug", false]) { + if (debug) { debugResolvers(behaviour, requirement, resolvers, frame, bot) } frame.fail(Reason.Requirement(requirement)) // No way to resolve @@ -175,8 +225,8 @@ class BotManager( } // Attempt resolution AuditLog.event(bot, "start_resolver", resolver.id, behaviour.id) - if (bot.player["debug", false]) { - logger.info { "Starting resolution: ${resolver.id} for ${behaviour.id} requirement: $requirement." } + if (debug) { + logger.info { "Starting resolver: ${resolver.id} for ${behaviour.id} requirement: $requirement." } } frame.blocked.add(resolver.id) val resolverFrame = BehaviourFrame(resolver) @@ -184,83 +234,64 @@ class BotManager( return } AuditLog.event(bot, "start_activity", behaviour.id) - if (bot.player["debug", false]) { + if (debug) { logger.info { "Starting activity: ${behaviour.id}." } } bot.blocked.add(behaviour.id) frame.start(bot) } - private fun availableResolvers(bot: Bot, requirement: Condition): MutableList { - val options = mutableListOf() - val dynamic = DynamicResolvers.resolver(bot.player, requirement) - if (dynamic != null) { - options.add(dynamic) - } - for (key in requirement.keys()) { - for (resolver in resolvers[key] ?: continue) { - options.add(resolver) + /** + * Move onto the next action in a behaviour or remove the behaviour from the stack if it is completed + */ + private fun nextAction(bot: Bot, frame: BehaviourFrame) { + val debug = bot.player["debug", false] + if (frame.next()) { + if (debug) { + logger.debug { "Next action: ${frame.action()} for ${frame.behaviour.id}." } } + frame.start(bot) + return + } + val behaviour = frame.behaviour + if (debug) { + logger.debug { "Completed action: ${frame.action()} for ${behaviour.id}." } + } + AuditLog.event(bot, "completed", frame.behaviour.id) + bot.frames.pop() + if (!bot.noTask()) { + bot.frame().blocked.remove(behaviour.id) + } + if (behaviour is BotActivity) { + bot.blocked.remove(behaviour.id) + slots.release(behaviour) } - return options } - private fun execute(bot: Bot) { - val frame = bot.frame() + /** + * Stop the current activity or remove the current resolver + */ + private fun handleFail(bot: Bot, frame: BehaviourFrame, state: BehaviourState.Failed) { val behaviour = frame.behaviour + val action = frame.action() if (bot.player["debug", false]) { - logger.trace { "Bot task: ${behaviour.id} state: ${frame.state} action: ${frame.action()}." } - bot["previous_state"] = frame.state + logger.warn { "Failed action: ${action::class.simpleName} for ${behaviour.id}, reason: ${state.reason}." } } - when (val state = frame.state) { - BehaviourState.Running -> frame.update(bot) - BehaviourState.Pending -> start(bot, behaviour, frame) - BehaviourState.Success -> { - val debug = bot.player["debug", false] - if (debug) { - logger.debug { "Completed action: ${frame.action()} for ${behaviour.id}." } - } - if (!frame.next()) { - AuditLog.event(bot, "completed", frame.behaviour.id) - bot.frames.pop() - if (!bot.noTask()) { - bot.frame().blocked.remove(behaviour.id) - } - if (behaviour is BotActivity) { - bot.blocked.remove(behaviour.id) - slots.release(behaviour) - } - } else { - if (debug) { - logger.debug { "Next action: ${frame.action()} for ${behaviour.id}." } - } - frame.start(bot) - } - } - is BehaviourState.Failed -> { - val action = frame.action() - if (bot.player["debug", false]) { - logger.warn { "Failed action: ${action::class.simpleName} for ${behaviour.id}, reason: ${state.reason}." } - } - AuditLog.event(bot, "failed", behaviour.id, state.reason, frame.index, action::class.simpleName) - if (state.reason is HardReason) { - stop(bot) - } else { - bot.frames.pop() - } - if (behaviour is BotActivity) { - bot.blocked.add(behaviour.id) - slots.release(behaviour) - } - } - is BehaviourState.Wait -> { - if (--state.ticks <= 0) { - frame.state = state.next - } - } + AuditLog.event(bot, "failed", behaviour.id, state.reason, frame.index, action::class.simpleName) + if (state.reason is HardReason) { + stop(bot) + } else { + bot.frames.pop() + } + if (behaviour is BotActivity) { + bot.blocked.add(behaviour.id) + slots.release(behaviour) } } + /** + * Remove all behaviours and free up activity slots + */ private fun stop(bot: Bot) { for (frame in bot.frames) { if (frame.behaviour is BotActivity) { @@ -270,7 +301,7 @@ class BotManager( bot.reset() } - private fun debugResolvers(behaviour: Behaviour, requirement: Condition, resolvers: MutableList, frame: BehaviourFrame, bot: Bot) { + private fun debugResolvers(behaviour: Behaviour, requirement: Condition, resolvers: List, frame: BehaviourFrame, bot: Bot) { logger.info { "No resolver found for ${behaviour.id} keys: ${requirement.keys()} requirement: $requirement." } for (resolver in resolvers) { if (frame.blocked.contains(resolver.id)) { diff --git a/game/src/main/kotlin/content/bot/BotUpdates.kt b/game/src/main/kotlin/content/bot/BotUpdates.kt index 18176ab0e5..a7829847a0 100644 --- a/game/src/main/kotlin/content/bot/BotUpdates.kt +++ b/game/src/main/kotlin/content/bot/BotUpdates.kt @@ -1,47 +1,70 @@ package content.bot import world.gregs.voidps.engine.Script +import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.network.client.instruction.InteractDialogue /** * Listen for state changes which would change which activities are available to a bot */ -class BotUpdates(val manager: BotManager) : Script { +class BotUpdates : Script { init { + /* + Track state changes to re-evaluate available activities + */ levelChanged { skill, _, _ -> if (isBot) { - manager.update(bot, "skill:${skill.name.lowercase()}") - } - } - -// moved { from -> -// if (isBot && tile != from) { -// manager.update(bot, "tile") // FIXME expensive -// } -// } - - variableSet { key, from, to -> - if (isBot && from != to) { - manager.update(bot, "var:$key") + bot.evaluate.add("skill:${skill.name.lowercase()}") } } inventoryUpdated { inventory, _ -> if (isBot) { - manager.update(bot, "inv:$inventory") + bot.evaluate.add("inv:$inventory") } } entered("*") { if (isBot) { - manager.update(bot, "enter:${it.name}") + resetTimeout("area:${it.name}") + bot.evaluate.add("area:${it.name}") } } + variableSet { key, from, to -> + if (isBot && from != to) { + bot.evaluate.add("var:$key") + resetTimeout("variable:${key}") + } + } + + itemAdded(inventory = "inventory") { + resetTimeout("item:${it.item.id}") + } + + experience { skill, _, _ -> + resetTimeout("skill:${skill.name.lowercase()}") + } + + // Close level-up dialogues interfaceOpened("dialogue_level_up") { if (isBot) { instructions.trySend(InteractDialogue(interfaceId = 740, componentId = 3, option = -1)) } } } + + /** + * Reset timeout when produce has been produced + */ + private fun Player.resetTimeout(key: String) { + if (!isBot || bot.noTask()) { + return + } + val frame = bot.frame() + val produces = frame.behaviour.produces + if (produces.contains(key)) { + frame.timeout = 0 + } + } } diff --git a/game/src/main/kotlin/content/bot/behaviour/Behaviour.kt b/game/src/main/kotlin/content/bot/behaviour/Behaviour.kt index 20607c8e6f..908509b182 100644 --- a/game/src/main/kotlin/content/bot/behaviour/Behaviour.kt +++ b/game/src/main/kotlin/content/bot/behaviour/Behaviour.kt @@ -1,20 +1,22 @@ package content.bot.behaviour -import content.bot.action.ActionParser -import content.bot.action.BotAction +import content.bot.behaviour.action.ActionParser +import content.bot.behaviour.action.BotAction import content.bot.behaviour.activity.BotActivity import content.bot.behaviour.navigation.NavigationShortcut import content.bot.behaviour.setup.Resolver -import content.bot.req.Condition import it.unimi.dsi.fastutil.objects.ObjectArrayList import world.gregs.config.Config import world.gregs.config.ConfigReader import world.gregs.voidps.engine.data.ConfigFiles import world.gregs.voidps.engine.data.Settings import world.gregs.voidps.engine.timedLoad +import world.gregs.voidps.engine.timer.toTicks +import java.util.concurrent.TimeUnit interface Behaviour { val id: String + val timeout: Int val requires: List val setup: List val actions: List @@ -47,15 +49,16 @@ fun loadBehaviours( private fun loadActivities(activities: MutableMap, templates: Map, paths: List) { timedLoad("bot activity") { val fragments = mutableListOf() - load(paths) { id, template, fields, capacity, requires, setup, actions, produces -> + load(paths) { id, template, fields, capacity, timeout, requires, setup, actions, produces -> if (template != null) { requireNotNull(fields) - fragments.add(Fragment(id, template, fields, capacity, requires, setup, actions, produces)) + fragments.add(Fragment(id, template, fields, capacity, timeout, requires, setup, actions, produces)) } else { val debug = "$id ${exception()}" activities[id] = BotActivity( id = id, capacity = capacity, + timeout = timeout, requires = Condition.parse(requires, debug), setup = Condition.parse(setup, debug), actions = ActionParser.parse(actions, debug), @@ -74,15 +77,16 @@ private fun loadActivities(activities: MutableMap, template private fun loadSetups(resolvers: MutableMap>, templates: Map, paths: List) { timedLoad("bot setup") { val fragments = mutableListOf() - load(paths) { id, template, fields, weight, requires, setup, actions, produces -> + load(paths) { id, template, fields, weight, timeout, requires, setup, actions, produces -> if (template != null) { requireNotNull(fields) - fragments.add(Fragment(id, template, fields, weight, requires, setup, actions, produces)) + fragments.add(Fragment(id, template, fields, weight, timeout, requires, setup, actions, produces)) } else { val debug = "$id ${exception()}" val resolver = Resolver( id = id, weight = weight, + timeout = timeout, requires = Condition.parse(requires, debug), setup = Condition.parse(setup, debug), actions = ActionParser.parse(actions, debug), @@ -107,16 +111,17 @@ private fun loadSetups(resolvers: MutableMap>, tem private fun loadShortcuts(shortcuts: MutableList, templates: Map, paths: List) { timedLoad("bot shortcut") { val fragments = mutableListOf() - load(paths) { id, template, fields, weight, requires, setup, actions, produces -> + load(paths) { id, template, fields, weight, timeout, requires, setup, actions, produces -> if (template != null) { requireNotNull(fields) { "No fields found for $id ${exception()}" } - fragments.add(Fragment(id, template, fields, weight, requires, setup, actions, produces)) + fragments.add(Fragment(id, template, fields, weight, timeout, requires, setup, actions, produces)) } else { val debug = "$id ${exception()}" shortcuts.add( NavigationShortcut( id = id, weight = weight, + timeout = timeout, requires = Condition.parse(requires, debug), setup = Condition.parse(setup, debug), actions = ActionParser.parse(actions, debug), @@ -133,7 +138,7 @@ private fun loadShortcuts(shortcuts: MutableList, templates: } } -private fun load(paths: List, block: ConfigReader.(String, String?, Map?, Int, List>>>, List>>>, List>>, Set) -> Unit) { +private fun load(paths: List, block: ConfigReader.(String, String?, Map?, Int, Int, List>>>, List>>>, List>>, Set) -> Unit) { for (path in paths) { Config.fileReader(path) { while (nextSection()) { @@ -141,6 +146,7 @@ private fun load(paths: List, block: ConfigReader.(String, String?, Map< var template: String? = null var fields: Map? = null var value = 1 + var timeout = TimeUnit.SECONDS.toTicks(30) val requires = mutableListOf>>>() val setup = mutableListOf>>>() val actions = mutableListOf>>() @@ -153,6 +159,7 @@ private fun load(paths: List, block: ConfigReader.(String, String?, Map< "actions" -> actions(actions) "produces" -> produces(produces) "weight", "capacity" -> value = int() + "timeout" -> timeout = int() "fields" -> fields = map() else -> throw IllegalArgumentException("Unexpected key: '$key' ${exception()}") } @@ -160,7 +167,7 @@ private fun load(paths: List, block: ConfigReader.(String, String?, Map< if (fields != null && template == null) { error("Found fields but no template for $id in ${exception()}") } - block.invoke(this, id, template, fields, value, requires, setup, actions, produces) + block.invoke(this, id, template, fields, value, timeout, requires, setup, actions, produces) } } } @@ -237,6 +244,7 @@ private data class Fragment( val template: String, val fields: Map, val int: Int, + val timeout: Int, val requires: List>>>, val setup: List>>>, val actions: List>>, @@ -245,6 +253,7 @@ private data class Fragment( fun activity(template: Template) = BotActivity( id = id, capacity = int, + timeout = timeout, requires = resolveRequirements(template.requires, requires), setup = resolveRequirements(template.setup, setup), actions = resolveActions(template.actions, actions), @@ -323,6 +332,7 @@ private data class Fragment( fun resolver(template: Template) = Resolver( id = id, weight = int, + timeout = timeout, requires = resolveRequirements(template.requires, requires), setup = resolveRequirements(template.setup, setup), actions = resolveActions(template.actions, actions), @@ -332,6 +342,7 @@ private data class Fragment( fun shortcut(template: Template) = NavigationShortcut( id = id, weight = int, + timeout = timeout, requires = resolveRequirements(template.requires, requires), setup = resolveRequirements(template.setup, setup), actions = resolveActions(template.actions, actions), diff --git a/game/src/main/kotlin/content/bot/behaviour/BehaviourFrame.kt b/game/src/main/kotlin/content/bot/behaviour/BehaviourFrame.kt index c2af41e94a..765980589e 100644 --- a/game/src/main/kotlin/content/bot/behaviour/BehaviourFrame.kt +++ b/game/src/main/kotlin/content/bot/behaviour/BehaviourFrame.kt @@ -1,7 +1,7 @@ package content.bot.behaviour import content.bot.Bot -import content.bot.action.BotAction +import content.bot.behaviour.action.BotAction data class BehaviourFrame( val behaviour: Behaviour, @@ -21,6 +21,10 @@ data class BehaviourFrame( } fun update(bot: Bot) { + if (++timeout > behaviour.timeout) { + fail(Reason.Timeout) + return + } val action = action() state = action.update(bot, this) ?: return } diff --git a/game/src/main/kotlin/content/bot/req/Condition.kt b/game/src/main/kotlin/content/bot/behaviour/Condition.kt similarity index 98% rename from game/src/main/kotlin/content/bot/req/Condition.kt rename to game/src/main/kotlin/content/bot/behaviour/Condition.kt index ad4fc0f469..e9cfd0214d 100644 --- a/game/src/main/kotlin/content/bot/req/Condition.kt +++ b/game/src/main/kotlin/content/bot/behaviour/Condition.kt @@ -1,4 +1,4 @@ -package content.bot.req +package content.bot.behaviour import content.entity.player.bank.bank import net.pearx.kasechange.toPascalCase @@ -13,9 +13,9 @@ import world.gregs.voidps.engine.entity.character.player.skill.level.Level.hasRe import world.gregs.voidps.engine.entity.obj.GameObjects import world.gregs.voidps.engine.event.Wildcard import world.gregs.voidps.engine.event.Wildcards -import world.gregs.voidps.engine.inv.equipment import world.gregs.voidps.engine.inv.inventory import world.gregs.voidps.network.login.protocol.visual.update.player.EquipSlot +import kotlin.collections.iterator sealed class Condition(val priority: Int) { abstract fun keys(): Set @@ -244,7 +244,7 @@ sealed class Condition(val priority: Int) { val items = mutableMapOf() for (map in list) { for ((key, value) in map) { - val slot = EquipSlot.by(key) + val slot = EquipSlot.Companion.by(key) require(slot != EquipSlot.None) { "Invalid equipment slot: $key in $list" } value as? Map ?: error("Equipment $key expecting map, found: $value") val id = value["id"] as? String ?: error("Missing item id in $list") @@ -358,7 +358,7 @@ sealed class Condition(val priority: Int) { val map = list.single() if (map.containsKey("id")) { return SkillLevel( - skill = Skill.of((map["id"] as String).toPascalCase()) ?: error("Unknown skill: '${map["id"]}'"), + skill = Skill.Companion.of((map["id"] as String).toPascalCase()) ?: error("Unknown skill: '${map["id"]}'"), min = map["min"] as? Int, max = map["max"] as? Int ) @@ -367,4 +367,4 @@ sealed class Condition(val priority: Int) { } } -} +} \ No newline at end of file diff --git a/game/src/main/kotlin/content/bot/behaviour/Reason.kt b/game/src/main/kotlin/content/bot/behaviour/Reason.kt index ae9657ea99..70b2a6a180 100644 --- a/game/src/main/kotlin/content/bot/behaviour/Reason.kt +++ b/game/src/main/kotlin/content/bot/behaviour/Reason.kt @@ -1,7 +1,5 @@ package content.bot.behaviour -import content.bot.req.Condition - interface Reason { data class Invalid(val message: String) : HardReason object Cancelled : HardReason diff --git a/game/src/main/kotlin/content/bot/action/ActionParser.kt b/game/src/main/kotlin/content/bot/behaviour/action/ActionParser.kt similarity index 99% rename from game/src/main/kotlin/content/bot/action/ActionParser.kt rename to game/src/main/kotlin/content/bot/behaviour/action/ActionParser.kt index f93511e1d7..4643415b72 100644 --- a/game/src/main/kotlin/content/bot/action/ActionParser.kt +++ b/game/src/main/kotlin/content/bot/behaviour/action/ActionParser.kt @@ -1,6 +1,6 @@ -package content.bot.action +package content.bot.behaviour.action -import content.bot.req.Condition +import content.bot.behaviour.Condition sealed class ActionParser { open val required = emptySet() diff --git a/game/src/main/kotlin/content/bot/action/BotAction.kt b/game/src/main/kotlin/content/bot/behaviour/action/BotAction.kt similarity index 98% rename from game/src/main/kotlin/content/bot/action/BotAction.kt rename to game/src/main/kotlin/content/bot/behaviour/action/BotAction.kt index 601ff05f4d..3143b320b3 100644 --- a/game/src/main/kotlin/content/bot/action/BotAction.kt +++ b/game/src/main/kotlin/content/bot/behaviour/action/BotAction.kt @@ -1,4 +1,4 @@ -package content.bot.action +package content.bot.behaviour.action import content.bot.Bot import content.bot.BotManager @@ -8,7 +8,7 @@ import content.bot.behaviour.Reason import content.bot.behaviour.navigation.Graph import content.bot.behaviour.navigation.NavigationShortcut import content.bot.behaviour.setup.Resolver -import content.bot.req.Condition +import content.bot.behaviour.Condition import content.entity.combat.attackers import content.entity.combat.dead import world.gregs.voidps.engine.GameLoop @@ -33,8 +33,11 @@ import world.gregs.voidps.engine.event.wildcardEquals import world.gregs.voidps.engine.get import world.gregs.voidps.engine.inv.inventory import world.gregs.voidps.engine.map.Spiral +import world.gregs.voidps.engine.timer.toTicks import world.gregs.voidps.network.client.instruction.* +import java.util.concurrent.TimeUnit import kotlin.collections.indexOf +import kotlin.collections.iterator sealed interface BotAction { fun start(bot: Bot, frame: BehaviourFrame): BehaviourState = BehaviourState.Running @@ -95,7 +98,7 @@ sealed interface BotAction { } } if (actions.isNotEmpty()) { - bot.queue(BehaviourFrame(Resolver("go_to_$target", 0, actions = actions))) + bot.queue(BehaviourFrame(Resolver("go_to_$target", 0, TimeUnit.SECONDS.toTicks(60), actions = actions))) } if (nav != null) { bot.queue(BehaviourFrame(nav)) @@ -147,10 +150,7 @@ sealed interface BotAction { val radius: Int = 10, ) : BotAction { - override fun start(bot: Bot, frame: BehaviourFrame): BehaviourState { - frame.timeout = 0 - return BehaviourState.Running - } + override fun start(bot: Bot, frame: BehaviourFrame) = BehaviourState.Running override fun update(bot: Bot, frame: BehaviourFrame) = when { success?.check(bot.player) == true -> BehaviourState.Success diff --git a/game/src/main/kotlin/content/bot/behaviour/activity/ActivitySlots.kt b/game/src/main/kotlin/content/bot/behaviour/activity/ActivitySlots.kt index efb366c4ef..12b86165c1 100644 --- a/game/src/main/kotlin/content/bot/behaviour/activity/ActivitySlots.kt +++ b/game/src/main/kotlin/content/bot/behaviour/activity/ActivitySlots.kt @@ -1,5 +1,8 @@ package content.bot.behaviour.activity +/** + * Track bots occupying and releasing access to a fixed number of slots per activity + */ class ActivitySlots { private val occupied = mutableMapOf() diff --git a/game/src/main/kotlin/content/bot/behaviour/activity/BotActivity.kt b/game/src/main/kotlin/content/bot/behaviour/activity/BotActivity.kt index 9605de0989..b073e5e7ea 100644 --- a/game/src/main/kotlin/content/bot/behaviour/activity/BotActivity.kt +++ b/game/src/main/kotlin/content/bot/behaviour/activity/BotActivity.kt @@ -1,8 +1,8 @@ package content.bot.behaviour.activity -import content.bot.action.BotAction +import content.bot.behaviour.action.BotAction import content.bot.behaviour.Behaviour -import content.bot.req.Condition +import content.bot.behaviour.Condition /** * An activity with a limited number of slots that bots can perform @@ -11,6 +11,7 @@ import content.bot.req.Condition data class BotActivity( override val id: String, val capacity: Int, + override val timeout: Int = 50, override val requires: List = emptyList(), override val setup: List = emptyList(), override val actions: List = emptyList(), diff --git a/game/src/main/kotlin/content/bot/behaviour/navigation/Graph.kt b/game/src/main/kotlin/content/bot/behaviour/navigation/Graph.kt index b501ff94cd..52463cb66d 100644 --- a/game/src/main/kotlin/content/bot/behaviour/navigation/Graph.kt +++ b/game/src/main/kotlin/content/bot/behaviour/navigation/Graph.kt @@ -1,12 +1,12 @@ package content.bot.behaviour.navigation -import content.bot.action.ActionParser -import content.bot.action.BotAction +import content.bot.behaviour.action.ActionParser +import content.bot.behaviour.action.BotAction import content.bot.behaviour.actions import content.bot.behaviour.requirements import content.bot.bot import content.bot.isBot -import content.bot.req.Condition +import content.bot.behaviour.Condition import world.gregs.config.Config import world.gregs.config.ConfigReader import world.gregs.voidps.engine.data.definition.Areas diff --git a/game/src/main/kotlin/content/bot/behaviour/navigation/NavigationShortcut.kt b/game/src/main/kotlin/content/bot/behaviour/navigation/NavigationShortcut.kt index 5887ddf0a6..afd42fcba4 100644 --- a/game/src/main/kotlin/content/bot/behaviour/navigation/NavigationShortcut.kt +++ b/game/src/main/kotlin/content/bot/behaviour/navigation/NavigationShortcut.kt @@ -1,12 +1,13 @@ package content.bot.behaviour.navigation -import content.bot.action.BotAction +import content.bot.behaviour.action.BotAction import content.bot.behaviour.Behaviour -import content.bot.req.Condition +import content.bot.behaviour.Condition data class NavigationShortcut( override val id: String, val weight: Int, + override val timeout: Int, override val requires: List = emptyList(), override val setup: List = emptyList(), override val actions: List = emptyList(), diff --git a/game/src/main/kotlin/content/bot/behaviour/setup/Deficit.kt b/game/src/main/kotlin/content/bot/behaviour/setup/Deficit.kt deleted file mode 100644 index 04469d412e..0000000000 --- a/game/src/main/kotlin/content/bot/behaviour/setup/Deficit.kt +++ /dev/null @@ -1,140 +0,0 @@ -package content.bot.behaviour.setup - - -/** - * Missing setup [Requirement]'s which can be produce dynamic [Resolver]'s - */ -/* -sealed interface Deficit { - fun resolve(player: Player): Resolver? - - data class NotInArea(val area: String) : Deficit { - override fun resolve(player: Player): Resolver = Resolver("go_to_$area", -1, actions = listOf(BotAction.GoTo(area))) - } - - */ -/* - TODO setup resolution order - if equipment - deposit all inventory items - withdraw needed items - equip all needed items - deposit all inv items - if inventory - deposit all items - withdraw needed items - *//* - - data class Entry(val filter: Predicate, val needed: Int) - - data class MissingEquipment(val entries: List) : Deficit { - override fun resolve(player: Player): Resolver? { - val entries = entries.toMutableList() - val actions = mutableListOf() - val uniqueName = StringBuilder() - for (item in player.inventory.items) { - if (item.isEmpty()) { - continue - } - val iterator = entries.iterator() - while (iterator.hasNext()) { - val (entry, needed) = iterator.next() - if (!entry.test(player, item)) { - continue - } - iterator.remove() - uniqueName.append("_${item.id}") - actions.add(BotAction.InterfaceOption("Equip", "inventory:inventory:${item.id}")) - } - } - val spaceNeeded = withdraw(actions, player, entries, uniqueName) - if (actions.isNotEmpty()) { - return Resolver( - "withdraw$uniqueName", - weight = 20, - setup = listOf( - Condition.Inventory(listOf(Condition.Entry("empty", min = spaceNeeded))), - ), - actions = actions, - ) - } - return null - } - - private fun withdraw(actions: MutableList, player: Player, entries: MutableList, uniqueName: StringBuilder): Int { - if (entries.isEmpty()) { - return 0 - } - var spaceNeeded = 0 - actions.add(BotAction.GoToNearest("bank")) - actions.add(BotAction.InteractObject("Use-quickly", "bank_booth*", success = Condition.InterfaceOpen("bank"))) - for (item in player.bank.items) { - if (item.isEmpty()) { - continue - } - val iterator = entries.iterator() - while (iterator.hasNext()) { - val (entry, needed) = iterator.next() - if (!entry.test(player, item)) { - continue - } - iterator.remove() - spaceNeeded += if (player.bank.stackable(item.id)) 1 else needed - uniqueName.append("_${item.id}") - actions.add(BotAction.InterfaceOption("Withdraw-1", "bank:inventory:${item.id}")) - } - } - if (spaceNeeded > 0) { - if (player.inventory.spaces < spaceNeeded) { - actions.add(2, BotAction.InteractObject("Deposit carried items", "bank:carried", success = Condition.Inventory(listOf(Condition.Entry("empty", min = 28))))) - } - actions.add(BotAction.CloseInterface) - } - return spaceNeeded - } - } - - data class MissingInventory(val entries: List) : Deficit { - override fun resolve(player: Player): Resolver? { - var spaceNeeded = 0 - val actions = mutableListOf( - BotAction.GoToNearest("bank"), - BotAction.InteractObject("Use-quickly", "bank_booth*", success = Condition.InterfaceOpen("bank")), - ) - val uniqueName = StringBuilder() - for (item in player.bank.items) { - if (item.isEmpty()) { - continue - } - for (entry in entries) { - if (!entry.filter.test(player, item)) { - continue - } - val needed = entry.needed - spaceNeeded += if (player.bank.stackable(item.id)) 1 else needed - uniqueName.append("_${item.id}") - if (needed == 1 || needed == 5 || needed == 10) { - actions.add(BotAction.InterfaceOption("Withdraw-$needed", "bank:inventory:${item.id}")) - } else { - BotAction.InterfaceOption("Withdraw-X", "bank:inventory:${item.id}") - BotAction.IntEntry(needed) - } - } - } - if (actions.size > 2) { - if (player.inventory.spaces < spaceNeeded) { - actions.add(2, BotAction.InteractObject("Deposit carried items", "bank:carried", success = Condition.Inventory(listOf(Condition.Entry("empty", min = 28))))) - } - actions.add(BotAction.CloseInterface) - return Resolver( - "withdraw_$uniqueName", - weight = 20, - setup = listOf(Condition.Inventory(listOf(Condition.Entry("empty", min = spaceNeeded)))), - actions = actions, - ) - } - return null - } - } -} -*/ diff --git a/game/src/main/kotlin/content/bot/behaviour/setup/DynamicResolvers.kt b/game/src/main/kotlin/content/bot/behaviour/setup/DynamicResolvers.kt index 76e977459f..d533b72ccd 100644 --- a/game/src/main/kotlin/content/bot/behaviour/setup/DynamicResolvers.kt +++ b/game/src/main/kotlin/content/bot/behaviour/setup/DynamicResolvers.kt @@ -1,8 +1,8 @@ package content.bot.behaviour.setup -import content.bot.action.BotAction -import content.bot.req.Condition -import content.bot.req.Condition.Entry +import content.bot.behaviour.action.BotAction +import content.bot.behaviour.Condition +import content.bot.behaviour.Condition.Entry import content.entity.player.bank.bank import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.entity.character.player.equip.equipped @@ -15,6 +15,8 @@ import world.gregs.voidps.network.login.protocol.visual.update.player.EquipSlot object DynamicResolvers { + fun ids() = setOf("withdraw_from_bank", "equip_from_bank") + fun resolver(player: Player, condition: Condition) = when (condition) { is Condition.InArea -> Resolver("go_to_${condition.id}", -1, actions = listOf(BotAction.GoTo(condition.id))) is Condition.Equipment -> resolveEquipment(player, condition.items) @@ -103,7 +105,7 @@ object DynamicResolvers { actions.add(BotAction.InteractObject("Use-quickly", "bank_booth*", success = Condition.InterfaceOpen("bank"))) // Free up inventory space if needed if (player.inventory.spaces < equipment.size) { - actions.add(BotAction.InterfaceOption("Deposit carried items", "bank:carried", success = Condition.Inventory(listOf(Condition.Entry(setOf("empty"), min = 28))))) + actions.add(BotAction.InterfaceOption("Deposit carried items", "bank:carried", success = Condition.Inventory(listOf(Entry(setOf("empty"), min = 28))))) } val toEquip = mutableSetOf() for (item in player.bank.items) { diff --git a/game/src/main/kotlin/content/bot/behaviour/setup/Resolver.kt b/game/src/main/kotlin/content/bot/behaviour/setup/Resolver.kt index 7221e12d93..c72e54ee95 100644 --- a/game/src/main/kotlin/content/bot/behaviour/setup/Resolver.kt +++ b/game/src/main/kotlin/content/bot/behaviour/setup/Resolver.kt @@ -1,17 +1,18 @@ package content.bot.behaviour.setup -import content.bot.action.BotAction +import content.bot.behaviour.action.BotAction import content.bot.behaviour.Behaviour -import content.bot.req.Condition +import content.bot.behaviour.Condition /** - * An activity that can be performed to resolve a requirement + * A behaviour that can be performed to resolve a requirement of another [Resolver] or [BotAction] * E.g. buying a pickaxe from a shop, getting items out of the bank, picking up an item off of the floor * [weight] specifies resolver preference; lower is more likely to be chosen. */ data class Resolver( override val id: String, val weight: Int, + override val timeout: Int = 50, override val requires: List = emptyList(), override val setup: List = emptyList(), override val actions: List = emptyList(), diff --git a/game/src/main/kotlin/content/entity/player/command/PathFindingCommands.kt b/game/src/main/kotlin/content/entity/player/command/PathFindingCommands.kt index 4d8c413df6..bbda65a8a0 100644 --- a/game/src/main/kotlin/content/entity/player/command/PathFindingCommands.kt +++ b/game/src/main/kotlin/content/entity/player/command/PathFindingCommands.kt @@ -2,7 +2,7 @@ package content.entity.player.command import content.bot.Bot import content.bot.BotManager -import content.bot.action.BotAction +import content.bot.behaviour.action.BotAction import content.bot.behaviour.BehaviourFrame import content.bot.behaviour.setup.Resolver import content.bot.bot diff --git a/game/src/test/kotlin/content/bot/BotManagerTest.kt b/game/src/test/kotlin/content/bot/BotManagerTest.kt index b44b96843c..a2010610dd 100644 --- a/game/src/test/kotlin/content/bot/BotManagerTest.kt +++ b/game/src/test/kotlin/content/bot/BotManagerTest.kt @@ -1,13 +1,13 @@ package content.bot -import content.bot.action.* import content.bot.behaviour.BehaviourFrame import content.bot.behaviour.BehaviourState import content.bot.behaviour.Reason import content.bot.behaviour.SoftReason import content.bot.behaviour.activity.BotActivity import content.bot.behaviour.setup.Resolver -import content.bot.req.Condition +import content.bot.behaviour.Condition +import content.bot.behaviour.action.BotAction import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.Test import world.gregs.voidps.engine.entity.character.player.Player @@ -384,5 +384,5 @@ class BotManagerTest { requires: List = emptyList(), resolves: List = emptyList(), plan: List, - ) = BotActivity(id, 1, requires, resolves, plan) + ) = BotActivity(id, 1, 50, requires, resolves, plan) } diff --git a/game/src/test/kotlin/content/bot/ActivitySlotsTest.kt b/game/src/test/kotlin/content/bot/behaviour/activity/ActivitySlotsTest.kt similarity index 67% rename from game/src/test/kotlin/content/bot/ActivitySlotsTest.kt rename to game/src/test/kotlin/content/bot/behaviour/activity/ActivitySlotsTest.kt index f544d2a4ec..1dc0a7a8c1 100644 --- a/game/src/test/kotlin/content/bot/ActivitySlotsTest.kt +++ b/game/src/test/kotlin/content/bot/behaviour/activity/ActivitySlotsTest.kt @@ -1,8 +1,6 @@ -package content.bot +package content.bot.behaviour.activity -import content.bot.behaviour.activity.ActivitySlots -import content.bot.behaviour.activity.BotActivity -import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -19,7 +17,7 @@ class ActivitySlotsTest { fun `Unoccupied slots are free`() { val activity = BotActivity("test", 2) - assertTrue(slots.hasFree(activity)) + Assertions.assertTrue(slots.hasFree(activity)) } @Test @@ -28,7 +26,7 @@ class ActivitySlotsTest { slots.occupy(activity) - assertFalse(slots.hasFree(activity)) + Assertions.assertFalse(slots.hasFree(activity)) } @Test @@ -38,6 +36,6 @@ class ActivitySlotsTest { slots.occupy(activity) slots.release(activity) - assertTrue(slots.hasFree(activity)) + Assertions.assertTrue(slots.hasFree(activity)) } -} +} \ No newline at end of file diff --git a/game/src/test/kotlin/content/bot/interact/path/GraphTest.kt b/game/src/test/kotlin/content/bot/behaviour/navigation/GraphTest.kt similarity index 96% rename from game/src/test/kotlin/content/bot/interact/path/GraphTest.kt rename to game/src/test/kotlin/content/bot/behaviour/navigation/GraphTest.kt index 7e3e966d14..73d03a83aa 100644 --- a/game/src/test/kotlin/content/bot/interact/path/GraphTest.kt +++ b/game/src/test/kotlin/content/bot/behaviour/navigation/GraphTest.kt @@ -1,9 +1,7 @@ -package content.bot.interact.path +package content.bot.behaviour.navigation -import content.bot.behaviour.navigation.Graph -import content.bot.behaviour.navigation.NavigationShortcut -import content.bot.req.Condition -import org.junit.jupiter.api.Assertions.assertFalse +import content.bot.behaviour.Condition +import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test import world.gregs.voidps.engine.data.definition.AreaDefinition import world.gregs.voidps.engine.data.definition.Areas @@ -161,7 +159,7 @@ class GraphTest { val output = mutableListOf() val success = builder.build().find(Player(), output, Graph.Node(b), a) - assertFalse(success) + Assertions.assertFalse(success) } @Test @@ -266,7 +264,7 @@ class GraphTest { val path = mutableListOf() val found = graph.find(player, path, start = Graph.Node(0), target = 1) - assertFalse(found, "Edge condition blocks traversal") + Assertions.assertFalse(found, "Edge condition blocks traversal") assertTrue(path.isEmpty()) } @@ -290,7 +288,7 @@ class GraphTest { assertTrue(starts.contains(Graph.Node(1, 9))) assertTrue(starts.contains(Graph.Node(2, 10))) - assertFalse(starts.contains(Graph.Node(3, 90))) + Assertions.assertFalse(starts.contains(Graph.Node(3, 90))) } @Test @@ -300,6 +298,7 @@ class GraphTest { val shortcut = NavigationShortcut( id = "teleport", weight = 1, + timeout = 50, requires = listOf(Condition.AtTile(50, 50)), produces = setOf("area:town"), ) @@ -375,4 +374,4 @@ class GraphTest { assertTrue(success) assertEquals(listOf(aj, jk, kl, lm, mh), output) } -} +} \ No newline at end of file From 4d448d846e6691d0bafb954028c8f9ea71dfac30 Mon Sep 17 00:00:00 2001 From: GregHib Date: Tue, 10 Feb 2026 16:16:21 +0000 Subject: [PATCH 085/101] Add documentation --- game/src/main/kotlin/GameModules.kt | 2 + game/src/main/kotlin/content/bot/Bot.kt | 2 +- .../src/main/kotlin/content/bot/BotManager.kt | 22 ++--- .../src/main/kotlin/content/bot/BotUpdates.kt | 2 +- .../kotlin/content/bot/behaviour/Behaviour.kt | 8 +- .../content/bot/behaviour/BehaviourFrame.kt | 4 + .../kotlin/content/bot/behaviour/Condition.kt | 60 ++++++------- .../bot/behaviour/action/ActionParser.kt | 4 +- .../content/bot/behaviour/action/BotAction.kt | 88 +++++-------------- .../bot/behaviour/activity/BotActivity.kt | 2 +- .../{Graph.kt => NavigationGraph.kt} | 76 ++++++++++------ .../navigation/NavigationShortcut.kt | 2 +- .../bot/behaviour/setup/DynamicResolvers.kt | 8 +- .../content/bot/behaviour/setup/Resolver.kt | 2 +- .../content/entity/obj/ObjectTeleports.kt | 2 +- .../player/command/PathFindingCommands.kt | 6 +- .../fairy_ring/FairyRingCodes.kt | 2 +- .../test/kotlin/content/bot/BotManagerTest.kt | 22 ++--- .../behaviour/activity/ActivitySlotsTest.kt | 2 +- .../{GraphTest.kt => NavigationGraphTest.kt} | 60 ++++++------- .../voidps/tools/map/view/draw/GraphDrawer.kt | 14 +-- .../voidps/tools/map/view/draw/MapView.kt | 4 +- 22 files changed, 188 insertions(+), 206 deletions(-) rename game/src/main/kotlin/content/bot/behaviour/navigation/{Graph.kt => NavigationGraph.kt} (80%) rename game/src/test/kotlin/content/bot/behaviour/navigation/{GraphTest.kt => NavigationGraphTest.kt} (81%) diff --git a/game/src/main/kotlin/GameModules.kt b/game/src/main/kotlin/GameModules.kt index e2f4df9bab..aa5397f88f 100644 --- a/game/src/main/kotlin/GameModules.kt +++ b/game/src/main/kotlin/GameModules.kt @@ -1,4 +1,5 @@ import content.bot.BotManager +import content.bot.behaviour.loadGraph import content.entity.obj.ship.CharterShips import content.entity.player.modal.book.Books import content.entity.world.music.MusicTracks @@ -19,6 +20,7 @@ import java.io.File fun gameModule(files: ConfigFiles) = module { single { ItemSpawns() } single { BotManager().load(files) } + single { loadGraph(files) } single(createdAtStart = true) { Books().load(files.list(Settings["definitions.books"])) } single(createdAtStart = true) { MusicTracks().load(files.find(Settings["map.music"])) } single(createdAtStart = true) { FairyRingCodes().load(files.find(Settings["definitions.fairyCodes"])) } diff --git a/game/src/main/kotlin/content/bot/Bot.kt b/game/src/main/kotlin/content/bot/Bot.kt index 4cd3d5ac08..b1bd35475b 100644 --- a/game/src/main/kotlin/content/bot/Bot.kt +++ b/game/src/main/kotlin/content/bot/Bot.kt @@ -1,9 +1,9 @@ package content.bot -import content.bot.behaviour.action.BotAction import content.bot.behaviour.BehaviourFrame import content.bot.behaviour.BehaviourState import content.bot.behaviour.Reason +import content.bot.behaviour.action.BotAction import content.bot.behaviour.activity.BotActivity import world.gregs.voidps.engine.entity.character.Character import world.gregs.voidps.engine.entity.character.player.Player diff --git a/game/src/main/kotlin/content/bot/BotManager.kt b/game/src/main/kotlin/content/bot/BotManager.kt index 9c86472621..9598d8ee22 100644 --- a/game/src/main/kotlin/content/bot/BotManager.kt +++ b/game/src/main/kotlin/content/bot/BotManager.kt @@ -4,37 +4,33 @@ import com.github.michaelbull.logging.InlineLogger import content.bot.behaviour.Behaviour import content.bot.behaviour.BehaviourFrame import content.bot.behaviour.BehaviourState +import content.bot.behaviour.Condition import content.bot.behaviour.HardReason import content.bot.behaviour.Reason +import content.bot.behaviour.action.BotAction import content.bot.behaviour.activity.ActivitySlots import content.bot.behaviour.activity.BotActivity import content.bot.behaviour.loadBehaviours -import content.bot.behaviour.navigation.Graph -import content.bot.behaviour.navigation.Graph.Companion.loadGraph -import content.bot.behaviour.navigation.NavigationShortcut import content.bot.behaviour.setup.DynamicResolvers import content.bot.behaviour.setup.Resolver -import content.bot.behaviour.Condition -import content.bot.behaviour.action.BotAction import world.gregs.voidps.engine.data.ConfigFiles -import world.gregs.voidps.engine.data.Settings import world.gregs.voidps.engine.event.AuditLog import world.gregs.voidps.engine.timer.toTicks import world.gregs.voidps.type.random import java.util.concurrent.TimeUnit /** - * Each tick checks - * 1. Assigns [activities] if a bot has none. - * 2. Moves bots onto their next action if they have completed their current. - * 3. Queues [resolvers] when a bot has an activity with unresolved requirements. + * Manages [Bot] behaviour execution and activity scheduling + * - Assigns [activities] to idle bots when possible. + * - Advances the current [BehaviourFrame]. + * - Resolves unmet [BotActivity.setup] requirements. + * - Handles activity completion, failure, and slot allocation. */ class BotManager( private val activities: MutableMap = mutableMapOf(), private val resolvers: MutableMap> = mutableMapOf(), private val groups: MutableMap> = mutableMapOf(), ) : Runnable { - lateinit var graph: Graph internal val slots = ActivitySlots() val bots = mutableListOf() private val logger = InlineLogger("BotManager") @@ -43,9 +39,7 @@ class BotManager( get() = activities.keys fun load(files: ConfigFiles): BotManager { - val shortcuts = mutableListOf() - loadBehaviours(files, activities, groups, resolvers, shortcuts) - graph = loadGraph(files.list(Settings["bots.nav.definitions"]), shortcuts) + loadBehaviours(files, activities, groups, resolvers) return this } diff --git a/game/src/main/kotlin/content/bot/BotUpdates.kt b/game/src/main/kotlin/content/bot/BotUpdates.kt index a7829847a0..c54c9baca9 100644 --- a/game/src/main/kotlin/content/bot/BotUpdates.kt +++ b/game/src/main/kotlin/content/bot/BotUpdates.kt @@ -34,7 +34,7 @@ class BotUpdates : Script { variableSet { key, from, to -> if (isBot && from != to) { bot.evaluate.add("var:$key") - resetTimeout("variable:${key}") + resetTimeout("variable:$key") } } diff --git a/game/src/main/kotlin/content/bot/behaviour/Behaviour.kt b/game/src/main/kotlin/content/bot/behaviour/Behaviour.kt index 908509b182..315a00cf51 100644 --- a/game/src/main/kotlin/content/bot/behaviour/Behaviour.kt +++ b/game/src/main/kotlin/content/bot/behaviour/Behaviour.kt @@ -3,6 +3,7 @@ package content.bot.behaviour import content.bot.behaviour.action.ActionParser import content.bot.behaviour.action.BotAction import content.bot.behaviour.activity.BotActivity +import content.bot.behaviour.navigation.NavigationGraph import content.bot.behaviour.navigation.NavigationShortcut import content.bot.behaviour.setup.Resolver import it.unimi.dsi.fastutil.objects.ObjectArrayList @@ -28,7 +29,6 @@ fun loadBehaviours( activities: MutableMap, groups: MutableMap>, resolvers: MutableMap>, - shortcuts: MutableList, ) { val templates = loadTemplates(files.list(Settings["bots.templates"])) loadActivities(activities, templates, files.list(Settings["bots.definitions"])) @@ -43,7 +43,13 @@ fun loadBehaviours( total += activity.capacity } loadSetups(resolvers, templates, files.list(Settings["bots.setups"])) +} + +fun loadGraph(files: ConfigFiles): NavigationGraph { + val templates = loadTemplates(files.list(Settings["bots.templates"])) + val shortcuts = mutableListOf() loadShortcuts(shortcuts, templates, files.list(Settings["bots.shortcuts"])) + return NavigationGraph.loadGraph(files.list(Settings["bots.nav.definitions"]), shortcuts) } private fun loadActivities(activities: MutableMap, templates: Map, paths: List) { diff --git a/game/src/main/kotlin/content/bot/behaviour/BehaviourFrame.kt b/game/src/main/kotlin/content/bot/behaviour/BehaviourFrame.kt index 765980589e..979ff0984f 100644 --- a/game/src/main/kotlin/content/bot/behaviour/BehaviourFrame.kt +++ b/game/src/main/kotlin/content/bot/behaviour/BehaviourFrame.kt @@ -3,6 +3,10 @@ package content.bot.behaviour import content.bot.Bot import content.bot.behaviour.action.BotAction +/** + * Tracks an ongoing [behaviour], it's [state] which action [index] is in progress + * [timeout] and any actions which are [blocked] from being retried. + */ data class BehaviourFrame( val behaviour: Behaviour, var state: BehaviourState = BehaviourState.Pending, diff --git a/game/src/main/kotlin/content/bot/behaviour/Condition.kt b/game/src/main/kotlin/content/bot/behaviour/Condition.kt index e9cfd0214d..93809a8457 100644 --- a/game/src/main/kotlin/content/bot/behaviour/Condition.kt +++ b/game/src/main/kotlin/content/bot/behaviour/Condition.kt @@ -17,6 +17,11 @@ import world.gregs.voidps.engine.inv.inventory import world.gregs.voidps.network.login.protocol.visual.update.player.EquipSlot import kotlin.collections.iterator +/** + * Checks if the world state is as expected or not + * Sorted by [priority] (lowest first) to avoid resolvers being executed in a weird order + * E.g. going to an area before getting the equipment. + */ sealed class Condition(val priority: Int) { abstract fun keys(): Set abstract fun events(): Set @@ -66,13 +71,13 @@ sealed class Condition(val priority: Int) { data class Entry(val ids: Set, val min: Int? = null, val max: Int? = null, val usable: Boolean = false, val equippable: Boolean = false) data class Inventory(val items: List) : Condition(100) { - override fun keys() = items.flatMap { entry -> entry.ids.map { "item:${it}" } }.toSet() + override fun keys() = items.flatMap { entry -> entry.ids.map { "item:$it" } }.toSet() override fun events() = setOf("inventory") override fun check(player: Player) = contains(player, player.inventory, items) } data class Equipment(val items: Map) : Condition(90) { - override fun keys() = items.values.flatMap { entry -> entry.ids.map { "item:${it}" } }.toSet() + override fun keys() = items.values.flatMap { entry -> entry.ids.map { "item:$it" } }.toSet() override fun events() = setOf("worn_equipment") override fun check(player: Player): Boolean { @@ -93,7 +98,7 @@ sealed class Condition(val priority: Int) { } data class Bank(val items: List) : Condition(80) { - override fun keys() = items.flatMap { entry -> entry.ids.map { "bank:${it}" } }.toSet() + override fun keys() = items.flatMap { entry -> entry.ids.map { "bank:$it" } }.toSet() override fun events() = setOf("bank") override fun check(player: Player) = contains(player, player.bank, items) } @@ -199,28 +204,24 @@ sealed class Condition(val priority: Int) { return requirements } - fun parse(type: String, list: List>): Condition? { - return when (type) { - "inventory" -> parseInventory(list) - "equipment" -> parseEquipment(list) - "bank" -> parseBank(list) - "variable" -> parseVariable(list) - "clock" -> parseClock(list) - "timer" -> parseTimer(list) - "queue" -> parseQueue(list) - "area" -> parseArea(list) - "tile" -> parseTile(list) - "object" -> parseObject(list) - "combat_level" -> parseCombat(list) - "interface_open" -> parseInterface(list) - "skill" -> parseSkills(list) - else -> null - } + fun parse(type: String, list: List>): Condition? = when (type) { + "inventory" -> parseInventory(list) + "equipment" -> parseEquipment(list) + "bank" -> parseBank(list) + "variable" -> parseVariable(list) + "clock" -> parseClock(list) + "timer" -> parseTimer(list) + "queue" -> parseQueue(list) + "area" -> parseArea(list) + "tile" -> parseTile(list) + "object" -> parseObject(list) + "combat_level" -> parseCombat(list) + "interface_open" -> parseInterface(list) + "skill" -> parseSkills(list) + else -> null } - private fun parseInventory(list: List>): Inventory { - return Inventory(parseItems(list)) - } + private fun parseInventory(list: List>): Inventory = Inventory(parseItems(list)) private fun parseItems(list: List>): MutableList { val items = mutableListOf() @@ -251,16 +252,14 @@ sealed class Condition(val priority: Int) { items[slot] = Entry( ids = toIds(id), min = value["min"] as? Int ?: 0, - max = value["max"] as? Int + max = value["max"] as? Int, ) } } return Equipment(items) } - private fun parseBank(list: List>): Bank { - return Bank(parseItems(list)) - } + private fun parseBank(list: List>): Bank = Bank(parseItems(list)) @Suppress("UNCHECKED_CAST") private fun parseVariable(list: List>): Condition? { @@ -312,7 +311,7 @@ sealed class Condition(val priority: Int) { return ObjectExists( id = map["id"] as String, x = map["x"] as Int, - y = map["y"] as Int + y = map["y"] as Int, ) } return null @@ -360,11 +359,10 @@ sealed class Condition(val priority: Int) { return SkillLevel( skill = Skill.Companion.of((map["id"] as String).toPascalCase()) ?: error("Unknown skill: '${map["id"]}'"), min = map["min"] as? Int, - max = map["max"] as? Int + max = map["max"] as? Int, ) } return null } } - -} \ No newline at end of file +} diff --git a/game/src/main/kotlin/content/bot/behaviour/action/ActionParser.kt b/game/src/main/kotlin/content/bot/behaviour/action/ActionParser.kt index 4643415b72..cabecb7d70 100644 --- a/game/src/main/kotlin/content/bot/behaviour/action/ActionParser.kt +++ b/game/src/main/kotlin/content/bot/behaviour/action/ActionParser.kt @@ -56,9 +56,7 @@ sealed class ActionParser { object CloseInterfaceParser : ActionParser() { override val required = setOf("id") - override fun parse(map: Map): BotAction { - return BotAction.CloseInterface - } + override fun parse(map: Map): BotAction = BotAction.CloseInterface } object DialogueParser : ActionParser() { diff --git a/game/src/main/kotlin/content/bot/behaviour/action/BotAction.kt b/game/src/main/kotlin/content/bot/behaviour/action/BotAction.kt index 3143b320b3..18343394f0 100644 --- a/game/src/main/kotlin/content/bot/behaviour/action/BotAction.kt +++ b/game/src/main/kotlin/content/bot/behaviour/action/BotAction.kt @@ -1,14 +1,13 @@ package content.bot.behaviour.action import content.bot.Bot -import content.bot.BotManager import content.bot.behaviour.BehaviourFrame import content.bot.behaviour.BehaviourState +import content.bot.behaviour.Condition import content.bot.behaviour.Reason -import content.bot.behaviour.navigation.Graph +import content.bot.behaviour.navigation.NavigationGraph import content.bot.behaviour.navigation.NavigationShortcut import content.bot.behaviour.setup.Resolver -import content.bot.behaviour.Condition import content.entity.combat.attackers import content.entity.combat.dead import world.gregs.voidps.engine.GameLoop @@ -43,16 +42,6 @@ sealed interface BotAction { fun start(bot: Bot, frame: BehaviourFrame): BehaviourState = BehaviourState.Running fun update(bot: Bot, frame: BehaviourFrame): BehaviourState? = null - data class Clone(val id: String) : BotAction { - override fun start(bot: Bot, frame: BehaviourFrame) = BehaviourState.Failed(Reason.Cancelled) - override fun update(bot: Bot, frame: BehaviourFrame) = BehaviourState.Failed(Reason.Cancelled) - } - - data class Reference(val action: BotAction, val references: Map) : BotAction { - override fun start(bot: Bot, frame: BehaviourFrame) = BehaviourState.Failed(Reason.Cancelled) - override fun update(bot: Bot, frame: BehaviourFrame) = BehaviourState.Failed(Reason.Cancelled) - } - data class Wait(val ticks: Int, val state: BehaviourState = BehaviourState.Success) : BotAction { override fun start(bot: Bot, frame: BehaviourFrame) = BehaviourState.Wait(ticks, state) } @@ -75,26 +64,25 @@ sealed interface BotAction { return BehaviourState.Success } - val manager = get() + val graph = get() val list = mutableListOf() - val graph = manager.graph val success = graph.find(bot.player, list, target) return queueRoute(success, list, graph, bot, target) } companion object { - internal fun queueRoute(success: Boolean, list: MutableList, graph: Graph, bot: Bot, target: String): BehaviourState { + internal fun queueRoute(success: Boolean, list: MutableList, graph: NavigationGraph, bot: Bot, target: String): BehaviourState { if (!success) { return BehaviourState.Failed(Reason.NoRoute) } val actions = mutableListOf() var nav: NavigationShortcut? = null for (edge in list) { - val shortcut = graph.shortcuts[edge] + val shortcut = graph.shortcut(edge) if (shortcut != null) { nav = shortcut } else { - actions.addAll(graph.actions[edge] ?: continue) + actions.addAll(graph.actions(edge) ?: continue) } } if (actions.isNotEmpty()) { @@ -112,21 +100,23 @@ sealed interface BotAction { } data class GoToNearest(val tag: String) : BotAction { - override fun start(bot: Bot, frame: BehaviourFrame): BehaviourState { - val set = Areas.tagged(tag) - if (set.isEmpty()) { - return BehaviourState.Failed(Reason.Invalid("No areas tagged with tag '$tag'.")) - } - if (set.any { bot.tile in it.area }) { - return BehaviourState.Success - } - return BehaviourState.Running - } + override fun start(bot: Bot, frame: BehaviourFrame) = inArea(bot) ?: BehaviourState.Running override fun update(bot: Bot, frame: BehaviourFrame): BehaviourState { if (bot.steps.isNotEmpty() || bot.mode != EmptyMode) { return BehaviourState.Running } + val state = inArea(bot) + if (state != null) { + return state + } + val graph = get() + val list = mutableListOf() + val success = graph.findNearest(bot.player, list, tag) + return GoTo.queueRoute(success, list, graph, bot, tag) + } + + private fun inArea(bot: Bot): BehaviourState? { val set = Areas.tagged(tag) if (set.isEmpty()) { return BehaviourState.Failed(Reason.Invalid("No areas tagged with tag '$tag'.")) @@ -134,11 +124,7 @@ sealed interface BotAction { if (set.any { bot.tile in it.area }) { return BehaviourState.Success } - val manager = get() - val list = mutableListOf() - val graph = manager.graph - val success = graph.findNearest(bot.player, list, tag) - return GoTo.queueRoute(success, list, graph, bot, tag) + return null } } @@ -200,11 +186,7 @@ sealed interface BotAction { val healPercentage: Int = 20, val lootOverValue: Int = 0, ) : BotAction { - - override fun start(bot: Bot, frame: BehaviourFrame): BehaviourState { - frame.timeout = 0 - return BehaviourState.Running - } + override fun start(bot: Bot, frame: BehaviourFrame) = BehaviourState.Running override fun update(bot: Bot, frame: BehaviourFrame) = when { healPercentage > 0 && bot.levels.get(Skill.Constitution) <= bot.levels.getMax(Skill.Constitution) / healPercentage -> eat(bot) @@ -290,11 +272,7 @@ sealed interface BotAction { val x: Int? = null, val y: Int? = null, ) : BotAction { - - override fun start(bot: Bot, frame: BehaviourFrame): BehaviourState { - frame.timeout = 0 - return BehaviourState.Running - } + override fun start(bot: Bot, frame: BehaviourFrame) = BehaviourState.Running override fun update(bot: Bot, frame: BehaviourFrame) = when { success?.check(bot.player) == true -> BehaviourState.Success @@ -548,28 +526,4 @@ sealed interface BotAction { else -> BehaviourState.Running } } - - /** - * TODO - * combat training dummy bots - * firemaking bot - * fishing bot - * rune mysteries quest bot - * bot spawning in other locations - * track timeouts by comparing previous to current produce for progress - * remove misc old bot data from areas - * bot saving? - * bot setups - * item tags? - * - * Idea: Reactions? - * A separate queue that runs "reactions" e.g. - * if my hp is low and I have food - eat it - * if I have bones and am not in combat - bury them - * if I have raw food and there's a fire nearby - cook it - * - * TODO behaviour loop detection - * - - */ } diff --git a/game/src/main/kotlin/content/bot/behaviour/activity/BotActivity.kt b/game/src/main/kotlin/content/bot/behaviour/activity/BotActivity.kt index b073e5e7ea..bbb05f359a 100644 --- a/game/src/main/kotlin/content/bot/behaviour/activity/BotActivity.kt +++ b/game/src/main/kotlin/content/bot/behaviour/activity/BotActivity.kt @@ -1,8 +1,8 @@ package content.bot.behaviour.activity -import content.bot.behaviour.action.BotAction import content.bot.behaviour.Behaviour import content.bot.behaviour.Condition +import content.bot.behaviour.action.BotAction /** * An activity with a limited number of slots that bots can perform diff --git a/game/src/main/kotlin/content/bot/behaviour/navigation/Graph.kt b/game/src/main/kotlin/content/bot/behaviour/navigation/NavigationGraph.kt similarity index 80% rename from game/src/main/kotlin/content/bot/behaviour/navigation/Graph.kt rename to game/src/main/kotlin/content/bot/behaviour/navigation/NavigationGraph.kt index 52463cb66d..b644889873 100644 --- a/game/src/main/kotlin/content/bot/behaviour/navigation/Graph.kt +++ b/game/src/main/kotlin/content/bot/behaviour/navigation/NavigationGraph.kt @@ -1,12 +1,12 @@ package content.bot.behaviour.navigation +import content.bot.behaviour.Condition import content.bot.behaviour.action.ActionParser import content.bot.behaviour.action.BotAction import content.bot.behaviour.actions import content.bot.behaviour.requirements import content.bot.bot import content.bot.isBot -import content.bot.behaviour.Condition import world.gregs.config.Config import world.gregs.config.ConfigReader import world.gregs.voidps.engine.data.definition.Areas @@ -16,27 +16,43 @@ import world.gregs.voidps.type.Distance import world.gregs.voidps.type.Tile import java.util.PriorityQueue -class Graph( - val endNodes: IntArray = intArrayOf(), - val edgeWeights: IntArray = intArrayOf(), - val edgeConditions: Array?> = emptyArray(), - val actions: Array?> = emptyArray(), - val adjacentEdges: Array = emptyArray(), - val tiles: IntArray = intArrayOf(), - val tags: Array?> = emptyArray(), - val shortcuts: Map = emptyMap(), +/** + * Weighted navigation graph for bot pathfinding. + * Represents a directed graph of nodes and edges with: + * - Per-edge weights, actions, and traversal conditions. + * - Optional shortcuts (e.g. teleports) treated as virtual edges. + * - Node metadata such as tiles and area tags. + */ +class NavigationGraph( + private val endNodes: IntArray = intArrayOf(), + private val edgeWeights: IntArray = intArrayOf(), + private val edgeConditions: Array?> = emptyArray(), + private val actions: Array?> = emptyArray(), + private val adjacentEdges: Array = emptyArray(), + private val tiles: IntArray = intArrayOf(), + private val tags: Array?> = emptyArray(), + private val shortcuts: Map = emptyMap(), var nodeCount: Int = 0, ) { + fun shortcut(edge: Int) = shortcuts[edge] + fun actions(edge: Int): List? = actions[edge] - fun conditions(edge: Int): List? = edgeConditions[edge] + fun weight(edge: Int): Int = edgeWeights[edge] + + fun edges(node: Int): IntArray? = adjacentEdges[node] + + fun tile(node: Int): Tile = Tile(tiles[node]) - fun tile(edge: Int): Tile { + fun endTile(edge: Int): Tile { val nodeIndex = endNodes[edge] return Tile(tiles[nodeIndex]) } + /** + * Find a path to the nearest area with a [tag]. + */ fun findNearest(player: Player, output: MutableList, tag: String): Boolean { val start = startingPoints(player) return find(player, output, start, target = { @@ -44,12 +60,16 @@ class Graph( }) } + /** + * Find a path to [area]. + */ fun find(player: Player, output: MutableList, area: String): Boolean { val start = startingPoints(player) return find(player, output, start, target = { Tile(tiles[it]) in Areas[area] }) } - fun startingPoints(player: Player): Set = buildSet { + internal fun startingPoints(player: Player): Set = buildSet { + // Append all nodes within 10 tiles for (index in 1 until tiles.size) { val tile = Tile(tiles[index]) if (player.tile.level != tile.level) { @@ -61,6 +81,7 @@ class Graph( } add(Node(index, distance.coerceAtLeast(0))) } + // Check for shortcuts (i.e. item teleports) val blocked = if (player.isBot) player.bot.blocked else emptySet() for (shortcut in shortcuts.values) { if (blocked.contains(shortcut.id)) { @@ -74,13 +95,19 @@ class Graph( } } - fun find(player: Player, output: MutableList, start: Node, target: Int) = find(player, output, setOf(start)) { it == target } + internal fun find(player: Player, output: MutableList, start: Node, target: Int) = find(player, output, setOf(start)) { it == target } - fun find(player: Player, output: MutableList, start: Set, target: Int) = find(player, output, start) { it == target } + internal fun find(player: Player, output: MutableList, start: Set, target: Int) = find(player, output, start) { it == target } - fun find(player: Player, output: MutableList, start: Node, target: (Int) -> Boolean) = find(player, output, setOf(start), target) + internal fun find(player: Player, output: MutableList, start: Node, target: (Int) -> Boolean) = find(player, output, setOf(start), target) - fun find(player: Player, output: MutableList, startingPoints: Set, target: (Int) -> Boolean): Boolean { + /** + * Dijkstra algorithm + * Searches from a virtual starting node traverses all [adjacentEdges] until a [target] is found. + * Appends the completed route of edge indices to [output]. + * @return successful route found. + */ + internal fun find(player: Player, output: MutableList, startingPoints: Set, target: (Int) -> Boolean): Boolean { output.clear() val queue = PriorityQueue() val visited = BooleanArray(nodeCount) @@ -90,9 +117,8 @@ class Graph( for (start in startingPoints) { if (target(start.index)) { - // As we're queuing all nearby points we don't want select any starting points which are in - // the target, otherwise we'll end up with no edges to traverse. - // (if this were normal dijkstra we'd produce points not edges and this wouldn't be an issue) + // Don't select target starting points, otherwise we'll have no edges to traverse. + // Not an issue as we queue all nearby points - normal dijkstra's would produce points not edges. continue } distance[start.index] = -1 @@ -135,11 +161,11 @@ class Graph( return false } - data class Node(val index: Int, val cost: Int = 0) : Comparable { + internal data class Node(val index: Int, val cost: Int = 0) : Comparable { override fun compareTo(other: Node) = cost.compareTo(other.cost) } - class Builder { + internal class Builder { // Nodes val tiles = LinkedHashSet() val nodes = mutableSetOf() @@ -166,7 +192,7 @@ class Graph( val area = Areas[name] val end = tiles.indexOfFirst { it in area } if (end == -1) { - throw IllegalArgumentException("Unable to find nav graph tile in shortcut area '${name}'.") + throw IllegalArgumentException("Unable to find nav graph tile in shortcut area '$name'.") } val index = addEdge(0, end, shortcut.weight, shortcut.actions, shortcut.requires) shortcuts[index] = shortcut @@ -207,7 +233,7 @@ class Graph( return edgeIndex } - fun build() = Graph( + fun build() = NavigationGraph( endNodes = endNodes.toIntArray(), edgeWeights = weights.toIntArray(), edgeConditions = conditions.toTypedArray(), @@ -233,7 +259,7 @@ class Graph( } companion object { - fun loadGraph(paths: List, shortcuts: List): Graph { + fun loadGraph(paths: List, shortcuts: List): NavigationGraph { val builder = Builder() timedLoad("nav graph edge") { for (path in paths) { diff --git a/game/src/main/kotlin/content/bot/behaviour/navigation/NavigationShortcut.kt b/game/src/main/kotlin/content/bot/behaviour/navigation/NavigationShortcut.kt index afd42fcba4..2ff088125f 100644 --- a/game/src/main/kotlin/content/bot/behaviour/navigation/NavigationShortcut.kt +++ b/game/src/main/kotlin/content/bot/behaviour/navigation/NavigationShortcut.kt @@ -1,8 +1,8 @@ package content.bot.behaviour.navigation -import content.bot.behaviour.action.BotAction import content.bot.behaviour.Behaviour import content.bot.behaviour.Condition +import content.bot.behaviour.action.BotAction data class NavigationShortcut( override val id: String, diff --git a/game/src/main/kotlin/content/bot/behaviour/setup/DynamicResolvers.kt b/game/src/main/kotlin/content/bot/behaviour/setup/DynamicResolvers.kt index d533b72ccd..b1545ba0a1 100644 --- a/game/src/main/kotlin/content/bot/behaviour/setup/DynamicResolvers.kt +++ b/game/src/main/kotlin/content/bot/behaviour/setup/DynamicResolvers.kt @@ -1,8 +1,8 @@ package content.bot.behaviour.setup -import content.bot.behaviour.action.BotAction import content.bot.behaviour.Condition import content.bot.behaviour.Condition.Entry +import content.bot.behaviour.action.BotAction import content.entity.player.bank.bank import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.entity.character.player.equip.equipped @@ -80,7 +80,7 @@ object DynamicResolvers { } val item = player.equipped(slot) if (item.isNotEmpty()) { - actions.add(BotAction.InterfaceOption("Remove", "worn_equipment:${slot.name.lowercase()}_slot:${item}")) + actions.add(BotAction.InterfaceOption("Remove", "worn_equipment:${slot.name.lowercase()}_slot:$item")) } equipment.remove(slot) } @@ -138,7 +138,7 @@ object DynamicResolvers { actions.add(BotAction.CloseInterface) // Equip all items for (id in toEquip) { - actions.add(BotAction.InterfaceOption("Equip", "inventory:inventory:${id}")) + actions.add(BotAction.InterfaceOption("Equip", "inventory:inventory:$id")) } } if (actions.isNotEmpty()) { @@ -155,4 +155,4 @@ object DynamicResolvers { actions.add(BotAction.InterfaceOption("Withdraw-1", "bank:inventory:${item.id}")) } } -} \ No newline at end of file +} diff --git a/game/src/main/kotlin/content/bot/behaviour/setup/Resolver.kt b/game/src/main/kotlin/content/bot/behaviour/setup/Resolver.kt index c72e54ee95..87cd6f1d74 100644 --- a/game/src/main/kotlin/content/bot/behaviour/setup/Resolver.kt +++ b/game/src/main/kotlin/content/bot/behaviour/setup/Resolver.kt @@ -1,8 +1,8 @@ package content.bot.behaviour.setup -import content.bot.behaviour.action.BotAction import content.bot.behaviour.Behaviour import content.bot.behaviour.Condition +import content.bot.behaviour.action.BotAction /** * A behaviour that can be performed to resolve a requirement of another [Resolver] or [BotAction] diff --git a/game/src/main/kotlin/content/entity/obj/ObjectTeleports.kt b/game/src/main/kotlin/content/entity/obj/ObjectTeleports.kt index 4627ff7686..ef3adc1851 100644 --- a/game/src/main/kotlin/content/entity/obj/ObjectTeleports.kt +++ b/game/src/main/kotlin/content/entity/obj/ObjectTeleports.kt @@ -1,6 +1,6 @@ package content.entity.obj -import content.bot.behaviour.navigation.Graph.Companion.readTile +import content.bot.behaviour.navigation.NavigationGraph.Companion.readTile import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap import world.gregs.config.Config diff --git a/game/src/main/kotlin/content/entity/player/command/PathFindingCommands.kt b/game/src/main/kotlin/content/entity/player/command/PathFindingCommands.kt index bbda65a8a0..f3fc4293c2 100644 --- a/game/src/main/kotlin/content/entity/player/command/PathFindingCommands.kt +++ b/game/src/main/kotlin/content/entity/player/command/PathFindingCommands.kt @@ -2,8 +2,9 @@ package content.entity.player.command import content.bot.Bot import content.bot.BotManager -import content.bot.behaviour.action.BotAction import content.bot.behaviour.BehaviourFrame +import content.bot.behaviour.action.BotAction +import content.bot.behaviour.navigation.NavigationGraph import content.bot.behaviour.setup.Resolver import content.bot.bot import content.bot.isBot @@ -135,8 +136,7 @@ class PathFindingCommands(val patrols: PatrolDefinitions) : Script { } adminCommand("walk_to_bank") { - val manager: BotManager = get() - val graph = manager.graph + val graph: NavigationGraph = get() val output = mutableListOf() println( "Path took ${ diff --git a/game/src/main/kotlin/content/quest/member/fairy_tale_part_2/fairy_ring/FairyRingCodes.kt b/game/src/main/kotlin/content/quest/member/fairy_tale_part_2/fairy_ring/FairyRingCodes.kt index 4f88157c2c..15d2b4a48a 100644 --- a/game/src/main/kotlin/content/quest/member/fairy_tale_part_2/fairy_ring/FairyRingCodes.kt +++ b/game/src/main/kotlin/content/quest/member/fairy_tale_part_2/fairy_ring/FairyRingCodes.kt @@ -1,6 +1,6 @@ package content.quest.member.fairy_tale_part_2.fairy_ring -import content.bot.behaviour.navigation.Graph.Companion.readTile +import content.bot.behaviour.navigation.NavigationGraph.Companion.readTile import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap import world.gregs.config.Config import world.gregs.voidps.engine.timedLoad diff --git a/game/src/test/kotlin/content/bot/BotManagerTest.kt b/game/src/test/kotlin/content/bot/BotManagerTest.kt index a2010610dd..ed8ff407c7 100644 --- a/game/src/test/kotlin/content/bot/BotManagerTest.kt +++ b/game/src/test/kotlin/content/bot/BotManagerTest.kt @@ -2,12 +2,12 @@ package content.bot import content.bot.behaviour.BehaviourFrame import content.bot.behaviour.BehaviourState +import content.bot.behaviour.Condition import content.bot.behaviour.Reason import content.bot.behaviour.SoftReason +import content.bot.behaviour.action.BotAction import content.bot.behaviour.activity.BotActivity import content.bot.behaviour.setup.Resolver -import content.bot.behaviour.Condition -import content.bot.behaviour.action.BotAction import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.Test import world.gregs.voidps.engine.entity.character.player.Player @@ -71,8 +71,8 @@ class BotManagerTest { val activity = testActivity( id = "task", plan = listOf( - BotAction.Clone("1"), - BotAction.Clone("1"), + BotAction.Wait(1), + BotAction.Wait(1), ), ) val frame = BehaviourFrame(activity) @@ -153,7 +153,7 @@ class BotManagerTest { fun `Failed activity is blocked`() { val activity = testActivity( id = "fish", - plan = listOf(BotAction.Clone("")), + plan = listOf(BotAction.Wait(1)), ) val manager = BotManager(mutableMapOf(activity.id to activity)) @@ -174,7 +174,7 @@ class BotManagerTest { val activity = testActivity( id = "test", requires = listOf( - Condition.SkillLevel(Skill.Attack, 99) + Condition.SkillLevel(Skill.Attack, 99), ), plan = listOf(BotAction.Wait(4)), ) @@ -222,13 +222,13 @@ class BotManagerTest { fun `Lowest weight resolver is selected`() { val condition = Condition.AtTile(100, 100, 2) - val bad = Resolver("bad", weight = 10, actions = listOf(BotAction.Clone(""))) - val good = Resolver("good", weight = 1, actions = listOf(BotAction.Clone(""))) + val bad = Resolver("bad", weight = 10, actions = listOf(BotAction.Wait(1))) + val good = Resolver("good", weight = 1, actions = listOf(BotAction.Wait(1))) val activity = testActivity( id = "mine", resolves = listOf(condition), - plan = listOf(BotAction.Clone("")), + plan = listOf(BotAction.Wait(1)), ) val manager = BotManager( @@ -246,7 +246,7 @@ class BotManagerTest { @Test fun `Blocked resolver is not reselected`() { val condition = Condition.AtTile(100, 100, 2) - val resolver = Resolver(id = "get_key", weight = 1, actions = listOf(BotAction.Clone(""))) + val resolver = Resolver(id = "get_key", weight = 1, actions = listOf(BotAction.Wait(1))) val activity = testActivity( id = "open_door", resolves = listOf(condition), @@ -332,7 +332,7 @@ class BotManagerTest { val resolver = Resolver( id = "mine_gem", weight = 1, - actions = listOf(BotAction.Clone("")), + actions = listOf(BotAction.Wait(1)), requires = listOf(Condition.SkillLevel(Skill.Mining, 99)), ) val activity = testActivity( diff --git a/game/src/test/kotlin/content/bot/behaviour/activity/ActivitySlotsTest.kt b/game/src/test/kotlin/content/bot/behaviour/activity/ActivitySlotsTest.kt index 1dc0a7a8c1..022de96907 100644 --- a/game/src/test/kotlin/content/bot/behaviour/activity/ActivitySlotsTest.kt +++ b/game/src/test/kotlin/content/bot/behaviour/activity/ActivitySlotsTest.kt @@ -38,4 +38,4 @@ class ActivitySlotsTest { Assertions.assertTrue(slots.hasFree(activity)) } -} \ No newline at end of file +} diff --git a/game/src/test/kotlin/content/bot/behaviour/navigation/GraphTest.kt b/game/src/test/kotlin/content/bot/behaviour/navigation/NavigationGraphTest.kt similarity index 81% rename from game/src/test/kotlin/content/bot/behaviour/navigation/GraphTest.kt rename to game/src/test/kotlin/content/bot/behaviour/navigation/NavigationGraphTest.kt index 73d03a83aa..7f02e492ed 100644 --- a/game/src/test/kotlin/content/bot/behaviour/navigation/GraphTest.kt +++ b/game/src/test/kotlin/content/bot/behaviour/navigation/NavigationGraphTest.kt @@ -11,7 +11,7 @@ import world.gregs.voidps.type.area.Rectangle import kotlin.test.assertEquals import kotlin.test.assertTrue -class GraphTest { +class NavigationGraphTest { @Test fun `Shortest path is found`() { @@ -24,7 +24,7 @@ class GraphTest { C */ - val builder = Graph.Builder() + val builder = NavigationGraph.Builder() val a = 0 val b = 1 val c = 2 @@ -34,7 +34,7 @@ class GraphTest { // builder.print() val output = mutableListOf() - val success = builder.build().find(Player(), output, Graph.Node(a), b) + val success = builder.build().find(Player(), output, NavigationGraph.Node(a), b) assertTrue(success) assertEquals(listOf(ac, cb), output) } @@ -50,7 +50,7 @@ class GraphTest { D-----F 2 */ - val builder = Graph.Builder() + val builder = NavigationGraph.Builder() val a = 0 val b = 1 val c = 2 @@ -69,7 +69,7 @@ class GraphTest { // builder.print() val output = mutableListOf() - val success = builder.build().find(Player(), output, Graph.Node(a), c) + val success = builder.build().find(Player(), output, NavigationGraph.Node(a), c) assertTrue(success) assertEquals(listOf(ab, bd, df, fc), output) } @@ -88,7 +88,7 @@ class GraphTest { 4 |/ A */ - val builder = Graph.Builder() + val builder = NavigationGraph.Builder() val a = 0 val b = 1 val c = 2 @@ -107,7 +107,7 @@ class GraphTest { // builder.print() val output = mutableListOf() - val success = builder.build().find(Player(), output, Graph.Node(f), d) + val success = builder.build().find(Player(), output, NavigationGraph.Node(f), d) assertTrue(success) assertEquals(listOf(fg, gb, bd), output) } @@ -123,7 +123,7 @@ class GraphTest { E-----D 7-X */ - val builder = Graph.Builder() + val builder = NavigationGraph.Builder() val a = 0 val b = 1 val c = 2 @@ -139,7 +139,7 @@ class GraphTest { // builder.print() val output = mutableListOf() - val success = builder.build().find(Player(), output, Graph.Node(a), d) + val success = builder.build().find(Player(), output, NavigationGraph.Node(a), d) assertTrue(success) assertEquals(listOf(ae, ec, cd), output) } @@ -151,14 +151,14 @@ class GraphTest { 1 / A */ - val builder = Graph.Builder() + val builder = NavigationGraph.Builder() val a = 0 val b = 1 builder.addEdge(a, b, 10) // builder.print() val output = mutableListOf() - val success = builder.build().find(Player(), output, Graph.Node(b), a) + val success = builder.build().find(Player(), output, NavigationGraph.Node(b), a) Assertions.assertFalse(success) } @@ -169,14 +169,14 @@ class GraphTest { 1 | A */ - val builder = Graph.Builder() + val builder = NavigationGraph.Builder() val a = 0 val b = 1 val ab = builder.addEdge(a, b, 0) // builder.print() val output = mutableListOf() - val success = builder.build().find(Player(), output, Graph.Node(a), b) + val success = builder.build().find(Player(), output, NavigationGraph.Node(a), b) assertTrue(success) assertEquals(listOf(ab), output) } @@ -195,7 +195,7 @@ class GraphTest { 4 |/ A */ - val builder = Graph.Builder() + val builder = NavigationGraph.Builder() val a = 0 val b = 1 val c = 2 @@ -214,14 +214,14 @@ class GraphTest { // builder.print() val output = mutableListOf() - val success = builder.build().find(Player(), output, setOf(Graph.Node(f), Graph.Node(a)), d) + val success = builder.build().find(Player(), output, setOf(NavigationGraph.Node(f), NavigationGraph.Node(a)), d) assertTrue(success) assertEquals(listOf(ab, bd), output) } @Test fun `Find returns shortest path`() { - val builder = Graph.Builder() + val builder = NavigationGraph.Builder() val a = Tile(0) val b = Tile(1) @@ -236,7 +236,7 @@ class GraphTest { player.tile = a val path = mutableListOf() - val found = graph.find(player, path, start = Graph.Node(0), target = 2) + val found = graph.find(player, path, start = NavigationGraph.Node(0), target = 2) assertTrue(found) assertEquals(listOf(0, 2), path) @@ -244,7 +244,7 @@ class GraphTest { @Test fun `Find respects edge conditions`() { - val builder = Graph.Builder() + val builder = NavigationGraph.Builder() val a = Tile(0) val b = Tile(1) @@ -262,7 +262,7 @@ class GraphTest { player.tile = a val path = mutableListOf() - val found = graph.find(player, path, start = Graph.Node(0), target = 1) + val found = graph.find(player, path, start = NavigationGraph.Node(0), target = 1) Assertions.assertFalse(found, "Edge condition blocks traversal") assertTrue(path.isEmpty()) @@ -270,7 +270,7 @@ class GraphTest { @Test fun `Starting points include nearby tiles`() { - val builder = Graph.Builder() + val builder = NavigationGraph.Builder() val a = Tile(1, 1) val b = Tile(20, 20) @@ -286,9 +286,9 @@ class GraphTest { val starts = graph.startingPoints(player) - assertTrue(starts.contains(Graph.Node(1, 9))) - assertTrue(starts.contains(Graph.Node(2, 10))) - Assertions.assertFalse(starts.contains(Graph.Node(3, 90))) + assertTrue(starts.contains(NavigationGraph.Node(1, 9))) + assertTrue(starts.contains(NavigationGraph.Node(2, 10))) + Assertions.assertFalse(starts.contains(NavigationGraph.Node(3, 90))) } @Test @@ -303,7 +303,7 @@ class GraphTest { produces = setOf("area:town"), ) - val builder = Graph.Builder() + val builder = NavigationGraph.Builder() builder.add(Tile(75, 75)) val edge = builder.add(shortcut) @@ -313,12 +313,12 @@ class GraphTest { val starts = graph.startingPoints(player) - assertTrue(starts.contains(Graph.Node(edge))) + assertTrue(starts.contains(NavigationGraph.Node(edge))) } @Test fun `Path reconstruction produces correct edge order`() { - val builder = Graph.Builder() + val builder = NavigationGraph.Builder() val a = Tile(0) @@ -330,7 +330,7 @@ class GraphTest { player.tile = a val path = mutableListOf() - val found = graph.find(player, path, start = Graph.Node(0), target = 2) + val found = graph.find(player, path, start = NavigationGraph.Node(0), target = 2) assertTrue(found) assertEquals(listOf(e1, e2), path) @@ -340,7 +340,7 @@ class GraphTest { fun `Complex route`() { /* */ - val builder = Graph.Builder() + val builder = NavigationGraph.Builder() val a = 0 val b = 1 val c = 2 @@ -370,8 +370,8 @@ class GraphTest { // builder.print() val output = mutableListOf() - val success = builder.build().find(Player(), output, Graph.Node(a), h) + val success = builder.build().find(Player(), output, NavigationGraph.Node(a), h) assertTrue(success) assertEquals(listOf(aj, jk, kl, lm, mh), output) } -} \ No newline at end of file +} diff --git a/tools/src/main/kotlin/world/gregs/voidps/tools/map/view/draw/GraphDrawer.kt b/tools/src/main/kotlin/world/gregs/voidps/tools/map/view/draw/GraphDrawer.kt index 1d8fe0278a..af09bfc601 100644 --- a/tools/src/main/kotlin/world/gregs/voidps/tools/map/view/draw/GraphDrawer.kt +++ b/tools/src/main/kotlin/world/gregs/voidps/tools/map/view/draw/GraphDrawer.kt @@ -1,6 +1,6 @@ package world.gregs.voidps.tools.map.view.draw -import content.bot.behaviour.navigation.Graph +import content.bot.behaviour.navigation.NavigationGraph import org.rsmod.game.pathfinder.StepValidator import org.rsmod.game.pathfinder.collision.CollisionStrategies import org.rsmod.game.pathfinder.collision.CollisionStrategy @@ -19,7 +19,7 @@ class GraphDrawer( private val view: MapView, private val area: AreaSet, ) { - private var graph: Graph? = null + private var graph: NavigationGraph? = null private val steps: StepValidator = StepValidator(Collisions.map) private val linkColour = Color(0.0f, 0.0f, 1.0f, 0.5f) @@ -29,7 +29,7 @@ class GraphDrawer( private val walkableColour = Color(0.0f, 1.0f, 0.0f, 0.3f) private val collisionColour = Color(1.0f, 0.0f, 0.0f, 0.3f) - fun reload(graph: Graph?) { + fun reload(graph: NavigationGraph?) { this.graph = graph } @@ -50,7 +50,7 @@ class GraphDrawer( if (graph != null) { val graph = graph!! for (i in 1 until graph.nodeCount) { - val tile = Tile(graph.tiles[i]) + val tile = graph.tile(i) if (tile.level != view.level) { continue } @@ -63,13 +63,13 @@ class GraphDrawer( val height = view.mapToImageY(1) g.fillOval(viewX, viewY, width, height) - val edges = graph.adjacentEdges[i] + val edges = graph.edges(i) edges?.forEachIndexed { index, edge -> - val end = graph.tile(edge) + val end = graph.endTile(edge) if (tile.level != view.level || end.level != view.level) { return@forEachIndexed } - val distance = graph.edgeWeights[edge] + val distance = graph.weight(edge) val endX = view.mapToViewX(end.x) + width / 2 val endY = view.mapToViewY(view.flipMapY(end.y)) + height / 2 val offset = width / 4 diff --git a/tools/src/main/kotlin/world/gregs/voidps/tools/map/view/draw/MapView.kt b/tools/src/main/kotlin/world/gregs/voidps/tools/map/view/draw/MapView.kt index 09ba3db25b..fe4c8cdcfa 100644 --- a/tools/src/main/kotlin/world/gregs/voidps/tools/map/view/draw/MapView.kt +++ b/tools/src/main/kotlin/world/gregs/voidps/tools/map/view/draw/MapView.kt @@ -1,6 +1,6 @@ package world.gregs.voidps.tools.map.view.draw -import content.bot.behaviour.navigation.Graph +import content.bot.behaviour.navigation.NavigationGraph import kotlinx.coroutines.* import world.gregs.voidps.engine.data.ConfigFiles import world.gregs.voidps.engine.data.Settings @@ -93,7 +93,7 @@ class MapView : JPanel() { fun reload(files: ConfigFiles = configFiles()) { val areaFiles = files.list(Settings["map.areas"]) AreaSet.load(areaFiles, areaSet) - graph.reload(Graph.loadGraph(files.list(Settings["bots.nav.definitions"]), emptyList())) + graph.reload(NavigationGraph.loadGraph(files.list(Settings["bots.nav.definitions"]), emptyList())) } fun updateLevel(level: Int) { From 147d5e9ae7564339428f07beabf59f7e5e8d57b8 Mon Sep 17 00:00:00 2001 From: GregHib Date: Tue, 10 Feb 2026 17:15:13 +0000 Subject: [PATCH 086/101] Fix product templates, taking items from shop samples, tidy messages --- .../misthalin/lumbridge/lumbridge.bots.toml | 4 +-- data/bot/bank.setups.toml | 20 ------------- .../src/main/kotlin/content/bot/BotManager.kt | 19 +++++++------ .../src/main/kotlin/content/bot/BotUpdates.kt | 3 ++ .../kotlin/content/bot/behaviour/Behaviour.kt | 17 ++++++++--- .../content/bot/behaviour/BehaviourState.kt | 20 +++++++++---- .../kotlin/content/bot/behaviour/Condition.kt | 8 +++--- .../kotlin/content/bot/behaviour/Reason.kt | 28 ++++++++++++++----- .../content/bot/behaviour/action/BotAction.kt | 11 ++++++-- .../bot/behaviour/setup/DynamicResolvers.kt | 5 ++-- 10 files changed, 79 insertions(+), 56 deletions(-) delete mode 100644 data/bot/bank.setups.toml diff --git a/data/area/misthalin/lumbridge/lumbridge.bots.toml b/data/area/misthalin/lumbridge/lumbridge.bots.toml index 2466a87515..56febfbf1d 100644 --- a/data/area/misthalin/lumbridge/lumbridge.bots.toml +++ b/data/area/misthalin/lumbridge/lumbridge.bots.toml @@ -56,7 +56,7 @@ requires = [ [lumbridge_crayfish] template = "fish_crayfish" capacity = 4 -fields = { location = "lumbridge_south_river_fishing_spot", spot = "fishing_spot_crayfish_lumbridge" } +fields = { location = "lumbridge_south_river_fishing_area", spot = "fishing_spot_crayfish_lumbridge" } requires = [ { skill = { id = "fishing", min = 1, max = 5 } } ] @@ -145,7 +145,7 @@ fields = { location = "lumbridge_swamp_west_mithril_mine" } [lumbridge_cook_chicken] template = "cooking_template" capacity = 4 -fields = { raw = "raw_chicken", cooked = "chicken", level = 1, location = "lumbridge_kitchen", obj = "cooking_range_lumbridge_castle" } +fields = { raw = "raw_chicken", cooked = "chicken", level = 1, location = "lumbridge_kitchen", obj = "cooking_range_lumbridge_castle", burnt = "burnt_chicken" } [lumbridge_cook_beef] template = "beef_template" diff --git a/data/bot/bank.setups.toml b/data/bot/bank.setups.toml deleted file mode 100644 index f5fe437e8d..0000000000 --- a/data/bot/bank.setups.toml +++ /dev/null @@ -1,20 +0,0 @@ -[deposit_carried_items] -actions = [ - { go_to = { nearest = "bank" } }, - { object = { option = "Use-quickly", id = "bank_booth*", success = { interface_open = { id = "bank" } } } }, - { interface = { option = "Deposit carried items", id = "bank:carried", success = { inventory = { id = "empty", min = 28 } } } }, -] -produces = [ - { item = "empty" } -] - -#[deposit_worn_items] -#type = "resolver" -#actions = [ -# { go_to_nearest = "bank" }, -# { option = "Use-quickly", object = "bank_booth*", success = { interface = "bank" } }, -# { option = "Deposit worn items", interface = "bank:worn", success = { equipment_space = 28 } }, -#] -#produces = [ -# { equipment_space = 28 } -#] diff --git a/game/src/main/kotlin/content/bot/BotManager.kt b/game/src/main/kotlin/content/bot/BotManager.kt index 9598d8ee22..f1522f51c2 100644 --- a/game/src/main/kotlin/content/bot/BotManager.kt +++ b/game/src/main/kotlin/content/bot/BotManager.kt @@ -99,7 +99,7 @@ class BotManager( updateAvailable(bot) } if (bot.player["debug", false]) { - logger.trace { "Picking bot ${bot.player.accountName} new task from available: ${bot.available}." } + logger.trace { "Picking new activity from ${bot.available} for bot ${bot.player.accountName}." } } var activity = if (hasRequirements(bot, bot.previous)) { bot.previous!! @@ -111,7 +111,7 @@ class BotManager( } if (activity == null) { if (bot.player["debug", false]) { - logger.info { "No activities with requirements met for bot: ${bot.player.accountName}." } + logger.debug { "No activities with requirements met for bot: ${bot.player.accountName}." } debugActivities(bot) } activity = idle @@ -152,7 +152,7 @@ class BotManager( private fun assign(bot: Bot, activity: BotActivity) { AuditLog.event(bot, "assigned", activity.id) if (bot.player["debug", false]) { - logger.info { "Assigned bot: '${bot.player.accountName}' task '${activity.id}'." } + logger.info { "Assigned task '${activity.id}' to bot '${bot.player.accountName}'." } } slots.occupy(activity) bot.previous = activity @@ -202,7 +202,7 @@ class BotManager( } frame.blocked.removeAll(DynamicResolvers.ids()) val resolvers = buildList { - DynamicResolvers.resolver(bot.player, requirement)?.let { add(it) } + DynamicResolvers.resolver(bot.player, requirement)?.also { add(it) } requirement.keys() .flatMap { resolvers[it].orEmpty() } .forEach(::add) @@ -220,7 +220,7 @@ class BotManager( // Attempt resolution AuditLog.event(bot, "start_resolver", resolver.id, behaviour.id) if (debug) { - logger.info { "Starting resolver: ${resolver.id} for ${behaviour.id} requirement: $requirement." } + logger.info { "Starting ${resolver.id} for ${behaviour.id} requirement: $requirement." } } frame.blocked.add(resolver.id) val resolverFrame = BehaviourFrame(resolver) @@ -269,7 +269,7 @@ class BotManager( val behaviour = frame.behaviour val action = frame.action() if (bot.player["debug", false]) { - logger.warn { "Failed action: ${action::class.simpleName} for ${behaviour.id}, reason: ${state.reason}." } + logger.warn { "Failed ${behaviour.id} action=${action::class.simpleName}, reason=${state.reason}." } } AuditLog.event(bot, "failed", behaviour.id, state.reason, frame.index, action::class.simpleName) if (state.reason is HardReason) { @@ -296,18 +296,19 @@ class BotManager( } private fun debugResolvers(behaviour: Behaviour, requirement: Condition, resolvers: List, frame: BehaviourFrame, bot: Bot) { - logger.info { "No resolver found for ${behaviour.id} keys: ${requirement.keys()} requirement: $requirement." } + logger.warn { "No resolver found for keys=${requirement.keys()} id=${behaviour.id}, requirement=$requirement." } for (resolver in resolvers) { if (frame.blocked.contains(resolver.id)) { - logger.debug { "Resolver: ${resolver.id} - Blocked by frame behaviour: ${frame.behaviour.id}." } + logger.debug { "Resolver ${resolver.id} - Blocked by frame behaviour: ${frame.behaviour.id}." } break } for (requirement in resolver.requires) { if (!requirement.check(bot.player)) { - logger.debug { "Resolver: ${resolver.id} - Failed requirement: $requirement." } + logger.debug { "Resolver ${resolver.id} - Failed requirement: $requirement." } break } } + logger.debug { "Resolver ${resolver.id} - Available." } } } diff --git a/game/src/main/kotlin/content/bot/BotUpdates.kt b/game/src/main/kotlin/content/bot/BotUpdates.kt index c54c9baca9..bba0df0f43 100644 --- a/game/src/main/kotlin/content/bot/BotUpdates.kt +++ b/game/src/main/kotlin/content/bot/BotUpdates.kt @@ -1,5 +1,6 @@ package content.bot +import com.github.michaelbull.logging.InlineLogger import world.gregs.voidps.engine.Script import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.network.client.instruction.InteractDialogue @@ -8,6 +9,8 @@ import world.gregs.voidps.network.client.instruction.InteractDialogue * Listen for state changes which would change which activities are available to a bot */ class BotUpdates : Script { + val logger = InlineLogger() + init { /* Track state changes to re-evaluate available activities diff --git a/game/src/main/kotlin/content/bot/behaviour/Behaviour.kt b/game/src/main/kotlin/content/bot/behaviour/Behaviour.kt index 315a00cf51..97a77c16c4 100644 --- a/game/src/main/kotlin/content/bot/behaviour/Behaviour.kt +++ b/game/src/main/kotlin/content/bot/behaviour/Behaviour.kt @@ -266,9 +266,18 @@ private data class Fragment( produces = resolve(template.produces) + produces, ) - private fun resolve(set: Set): Set = set + private fun resolve(set: Set) = set.map { value -> + if (value.contains('$')) { + val ref = value.reference() + val name = ref.trim('$', '{', '}') + val replacement = fields[name] as? String ?: error("No field found for behaviour=$id ref=$ref") + value.replace(ref, replacement) + } else { + value + } + }.toSet() - private fun resolveRequirements(templated: List>>>, original: List>>>, requirePredicates: Boolean = true): List { + private fun resolveRequirements(templated: List>>>, original: List>>>): List { val combinedList = mutableListOf>>>() combinedList.addAll(original) for ((type, list) in templated) { @@ -283,7 +292,7 @@ private data class Fragment( return Condition.parse(combinedList, "$id template $template") } - private fun resolveActions(templated: List>>, original: List>>, requirePredicates: Boolean = true): List { + private fun resolveActions(templated: List>>, original: List>>): List { val combinedList = mutableListOf>>() combinedList.addAll(original) for ((type, map) in templated) { @@ -295,7 +304,7 @@ private data class Fragment( if (combinedList.isEmpty()) { return emptyList() } - return ActionParser.Companion.parse(combinedList, "$id template $template") + return ActionParser.parse(combinedList, "$id template $template") } @Suppress("UNCHECKED_CAST") diff --git a/game/src/main/kotlin/content/bot/behaviour/BehaviourState.kt b/game/src/main/kotlin/content/bot/behaviour/BehaviourState.kt index 77123599fc..65c17ba243 100644 --- a/game/src/main/kotlin/content/bot/behaviour/BehaviourState.kt +++ b/game/src/main/kotlin/content/bot/behaviour/BehaviourState.kt @@ -1,9 +1,19 @@ package content.bot.behaviour sealed interface BehaviourState { - object Pending : BehaviourState - object Running : BehaviourState - object Success : BehaviourState - data class Failed(val reason: Reason) : BehaviourState - data class Wait(var ticks: Int, val next: BehaviourState) : BehaviourState + object Pending : BehaviourState { + override fun toString() = "Pending" + } + object Running : BehaviourState { + override fun toString() = "Running" + } + object Success : BehaviourState { + override fun toString() = "Success" + } + data class Failed(val reason: Reason) : BehaviourState { + override fun toString() = "Failed($reason)" + } + data class Wait(var ticks: Int, val next: BehaviourState) : BehaviourState { + override fun toString() = "Wait($ticks, $next)" + } } diff --git a/game/src/main/kotlin/content/bot/behaviour/Condition.kt b/game/src/main/kotlin/content/bot/behaviour/Condition.kt index 93809a8457..5ae77c925f 100644 --- a/game/src/main/kotlin/content/bot/behaviour/Condition.kt +++ b/game/src/main/kotlin/content/bot/behaviour/Condition.kt @@ -230,7 +230,7 @@ sealed class Condition(val priority: Int) { var min = map["min"] as? Int val max = map["max"] as? Int if (min == null && max == null) { - min = 0 + min = 1 } val ids = toIds(id) items.add(Entry(ids, min, max)) @@ -245,13 +245,13 @@ sealed class Condition(val priority: Int) { val items = mutableMapOf() for (map in list) { for ((key, value) in map) { - val slot = EquipSlot.Companion.by(key) + val slot = EquipSlot.by(key) require(slot != EquipSlot.None) { "Invalid equipment slot: $key in $list" } value as? Map ?: error("Equipment $key expecting map, found: $value") val id = value["id"] as? String ?: error("Missing item id in $list") items[slot] = Entry( ids = toIds(id), - min = value["min"] as? Int ?: 0, + min = value["min"] as? Int ?: 1, max = value["max"] as? Int, ) } @@ -357,7 +357,7 @@ sealed class Condition(val priority: Int) { val map = list.single() if (map.containsKey("id")) { return SkillLevel( - skill = Skill.Companion.of((map["id"] as String).toPascalCase()) ?: error("Unknown skill: '${map["id"]}'"), + skill = Skill.of((map["id"] as String).toPascalCase()) ?: error("Unknown skill: '${map["id"]}'"), min = map["min"] as? Int, max = map["max"] as? Int, ) diff --git a/game/src/main/kotlin/content/bot/behaviour/Reason.kt b/game/src/main/kotlin/content/bot/behaviour/Reason.kt index 70b2a6a180..6e4e228809 100644 --- a/game/src/main/kotlin/content/bot/behaviour/Reason.kt +++ b/game/src/main/kotlin/content/bot/behaviour/Reason.kt @@ -1,13 +1,27 @@ package content.bot.behaviour interface Reason { - data class Invalid(val message: String) : HardReason - object Cancelled : HardReason - object NoRoute : HardReason - object Timeout : HardReason - object Stuck : SoftReason - object NoTarget : SoftReason - data class Requirement(val condition: Condition) : HardReason + data class Invalid(val message: String) : HardReason { + override fun toString() = "Invalid(\"$message\")" + } + object Cancelled : HardReason { + override fun toString() = "Cancelled" + } + object NoRoute : HardReason { + override fun toString() = "NoRoute" + } + object Timeout : HardReason { + override fun toString() = "Timeout" + } + object Stuck : SoftReason { + override fun toString() = "Stuck" + } + object NoTarget : SoftReason { + override fun toString() = "NoTarget" + } + data class Requirement(val condition: Condition) : HardReason { + override fun toString() = "Requirement(${condition})" + } } interface SoftReason : Reason interface HardReason : Reason diff --git a/game/src/main/kotlin/content/bot/behaviour/action/BotAction.kt b/game/src/main/kotlin/content/bot/behaviour/action/BotAction.kt index 18343394f0..8d7cb3a1c4 100644 --- a/game/src/main/kotlin/content/bot/behaviour/action/BotAction.kt +++ b/game/src/main/kotlin/content/bot/behaviour/action/BotAction.kt @@ -408,9 +408,14 @@ sealed interface BotAction { } val itemDef = if (item != null) ItemDefinitions.getOrNull(item) else null - val inv = InterfaceHandler.getInventory(bot.player, id, component, componentDef) + var inv = InterfaceHandler.getInventory(bot.player, id, component, componentDef) + if (inv != null && component == "sample") { + inv = "${inv}_sample" + } var itemSlot = if (item != null && inv != null) bot.player.inventories.inventory(inv).indexOf(item) else -1 + var itemId = itemDef?.id ?: -1 if (id == "shop") { + itemId = -1 itemSlot *= 6 } val valid = get().handle( @@ -418,13 +423,13 @@ sealed interface BotAction { InteractInterface( interfaceId = def.id, componentId = componentId, - itemId = itemDef?.id ?: -1, + itemId = itemId, itemSlot = itemSlot, option = index, ), ) return when { - !valid -> BehaviourState.Failed(Reason.Invalid("Invalid interaction: ${def.id}:$componentId:${itemDef?.id} slot $itemSlot option $index.")) + !valid -> BehaviourState.Failed(Reason.Invalid("Invalid interaction: ${def.id}:$componentId:${itemDef?.stringId} slot $itemSlot option $index.")) success == null -> BehaviourState.Wait(1, BehaviourState.Success) success.check(bot.player) -> BehaviourState.Success else -> BehaviourState.Running diff --git a/game/src/main/kotlin/content/bot/behaviour/setup/DynamicResolvers.kt b/game/src/main/kotlin/content/bot/behaviour/setup/DynamicResolvers.kt index b1545ba0a1..c4326d5f48 100644 --- a/game/src/main/kotlin/content/bot/behaviour/setup/DynamicResolvers.kt +++ b/game/src/main/kotlin/content/bot/behaviour/setup/DynamicResolvers.kt @@ -15,10 +15,10 @@ import world.gregs.voidps.network.login.protocol.visual.update.player.EquipSlot object DynamicResolvers { - fun ids() = setOf("withdraw_from_bank", "equip_from_bank") + fun ids() = setOf("withdraw_from_bank", "equip_from_bank", "go_to_area") fun resolver(player: Player, condition: Condition) = when (condition) { - is Condition.InArea -> Resolver("go_to_${condition.id}", -1, actions = listOf(BotAction.GoTo(condition.id))) + is Condition.InArea -> Resolver("go_to_area", -1, actions = listOf(BotAction.GoTo(condition.id))) is Condition.Equipment -> resolveEquipment(player, condition.items) is Condition.Inventory -> resolveInventory(player, condition.items) else -> null @@ -34,6 +34,7 @@ object DynamicResolvers { var found = false for (entry in items) { if (entry.ids.contains("empty")) { + found = true continue } val index = player.bank.items.indexOfFirst { valid(player, it, entry) } From cccc103333c77d2fc32e7628ae4e6f4591b3f8e3 Mon Sep 17 00:00:00 2001 From: GregHib Date: Tue, 10 Feb 2026 17:15:41 +0000 Subject: [PATCH 087/101] Formatting --- game/src/main/kotlin/content/bot/behaviour/Reason.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/game/src/main/kotlin/content/bot/behaviour/Reason.kt b/game/src/main/kotlin/content/bot/behaviour/Reason.kt index 6e4e228809..d785f574d1 100644 --- a/game/src/main/kotlin/content/bot/behaviour/Reason.kt +++ b/game/src/main/kotlin/content/bot/behaviour/Reason.kt @@ -20,7 +20,7 @@ interface Reason { override fun toString() = "NoTarget" } data class Requirement(val condition: Condition) : HardReason { - override fun toString() = "Requirement(${condition})" + override fun toString() = "Requirement($condition)" } } interface SoftReason : Reason From 628f404f42e20ecdc52300ab6e1b06ca10bddcab Mon Sep 17 00:00:00 2001 From: GregHib Date: Tue, 10 Feb 2026 17:27:22 +0000 Subject: [PATCH 088/101] Fix fishing timeout --- data/area/misthalin/lumbridge/lumbridge.bots.toml | 8 ++++++++ data/bot/fishing.templates.toml | 1 + game/src/main/kotlin/content/bot/behaviour/Behaviour.kt | 2 +- .../kotlin/content/bot/behaviour/activity/BotActivity.kt | 4 +++- .../main/kotlin/content/bot/behaviour/setup/Resolver.kt | 4 +++- 5 files changed, 16 insertions(+), 3 deletions(-) diff --git a/data/area/misthalin/lumbridge/lumbridge.bots.toml b/data/area/misthalin/lumbridge/lumbridge.bots.toml index 56febfbf1d..cfd4d31134 100644 --- a/data/area/misthalin/lumbridge/lumbridge.bots.toml +++ b/data/area/misthalin/lumbridge/lumbridge.bots.toml @@ -61,6 +61,14 @@ requires = [ { skill = { id = "fishing", min = 1, max = 5 } } ] +[lumbridge_net_fishing] +template = "fish_small_net" +capacity = 5 +fields = { location = "lumbridge_swamp_fishing_area", spot = "fishing_spot_small_net_bait_lumbridge" } +requires = [ + { skill = { id = "fishing", min = 1, max = 5 } } +] + # Fletching [lumbridge_fletch_arrow_shafts] template = "fletching_arrow_shafts_template" diff --git a/data/bot/fishing.templates.toml b/data/bot/fishing.templates.toml index 5540d09e8b..876507eff7 100644 --- a/data/bot/fishing.templates.toml +++ b/data/bot/fishing.templates.toml @@ -30,6 +30,7 @@ actions = [ ] produces = [ { item = "raw_shrimp" }, + { item = "raw_anchovies" }, { skill = "fishing" } ] diff --git a/game/src/main/kotlin/content/bot/behaviour/Behaviour.kt b/game/src/main/kotlin/content/bot/behaviour/Behaviour.kt index 97a77c16c4..e8e28c73eb 100644 --- a/game/src/main/kotlin/content/bot/behaviour/Behaviour.kt +++ b/game/src/main/kotlin/content/bot/behaviour/Behaviour.kt @@ -152,7 +152,7 @@ private fun load(paths: List, block: ConfigReader.(String, String?, Map< var template: String? = null var fields: Map? = null var value = 1 - var timeout = TimeUnit.SECONDS.toTicks(30) + var timeout = TimeUnit.MINUTES.toTicks(1) val requires = mutableListOf>>>() val setup = mutableListOf>>>() val actions = mutableListOf>>() diff --git a/game/src/main/kotlin/content/bot/behaviour/activity/BotActivity.kt b/game/src/main/kotlin/content/bot/behaviour/activity/BotActivity.kt index bbb05f359a..6e1591b1a7 100644 --- a/game/src/main/kotlin/content/bot/behaviour/activity/BotActivity.kt +++ b/game/src/main/kotlin/content/bot/behaviour/activity/BotActivity.kt @@ -3,6 +3,8 @@ package content.bot.behaviour.activity import content.bot.behaviour.Behaviour import content.bot.behaviour.Condition import content.bot.behaviour.action.BotAction +import world.gregs.voidps.engine.timer.toTicks +import java.util.concurrent.TimeUnit /** * An activity with a limited number of slots that bots can perform @@ -11,7 +13,7 @@ import content.bot.behaviour.action.BotAction data class BotActivity( override val id: String, val capacity: Int, - override val timeout: Int = 50, + override val timeout: Int = TimeUnit.MINUTES.toTicks(1), override val requires: List = emptyList(), override val setup: List = emptyList(), override val actions: List = emptyList(), diff --git a/game/src/main/kotlin/content/bot/behaviour/setup/Resolver.kt b/game/src/main/kotlin/content/bot/behaviour/setup/Resolver.kt index 87cd6f1d74..37a64a2c43 100644 --- a/game/src/main/kotlin/content/bot/behaviour/setup/Resolver.kt +++ b/game/src/main/kotlin/content/bot/behaviour/setup/Resolver.kt @@ -3,6 +3,8 @@ package content.bot.behaviour.setup import content.bot.behaviour.Behaviour import content.bot.behaviour.Condition import content.bot.behaviour.action.BotAction +import world.gregs.voidps.engine.timer.toTicks +import java.util.concurrent.TimeUnit /** * A behaviour that can be performed to resolve a requirement of another [Resolver] or [BotAction] @@ -12,7 +14,7 @@ import content.bot.behaviour.action.BotAction data class Resolver( override val id: String, val weight: Int, - override val timeout: Int = 50, + override val timeout: Int = TimeUnit.MINUTES.toTicks(1), override val requires: List = emptyList(), override val setup: List = emptyList(), override val actions: List = emptyList(), From af5d50739f5c53009c4beaafd4fa37643f8070b3 Mon Sep 17 00:00:00 2001 From: GregHib Date: Tue, 10 Feb 2026 23:39:18 +0000 Subject: [PATCH 089/101] Add dynamic shop buying resolvers --- .../al_kharid/al_kharid.npcs.toml | 1 + .../al_kharid/al_kharid.setups.toml | 28 --- .../misthalin/lumbridge/lumbridge.bots.toml | 10 +- .../misthalin/lumbridge/lumbridge.npcs.toml | 2 + .../misthalin/lumbridge/lumbridge.setups.toml | 89 ------- .../misthalin/varrock/varrock.bot-setups.toml | 58 ----- data/area/misthalin/varrock/varrock.npcs.toml | 3 + data/bot/fishing.templates.toml | 7 +- data/bot/shop.templates.toml | 81 ------- .../src/main/kotlin/content/bot/BotManager.kt | 2 +- .../src/main/kotlin/content/bot/BotUpdates.kt | 39 ++- .../content/bot/behaviour/action/BotAction.kt | 2 +- .../bot/behaviour/setup/DynamicResolvers.kt | 223 ++++++++++++------ .../content/entity/npc/shop/stock/Price.kt | 19 ++ 14 files changed, 228 insertions(+), 336 deletions(-) delete mode 100644 data/area/kharidian_desert/al_kharid/al_kharid.setups.toml delete mode 100644 data/area/misthalin/lumbridge/lumbridge.setups.toml delete mode 100644 data/area/misthalin/varrock/varrock.bot-setups.toml delete mode 100644 data/bot/shop.templates.toml diff --git a/data/area/kharidian_desert/al_kharid/al_kharid.npcs.toml b/data/area/kharidian_desert/al_kharid/al_kharid.npcs.toml index b7f7ab7d18..bcf6e7dbc2 100644 --- a/data/area/kharidian_desert/al_kharid/al_kharid.npcs.toml +++ b/data/area/kharidian_desert/al_kharid/al_kharid.npcs.toml @@ -18,6 +18,7 @@ examine = "Likes you more, the more you spend." id = 541 categories = ["human"] shop = "zekes_superior_scimitars" +area = "zekes_scimitar_shop" wander_range = 3 collision = "indoors" examine = "Sells superior scimitars." diff --git a/data/area/kharidian_desert/al_kharid/al_kharid.setups.toml b/data/area/kharidian_desert/al_kharid/al_kharid.setups.toml deleted file mode 100644 index c609763da9..0000000000 --- a/data/area/kharidian_desert/al_kharid/al_kharid.setups.toml +++ /dev/null @@ -1,28 +0,0 @@ -# Zekes Superior Scimitars -[buy_bronze_scimitar] -template = "buy_from_shop" -fields = { cost = 32, shop_location = "zekes_scimitar_shop", shopkeeper = "zeke", item = "bronze_scimitar" } -requires = [ - { skill = { id = "attack", min = 1 } }, -] - -[buy_iron_scimitar] -template = "buy_from_shop" -fields = { cost = 112, shop_location = "zekes_scimitar_shop", shopkeeper = "zeke", item = "iron_scimitar" } -requires = [ - { skill = { id = "attack", min = 1 } }, -] - -[buy_steel_scimitar] -template = "buy_from_shop" -fields = { cost = 400, shop_location = "zekes_scimitar_shop", shopkeeper = "zeke", item = "steel_scimitar" } -requires = [ - { skill = { id = "attack", min = 5 } }, -] - -[buy_mithril_scimitar] -template = "buy_from_shop" -fields = { cost = 1040, shop_location = "zekes_scimitar_shop", shopkeeper = "zeke", item = "mithril_scimitar" } -requires = [ - { skill = { id = "attack", min = 20 } }, -] diff --git a/data/area/misthalin/lumbridge/lumbridge.bots.toml b/data/area/misthalin/lumbridge/lumbridge.bots.toml index cfd4d31134..a8e88a6be8 100644 --- a/data/area/misthalin/lumbridge/lumbridge.bots.toml +++ b/data/area/misthalin/lumbridge/lumbridge.bots.toml @@ -66,7 +66,15 @@ template = "fish_small_net" capacity = 5 fields = { location = "lumbridge_swamp_fishing_area", spot = "fishing_spot_small_net_bait_lumbridge" } requires = [ - { skill = { id = "fishing", min = 1, max = 5 } } + { skill = { id = "fishing", min = 1, max = 10 } } +] + +[lumbridge_bait_fishing] +template = "fish_bait" +capacity = 3 +fields = { location = "lumbridge_river_fishing_area", spot = "fishing_spot_lure_bait_lumbridge" } +requires = [ + { skill = { id = "fishing", min = 25, max = 35 } } ] # Fletching diff --git a/data/area/misthalin/lumbridge/lumbridge.npcs.toml b/data/area/misthalin/lumbridge/lumbridge.npcs.toml index 9bfb7ff42e..f42746bf11 100644 --- a/data/area/misthalin/lumbridge/lumbridge.npcs.toml +++ b/data/area/misthalin/lumbridge/lumbridge.npcs.toml @@ -9,6 +9,7 @@ examine = "Servant of the Duke of Lumbridge." id = 519 categories = ["human"] shop = "bobs_brilliant_axes" +area = "bobs_axe_shop" collision = "indoors" examine = "An expert on axes." @@ -32,6 +33,7 @@ examine = "Helps sell stuff." id = 8864 categories = ["human"] shop = "lumbridge_fishing_supplies" +area = "hanks_fishing_shop" collision = "indoors" examine = "A product of a consumerist society." diff --git a/data/area/misthalin/lumbridge/lumbridge.setups.toml b/data/area/misthalin/lumbridge/lumbridge.setups.toml deleted file mode 100644 index 7e058dfff2..0000000000 --- a/data/area/misthalin/lumbridge/lumbridge.setups.toml +++ /dev/null @@ -1,89 +0,0 @@ -# Hanks fishing shop -[take_small_fishing_net_hank] -template = "take_shop_sample" -weight = 30 -fields = { shop_location = "hanks_fishing_shop", shopkeeper = "hank", item = "small_fishing_net" } - -[take_crayfish_cage_hank] -template = "take_shop_sample" -weight = 30 -fields = { shop_location = "hanks_fishing_shop", shopkeeper = "hank", item = "crayfish_cage" } - -[buy_small_fishing_net_hank] -template = "buy_from_shop" -weight = 35 -fields = { cost = 40, shop_location = "hanks_fishing_shop", shopkeeper = "hank", item = "small_fishing_net" } - -[buy_crayfish_cage_hank] -template = "buy_from_shop" -weight = 35 -fields = { cost = 20, shop_location = "hanks_fishing_shop", shopkeeper = "hank", item = "crayfish_cage" } - -[buy_fishing_bait_hank] -template = "buy_from_shop" -weight = 35 -fields = { cost = 3, shop_location = "hanks_fishing_shop", shopkeeper = "hank", item = "fishing_bait" } - -[buy_feather_hank] -template = "buy_from_shop" -weight = 35 -fields = { cost = 6, shop_location = "hanks_fishing_shop", shopkeeper = "hank", item = "feather" } - -[buy_fishing_rod_hank] -template = "buy_from_shop" -weight = 35 -fields = { cost = 5, shop_location = "hanks_fishing_shop", shopkeeper = "hank", item = "fishing_rod" } -requires = [ - { skill = { id = "fishing", min = 5 } }, -] - -[buy_fly_fishing_rod_hank] -template = "buy_from_shop" -weight = 35 -fields = { cost = 5, shop_location = "hanks_fishing_shop", shopkeeper = "hank", item = "fishing_rod" } -requires = [ - { skill = { id = "fishing", min = 20 } }, -] - -# Bobs Brilliant Axes -[take_bronze_hatchet_bobs] -template = "take_shop_sample" -weight = 30 -fields = { shop_location = "bobs_axe_shop", shopkeeper = "bob", item = "bronze_hatchet" } - -[take_bronze_pickaxe_bobs] -template = "take_shop_sample" -weight = 30 -fields = { shop_location = "bobs_axe_shop", shopkeeper = "bob", item = "bronze_pickaxe" } - -[buy_bronze_pickaxe] -template = "buy_from_shop" -weight = 35 -fields = { cost = 1, shop_location = "bobs_axe_shop", shopkeeper = "bob", item = "bronze_pickaxe" } -requires = [ - { skill = { id = "mining", min = 1 } }, -] - -[buy_bronze_hatchet] -template = "buy_from_shop" -weight = 35 -fields = { cost = 16, shop_location = "bobs_axe_shop", shopkeeper = "bob", item = "bronze_hatchet" } -requires = [ - { skill = { id = "woodcutting", min = 1 } }, -] - -[buy_iron_hatchet] -template = "buy_from_shop" -weight = 45 -fields = { cost = 56, shop_location = "bobs_axe_shop", shopkeeper = "bob", item = "iron_hatchet" } -requires = [ - { skill = { id = "woodcutting", min = 1 } }, -] - -[buy_steel_hatchet] -template = "buy_from_shop" -weight = 50 -fields = { cost = 200, shop_location = "bobs_axe_shop", shopkeeper = "bob", item = "steel_hatchet" } -requires = [ - { skill = { id = "woodcutting", min = 6 } }, -] diff --git a/data/area/misthalin/varrock/varrock.bot-setups.toml b/data/area/misthalin/varrock/varrock.bot-setups.toml deleted file mode 100644 index 263e4c7e2f..0000000000 --- a/data/area/misthalin/varrock/varrock.bot-setups.toml +++ /dev/null @@ -1,58 +0,0 @@ -# Varrock sword shop - -[take_bronze_sword] -template = "take_shop_sample" -type = "resolver" -weight = 30 -fields = { shop_location = "varrock_sword_shop", shopkeeper = "sword_shop*,dealga", item = "bronze_sword" } - -[buy_bronze_sword] -template = "buy_from_shop" -type = "resolver" -weight = 35 -fields = { cost = 26, shop_location = "varrock_sword_shop", shopkeeper = "sword_shop*,dealga", item = "bronze_sword" } - -[buy_iron_sword] -template = "buy_from_shop" -type = "resolver" -weight = 35 -fields = { cost = 91, shop_location = "varrock_sword_shop", shopkeeper = "sword_shop*,dealga", item = "iron_sword" } - -[buy_steel_sword] -template = "buy_from_shop" -type = "resolver" -weight = 35 -fields = { cost = 325, shop_location = "varrock_sword_shop", shopkeeper = "sword_shop*,dealga", item = "steel_sword" } -requires = [ - { skill = { id = "attack", min = 5 } }, -] - -[buy_black_sword] -template = "buy_from_shop" -type = "resolver" -weight = 35 -fields = { cost = 624, shop_location = "varrock_sword_shop", shopkeeper = "sword_shop*,dealga", item = "black_sword" } -requires = [ - { skill = { id = "attack", min = 10 } }, -] - -[buy_bronze_dagger] -template = "buy_from_shop" -type = "resolver" -weight = 35 -fields = { cost = 10, shop_location = "varrock_sword_shop", shopkeeper = "sword_shop*,dealga", item = "bronze_dagger" } - -[buy_iron_dagger] -template = "buy_from_shop" -type = "resolver" -weight = 35 -fields = { cost = 35, shop_location = "varrock_sword_shop", shopkeeper = "sword_shop*,dealga", item = "iron_dagger" } - -[buy_steel_dagger] -template = "buy_from_shop" -type = "resolver" -weight = 35 -fields = { cost = 125, shop_location = "varrock_sword_shop", shopkeeper = "sword_shop*,dealga", item = "steel_dagger" } -requires = [ - { skill = { id = "attack", min = 5 } }, -] diff --git a/data/area/misthalin/varrock/varrock.npcs.toml b/data/area/misthalin/varrock/varrock.npcs.toml index f445aa4752..24f790a83e 100644 --- a/data/area/misthalin/varrock/varrock.npcs.toml +++ b/data/area/misthalin/varrock/varrock.npcs.toml @@ -221,6 +221,7 @@ examine = "She looks happy." [dealga] id = 11475 shop = "dealgas_scimitar_emporium" +area = "varrock_sword_shop" collision = "indoors" examine = "A shrewd-looking monkey salesman." @@ -644,11 +645,13 @@ examine = "Sells arrows." [sword_shopkeeper_varrock] id = 551 shop = "varrock_sword_shop" +area = "varrock_sword_shop" examine = "Ironically, makes a living from swords." [sword_shop_assistant_varrock] id = 552 shop = "varrock_sword_shop" +area = "varrock_sword_shop" examine = "Helps the shopkeeper sell swords." [cook_blue_moon_inn] diff --git a/data/bot/fishing.templates.toml b/data/bot/fishing.templates.toml index 876507eff7..54ed9ba3c9 100644 --- a/data/bot/fishing.templates.toml +++ b/data/bot/fishing.templates.toml @@ -37,13 +37,14 @@ produces = [ [fish_bait] setup = [ { inventory = [ - { id = "small_fishing_net" }, - { id = "empty", min = 27 } + { id = "fishing_rod" }, + { id = "feather", min = 52 }, + { id = "empty", min = 25 } ] }, { area = { id = "$location" } }, ] actions = [ - { npc = { option = "Net", id = "$spot", delay = 5, success = { inventory = { id = "empty", max = 0 } } } }, + { npc = { option = "Bait", id = "$spot", delay = 5, success = { inventory = { id = "empty", max = 0 } } } }, ] produces = [ { skill = "fishing" }, diff --git a/data/bot/shop.templates.toml b/data/bot/shop.templates.toml deleted file mode 100644 index 9dac5516bc..0000000000 --- a/data/bot/shop.templates.toml +++ /dev/null @@ -1,81 +0,0 @@ -[buy_from_shop] -setup = [ - { inventory = [{ id = "coins", min = "$cost" }, { id = "empty", min = 1 }] }, - { area = { id = "$shop_location" } }, -] -actions = [ - { npc = { option = "Trade", id = "$shopkeeper", success = { interface_open = { id = "shop" } } } }, - { interface = { option = "Buy-1", id = "shop:stock:$item", success = { inventory = [{ id = "$item" }] } } }, -] -produces = [ - { item = "$item" } -] - -[take_shop_sample] -setup = [ - { inventory = [{ id = "empty", min = 1 }] }, - { area = { id = "$shop_location" } }, -] -actions = [ - { npc = { option = "Trade", id = "$shopkeeper", success = { interface_open = { id = "shop" } } } }, - { interface = { option = "Take-1", id = "shop:sample:$item", success = { inventory = [{ id = "$item" }] } } }, -] -produces = [ - { item = "$item" } -] - -[sell_50_to_shop] -setup = [ - { inventory = [{ id = "$item", min = "50" }, { id = "empty", min = 1 }] }, - { area = { id = "$shop_location" } }, -] -actions = [ - { npc = { option = "Trade", id = "$shopkeeper", success = { interface_open = { id = "shop" } } } }, - { interface = { option = "Sell-50", id = "shop_side:inventory:$item", success = { inventory = [{ id = "coins" }] } } }, - { interface_close = { id = "shop" } } -] -produces = [ - { item = "coins" } -] - -[sell_10_to_shop] -setup = [ - { inventory = [{ id = "$item", min = "10" }, { id = "empty", min = 1 }] }, - { area = { id = "$shop_location" } }, -] -actions = [ - { npc = { option = "Trade", id = "$shopkeeper", success = { interface_open = { id = "shop" } } } }, - { interface = { option = "Sell-10", id = "shop_side:inventory:$item", success = { inventory = [{ id = "coins" }] } } }, - { interface_close = { id = "shop" } } -] -produces = [ - { item = "coins" } -] - -[sell_5_to_shop] -setup = [ - { inventory = [{ id = "$item", min = "5" }, { id = "empty", min = 1 }] }, - { area = { id = "$shop_location" } }, -] -actions = [ - { npc = { option = "Trade", id = "$shopkeeper", success = { interface_open = { id = "shop" } } } }, - { interface = { option = "Sell-5", id = "shop_side:inventory:$item", success = { inventory = [{ id = "coins" }] } } }, - { interface_close = { id = "shop" } } -] -produces = [ - { item = "coins" } -] - -[sell_to_shop] -setup = [ - { inventory = [{ id = "$item", min = "1" }, { id = "empty", min = 1 }] }, - { area = { id = "$shop_location" } }, -] -actions = [ - { npc = { option = "Trade", id = "$shopkeeper", success = { interface_open = { id = "shop" } } } }, - { interface = { option = "Sell-1", id = "shop_side:inventory:$item", success = { inventory = [{ id = "coins" }] } } }, - { interface_close = { id = "shop" } } -] -produces = [ - { item = "coins" } -] diff --git a/game/src/main/kotlin/content/bot/BotManager.kt b/game/src/main/kotlin/content/bot/BotManager.kt index f1522f51c2..e6e45502ac 100644 --- a/game/src/main/kotlin/content/bot/BotManager.kt +++ b/game/src/main/kotlin/content/bot/BotManager.kt @@ -28,7 +28,7 @@ import java.util.concurrent.TimeUnit */ class BotManager( private val activities: MutableMap = mutableMapOf(), - private val resolvers: MutableMap> = mutableMapOf(), + val resolvers: MutableMap> = mutableMapOf(), private val groups: MutableMap> = mutableMapOf(), ) : Runnable { internal val slots = ActivitySlots() diff --git a/game/src/main/kotlin/content/bot/BotUpdates.kt b/game/src/main/kotlin/content/bot/BotUpdates.kt index bba0df0f43..4cc0599d72 100644 --- a/game/src/main/kotlin/content/bot/BotUpdates.kt +++ b/game/src/main/kotlin/content/bot/BotUpdates.kt @@ -1,14 +1,25 @@ package content.bot import com.github.michaelbull.logging.InlineLogger +import content.bot.behaviour.navigation.NavigationGraph +import content.bot.behaviour.setup.DynamicResolvers +import world.gregs.voidps.cache.config.data.InventoryDefinition import world.gregs.voidps.engine.Script +import world.gregs.voidps.engine.data.definition.Areas +import world.gregs.voidps.engine.data.definition.InventoryDefinitions +import world.gregs.voidps.engine.data.definition.ItemDefinitions +import world.gregs.voidps.engine.entity.character.npc.NPC import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.network.client.instruction.InteractDialogue +import world.gregs.voidps.type.Tile /** * Listen for state changes which would change which activities are available to a bot */ -class BotUpdates : Script { +class BotUpdates( + val inventoryDefinitions: InventoryDefinitions, + val graph: NavigationGraph, +) : Script { val logger = InlineLogger() init { @@ -55,6 +66,17 @@ class BotUpdates : Script { instructions.trySend(InteractDialogue(interfaceId = 740, componentId = 3, option = -1)) } } + // Register shops + + worldSpawn { + DynamicResolvers.shopItems.clear() + } + npcSpawn { + if (def.contains("shop")) { + val def = inventoryDefinitions.get(def.get("shop")) + registerShop(this, def) + } + } } /** @@ -70,4 +92,19 @@ class BotUpdates : Script { frame.timeout = 0 } } + + private fun registerShop(npc: NPC, definition: InventoryDefinition) { + val area: String = npc.def.getOrNull("area") ?: return + val ids = definition.ids ?: return + val amounts = definition.amounts ?: return + for (index in ids.indices) { + val id = ids[index] + val amount = amounts[index] + if (amount <= 0) { + continue + } + val def = ItemDefinitions.getOrNull(id) ?: continue + DynamicResolvers.shopItems.getOrPut(def.stringId) { mutableListOf() }.add(area to npc.id) + } + } } diff --git a/game/src/main/kotlin/content/bot/behaviour/action/BotAction.kt b/game/src/main/kotlin/content/bot/behaviour/action/BotAction.kt index 8d7cb3a1c4..3797509d17 100644 --- a/game/src/main/kotlin/content/bot/behaviour/action/BotAction.kt +++ b/game/src/main/kotlin/content/bot/behaviour/action/BotAction.kt @@ -103,7 +103,7 @@ sealed interface BotAction { override fun start(bot: Bot, frame: BehaviourFrame) = inArea(bot) ?: BehaviourState.Running override fun update(bot: Bot, frame: BehaviourFrame): BehaviourState { - if (bot.steps.isNotEmpty() || bot.mode != EmptyMode) { + if (bot.mode != EmptyMode) { return BehaviourState.Running } val state = inArea(bot) diff --git a/game/src/main/kotlin/content/bot/behaviour/setup/DynamicResolvers.kt b/game/src/main/kotlin/content/bot/behaviour/setup/DynamicResolvers.kt index c4326d5f48..3f0b85a5d3 100644 --- a/game/src/main/kotlin/content/bot/behaviour/setup/DynamicResolvers.kt +++ b/game/src/main/kotlin/content/bot/behaviour/setup/DynamicResolvers.kt @@ -3,7 +3,9 @@ package content.bot.behaviour.setup import content.bot.behaviour.Condition import content.bot.behaviour.Condition.Entry import content.bot.behaviour.action.BotAction +import content.entity.npc.shop.stock.Price import content.entity.player.bank.bank +import content.entity.player.bank.ownsItem import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.entity.character.player.equip.equipped import world.gregs.voidps.engine.entity.character.player.skill.level.Level.hasRequirements @@ -15,9 +17,12 @@ import world.gregs.voidps.network.login.protocol.visual.update.player.EquipSlot object DynamicResolvers { - fun ids() = setOf("withdraw_from_bank", "equip_from_bank", "go_to_area") + fun ids() = setOf("withdraw_from_bank", "equip_from_bank", "go_to_area", "buy_from_shop") - fun resolver(player: Player, condition: Condition) = when (condition) { + // location, npc + val shopItems = mutableMapOf>>() + + fun resolver(player: Player, condition: Condition): Resolver? = when (condition) { is Condition.InArea -> Resolver("go_to_area", -1, actions = listOf(BotAction.GoTo(condition.id))) is Condition.Equipment -> resolveEquipment(player, condition.items) is Condition.Inventory -> resolveInventory(player, condition.items) @@ -25,31 +30,55 @@ object DynamicResolvers { } private fun resolveInventory(player: Player, items: List): Resolver? { - val actions = mutableListOf() - val items = items.toMutableList() - actions.add(BotAction.GoToNearest("bank")) - actions.add(BotAction.InteractObject("Use-quickly", "bank_booth*", success = Condition.InterfaceOpen("bank"))) - // Free up inventory space - actions.add(BotAction.InterfaceOption("Deposit carried items", "bank:carried", success = Condition.Inventory(listOf(Entry(setOf("empty"), min = 28))))) - var found = false + var resolver = withdraw(player, items) + if (resolver != null) { + return resolver + } + resolver = buyItems(player, items) + if (resolver != null) { + return resolver + } + return null + } + + private fun buyItems(player: Player, items: List): Resolver? { for (entry in items) { - if (entry.ids.contains("empty")) { - found = true + val amount = entry.min ?: 1 + if (entry.ids.any { id -> player.inventory.contains(id, amount) }) { continue } - val index = player.bank.items.indexOfFirst { valid(player, it, entry) } - if (index == -1) { - // No valid items in the bank - return null + for (id in entry.ids) { + for ((location, npc) in shopItems[id] ?: continue) { + val actions = mutableListOf() + val price = Price.of(id) + if (!player.ownsItem("coins", price * amount)) { + continue + } + actions.add(BotAction.GoTo(location)) + actions.add(BotAction.InteractNpc("Trade", npc, success = Condition.InterfaceOpen("shop"))) + var remaining = amount + while (remaining > 0) { + val amount = when { + remaining >= 500 -> 500 + remaining >= 50 -> 50 + remaining >= 10 -> 10 + remaining >= 5 -> 5 + else -> 1 + } + actions.add(BotAction.InterfaceOption("Buy-${amount}", "shop:stock:${id}")) + remaining -= amount + } + actions.add(BotAction.CloseInterface) + val spaces = if (player.inventory.stackable(id)) 1 else amount + return Resolver( + id = "buy_from_shop", + weight = 25, + setup = listOf(Condition.Inventory(listOf(Entry(setOf("coins"), min = price * amount), Entry(setOf("empty"), min = spaces)))), + actions = actions, + produces = setOf("item:${id}") + ) + } } - val item = player.bank.items[index] - // Withdraw the necessary item - withdraw(actions, entry, item) - found = true - } - actions.add(BotAction.CloseInterface) - if (found) { - return Resolver("withdraw_from_bank", weight = 20, actions = actions) } return null } @@ -71,22 +100,97 @@ object DynamicResolvers { } private fun resolveEquipment(player: Player, equipment: Map): Resolver? { - val actions = mutableListOf() val equipment = equipment.toMutableMap() + var resolver = unequipItems(player, equipment) + if (resolver != null) { + return resolver + } + resolver = equipItems(player, equipment) + if (resolver == null) { + return resolver + } + resolver = withdrawEquip(player, equipment) + if (resolver != null) { + return resolver + } + resolver = buyItems(player, equipment.values.toList()) + if (resolver != null) { + return resolver + } + return null + } - // Unequip items - for ((slot, entry) in equipment) { - if (!entry.ids.contains("empty")) { + private fun withdraw(player: Player, items: List): Resolver? { + val actions = mutableListOf() + actions.add(BotAction.GoToNearest("bank")) + actions.add(BotAction.InteractObject("Use-quickly", "bank_booth*", success = Condition.InterfaceOpen("bank"))) + actions.add(BotAction.InterfaceOption("Deposit carried items", "bank:carried", success = Condition.Inventory(listOf(Entry(setOf("empty"), min = 28))))) + var found = false + for (entry in items) { + val item = player.bank.items.firstOrNull { item -> valid(player, item, entry) } + if (item == null) { continue } - val item = player.equipped(slot) - if (item.isNotEmpty()) { - actions.add(BotAction.InterfaceOption("Remove", "worn_equipment:${slot.name.lowercase()}_slot:$item")) + withdraw(actions, entry, item) + found = true + } + if (found) { + actions.add(BotAction.CloseInterface) + return Resolver("withdraw_from_bank", weight = 20, actions = actions) + } + return null + } + + private fun withdrawEquip(player: Player, equipment: Map): Resolver? { + val actions = mutableListOf() + actions.add(BotAction.GoToNearest("bank")) + actions.add(BotAction.InteractObject("Use-quickly", "bank_booth*", success = Condition.InterfaceOpen("bank"))) + // Free up inventory space if needed + if (player.inventory.spaces < equipment.size) { + actions.add(BotAction.InterfaceOption("Deposit carried items", "bank:carried", success = Condition.Inventory(listOf(Entry(setOf("empty"), min = 28))))) + } + var found = false + val toEquip = mutableSetOf() + for (item in player.bank.items) { + if (item.isEmpty()) { + continue + } + val slot = item.slot + if (slot == EquipSlot.None) { + continue + } + if (equipment.isEmpty()) { + break + } + // Check if item meets requirement + val entry = equipment[slot] ?: continue + if (!entry.ids.contains(item.id)) { + continue + } + if (entry.usable && !player.hasRequirementsToUse(item)) { + continue } - equipment.remove(slot) + if (entry.equippable && !player.hasRequirements(item)) { + continue + } + // Withdraw the necessary item + withdraw(actions, entry, item) + toEquip.add(item.id) + found = true + } + actions.add(BotAction.CloseInterface) + // Equip all items + for (id in toEquip) { + actions.add(BotAction.InterfaceOption("Equip", "inventory:inventory:$id")) + } + if (found) { + return Resolver("equip_from_bank", weight = 20, actions = actions) } + return null + } - // Equip any held items + private fun equipItems(player: Player, equipment: Map): Resolver? { + val actions = mutableListOf() for (item in player.inventory.items) { val slot = item.slot if (slot == EquipSlot.None) { @@ -97,53 +201,26 @@ object DynamicResolvers { continue } actions.add(BotAction.InterfaceOption("Equip", "inventory:inventory:${item.id}")) - equipment.remove(slot) } + if (actions.isNotEmpty()) { + Resolver("equip_items", weight = 15, actions = actions) + } + return null + } - // Grab anything else from the bank - if (equipment.isNotEmpty()) { - actions.add(BotAction.GoToNearest("bank")) - actions.add(BotAction.InteractObject("Use-quickly", "bank_booth*", success = Condition.InterfaceOpen("bank"))) - // Free up inventory space if needed - if (player.inventory.spaces < equipment.size) { - actions.add(BotAction.InterfaceOption("Deposit carried items", "bank:carried", success = Condition.Inventory(listOf(Entry(setOf("empty"), min = 28))))) - } - val toEquip = mutableSetOf() - for (item in player.bank.items) { - if (item.isEmpty()) { - continue - } - val slot = item.slot - if (slot == EquipSlot.None) { - continue - } - if (equipment.isEmpty()) { - break - } - // Check if item meets requirement - val entry = equipment[slot] ?: continue - if (!entry.ids.contains(item.id)) { - continue - } - if (entry.usable && !player.hasRequirementsToUse(item)) { - continue - } - if (entry.equippable && !player.hasRequirements(item)) { - continue - } - // Withdraw the necessary item - withdraw(actions, entry, item) - toEquip.add(item.id) - equipment.remove(slot) + private fun unequipItems(player: Player, equipment: Map): Resolver? { + val actions = mutableListOf() + for ((slot, entry) in equipment) { + if (!entry.ids.contains("empty")) { + continue } - actions.add(BotAction.CloseInterface) - // Equip all items - for (id in toEquip) { - actions.add(BotAction.InterfaceOption("Equip", "inventory:inventory:$id")) + val item = player.equipped(slot) + if (item.isNotEmpty()) { + actions.add(BotAction.InterfaceOption("Remove", "worn_equipment:${slot.name.lowercase()}_slot:$item")) } } if (actions.isNotEmpty()) { - return Resolver("equip_from_bank", weight = 20, actions = actions) + return Resolver("unequip_items", weight = 10, actions = actions) } return null } diff --git a/game/src/main/kotlin/content/entity/npc/shop/stock/Price.kt b/game/src/main/kotlin/content/entity/npc/shop/stock/Price.kt index 1afede665d..c82aa0f465 100644 --- a/game/src/main/kotlin/content/entity/npc/shop/stock/Price.kt +++ b/game/src/main/kotlin/content/entity/npc/shop/stock/Price.kt @@ -53,4 +53,23 @@ object Price { } return max(price, 1) } + + fun of(item: String, currency: String = "coins"): Int { + val itemId = getRealItem(item) + val enums = get() + var price = enums.get("price_runes").getInt(itemId) + if (currency == "tokkul" && price != -1 && price > 0) { + return price + } + price = enums.get("price_garden").getInt(itemId) + if (price != -1 && price > 0) { + return price + } + val def = ItemDefinitions.get(itemId) + if (def.contains("skill_cape") || def.contains("skill_cape_t")) { + return 99000 + } + price = def.cost + return max(price, 1) + } } From 56c14323db2c4047f24049ced39df5dc6ba5556b Mon Sep 17 00:00:00 2001 From: GregHib Date: Wed, 11 Feb 2026 13:08:44 +0000 Subject: [PATCH 090/101] Add lumbridge combat tutor bots, fix lots of conditions and resolvers --- .../misthalin/lumbridge/lumbridge.bots.toml | 74 ++++++++++ .../misthalin/lumbridge/lumbridge.setups.toml | 59 ++++++++ data/bot/lumbridge.nav-edges.toml | 6 +- .../handle/DialogueContinueHandler.kt | 4 +- .../lumbridge/combat_hall/ArcheryTarget.kt | 4 +- .../main/kotlin/content/bot/BotCommands.kt | 6 + .../src/main/kotlin/content/bot/BotManager.kt | 2 +- .../src/main/kotlin/content/bot/BotUpdates.kt | 1 + .../kotlin/content/bot/behaviour/Condition.kt | 127 +++++++++++++++++- .../bot/behaviour/action/ActionParser.kt | 23 +++- .../content/bot/behaviour/action/BotAction.kt | 2 +- .../bot/behaviour/setup/DynamicResolvers.kt | 60 +-------- 12 files changed, 299 insertions(+), 69 deletions(-) create mode 100644 data/area/misthalin/lumbridge/lumbridge.setups.toml diff --git a/data/area/misthalin/lumbridge/lumbridge.bots.toml b/data/area/misthalin/lumbridge/lumbridge.bots.toml index a8e88a6be8..e14cbcf224 100644 --- a/data/area/misthalin/lumbridge/lumbridge.bots.toml +++ b/data/area/misthalin/lumbridge/lumbridge.bots.toml @@ -1,3 +1,4 @@ +# Combat [kill_freds_chickens_attack] template = "kill_chickens" capacity = 2 @@ -43,6 +44,79 @@ template = "kill_cows" capacity = 2 fields = { skill = "defence", location = "lumbridge_west_cow_field", style = "style4" } +# Tutors +[magic_tutor] +capacity = 2 +requires = [ + { skill = { id = "magic", min = 1, max = 5 } }, + { area = { id = "lumbridge_combat_tutors" } } +] +setup = [ + { inventory = [ + { id = "air_rune", min = 30 }, + { id = "mind_rune", min = 30 }, + ] }, +] +actions = [ + { interface = { option = "Autocast", id = "modern_spellbook:wind_strike", success = { variable = { id = "autocast", min = 0, default = -1 } } } }, + { npc = { option = "Attack", id = "magic_dummy", success = { inventory = { id = "empty", min = 28 } } } } +] +produces = [ + { skill = "magic" } +] + +[melee_tutor_attack] +capacity = 1 +requires = [ + { skill = { id = "attack", min = 1, max = 5 } }, + { area = { id = "lumbridge_combat_tutors" } } +] +setup = [ + { equipment = { weapon = { id = "training_sword" }, shield = { id = "training_shield" } } }, +] +actions = [ + { interface = { option = "Select", id = "combat_styles:style1" } }, + { npc = { option = "Attack", id = "melee_dummy", success = { skill = { id = "attack", min = 5 } } } } +] +produces = [ + { skill = "attack" } +] + +[melee_tutor_strength] +capacity = 1 +requires = [ + { skill = { id = "strength", min = 1, max = 5 } }, + { area = { id = "lumbridge_combat_tutors" } } +] +setup = [ + { equipment = { weapon = { id = "training_sword" }, shield = { id = "training_shield" } } }, +] +actions = [ + { interface = { option = "Select", id = "combat_styles:style2" } }, + { npc = { option = "Attack", id = "melee_dummy", success = { skill = { id = "strength", min = 5 } } } } +] +produces = [ + { skill = "strength" } +] + +[ranged_tutor] +capacity = 1 +requires = [ + { skill = { id = "ranged", min = 1, max = 5 } }, + { area = { id = "lumbridge_combat_tutors" } } +] +setup = [ + { equipment = { weapon = { id = "training_bow" }, ammo = { id = "training_arrows", min = 10 } } }, +] +actions = [ + { interface = { option = "Select", id = "combat_styles:style1" } }, + { object = { option = "Shoot-at", id = "archery_target", success = { queue = { id = "archery" } } } }, + { restart = { wait_if = [{ queue = { id = "archery" } }, { mode = { id = "player_on_object" } }], success = { equipment = { ammo = { id = "empty" } } } } } +] +produces = [ + { skill = "ranged" } +] + # Thieving [lumbridge_pickpocketing] template = "pickpocketting_template" diff --git a/data/area/misthalin/lumbridge/lumbridge.setups.toml b/data/area/misthalin/lumbridge/lumbridge.setups.toml new file mode 100644 index 0000000000..9f34949c5e --- /dev/null +++ b/data/area/misthalin/lumbridge/lumbridge.setups.toml @@ -0,0 +1,59 @@ +[mikasi_runes] +requires = [ + { clock = { id = "claimed_tutor_consumables", max = 0, seconds = true } }, + { bank = [ + { id = "air_rune", max = 0 }, + { id = "mind_rune", max = 0 }, + ] }, + { area = { id = "lumbridge_combat_tutors" } } +] +actions = [ + { npc = { option = "Talk-to", id = "mikasi", delay = 5, success = { interface_open = { id = "dialogue_npc_chat3" } } } }, + { continue = { id = "dialogue_npc_chat3:continue", success = { interface_open = { id = "dialogue_multi4" } } } }, + { continue = { id = "dialogue_multi4:line3", success = { interface_open = { id = "dialogue_obj_box" } } } }, + { continue = { id = "dialogue_obj_box:continue", success = { inventory = { id = "air_rune", min = 30 } } } }, + { continue = { id = "dialogue_obj_box:continue", success = { inventory = { id = "mind_rune", min = 30 } } } }, +] +produces = [ + { item = "air_rune" }, + { item = "mind_rune" } +] + +[harlan_sword] +requires = [ + { clock = { id = "claimed_tutor_consumables", max = 0, seconds = true } }, + { owns = { id = "training_sword", max = 0 } }, + { owns = { id = "training_shield", max = 0 } }, + { area = { id = "lumbridge_combat_tutors" } } +] +actions = [ + { npc = { option = "Talk-to", id = "harlan", delay = 5, success = { interface_open = { id = "dialogue_npc_chat2" } } } }, + { continue = { id = "dialogue_npc_chat2:continue", success = { interface_open = { id = "dialogue_multi5" } } } }, + { continue = { id = "dialogue_multi5:line4", success = { interface_open = { id = "dialogue_chat1" } } } }, + { continue = { id = "dialogue_chat1:continue", success = { interface_open = { id = "dialogue_obj_box" } } } }, + { continue = { id = "dialogue_obj_box:continue", success = { inventory = { id = "training_sword", min = 1 } } } }, + { continue = { id = "dialogue_obj_box:continue", success = { inventory = { id = "training_shield", min = 1 } } } }, +] +produces = [ + { item = "training_sword" }, + { item = "training_shield" } +] + +[nemarti_bow] +requires = [ + { clock = { id = "claimed_tutor_consumables", max = 0, seconds = true } }, + { owns = { id = "training_bow", max = 0 } }, + { owns = { id = "training_arrows", max = 0 } }, + { area = { id = "lumbridge_combat_tutors" } } +] +actions = [ + { npc = { option = "Talk-to", id = "nemarti", delay = 5, success = { interface_open = { id = "dialogue_npc_chat2" } } } }, + { continue = { id = "dialogue_npc_chat2:continue", success = { interface_open = { id = "dialogue_multi4" } } } }, + { continue = { id = "dialogue_multi4:line3", success = { interface_open = { id = "dialogue_obj_box" } } } }, + { continue = { id = "dialogue_obj_box:continue", success = { inventory = { id = "training_bow", min = 1 } } } }, + { continue = { id = "dialogue_obj_box:continue", success = { inventory = { id = "training_arrows", min = 30 } } } }, +] +produces = [ + { item = "training_bow" }, + { item = "training_arrows" } +] \ No newline at end of file diff --git a/data/bot/lumbridge.nav-edges.toml b/data/bot/lumbridge.nav-edges.toml index d555d6118e..e97deb68e5 100644 --- a/data/bot/lumbridge.nav-edges.toml +++ b/data/bot/lumbridge.nav-edges.toml @@ -23,9 +23,9 @@ edges = [ { from = { x = 3205, y = 3209, level = 1 }, to = { x = 3205, y = 3209, level = 2 }, cost = 1, actions = [{ object = { option = "Climb-up", id = "lumbridge_castle_staircase_south_middle", x = 3204, y = 3207, success = { tile = { level = 2 } } } }] }, # lumbridge_castle_1st_floor_south_stairs_to_2nd_floor { from = { x = 3209, y = 3205 }, to = { x = 3199, y = 3218 } }, # lumbridge_castle_grounds_south_to_tower_west { from = { x = 3199, y = 3218 }, to = { x = 3184, y = 3225 } }, # lumbridge_castle_tower_west_to_yew_trees - { from = { x = 3199, y = 3218 }, to = { x = 3193, y = 3236 } }, # lumbridge_castle_tower_west_to_tree_patch + { from = { x = 3199, y = 3218 }, to = { x = 3195, y = 3236 } }, # lumbridge_castle_tower_west_to_tree_patch { from = { x = 3184, y = 3225 }, to = { x = 3168, y = 3221 } }, # lumbridge_castle_yew_trees_to_yew_trees_west - { from = { x = 3184, y = 3225 }, to = { x = 3193, y = 3236 } }, # lumbridge_castle_yew_trees_to_tree_patch + { from = { x = 3184, y = 3225 }, to = { x = 3195, y = 3236 } }, # lumbridge_castle_yew_trees_to_tree_patch { from = { x = 3226, y = 3214 }, to = { x = 3227, y = 3214 }, cost = 3, actions = [{ object = { option = "Open", id = "door_627_closed", x = 3226, y = 3214, success = { object = { id = "door_627_opened", x = 3227, y = 3214 } } } }] }, # lumbridge_south_tower_to_ground_floor { from = { x = 3227, y = 3214 }, to = { x = 3229, y = 3214, level = 1 }, cost = 1, actions = [{ object = { option = "Climb-up", id = "36768", x = 3229, y = 3213, success = { tile = { level = 1 } } } }] }, # lumbridge_south_tower_ground_floor_to_1st_floor { from = { x = 3229, y = 3214, level = 1 }, to = { x = 3229, y = 3214, level = 2 }, cost = 1, actions = [{ object = { option = "Climb-up", id = "36769", x = 3229, y = 3213, success = { tile = { level = 2 } } } }] }, # lumbridge_south_tower_1st_floor_to_2nd_floor @@ -91,7 +91,7 @@ edges = [ { from = { x = 3217, y = 3241 }, to = { x = 3204, y = 3247 } }, # lumbridge_general_store_to_task_building { from = { x = 3204, y = 3247 }, to = { x = 3194, y = 3247 } }, # lumbridge_task_building_to_fishing_shop_entrance { from = { x = 3194, y = 3247 }, to = { x = 3172, y = 3239 } }, # lumbridge_fishing_shop_entrance_to_west_path - { from = { x = 3194, y = 3247 }, to = { x = 3193, y = 3236 } }, # lumbridge_fishing_shop_entrance_to_tree_patch + { from = { x = 3194, y = 3247 }, to = { x = 3195, y = 3236 } }, # lumbridge_fishing_shop_entrance_to_tree_patch { from = { x = 3194, y = 3247 }, to = { x = 3195, y = 3251 } }, # lumbridge_fishing_shop_entrance_to_fishing_shop { from = { x = 3172, y = 3239 }, to = { x = 3157, y = 3234 } }, # lumbridge_west_path_to_ham_path { from = { x = 3172, y = 3239 }, to = { x = 3184, y = 3225 } }, # lumbridge_west_path_to_yew_trees diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/DialogueContinueHandler.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/DialogueContinueHandler.kt index 30685db9ed..d61c7d2f93 100644 --- a/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/DialogueContinueHandler.kt +++ b/engine/src/main/kotlin/world/gregs/voidps/engine/client/instruction/handle/DialogueContinueHandler.kt @@ -26,7 +26,9 @@ class DialogueContinueHandler( logger.debug { "Dialogue $interfaceId component $componentId not found for player $player" } return false } - + if (player["debug", false]) { + logger.info { "$player - $id:${component.stringId}" } + } Dialogues.continueDialogue(player, "$id:${component.stringId}") return true } diff --git a/game/src/main/kotlin/content/area/misthalin/lumbridge/combat_hall/ArcheryTarget.kt b/game/src/main/kotlin/content/area/misthalin/lumbridge/combat_hall/ArcheryTarget.kt index 1ba7fe68fb..2dbac1b096 100644 --- a/game/src/main/kotlin/content/area/misthalin/lumbridge/combat_hall/ArcheryTarget.kt +++ b/game/src/main/kotlin/content/area/misthalin/lumbridge/combat_hall/ArcheryTarget.kt @@ -50,11 +50,11 @@ class ArcheryTarget : Script { } player.ammo = "" val ammo = player.equipped(EquipSlot.Ammo) - if (ammo.id != "training_arrows") { + if (ammo.isNotEmpty() && ammo.id != "training_arrows") { player.message("You can't use that ammo with your bow.") return@weakQueue } - if (ammo.amount < 1) { + if (ammo.isEmpty()) { player.message("There is no ammo left in your quiver.") return@weakQueue } diff --git a/game/src/main/kotlin/content/bot/BotCommands.kt b/game/src/main/kotlin/content/bot/BotCommands.kt index 4fdee5aaee..7efb15cdee 100644 --- a/game/src/main/kotlin/content/bot/BotCommands.kt +++ b/game/src/main/kotlin/content/bot/BotCommands.kt @@ -56,6 +56,12 @@ class BotCommands( return@worldTimerTick Timer.CONTINUE } + playerDespawn { + if (isBot) { + manager.remove(bot) + } + } + worldSpawn { loadSettings() } diff --git a/game/src/main/kotlin/content/bot/BotManager.kt b/game/src/main/kotlin/content/bot/BotManager.kt index e6e45502ac..ed71372a17 100644 --- a/game/src/main/kotlin/content/bot/BotManager.kt +++ b/game/src/main/kotlin/content/bot/BotManager.kt @@ -305,7 +305,7 @@ class BotManager( for (requirement in resolver.requires) { if (!requirement.check(bot.player)) { logger.debug { "Resolver ${resolver.id} - Failed requirement: $requirement." } - break + return } } logger.debug { "Resolver ${resolver.id} - Available." } diff --git a/game/src/main/kotlin/content/bot/BotUpdates.kt b/game/src/main/kotlin/content/bot/BotUpdates.kt index 4cc0599d72..7fcc537345 100644 --- a/game/src/main/kotlin/content/bot/BotUpdates.kt +++ b/game/src/main/kotlin/content/bot/BotUpdates.kt @@ -71,6 +71,7 @@ class BotUpdates( worldSpawn { DynamicResolvers.shopItems.clear() } + npcSpawn { if (def.contains("shop")) { val def = inventoryDefinitions.get(def.get("shop")) diff --git a/game/src/main/kotlin/content/bot/behaviour/Condition.kt b/game/src/main/kotlin/content/bot/behaviour/Condition.kt index 5ae77c925f..d4f0024765 100644 --- a/game/src/main/kotlin/content/bot/behaviour/Condition.kt +++ b/game/src/main/kotlin/content/bot/behaviour/Condition.kt @@ -2,8 +2,37 @@ package content.bot.behaviour import content.entity.player.bank.bank import net.pearx.kasechange.toPascalCase +import world.gregs.voidps.engine.GameLoop import world.gregs.voidps.engine.client.variable.hasClock +import world.gregs.voidps.engine.client.variable.remaining import world.gregs.voidps.engine.data.definition.Areas +import world.gregs.voidps.engine.entity.character.mode.EmptyMode +import world.gregs.voidps.engine.entity.character.mode.Face +import world.gregs.voidps.engine.entity.character.mode.Follow +import world.gregs.voidps.engine.entity.character.mode.Patrol +import world.gregs.voidps.engine.entity.character.mode.PauseMode +import world.gregs.voidps.engine.entity.character.mode.Rest +import world.gregs.voidps.engine.entity.character.mode.Retreat +import world.gregs.voidps.engine.entity.character.mode.Wander +import world.gregs.voidps.engine.entity.character.mode.combat.CombatMovement +import world.gregs.voidps.engine.entity.character.mode.interact.Interact +import world.gregs.voidps.engine.entity.character.mode.interact.InteractOption +import world.gregs.voidps.engine.entity.character.mode.interact.InterfaceOnFloorItemInteract +import world.gregs.voidps.engine.entity.character.mode.interact.InterfaceOnNPCInteract +import world.gregs.voidps.engine.entity.character.mode.interact.InterfaceOnObjectInteract +import world.gregs.voidps.engine.entity.character.mode.interact.ItemOnFloorItemInteract +import world.gregs.voidps.engine.entity.character.mode.interact.ItemOnNPCInteract +import world.gregs.voidps.engine.entity.character.mode.interact.ItemOnObjectInteract +import world.gregs.voidps.engine.entity.character.mode.interact.ItemOnPlayerInteract +import world.gregs.voidps.engine.entity.character.mode.interact.NPCOnFloorItemInteract +import world.gregs.voidps.engine.entity.character.mode.interact.NPCOnNPCInteract +import world.gregs.voidps.engine.entity.character.mode.interact.NPCOnObjectInteract +import world.gregs.voidps.engine.entity.character.mode.interact.NPCOnPlayerInteract +import world.gregs.voidps.engine.entity.character.mode.interact.PlayerOnFloorItemInteract +import world.gregs.voidps.engine.entity.character.mode.interact.PlayerOnNPCInteract +import world.gregs.voidps.engine.entity.character.mode.interact.PlayerOnObjectInteract +import world.gregs.voidps.engine.entity.character.mode.interact.PlayerOnPlayerInteract +import world.gregs.voidps.engine.entity.character.mode.move.Movement import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.entity.character.player.combatLevel import world.gregs.voidps.engine.entity.character.player.equip.equipped @@ -13,7 +42,9 @@ import world.gregs.voidps.engine.entity.character.player.skill.level.Level.hasRe import world.gregs.voidps.engine.entity.obj.GameObjects import world.gregs.voidps.engine.event.Wildcard import world.gregs.voidps.engine.event.Wildcards +import world.gregs.voidps.engine.inv.equipment import world.gregs.voidps.engine.inv.inventory +import world.gregs.voidps.engine.timer.epochSeconds import world.gregs.voidps.network.login.protocol.visual.update.player.EquipSlot import kotlin.collections.iterator @@ -68,6 +99,15 @@ sealed class Condition(val priority: Int) { override fun check(player: Player) = player.hasClock(id) } + data class ClockRemaining(val id: String, val min: Int? = null, val max: Int? = null, val seconds: Boolean = false) : Condition(1) { + override fun keys() = setOf("clock:$id") + override fun events() = setOf("clock") + override fun check(player: Player): Boolean { + val remaining = player.remaining(id, if (seconds) epochSeconds() else GameLoop.tick) + return inRange(remaining, min, max) + } + } + data class Entry(val ids: Set, val min: Int? = null, val max: Int? = null, val usable: Boolean = false, val equippable: Boolean = false) data class Inventory(val items: List) : Condition(100) { @@ -83,6 +123,12 @@ sealed class Condition(val priority: Int) { override fun check(player: Player): Boolean { for ((slot, entry) in items) { val item = player.equipped(slot) + if (entry.ids.contains("empty")) { + if (item.isEmpty()) { + continue + } + return false + } if (!entry.ids.contains(item.id)) { return false } @@ -103,6 +149,15 @@ sealed class Condition(val priority: Int) { override fun check(player: Player) = contains(player, player.bank, items) } + data class Owns(val id: String, val min: Int? = null, val max: Int? = null) : Condition(110) { + override fun keys() = setOf("item:$id") + override fun events() = setOf("worn_equipment", "inventory", "bank") + override fun check(player: Player): Boolean { + val count = player.inventory.count(id) + player.bank.count(id) + player.equipment.count(id) + return inRange(count, min, max) + } + } + data class Variable(val id: String, val equals: Any, val default: Any) : Condition(1) { override fun keys() = setOf("var:$id") override fun events() = setOf("variable") @@ -142,6 +197,41 @@ sealed class Condition(val priority: Int) { override fun check(player: Player) = inRange(player.levels.get(skill), min, max) } + data class HasMode(val id: String) : Condition(1) { + override fun keys() = setOf("mode:${id}") + override fun events() = setOf("mode") + override fun check(player: Player) = when (id) { + "empty" -> player.mode == EmptyMode + "interact" -> player.mode is Interact + "interact_on" -> player.mode is InteractOption + "combat_movement" -> player.mode is CombatMovement + "interface_on_floor_item" -> player.mode is InterfaceOnFloorItemInteract + "interface_on_npc" -> player.mode is InterfaceOnNPCInteract + "interface_on_object" -> player.mode is InterfaceOnObjectInteract + "item_on_floor_item" -> player.mode is ItemOnFloorItemInteract + "item_on_npc" -> player.mode is ItemOnNPCInteract + "item_on_object" -> player.mode is ItemOnObjectInteract + "item_on_player" -> player.mode is ItemOnPlayerInteract + "npc_on_floor_item" -> player.mode is NPCOnFloorItemInteract + "npc_on_npc" -> player.mode is NPCOnNPCInteract + "npc_on_object" -> player.mode is NPCOnObjectInteract + "npc_on_player" -> player.mode is NPCOnPlayerInteract + "player_on_floor_item" -> player.mode is PlayerOnFloorItemInteract + "player_on_npc" -> player.mode is PlayerOnNPCInteract + "player_on_object" -> player.mode is PlayerOnObjectInteract + "player_on_player" -> player.mode is PlayerOnPlayerInteract + "movement" -> player.mode is Movement + "follow" -> player.mode is Follow + "face" -> player.mode is Face + "patrol" -> player.mode is Patrol + "pause" -> player.mode == PauseMode + "rest" -> player.mode is Rest + "retreat" -> player.mode is Retreat + "wander" -> player.mode is Wander + else -> false + } + } + companion object { private fun inRange(value: Int, min: Int?, max: Int?): Boolean { if (min != null && value < min) { @@ -208,6 +298,7 @@ sealed class Condition(val priority: Int) { "inventory" -> parseInventory(list) "equipment" -> parseEquipment(list) "bank" -> parseBank(list) + "owns" -> parseOwns(list) "variable" -> parseVariable(list) "clock" -> parseClock(list) "timer" -> parseTimer(list) @@ -217,6 +308,7 @@ sealed class Condition(val priority: Int) { "object" -> parseObject(list) "combat_level" -> parseCombat(list) "interface_open" -> parseInterface(list) + "mode" -> parseMode(list) "skill" -> parseSkills(list) else -> null } @@ -249,10 +341,15 @@ sealed class Condition(val priority: Int) { require(slot != EquipSlot.None) { "Invalid equipment slot: $key in $list" } value as? Map ?: error("Equipment $key expecting map, found: $value") val id = value["id"] as? String ?: error("Missing item id in $list") + var min = value["min"] as? Int + val max = value["max"] as? Int + if (min == null && max == null) { + min = 1 + } items[slot] = Entry( ids = toIds(id), - min = value["min"] as? Int ?: 1, - max = value["max"] as? Int, + min = min, + max = max, ) } } @@ -261,6 +358,14 @@ sealed class Condition(val priority: Int) { private fun parseBank(list: List>): Bank = Bank(parseItems(list)) + private fun parseOwns(list: List>): Owns? { + val map = list.single() + if (map.containsKey("id")) { + return Owns(map["id"] as String, min = map["min"] as? Int, max = map["max"] as? Int) + } + return null + } + @Suppress("UNCHECKED_CAST") private fun parseVariable(list: List>): Condition? { val map = list.single() @@ -270,7 +375,7 @@ sealed class Condition(val priority: Int) { equals = map["equals"]!!, default = map["default"]!!, ) - } else if (map.containsKey("min")) { + } else if (map.containsKey("min") || map.containsKey("max")) { return VariableIn( id = map["id"] as String, default = map["default"] as Int, @@ -284,6 +389,14 @@ sealed class Condition(val priority: Int) { private fun parseClock(list: List>): Condition? { val map = list.single() if (map.containsKey("id")) { + if (map.containsKey("min") || map.containsKey("max")) { + return ClockRemaining( + id = map["id"] as String, + min = map["min"] as? Int, + max = map["max"] as? Int, + seconds = map["seconds"] as? Boolean ?: false, + ) + } return Clock(id = map["id"] as String) } return null @@ -353,6 +466,14 @@ sealed class Condition(val priority: Int) { return null } + private fun parseMode(list: List>): Condition? { + val map = list.single() + if (map.containsKey("id")) { + return HasMode(id = map["id"] as String) + } + return null + } + private fun parseSkills(list: List>): Condition? { val map = list.single() if (map.containsKey("id")) { diff --git a/game/src/main/kotlin/content/bot/behaviour/action/ActionParser.kt b/game/src/main/kotlin/content/bot/behaviour/action/ActionParser.kt index cabecb7d70..b9f595d843 100644 --- a/game/src/main/kotlin/content/bot/behaviour/action/ActionParser.kt +++ b/game/src/main/kotlin/content/bot/behaviour/action/ActionParser.kt @@ -144,9 +144,21 @@ sealed class ActionParser { override val required = setOf("success") override val optional = setOf("wait_if") + @Suppress("UNCHECKED_CAST") override fun parse(map: Map): BotAction { val requirement = requirement(map, "success").single() - return BotAction.Restart(wait = requirement(map, "wait_if"), success = requirement) + val wait = map["wait_if"] + val waitIf = if (wait is List<*>) { + val requirements = mutableListOf() + wait as List> + for(element in wait) { + requirements.addAll(requirement(element)) + } + requirements + } else { + requirement(map, "wait_if") + } + return BotAction.Restart(wait = waitIf, success = requirement) } } @@ -154,8 +166,13 @@ sealed class ActionParser { @Suppress("UNCHECKED_CAST") private fun requirement(map: Map, key: String): List { val parent = map[key] as? Map ?: return listOf() - val key = parent.keys.singleOrNull() ?: error("Collection $map has more than one element.") - val value = parent[key] ?: return listOf() + return requirement(parent) + } + + @Suppress("UNCHECKED_CAST") + private fun requirement(map: Map): List { + val key = map.keys.singleOrNull() ?: error("Collection $map has more than one element.") + val value = map[key] ?: return listOf() val list = when (value) { is Map<*, *> -> listOf(value as Map) is List<*> -> value as List> diff --git a/game/src/main/kotlin/content/bot/behaviour/action/BotAction.kt b/game/src/main/kotlin/content/bot/behaviour/action/BotAction.kt index 3797509d17..20a7f3402f 100644 --- a/game/src/main/kotlin/content/bot/behaviour/action/BotAction.kt +++ b/game/src/main/kotlin/content/bot/behaviour/action/BotAction.kt @@ -56,7 +56,7 @@ sealed interface BotAction { } override fun update(bot: Bot, frame: BehaviourFrame): BehaviourState { - if (bot.steps.isNotEmpty() || bot.mode != EmptyMode) { + if (bot.mode != EmptyMode) { return BehaviourState.Running } val def = Areas.getOrNull(target) ?: return BehaviourState.Failed(Reason.Invalid("No areas found with id '$target'.")) diff --git a/game/src/main/kotlin/content/bot/behaviour/setup/DynamicResolvers.kt b/game/src/main/kotlin/content/bot/behaviour/setup/DynamicResolvers.kt index 3f0b85a5d3..c68df77777 100644 --- a/game/src/main/kotlin/content/bot/behaviour/setup/DynamicResolvers.kt +++ b/game/src/main/kotlin/content/bot/behaviour/setup/DynamicResolvers.kt @@ -106,14 +106,10 @@ object DynamicResolvers { return resolver } resolver = equipItems(player, equipment) - if (resolver == null) { - return resolver - } - resolver = withdrawEquip(player, equipment) if (resolver != null) { return resolver } - resolver = buyItems(player, equipment.values.toList()) + resolver = resolveInventory(player, equipment.values.toList()) if (resolver != null) { return resolver } @@ -141,56 +137,9 @@ object DynamicResolvers { return null } - private fun withdrawEquip(player: Player, equipment: Map): Resolver? { - val actions = mutableListOf() - actions.add(BotAction.GoToNearest("bank")) - actions.add(BotAction.InteractObject("Use-quickly", "bank_booth*", success = Condition.InterfaceOpen("bank"))) - // Free up inventory space if needed - if (player.inventory.spaces < equipment.size) { - actions.add(BotAction.InterfaceOption("Deposit carried items", "bank:carried", success = Condition.Inventory(listOf(Entry(setOf("empty"), min = 28))))) - } - var found = false - val toEquip = mutableSetOf() - for (item in player.bank.items) { - if (item.isEmpty()) { - continue - } - val slot = item.slot - if (slot == EquipSlot.None) { - continue - } - if (equipment.isEmpty()) { - break - } - // Check if item meets requirement - val entry = equipment[slot] ?: continue - if (!entry.ids.contains(item.id)) { - continue - } - if (entry.usable && !player.hasRequirementsToUse(item)) { - continue - } - if (entry.equippable && !player.hasRequirements(item)) { - continue - } - // Withdraw the necessary item - withdraw(actions, entry, item) - toEquip.add(item.id) - found = true - } - actions.add(BotAction.CloseInterface) - // Equip all items - for (id in toEquip) { - actions.add(BotAction.InterfaceOption("Equip", "inventory:inventory:$id")) - } - if (found) { - return Resolver("equip_from_bank", weight = 20, actions = actions) - } - return null - } - private fun equipItems(player: Player, equipment: Map): Resolver? { val actions = mutableListOf() + val produces = mutableSetOf() for (item in player.inventory.items) { val slot = item.slot if (slot == EquipSlot.None) { @@ -200,10 +149,11 @@ object DynamicResolvers { if (!entry.ids.contains(item.id)) { continue } - actions.add(BotAction.InterfaceOption("Equip", "inventory:inventory:${item.id}")) + actions.add(BotAction.InterfaceOption("Equip", "inventory:inventory:${item.id}", success = Condition.Equipment(mapOf(slot to entry)))) + produces.add("equipment:${item.id}") } if (actions.isNotEmpty()) { - Resolver("equip_items", weight = 15, actions = actions) + return Resolver("equip_items", weight = 15, actions = actions, produces = produces) } return null } From 0e1fd050289797195c2744bec3b98aeae4281ea1 Mon Sep 17 00:00:00 2001 From: GregHib Date: Wed, 11 Feb 2026 14:36:37 +0000 Subject: [PATCH 091/101] Remove old area bot data --- .../al_kharid/al_kharid.areas.toml | 4 -- .../area/misthalin/draynor/draynor.areas.toml | 16 ----- .../misthalin/lumbridge/lumbridge.areas.toml | 72 ------------------- .../area/misthalin/varrock/varrock.areas.toml | 8 --- .../runecrafting/runecrafting.areas.toml | 10 --- .../area/kharidian_desert/al_kharid/Ellis.kt | 2 - .../skill/fletching/FletchUnfinished.kt | 2 - 7 files changed, 114 deletions(-) diff --git a/data/area/kharidian_desert/al_kharid/al_kharid.areas.toml b/data/area/kharidian_desert/al_kharid/al_kharid.areas.toml index 9bb68a1d73..59b9472dd7 100644 --- a/data/area/kharidian_desert/al_kharid/al_kharid.areas.toml +++ b/data/area/kharidian_desert/al_kharid/al_kharid.areas.toml @@ -6,14 +6,10 @@ tags = ["bank"] [zekes_scimitar_shop] x = [3285, 3290] y = [3187, 3192] -tags = ["shop"] -items = ["bronze_scimitar", "iron_scimitar", "steel_scimitar", "mithril_scimitar"] [al_kharid_kebab_shop] x = [3271, 3275] y = [3179, 3183] -type = "range" -tags = ["cooking", "spaces_2"] [al_kharid_multi_area] x = [3264, 3327] diff --git a/data/area/misthalin/draynor/draynor.areas.toml b/data/area/misthalin/draynor/draynor.areas.toml index ec7fa65a1e..c643e90bb3 100644 --- a/data/area/misthalin/draynor/draynor.areas.toml +++ b/data/area/misthalin/draynor/draynor.areas.toml @@ -11,31 +11,18 @@ hint = "around a village between a great tower and a vampyre's manor." [draynor_willow_trees] x = [3083, 3090] y = [3227, 3238] -tags = ["trees"] -trees = ["willow"] -levels = "30-45" -spaces = 6 [draynor_oak_trees] x = [3098, 3103] y = [3241, 3253] -tags = ["trees"] -trees = ["tree", "oak"] -levels = "0-30" [draynor_west_oak_trees] x = [3068, 3072] y = [3248, 3260] -tags = ["trees"] -trees = ["tree", "oak"] -levels = "0-30" [draynor_north_trees] x = [3075, 3085] y = [3265, 3274] -tags = ["trees"] -trees = ["tree", "oak"] -levels = "0-30" [draynor_bank] x = [3088, 3097] @@ -45,9 +32,6 @@ tags = ["bank"] [draynor_fishing_area] x = [3085, 3086] y = [3227, 3231] -tags = ["fish"] -type = "lure_bait" -spaces = 5 [draynor_village_multi_area] x = [3112, 3136, 3136, 3104, 3104, 3112] diff --git a/data/area/misthalin/lumbridge/lumbridge.areas.toml b/data/area/misthalin/lumbridge/lumbridge.areas.toml index e00b04d63f..5265697c3b 100644 --- a/data/area/misthalin/lumbridge/lumbridge.areas.toml +++ b/data/area/misthalin/lumbridge/lumbridge.areas.toml @@ -5,51 +5,30 @@ y = [3136, 3136, 3336, 3336, 3346, 3349, 3347, 3347, 3337, 3333, 3333, 3330, 333 [lumbridge_north_trees] x = [3220, 3234] y = [3244, 3249] -tags = ["trees"] -trees = ["tree", "oak"] -levels = "0-30" [lumbridge_west_yew_tree] x = [3164, 3168] y = [3218, 3222] -tags = ["trees"] -trees = ["tree", "yew"] -levels = "60-68" [lumbridge_north_west_yew_tree] x = [3183, 3187] y = [3225, 3229] -tags = ["trees"] -trees = ["tree", "yew"] -levels = "60-68" [lumbridge_west_trees] x = [3186, 3197] y = [3238, 3250] -tags = ["trees"] -trees = ["tree", "oak"] -spaces = 2 -levels = "0-30" [lumbridge_east_trees] x = [3260, 3267] y = [3214, 3226] -tags = ["trees"] -trees = ["tree", "oak"] -levels = "0-30" [lumbridge_cow_trees] x = [3254, 3264] y = [3249, 3254] -tags = ["trees"] -trees = ["tree", "oak"] -levels = "0-30" [bobs_axe_shop] x = [3227, 3233] y = [3201, 3205] -tags = ["shop"] -items = ["bronze_pickaxe", "bronze_hatchet", "iron_hatchet", "steel_hatchet", "iron_battleaxe", "steel_battleaxe", "mithril_battleaxe"] [lumbridge_castle_bank] x = [3207, 3210] @@ -66,64 +45,34 @@ tags = ["high"] [lumbridge_swamp_east_copper_mine] x = [3227, 3231] y = [3143, 3149] -tags = ["mine"] -rocks = ["copper"] -levels = "1-15" -spaces = 2 [lumbridge_swamp_east_tin_mine] x = [3222, 3226] y = [3145, 3149] -tags = ["mine"] -rocks = ["tin"] -levels = "1-15" -spaces = 2 [lumbridge_swamp_west_coal_mine] x = [3143, 3147] y = [3148, 3154] -tags = ["mine", "coal"] -rocks = ["coal"] -levels = "30-40" -spaces = 2 [lumbridge_swamp_west_mithril_mine] x = [3143, 3149] y = [3143, 3147] -tags = ["mine"] -rocks = ["mithril"] -levels = "55-70" [lumbridge_swamp_west_adamantite_mine] x = [3147, 3149] y = [3145, 3149] -tags = ["mine"] -rocks = ["adamantite"] -levels = "70-85" [lumbridge_east_cow_field] x = [3253, 3253, 3251, 3251, 3249, 3246, 3244, 3244, 3240, 3240, 3242, 3242, 3240, 3240, 3241, 3256, 3257, 3260, 3261, 3263, 3265, 3265, 3266, 3266, 3265, 3265, 3266, 3266, 3265, 3265, 3264] y = [3255, 3272, 3274, 3276, 3278, 3278, 3280, 3281, 3285, 3286, 3288, 3294, 3296, 3297, 3298, 3298, 3299, 3299, 3298, 3298, 3296, 3293, 3292, 3286, 3285, 3276, 3275, 3272, 3271, 3256, 3255] -tags = ["combat_training"] -npcs = ["cows"] -spaces = 7 -levels = "10-20" [lumbridge_east_goblin_settlement] x = [3262, 3259, 3253, 3251, 3251, 3249, 3245, 3242, 3242, 3240, 3240, 3239, 3239, 3252, 3252, 3264, 3264, 3265, 3265, 3266, 3266, 3267, 3267, 3266, 3266] y = [3215, 3218, 3222, 3223, 3227, 3228, 3232, 3235, 3239, 3241, 3249, 3251, 3256, 3256, 3254, 3254, 3247, 3246, 3238, 3237, 3226, 3225, 3220, 3219, 3215] -tags = ["combat_training"] -npcs = ["goblins", "giant_spiders"] -spaces = 6 -levels = "5-15" [lumbridge_chicken_pen] x = [3225, 3225, 3237, 3237] y = [3287, 3301, 3301, 3287] -tags = ["combat_training"] -npcs = ["chickens"] -spaces = 5 -levels = "1-10" [lumbridge_castle_courtyard] x = [3218, 3224, 3224, 3218] @@ -136,29 +85,18 @@ y = [3239, 3243] [lumbridge_swamp_fishing_area] x = [3240, 3247] y = [3142, 3157] -tags = ["fish"] -type = "small_net_bait" -spaces = 8 [lumbridge_river_fishing_area] x = [3238, 3240] y = [3241, 3255] -tags = ["fish"] -type = "lure_bait" -spaces = 3 [hanks_fishing_shop] x = [3191, 3198] y = [3251, 3255] -tags = ["shop"] -items = ["small_fishing_net", "fishing_rod", "fly_fishing_rod", "crayfish_cage", "fishing_bait", "feather"] [lumbridge_south_river_fishing_area] x = [3258, 3259] y = [3203, 3207] -tags = ["fish"] -type = "crayfish" -spaces = 2 [lumbridge_teleport] x = [3221, 3222] @@ -172,33 +110,23 @@ y = [3250, 3256] [lumbridge_swamp_giant_rats] x = [3212, 3232] y = [3173, 3190] -tags = ["combat_training"] -npcs = ["giant_rats"] -spaces = 2 -levels = "10-14" [lumbridge_firemaking_spot] x = [3219, 3219] y = [3247, 3248] -tags = ["fire_making", "spaces_2"] [lumbridge_kitchen] x = [3205, 3212] y = [3212, 3217] level = 0 -type = "range" -tags = ["cooking", "spaces_2"] [lumbridge_south_cooking_range] x = [3230, 3237] y = [3195, 3198] -type = "range" -tags = ["cooking", "spaces_2"] [lumbridge_furnace] x = [3222, 3229] y = [3252, 3257] -tags = ["smithing", "smelting", "spaces_2"] [freds_farmhouse] x = [3184, 3192] diff --git a/data/area/misthalin/varrock/varrock.areas.toml b/data/area/misthalin/varrock/varrock.areas.toml index 347c0a6356..db92889c14 100644 --- a/data/area/misthalin/varrock/varrock.areas.toml +++ b/data/area/misthalin/varrock/varrock.areas.toml @@ -16,18 +16,10 @@ tags = ["multi_combat"] [varrock_south_east_mine] x = [3281, 3290] y = [3360, 3371] -tags = ["mine"] -rocks = ["copper", "tin", "iron"] -levels = "1-45" -spaces = 4 [varrock_south_west_mine] x = [3171, 3184] y = [3364, 3380] -tags = ["mine"] -rocks = ["copper", "tin", "clay", "iron", "silver"] -levels = "1-45" -spaces = 4 [varrock_east_bank] x = [3250, 3257] diff --git a/data/skill/runecrafting/runecrafting.areas.toml b/data/skill/runecrafting/runecrafting.areas.toml index 8f966fa422..fc642054c6 100644 --- a/data/skill/runecrafting/runecrafting.areas.toml +++ b/data/skill/runecrafting/runecrafting.areas.toml @@ -71,25 +71,15 @@ tags = ["multi_combat"] [earth_altar_ruins] x = [3304, 3308] y = [3472, 3476] -tags = ["ruins", "earth"] [air_altar_ruins] x = [3125, 3129] y = [3403, 3407] -tags = ["ruins", "air"] [air_altar] x = [2816, 2879] y = [4800, 4863] -tags = ["altar"] -type = "air" -spaces = 3 -levels = "1-9" [earth_altar] x = [2624, 2687] y = [4800, 4863] -tags = ["altar"] -type = "earth" -spaces = 3 -levels = "9-14" diff --git a/game/src/main/kotlin/content/area/kharidian_desert/al_kharid/Ellis.kt b/game/src/main/kotlin/content/area/kharidian_desert/al_kharid/Ellis.kt index ff1f205281..57b94f7dee 100644 --- a/game/src/main/kotlin/content/area/kharidian_desert/al_kharid/Ellis.kt +++ b/game/src/main/kotlin/content/area/kharidian_desert/al_kharid/Ellis.kt @@ -90,7 +90,6 @@ class Ellis : Script { player.message("You don't have any ${item.toLowerSpaceCase()} to tan.") return } - player.softTimers.start("tanning") val tanning: Tanning = ItemDefinitions.get(item)["tanning"] val (leather, cost) = tanning.prices[if (type.endsWith("_1")) 1 else 0] var tanned = 0 @@ -108,7 +107,6 @@ class Ellis : Script { } tanned++ } - player.softTimers.stop("tanning") if (tanned == 1) { player.message("The tanner tans your ${item.toLowerSpaceCase()}.") } else if (tanned > 0) { diff --git a/game/src/main/kotlin/content/skill/fletching/FletchUnfinished.kt b/game/src/main/kotlin/content/skill/fletching/FletchUnfinished.kt index 5c151de542..29562eb093 100644 --- a/game/src/main/kotlin/content/skill/fletching/FletchUnfinished.kt +++ b/game/src/main/kotlin/content/skill/fletching/FletchUnfinished.kt @@ -38,12 +38,10 @@ class FletchUnfinished : Script { fun Player.fletch(addItem: String, addItemDef: Fletching, removeItem: String, amount: Int) { if (amount <= 0) { - softTimers.stop("fletching") return } if (!inventory.contains("knife") || !inventory.contains(removeItem)) { - softTimers.stop("fletching") return } From df74e2434ce0a506100f7d68becf8f717c0c8b4e Mon Sep 17 00:00:00 2001 From: GregHib Date: Wed, 11 Feb 2026 15:48:09 +0000 Subject: [PATCH 092/101] Add firemaking bot --- .../misthalin/lumbridge/lumbridge.areas.toml | 4 +- .../misthalin/lumbridge/lumbridge.bots.toml | 29 ++++-- .../misthalin/lumbridge/lumbridge.npcs.toml | 2 + data/bot/firemaking.templates.toml | 11 +++ data/bot/lumbridge.nav-edges.toml | 1 + .../bot/behaviour/action/ActionParser.kt | 11 +++ .../content/bot/behaviour/action/BotAction.kt | 89 ++++++++++++++++--- 7 files changed, 127 insertions(+), 20 deletions(-) create mode 100644 data/bot/firemaking.templates.toml diff --git a/data/area/misthalin/lumbridge/lumbridge.areas.toml b/data/area/misthalin/lumbridge/lumbridge.areas.toml index 5265697c3b..d07859e8ca 100644 --- a/data/area/misthalin/lumbridge/lumbridge.areas.toml +++ b/data/area/misthalin/lumbridge/lumbridge.areas.toml @@ -112,8 +112,8 @@ x = [3212, 3232] y = [3173, 3190] [lumbridge_firemaking_spot] -x = [3219, 3219] -y = [3247, 3248] +x = [3220, 3221] +y = [3257, 3259] [lumbridge_kitchen] x = [3205, 3212] diff --git a/data/area/misthalin/lumbridge/lumbridge.bots.toml b/data/area/misthalin/lumbridge/lumbridge.bots.toml index e14cbcf224..81fce1bff8 100644 --- a/data/area/misthalin/lumbridge/lumbridge.bots.toml +++ b/data/area/misthalin/lumbridge/lumbridge.bots.toml @@ -123,7 +123,7 @@ template = "pickpocketting_template" capacity = 2 fields = { location = "lumbridge_thieving_area", npc = "lumbridge_man*,lumbridge_woman*" } requires = [ - { skill = { id = "thieving", min = 1 } } + { skill = { id = "thieving", min = 1, max = 10 } } ] # Fishing @@ -132,7 +132,7 @@ template = "fish_crayfish" capacity = 4 fields = { location = "lumbridge_south_river_fishing_area", spot = "fishing_spot_crayfish_lumbridge" } requires = [ - { skill = { id = "fishing", min = 1, max = 5 } } + { skill = { id = "fishing", min = 1, max = 10 } } ] [lumbridge_net_fishing] @@ -140,7 +140,7 @@ template = "fish_small_net" capacity = 5 fields = { location = "lumbridge_swamp_fishing_area", spot = "fishing_spot_small_net_bait_lumbridge" } requires = [ - { skill = { id = "fishing", min = 1, max = 10 } } + { skill = { id = "fishing", min = 1, max = 15 } } ] [lumbridge_bait_fishing] @@ -157,7 +157,7 @@ template = "fletching_arrow_shafts_template" capacity = 2 fields = { location = "lumbridge_castle_bank", logs = "logs" } requires = [ - { skill = { id = "fletching", min = 1 } } + { skill = { id = "fletching", min = 1, max = 10 } } ] [lumbridge_fletch_shortbows] @@ -165,7 +165,7 @@ template = "fletching_shortbow_template" capacity = 2 fields = { location = "lumbridge_castle_bank", logs = "logs" } requires = [ - { skill = { id = "fletching", min = 5 } } + { skill = { id = "fletching", min = 5, max = 15 } } ] produces = [ { item = "shortbow_u" } @@ -175,7 +175,7 @@ produces = [ template = "fletching_longbow_template" fields = { location = "lumbridge_castle_bank", logs = "logs" } requires = [ - { skill = { id = "fletching", min = 10 } } + { skill = { id = "fletching", min = 10, max = 20 } } ] produces = [ { item = "longbow_u" } @@ -231,6 +231,23 @@ template = "mithril_ore_template" capacity = 2 fields = { location = "lumbridge_swamp_west_mithril_mine" } +# Firemaking +[lumbridge_firemaking_logs] +template = "firemaking" +capacity = 2 +fields = { logs = "logs", location = "lumbridge_firemaking_spot" } +requires = [ + { skill = { id = "firemaking", min = 1, max = 15 } }, +] + +[lumbridge_firemaking_oak_logs] +template = "firemaking" +capacity = 1 +fields = { logs = "oak_logs", location = "lumbridge_firemaking_spot" } +requires = [ + { skill = { id = "firemaking", min = 15, max = 25 } }, +] + # Cooking [lumbridge_cook_chicken] template = "cooking_template" diff --git a/data/area/misthalin/lumbridge/lumbridge.npcs.toml b/data/area/misthalin/lumbridge/lumbridge.npcs.toml index f42746bf11..20c73faf6a 100644 --- a/data/area/misthalin/lumbridge/lumbridge.npcs.toml +++ b/data/area/misthalin/lumbridge/lumbridge.npcs.toml @@ -17,6 +17,7 @@ examine = "An expert on axes." id = 520 categories = ["human"] shop = "lumbridge_general_store" +area = "lumbridge_general_store" wander_range = 2 collision = "indoors" examine = "Sells stuff." @@ -25,6 +26,7 @@ examine = "Sells stuff." id = 521 categories = ["human"] shop = "lumbridge_general_store" +area = "lumbridge_general_store" wander_range = 2 collision = "indoors" examine = "Helps sell stuff." diff --git a/data/bot/firemaking.templates.toml b/data/bot/firemaking.templates.toml new file mode 100644 index 0000000000..e4a5d7bfaa --- /dev/null +++ b/data/bot/firemaking.templates.toml @@ -0,0 +1,11 @@ +[firemaking] +setup = [ + { area = { id = "$location" } }, + { inventory = [{ id = "$logs", min = 27 }, { id = "tinderbox", min = 1 }] }, +] +actions = [ + { firemaking = { id = "$logs", area = "$location" } }, +] +produces = [ + { skill = "firemaking" } +] \ No newline at end of file diff --git a/data/bot/lumbridge.nav-edges.toml b/data/bot/lumbridge.nav-edges.toml index e97deb68e5..c25bf1c4b3 100644 --- a/data/bot/lumbridge.nav-edges.toml +++ b/data/bot/lumbridge.nav-edges.toml @@ -110,6 +110,7 @@ edges = [ { from = { x = 3218, y = 3255 }, to = { x = 3223, y = 3260 } }, { from = { x = 3223, y = 3260 }, to = { x = 3235, y = 3261 } }, { from = { x = 3218, y = 3255 }, to = { x = 3217, y = 3268 } }, + { from = { x = 3218, y = 3255 }, to = { x = 3220, y = 3258 } }, { from = { x = 3217, y = 3268 }, to = { x = 3213, y = 3277 } }, { from = { x = 3213, y = 3277 }, to = { x = 3199, y = 3279 } }, { from = { x = 3199, y = 3279 }, to = { x = 3190, y = 3283 } }, diff --git a/game/src/main/kotlin/content/bot/behaviour/action/ActionParser.kt b/game/src/main/kotlin/content/bot/behaviour/action/ActionParser.kt index b9f595d843..9168f77725 100644 --- a/game/src/main/kotlin/content/bot/behaviour/action/ActionParser.kt +++ b/game/src/main/kotlin/content/bot/behaviour/action/ActionParser.kt @@ -54,6 +54,16 @@ sealed class ActionParser { } } + object Firemaking : ActionParser() { + override val required = setOf("id", "area") + + override fun parse(map: Map): BotAction { + val area = map["area"] as String + val id = map["id"] as String + return BotAction.Firemaking(id, area) + } + } + object CloseInterfaceParser : ActionParser() { override val required = setOf("id") override fun parse(map: Map): BotAction = BotAction.CloseInterface @@ -208,6 +218,7 @@ sealed class ActionParser { "interface_close" to CloseInterfaceParser, "continue" to DialogueParser, "enter" to EnterParser, + "firemaking" to Firemaking, ) } } diff --git a/game/src/main/kotlin/content/bot/behaviour/action/BotAction.kt b/game/src/main/kotlin/content/bot/behaviour/action/BotAction.kt index 20a7f3402f..d7d96fda06 100644 --- a/game/src/main/kotlin/content/bot/behaviour/action/BotAction.kt +++ b/game/src/main/kotlin/content/bot/behaviour/action/BotAction.kt @@ -10,6 +10,7 @@ import content.bot.behaviour.navigation.NavigationShortcut import content.bot.behaviour.setup.Resolver import content.entity.combat.attackers import content.entity.combat.dead +import org.rsmod.game.pathfinder.StepValidator import world.gregs.voidps.engine.GameLoop import world.gregs.voidps.engine.client.instruction.InstructionHandlers import world.gregs.voidps.engine.client.instruction.InterfaceHandler @@ -21,6 +22,7 @@ import world.gregs.voidps.engine.entity.character.mode.EmptyMode import world.gregs.voidps.engine.entity.character.mode.interact.PlayerOnFloorItemInteract import world.gregs.voidps.engine.entity.character.mode.interact.PlayerOnNPCInteract import world.gregs.voidps.engine.entity.character.mode.interact.PlayerOnObjectInteract +import world.gregs.voidps.engine.entity.character.mode.move.canTravel import world.gregs.voidps.engine.entity.character.npc.NPCs import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.entity.character.player.skill.Skill @@ -28,16 +30,32 @@ import world.gregs.voidps.engine.entity.item.Item import world.gregs.voidps.engine.entity.item.floor.FloorItems import world.gregs.voidps.engine.entity.obj.GameObject import world.gregs.voidps.engine.entity.obj.GameObjects +import world.gregs.voidps.engine.entity.obj.ObjectLayer import world.gregs.voidps.engine.event.wildcardEquals import world.gregs.voidps.engine.get import world.gregs.voidps.engine.inv.inventory import world.gregs.voidps.engine.map.Spiral import world.gregs.voidps.engine.timer.toTicks import world.gregs.voidps.network.client.instruction.* +import world.gregs.voidps.type.Tile import java.util.concurrent.TimeUnit import kotlin.collections.indexOf import kotlin.collections.iterator +/** + * TODO + * Can InteractNpc/Object be handled entirely by restart now? - If not entirely what about just leaving search inside interact + * firemaking bot + * rune mysteries quest bot + * improvements: + * bot spawning in other locations - banks? + * bot saving? + * bot tests & coverage + * weight dynamic resolvers based on distance (or just for shops)? + * Increase world coverage + * Bot armour setups + * Combat escaping/running away + **/ sealed interface BotAction { fun start(bot: Bot, frame: BehaviourFrame): BehaviourState = BehaviourState.Running fun update(bot: Bot, frame: BehaviourFrame): BehaviourState? = null @@ -323,25 +341,37 @@ sealed interface BotAction { if (success != null && success.check(bot.player)) { return BehaviourState.Success } - val inventory = bot.player.inventory - val fromSlot = inventory.indexOf(item) - if (fromSlot == -1) { - return BehaviourState.Failed(Reason.Invalid("No inventory item '$item'.")) - } - val toSlot = inventory.indexOf(on) - if (toSlot == -1) { - return BehaviourState.Failed(Reason.Invalid("No inventory item '$on'.")) + val state = itemOnItem(bot.player, item, on) + if (state != null) { + return state } - val from = inventory[fromSlot] - val to = inventory[toSlot] - val valid = get().handle(bot.player, InteractInterfaceItem(from.def.id, to.def.id, fromSlot, toSlot, 149, 0, 149, 0)) return when { - !valid -> BehaviourState.Failed(Reason.Invalid("Invalid item on item: ${from.def.id}:$fromSlot -> ${to.def.id}:$toSlot.")) success == null -> BehaviourState.Wait(1, BehaviourState.Success) success.check(bot.player) -> BehaviourState.Success else -> BehaviourState.Running } } + + companion object { + fun itemOnItem(player: Player, item: String, on: String): BehaviourState? { + val inventory = player.inventory + val fromSlot = inventory.indexOf(item) + if (fromSlot == -1) { + return BehaviourState.Failed(Reason.Invalid("No inventory item '$item'.")) + } + val toSlot = inventory.indexOf(on) + if (toSlot == -1) { + return BehaviourState.Failed(Reason.Invalid("No inventory item '$on'.")) + } + val from = inventory[fromSlot] + val to = inventory[toSlot] + val valid = get().handle(player, InteractInterfaceItem(from.def.id, to.def.id, fromSlot, toSlot, 149, 0, 149, 0)) + if (valid) { + return null + } + return BehaviourState.Failed(Reason.Invalid("Invalid item on item: ${from.def.id}:$fromSlot -> ${to.def.id}:$toSlot.")) + } + } } data class ItemOnObject(val item: String, val id: String, val success: Condition? = null) : BotAction { @@ -531,4 +561,39 @@ sealed interface BotAction { else -> BehaviourState.Running } } + + data class Firemaking(val item: String, val area: String) : BotAction { + override fun update(bot: Bot, frame: BehaviourFrame): BehaviourState { + when { + bot.mode != EmptyMode -> return BehaviourState.Running + cantLightOn(bot.tile) -> { + val handlers = get() + val steps = get() + if (steps.canTravel(bot, -1, 0)) { + handlers.handle(bot.player, Walk(bot.tile.x - 1, bot.tile.y)) + return BehaviourState.Wait(1, BehaviourState.Running) + } + if (steps.canTravel(bot.tile.level, bot.tile.x - 1, bot.tile.y, -1, 0)) { + handlers.handle(bot.player, Walk(bot.tile.x - 2, bot.tile.y)) + return BehaviourState.Wait(1, BehaviourState.Running) + } + val area = Areas[area] + for (tile in area) { + if (cantLightOn(tile)) { + continue + } + handlers.handle(bot.player, Walk(tile.x, tile.y)) + return BehaviourState.Wait(1, BehaviourState.Running) + } + return BehaviourState.Failed(Reason.Stuck) + } + bot.player.inventory.contains(item) -> return ItemOnItem.itemOnItem(bot.player, "tinderbox", item) ?: BehaviourState.Running + else -> return BehaviourState.Success + } + } + + private fun cantLightOn(tile: Tile): Boolean { + return GameObjects.getLayer(tile, ObjectLayer.GROUND) != null + } + } } From c7f2f1fdb0cb7dd9e521c4cd50fb086af629ccd2 Mon Sep 17 00:00:00 2001 From: GregHib Date: Wed, 11 Feb 2026 15:48:41 +0000 Subject: [PATCH 093/101] Remove comments --- .../content/bot/behaviour/action/BotAction.kt | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/game/src/main/kotlin/content/bot/behaviour/action/BotAction.kt b/game/src/main/kotlin/content/bot/behaviour/action/BotAction.kt index d7d96fda06..b422976c45 100644 --- a/game/src/main/kotlin/content/bot/behaviour/action/BotAction.kt +++ b/game/src/main/kotlin/content/bot/behaviour/action/BotAction.kt @@ -42,20 +42,6 @@ import java.util.concurrent.TimeUnit import kotlin.collections.indexOf import kotlin.collections.iterator -/** - * TODO - * Can InteractNpc/Object be handled entirely by restart now? - If not entirely what about just leaving search inside interact - * firemaking bot - * rune mysteries quest bot - * improvements: - * bot spawning in other locations - banks? - * bot saving? - * bot tests & coverage - * weight dynamic resolvers based on distance (or just for shops)? - * Increase world coverage - * Bot armour setups - * Combat escaping/running away - **/ sealed interface BotAction { fun start(bot: Bot, frame: BehaviourFrame): BehaviourState = BehaviourState.Running fun update(bot: Bot, frame: BehaviourFrame): BehaviourState? = null From 425fbfae97ff3de7aebdb48a3bc9997fd0f59b91 Mon Sep 17 00:00:00 2001 From: GregHib Date: Wed, 11 Feb 2026 16:48:00 +0000 Subject: [PATCH 094/101] Add shop sample resolver and picking up item action, fix chicken fence --- .../misthalin/lumbridge/lumbridge.setups.toml | 12 ++++ .../swamp/lumbridge_swamp.item-spawns.toml | 1 - data/bot/firemaking.templates.toml | 3 + data/bot/lumbridge.nav-edges.toml | 8 +-- .../main/kotlin/content/bot/BotCommands.kt | 32 ++++++++--- .../src/main/kotlin/content/bot/BotUpdates.kt | 13 +++-- .../bot/behaviour/action/ActionParser.kt | 17 ++++++ .../content/bot/behaviour/action/BotAction.kt | 56 +++++++++++++++++++ .../bot/behaviour/setup/DynamicResolvers.kt | 45 +++++++++++++++ 9 files changed, 168 insertions(+), 19 deletions(-) diff --git a/data/area/misthalin/lumbridge/lumbridge.setups.toml b/data/area/misthalin/lumbridge/lumbridge.setups.toml index 9f34949c5e..e118f1d284 100644 --- a/data/area/misthalin/lumbridge/lumbridge.setups.toml +++ b/data/area/misthalin/lumbridge/lumbridge.setups.toml @@ -56,4 +56,16 @@ actions = [ produces = [ { item = "training_bow" }, { item = "training_arrows" } +] + +[pick_up_lumbridge_swamp_fishing_net] +requires = [ + { inventory = { id = "empty", min = 1 } }, + { area = { id = "lumbridge_swamp_fishing_area" } } +] +actions = [ + { floor_item = { option = "Take", id = "small_fishing_net", delay = 10, success = { inventory = { id = "small_fishing_net", min = 1 } } } }, +] +produces = [ + { item = "small_fishing_net" }, ] \ No newline at end of file diff --git a/data/area/misthalin/lumbridge/swamp/lumbridge_swamp.item-spawns.toml b/data/area/misthalin/lumbridge/swamp/lumbridge_swamp.item-spawns.toml index d262c90272..ffc137d6b6 100644 --- a/data/area/misthalin/lumbridge/swamp/lumbridge_swamp.item-spawns.toml +++ b/data/area/misthalin/lumbridge/swamp/lumbridge_swamp.item-spawns.toml @@ -19,6 +19,5 @@ spawns = [ { id = "swamp_tar", x = 3193, y = 3181, delay = 100, members = true }, # 12849 { id = "small_fishing_net", x = 3244, y = 3157, delay = 10 }, - { id = "small_fishing_net", x = 3244, y = 3160, delay = 10 }, { id = "leather_gloves", x = 3206, y = 3147, delay = 100 }, ] \ No newline at end of file diff --git a/data/bot/firemaking.templates.toml b/data/bot/firemaking.templates.toml index e4a5d7bfaa..296d78d60e 100644 --- a/data/bot/firemaking.templates.toml +++ b/data/bot/firemaking.templates.toml @@ -1,4 +1,7 @@ [firemaking] +require = [ + { owns = { id = "$logs", min = 54 }} +] setup = [ { area = { id = "$location" } }, { inventory = [{ id = "$logs", min = 27 }, { id = "tinderbox", min = 1 }] }, diff --git a/data/bot/lumbridge.nav-edges.toml b/data/bot/lumbridge.nav-edges.toml index c25bf1c4b3..b87efe7d63 100644 --- a/data/bot/lumbridge.nav-edges.toml +++ b/data/bot/lumbridge.nav-edges.toml @@ -46,10 +46,10 @@ edges = [ { from = { x = 3251, y = 3252 }, to = { x = 3258, y = 3250 } }, { from = { x = 3250, y = 3266 }, to = { x = 3240, y = 3280 } }, # lumbridge_cow_entrance_to_cow_path { from = { x = 3240, y = 3280 }, to = { x = 3238, y = 3295 } }, # lumbridge_cow_path_to_chicken_entrance - { from = { x = 3238, y = 3295 }, to = { x = 3235, y = 3295 }, cost = 0, actions = [{ object = { option = "Open", id = "gate_235_closed", x = 3237, y = 3295, success = { object = { id = "gate_235_opened", x = 3236, y = 3295 } } } }] }, # lumbridge_chicken_entrance_to_chicken_pen - { from = { x = 3235, y = 3295 }, to = { x = 3238, y = 3295 }, cost = 0, actions = [{ object = { option = "Open", id = "gate_237_closed", x = 3237, y = 3296, success = { object = { id = "gate_235_opened", x = 3236, y = 3295 } } } }] }, # lumbridge_chicken_pen_to_chicken_entrance - { from = { x = 3250, y = 3266 }, to = { x = 3255, y = 3266 }, cost = 0, actions = [{ object = { option = "Open", id = "gate_241_closed", x = 3252, y = 3266, success = { object = { id = "gate_239_opened", x = 3253, y = 3267 } } } }] }, # lumbridge_cow_entrance_to_cow_field - { from = { x = 3255, y = 3266 }, to = { x = 3250, y = 3266 }, cost = 0, actions = [{ object = { option = "Open", id = "gate_239_closed", x = 3252, y = 3267, success = { object = { id = "gate_239_opened", x = 3253, y = 3267 } } } }] }, # lumbridge_cow_field_to_cow_entrance + { from = { x = 3238, y = 3295 }, to = { x = 3235, y = 3295 }, cost = 0, actions = [{ object = { option = "Open", id = "gate_235_closed", x = 3237, y = 3295, success = { object = { id = "gate_235_opened", x = 3236, y = 3295 } } } }, { tile = { x = 3235, y = 3295 } }] }, # lumbridge_chicken_entrance_to_chicken_pen + { from = { x = 3235, y = 3295 }, to = { x = 3238, y = 3295 }, cost = 0, actions = [{ object = { option = "Open", id = "gate_237_closed", x = 3237, y = 3296, success = { object = { id = "gate_235_opened", x = 3236, y = 3295 } } } }, { tile = { x = 3238, y = 3295 } }] }, # lumbridge_chicken_pen_to_chicken_entrance + { from = { x = 3250, y = 3266 }, to = { x = 3255, y = 3266 }, cost = 0, actions = [{ object = { option = "Open", id = "gate_241_closed", x = 3252, y = 3266, success = { object = { id = "gate_239_opened", x = 3253, y = 3267 } } } }, { tile = { x = 3255, y = 3266 } }] }, # lumbridge_cow_entrance_to_cow_field + { from = { x = 3255, y = 3266 }, to = { x = 3250, y = 3266 }, cost = 0, actions = [{ object = { option = "Open", id = "gate_239_closed", x = 3252, y = 3267, success = { object = { id = "gate_239_opened", x = 3253, y = 3267 } } } }, { tile = { x = 3250, y = 3266 } }] }, # lumbridge_cow_field_to_cow_entrance { from = { x = 3244, y = 3190 }, to = { x = 3241, y = 3176 } }, # lumbridge_graveyard_exit_to_swamp_path { from = { x = 3241, y = 3176 }, to = { x = 3239, y = 3160 } }, # lumbridge_swamp_path_to_swamp_cross_roads { from = { x = 3244, y = 3190 }, to = { x = 3231, y = 3188 } }, # lumbridge_graveyard_exit_to_rats_north_east diff --git a/game/src/main/kotlin/content/bot/BotCommands.kt b/game/src/main/kotlin/content/bot/BotCommands.kt index 7efb15cdee..ab9ffe772a 100644 --- a/game/src/main/kotlin/content/bot/BotCommands.kt +++ b/game/src/main/kotlin/content/bot/BotCommands.kt @@ -1,5 +1,7 @@ package content.bot +import content.entity.player.bank.bank +import content.quest.questJournal import kotlinx.coroutines.* import world.gregs.voidps.engine.Contexts import world.gregs.voidps.engine.Script @@ -10,6 +12,7 @@ import world.gregs.voidps.engine.client.command.stringArg import world.gregs.voidps.engine.client.message import world.gregs.voidps.engine.data.AccountManager import world.gregs.voidps.engine.data.Settings +import world.gregs.voidps.engine.data.definition.AccountDefinitions import world.gregs.voidps.engine.data.definition.Areas import world.gregs.voidps.engine.data.definition.EnumDefinitions import world.gregs.voidps.engine.data.definition.StructDefinitions @@ -19,6 +22,7 @@ import world.gregs.voidps.engine.entity.character.move.running import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.entity.character.player.appearance import world.gregs.voidps.engine.entity.character.player.chat.ChatType +import world.gregs.voidps.engine.entity.character.player.name import world.gregs.voidps.engine.entity.character.player.sex import world.gregs.voidps.engine.inv.add import world.gregs.voidps.engine.inv.inventory @@ -38,6 +42,7 @@ class BotCommands( val loader: PlayerAccountLoader, val manager: BotManager, val accounts: AccountManager, + val accountDefinitions: AccountDefinitions, ) : Script { val bots = mutableListOf() @@ -73,7 +78,7 @@ class BotCommands( adminCommand("bots", intArg("count", optional = true), desc = "Spawn (count) number of bots", handler = ::spawn) adminCommand("clear_bots", intArg("count", optional = true), desc = "Clear all or some amount of bots", handler = ::clear) adminCommand("bot", stringArg("task", optional = true, autofill = manager.activityNames), desc = "Toggle yourself on/off as a bot player", handler = ::toggle) - adminCommand("bot_info", desc = "Print bot info", handler = ::info) + adminCommand("bot_info", stringArg("name", optional = true, desc = "Filter by bot name", autofill = accountDefinitions.displayNames.keys), desc = "Print bot info", handler = ::info) } private fun loadSettings() { @@ -124,11 +129,24 @@ class BotCommands( } fun info(player: Player, args: List) { - val bot = player.bot - player.message("Available activities:", ChatType.Console) - for (activity in bot.available) { - player.message(" $activity", ChatType.Console) + if (player.isBot) { + player.message("Available activities:", ChatType.Console) + for (activity in player.bot.available) { + player.message(" $activity", ChatType.Console) + } + } + val filter = args.getOrNull(0) + val info = mutableListOf() + for (bot in manager.bots) { + if (filter != null && !filter.equals(bot.player.name, ignoreCase = true)) { + continue + } + info.add(bot.player.name) + for (frame in bot.frames) { + info.add("${frame.behaviour.id} - ${frame.state}") + } } + player.questJournal("Bots List", info.take(300)) } fun toggle(player: Player, args: List) { @@ -171,9 +189,7 @@ class BotCommands( val bot = player.initBot() loader.connect(player, DummyClient(), viewport = Settings["development.bots.live", false]) setAppearance(player) - if (player.inventory.isEmpty()) { - player.inventory.add("coins", 10000) - } + player.inventory.add("coins", 10000) player.viewport?.loaded = true delay(3) manager.add(bot) diff --git a/game/src/main/kotlin/content/bot/BotUpdates.kt b/game/src/main/kotlin/content/bot/BotUpdates.kt index 7fcc537345..c079f92e90 100644 --- a/game/src/main/kotlin/content/bot/BotUpdates.kt +++ b/game/src/main/kotlin/content/bot/BotUpdates.kt @@ -5,13 +5,11 @@ import content.bot.behaviour.navigation.NavigationGraph import content.bot.behaviour.setup.DynamicResolvers import world.gregs.voidps.cache.config.data.InventoryDefinition import world.gregs.voidps.engine.Script -import world.gregs.voidps.engine.data.definition.Areas import world.gregs.voidps.engine.data.definition.InventoryDefinitions import world.gregs.voidps.engine.data.definition.ItemDefinitions import world.gregs.voidps.engine.entity.character.npc.NPC import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.network.client.instruction.InteractDialogue -import world.gregs.voidps.type.Tile /** * Listen for state changes which would change which activities are available to a bot @@ -74,8 +72,11 @@ class BotUpdates( npcSpawn { if (def.contains("shop")) { - val def = inventoryDefinitions.get(def.get("shop")) - registerShop(this, def) + val shop = def.get("shop") + val def = inventoryDefinitions.get(shop) + registerShop(this, def, DynamicResolvers.shopItems) + val sample = inventoryDefinitions.getOrNull("${shop}_sample") ?: return@npcSpawn + registerShop(this, sample, DynamicResolvers.sampleItems) } } } @@ -94,7 +95,7 @@ class BotUpdates( } } - private fun registerShop(npc: NPC, definition: InventoryDefinition) { + private fun registerShop(npc: NPC, definition: InventoryDefinition, map: MutableMap>>) { val area: String = npc.def.getOrNull("area") ?: return val ids = definition.ids ?: return val amounts = definition.amounts ?: return @@ -105,7 +106,7 @@ class BotUpdates( continue } val def = ItemDefinitions.getOrNull(id) ?: continue - DynamicResolvers.shopItems.getOrPut(def.stringId) { mutableListOf() }.add(area to npc.id) + map.getOrPut(def.stringId) { mutableListOf() }.add(area to npc.id) } } } diff --git a/game/src/main/kotlin/content/bot/behaviour/action/ActionParser.kt b/game/src/main/kotlin/content/bot/behaviour/action/ActionParser.kt index 9168f77725..8df901ba03 100644 --- a/game/src/main/kotlin/content/bot/behaviour/action/ActionParser.kt +++ b/game/src/main/kotlin/content/bot/behaviour/action/ActionParser.kt @@ -121,6 +121,22 @@ sealed class ActionParser { } } + object InteractFloorItemParser : ActionParser() { + override val required = setOf("option", "id", "success") + override val optional = setOf("delay", "radius", "x", "y") + + override fun parse(map: Map): BotAction { + val option = map["option"] as String + val id = map["id"] as String + val delay = map["delay"] as? Int ?: 0 + val success = requirement(map, "success").singleOrNull() + val radius = map["radius"] as? Int ?: 10 + val x = map["x"] as? Int + val y = map["y"] as? Int + return BotAction.InteractFloorItem(option, id, delay, success, radius, x, y) + } + } + object GoToParser : ActionParser() { override val optional = setOf("area", "nearest") override fun parse(map: Map) = when { @@ -208,6 +224,7 @@ sealed class ActionParser { private val parsers = mapOf( "npc" to InteractNpcParser, "object" to InteractObjectParser, + "floor_item" to InteractFloorItemParser, "item_on_object" to ItemOnObjectParser, "item_on_item" to ItemOnItemParser, "go_to" to GoToParser, diff --git a/game/src/main/kotlin/content/bot/behaviour/action/BotAction.kt b/game/src/main/kotlin/content/bot/behaviour/action/BotAction.kt index b422976c45..aa0fe515dc 100644 --- a/game/src/main/kotlin/content/bot/behaviour/action/BotAction.kt +++ b/game/src/main/kotlin/content/bot/behaviour/action/BotAction.kt @@ -27,6 +27,7 @@ import world.gregs.voidps.engine.entity.character.npc.NPCs import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.entity.character.player.skill.Skill import world.gregs.voidps.engine.entity.item.Item +import world.gregs.voidps.engine.entity.item.floor.FloorItem import world.gregs.voidps.engine.entity.item.floor.FloorItems import world.gregs.voidps.engine.entity.obj.GameObject import world.gregs.voidps.engine.entity.obj.GameObjects @@ -322,6 +323,61 @@ sealed interface BotAction { } } + data class InteractFloorItem( + val option: String, + val id: String, + val delay: Int = 0, + val success: Condition? = null, + val radius: Int = 10, + val x: Int? = null, + val y: Int? = null, + ) : BotAction { + override fun start(bot: Bot, frame: BehaviourFrame) = BehaviourState.Running + + override fun update(bot: Bot, frame: BehaviourFrame) = when { + success?.check(bot.player) == true -> BehaviourState.Success + bot.mode is PlayerOnFloorItemInteract -> if (success == null) BehaviourState.Success else BehaviourState.Running + bot.mode is EmptyMode -> search(bot) + else -> null + } + + private fun search(bot: Bot): BehaviourState { + val player = bot.player + val start = if (x != null && y != null) player.tile.copy(x = x, y = y) else player.tile + for (tile in Spiral.spiral(start, radius)) { + for (obj in FloorItems.at(tile)) { + return interact(player, obj) ?: continue + } + } + return handleNoTarget() + } + + private fun interact(player: Player, item: FloorItem): BehaviourState? { + if (!wildcardEquals(id, item.id)) { + return null + } + val index = item.def.floorOptions.indexOf(option) + if (index == -1) { + return null + } + val valid = get().handle(player, InteractFloorItem(item.def.id, item.tile.x, item.tile.y, index + 1)) + if (!valid) { + return BehaviourState.Failed(Reason.Invalid("Invalid floor item interaction: $item ${index + 1}")) + } + return BehaviourState.Running + } + + private fun handleNoTarget(): BehaviourState { + if (success == null) { + return BehaviourState.Failed(Reason.NoTarget) + } + if (delay > 0) { + return BehaviourState.Wait(delay, BehaviourState.Running) + } + return BehaviourState.Running + } + } + data class ItemOnItem(val item: String, val on: String, val success: Condition? = null) : BotAction { override fun update(bot: Bot, frame: BehaviourFrame): BehaviourState? { if (success != null && success.check(bot.player)) { diff --git a/game/src/main/kotlin/content/bot/behaviour/setup/DynamicResolvers.kt b/game/src/main/kotlin/content/bot/behaviour/setup/DynamicResolvers.kt index c68df77777..108605e2b4 100644 --- a/game/src/main/kotlin/content/bot/behaviour/setup/DynamicResolvers.kt +++ b/game/src/main/kotlin/content/bot/behaviour/setup/DynamicResolvers.kt @@ -21,6 +21,7 @@ object DynamicResolvers { // location, npc val shopItems = mutableMapOf>>() + val sampleItems = mutableMapOf>>() fun resolver(player: Player, condition: Condition): Resolver? = when (condition) { is Condition.InArea -> Resolver("go_to_area", -1, actions = listOf(BotAction.GoTo(condition.id))) @@ -38,6 +39,10 @@ object DynamicResolvers { if (resolver != null) { return resolver } + resolver = depositItems(player, items) + if (resolver != null) { + return resolver + } return null } @@ -48,6 +53,31 @@ object DynamicResolvers { continue } for (id in entry.ids) { + for ((location, npc) in sampleItems[id] ?: continue) { + val actions = mutableListOf() + actions.add(BotAction.GoTo(location)) + actions.add(BotAction.InteractNpc("Trade", npc, success = Condition.InterfaceOpen("shop"))) + var remaining = amount + while (remaining > 0) { + val amount = when { + remaining >= 50 -> 50 + remaining >= 10 -> 10 + remaining >= 5 -> 5 + else -> 1 + } + actions.add(BotAction.InterfaceOption("Take-${amount}", "shop:sample:${id}")) + remaining -= amount + } + actions.add(BotAction.CloseInterface) + val spaces = if (player.inventory.stackable(id)) 1 else amount + return Resolver( + id = "take_from_shop", + weight = 20, + setup = listOf(Condition.Inventory(listOf(Entry(setOf("empty"), min = spaces)))), + actions = actions, + produces = setOf("item:${id}") + ) + } for ((location, npc) in shopItems[id] ?: continue) { val actions = mutableListOf() val price = Price.of(id) @@ -137,6 +167,21 @@ object DynamicResolvers { return null } + private fun depositItems(player: Player, items: List): Resolver? { + val empty = items.firstOrNull { it.ids.contains("empty") } ?: return null + val spaces = player.inventory.spaces + if (empty.min != null && spaces > empty.min) { + return null + } + val actions = mutableListOf( + BotAction.GoToNearest("bank"), + BotAction.InteractObject("Use-quickly", "bank_booth*", success = Condition.InterfaceOpen("bank")), + BotAction.InterfaceOption("Deposit carried items", "bank:carried", success = Condition.Inventory(listOf(Entry(setOf("empty"), min = 28)))), + BotAction.CloseInterface, + ) + return Resolver("deposit_all_bank", weight = 20, actions = actions) + } + private fun equipItems(player: Player, equipment: Map): Resolver? { val actions = mutableListOf() val produces = mutableSetOf() From 3fe7c4af62c78b5be62dd5da64528fe7a445a8dd Mon Sep 17 00:00:00 2001 From: GregHib Date: Wed, 11 Feb 2026 16:49:25 +0000 Subject: [PATCH 095/101] Formatting --- game/src/main/kotlin/content/bot/BotCommands.kt | 1 - game/src/main/kotlin/content/bot/behaviour/Condition.kt | 2 +- .../kotlin/content/bot/behaviour/action/ActionParser.kt | 2 +- .../main/kotlin/content/bot/behaviour/action/BotAction.kt | 4 +--- .../content/bot/behaviour/setup/DynamicResolvers.kt | 8 ++++---- game/src/main/resources/game.properties | 2 +- 6 files changed, 8 insertions(+), 11 deletions(-) diff --git a/game/src/main/kotlin/content/bot/BotCommands.kt b/game/src/main/kotlin/content/bot/BotCommands.kt index ab9ffe772a..2904607127 100644 --- a/game/src/main/kotlin/content/bot/BotCommands.kt +++ b/game/src/main/kotlin/content/bot/BotCommands.kt @@ -1,6 +1,5 @@ package content.bot -import content.entity.player.bank.bank import content.quest.questJournal import kotlinx.coroutines.* import world.gregs.voidps.engine.Contexts diff --git a/game/src/main/kotlin/content/bot/behaviour/Condition.kt b/game/src/main/kotlin/content/bot/behaviour/Condition.kt index d4f0024765..1194275a1a 100644 --- a/game/src/main/kotlin/content/bot/behaviour/Condition.kt +++ b/game/src/main/kotlin/content/bot/behaviour/Condition.kt @@ -198,7 +198,7 @@ sealed class Condition(val priority: Int) { } data class HasMode(val id: String) : Condition(1) { - override fun keys() = setOf("mode:${id}") + override fun keys() = setOf("mode:$id") override fun events() = setOf("mode") override fun check(player: Player) = when (id) { "empty" -> player.mode == EmptyMode diff --git a/game/src/main/kotlin/content/bot/behaviour/action/ActionParser.kt b/game/src/main/kotlin/content/bot/behaviour/action/ActionParser.kt index 8df901ba03..33ae59161a 100644 --- a/game/src/main/kotlin/content/bot/behaviour/action/ActionParser.kt +++ b/game/src/main/kotlin/content/bot/behaviour/action/ActionParser.kt @@ -177,7 +177,7 @@ sealed class ActionParser { val waitIf = if (wait is List<*>) { val requirements = mutableListOf() wait as List> - for(element in wait) { + for (element in wait) { requirements.addAll(requirement(element)) } requirements diff --git a/game/src/main/kotlin/content/bot/behaviour/action/BotAction.kt b/game/src/main/kotlin/content/bot/behaviour/action/BotAction.kt index aa0fe515dc..9b1ab3a075 100644 --- a/game/src/main/kotlin/content/bot/behaviour/action/BotAction.kt +++ b/game/src/main/kotlin/content/bot/behaviour/action/BotAction.kt @@ -634,8 +634,6 @@ sealed interface BotAction { } } - private fun cantLightOn(tile: Tile): Boolean { - return GameObjects.getLayer(tile, ObjectLayer.GROUND) != null - } + private fun cantLightOn(tile: Tile): Boolean = GameObjects.getLayer(tile, ObjectLayer.GROUND) != null } } diff --git a/game/src/main/kotlin/content/bot/behaviour/setup/DynamicResolvers.kt b/game/src/main/kotlin/content/bot/behaviour/setup/DynamicResolvers.kt index 108605e2b4..9e3d35d2bb 100644 --- a/game/src/main/kotlin/content/bot/behaviour/setup/DynamicResolvers.kt +++ b/game/src/main/kotlin/content/bot/behaviour/setup/DynamicResolvers.kt @@ -65,7 +65,7 @@ object DynamicResolvers { remaining >= 5 -> 5 else -> 1 } - actions.add(BotAction.InterfaceOption("Take-${amount}", "shop:sample:${id}")) + actions.add(BotAction.InterfaceOption("Take-$amount", "shop:sample:$id")) remaining -= amount } actions.add(BotAction.CloseInterface) @@ -75,7 +75,7 @@ object DynamicResolvers { weight = 20, setup = listOf(Condition.Inventory(listOf(Entry(setOf("empty"), min = spaces)))), actions = actions, - produces = setOf("item:${id}") + produces = setOf("item:$id"), ) } for ((location, npc) in shopItems[id] ?: continue) { @@ -95,7 +95,7 @@ object DynamicResolvers { remaining >= 5 -> 5 else -> 1 } - actions.add(BotAction.InterfaceOption("Buy-${amount}", "shop:stock:${id}")) + actions.add(BotAction.InterfaceOption("Buy-$amount", "shop:stock:$id")) remaining -= amount } actions.add(BotAction.CloseInterface) @@ -105,7 +105,7 @@ object DynamicResolvers { weight = 25, setup = listOf(Condition.Inventory(listOf(Entry(setOf("coins"), min = price * amount), Entry(setOf("empty"), min = spaces)))), actions = actions, - produces = setOf("item:${id}") + produces = setOf("item:$id"), ) } } diff --git a/game/src/main/resources/game.properties b/game/src/main/resources/game.properties index 807e360609..5f5651853e 100644 --- a/game/src/main/resources/game.properties +++ b/game/src/main/resources/game.properties @@ -236,7 +236,7 @@ fightCave.startWave = 1 #=================================== # The number of AI-controlled bots spawned on startup -bots.count=10 +bots.count=30 # Frequency between spawning bots on startup bots.spawnSeconds=60 From 6b1c1dae27d5c781f17294096a34c639e09d7e4b Mon Sep 17 00:00:00 2001 From: GregHib Date: Wed, 11 Feb 2026 17:38:09 +0000 Subject: [PATCH 096/101] Tweaks --- data/bot/firemaking.templates.toml | 2 +- data/bot/teleport.shortcuts.toml | 2 ++ .../main/kotlin/content/bot/behaviour/Condition.kt | 12 ++++++------ 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/data/bot/firemaking.templates.toml b/data/bot/firemaking.templates.toml index 296d78d60e..af10753254 100644 --- a/data/bot/firemaking.templates.toml +++ b/data/bot/firemaking.templates.toml @@ -1,5 +1,5 @@ [firemaking] -require = [ +requires = [ { owns = { id = "$logs", min = 54 }} ] setup = [ diff --git a/data/bot/teleport.shortcuts.toml b/data/bot/teleport.shortcuts.toml index bf0be6cff8..8a968cea77 100644 --- a/data/bot/teleport.shortcuts.toml +++ b/data/bot/teleport.shortcuts.toml @@ -28,6 +28,7 @@ [teleport_varrock] weight = 125 requires = [ + { skill = { id = "magic", min = 25 } }, { inventory = [ { id = "fire_rune", min = 1 }, { id = "air_rune", min = 3 }, @@ -46,6 +47,7 @@ produces = [ [teleport_varrock_via_bank] weight = 150 requires = [ + { skill = { id = "magic", min = 25 } }, { variable = { id = "spellbook_config", equals = 0, default = 0 }}, { bank = [ { id = "fire_rune", min = 1 }, diff --git a/game/src/main/kotlin/content/bot/behaviour/Condition.kt b/game/src/main/kotlin/content/bot/behaviour/Condition.kt index 1194275a1a..b4d758d24d 100644 --- a/game/src/main/kotlin/content/bot/behaviour/Condition.kt +++ b/game/src/main/kotlin/content/bot/behaviour/Condition.kt @@ -112,13 +112,13 @@ sealed class Condition(val priority: Int) { data class Inventory(val items: List) : Condition(100) { override fun keys() = items.flatMap { entry -> entry.ids.map { "item:$it" } }.toSet() - override fun events() = setOf("inventory") + override fun events() = setOf("inv:inventory") override fun check(player: Player) = contains(player, player.inventory, items) } data class Equipment(val items: Map) : Condition(90) { override fun keys() = items.values.flatMap { entry -> entry.ids.map { "item:$it" } }.toSet() - override fun events() = setOf("worn_equipment") + override fun events() = setOf("inv:worn_equipment") override fun check(player: Player): Boolean { for ((slot, entry) in items) { @@ -145,13 +145,13 @@ sealed class Condition(val priority: Int) { data class Bank(val items: List) : Condition(80) { override fun keys() = items.flatMap { entry -> entry.ids.map { "bank:$it" } }.toSet() - override fun events() = setOf("bank") + override fun events() = setOf("inv:bank") override fun check(player: Player) = contains(player, player.bank, items) } data class Owns(val id: String, val min: Int? = null, val max: Int? = null) : Condition(110) { override fun keys() = setOf("item:$id") - override fun events() = setOf("worn_equipment", "inventory", "bank") + override fun events() = setOf("inv:worn_equipment", "inv:inventory", "inv:bank") override fun check(player: Player): Boolean { val count = player.inventory.count(id) + player.bank.count(id) + player.equipment.count(id) return inRange(count, min, max) @@ -160,13 +160,13 @@ sealed class Condition(val priority: Int) { data class Variable(val id: String, val equals: Any, val default: Any) : Condition(1) { override fun keys() = setOf("var:$id") - override fun events() = setOf("variable") + override fun events() = setOf("var:$id") override fun check(player: Player) = player.variables.get(id, default) == equals } data class VariableIn(val id: String, val default: Int, val min: Int?, val max: Int?) : Condition(1) { override fun keys() = setOf("var:$id") - override fun events() = setOf("variable") + override fun events() = setOf("var:$id") override fun check(player: Player): Boolean { val value = player.variables.get(id, default) return inRange(value, min, max) From b8b48d3e967e075a82d6bbe628b2a0e2520aa05c Mon Sep 17 00:00:00 2001 From: GregHib Date: Wed, 11 Feb 2026 17:39:39 +0000 Subject: [PATCH 097/101] Add crayfish cooking bot --- data/area/misthalin/lumbridge/lumbridge.bots.toml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/data/area/misthalin/lumbridge/lumbridge.bots.toml b/data/area/misthalin/lumbridge/lumbridge.bots.toml index 81fce1bff8..27b53d3544 100644 --- a/data/area/misthalin/lumbridge/lumbridge.bots.toml +++ b/data/area/misthalin/lumbridge/lumbridge.bots.toml @@ -254,6 +254,11 @@ template = "cooking_template" capacity = 4 fields = { raw = "raw_chicken", cooked = "chicken", level = 1, location = "lumbridge_kitchen", obj = "cooking_range_lumbridge_castle", burnt = "burnt_chicken" } +[lumbridge_cook_crayfish] +template = "cooking_template" +capacity = 4 +fields = { raw = "raw_crayfish", cooked = "crayfish", level = 1, location = "lumbridge_kitchen", obj = "cooking_range_lumbridge_castle", burnt = "burnt_crayfish" } + [lumbridge_cook_beef] template = "beef_template" capacity = 4 From f47ddaa81ea1794ec978cc89bca24ee0c312df7a Mon Sep 17 00:00:00 2001 From: GregHib Date: Mon, 16 Feb 2026 23:38:12 +0000 Subject: [PATCH 098/101] Fix viewport --- .../gregs/voidps/engine/entity/character/player/Player.kt | 3 ++- game/src/main/kotlin/content/entity/world/RegionLoading.kt | 2 +- game/src/test/kotlin/WorldTest.kt | 3 +-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/player/Player.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/player/Player.kt index af834e9ae7..c047ca5687 100644 --- a/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/player/Player.kt +++ b/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/player/Player.kt @@ -27,6 +27,7 @@ import world.gregs.voidps.engine.suspend.Suspension import world.gregs.voidps.engine.timer.TimerQueue import world.gregs.voidps.engine.timer.Timers import world.gregs.voidps.network.client.Client +import world.gregs.voidps.network.client.DummyClient import world.gregs.voidps.network.client.Instruction import world.gregs.voidps.network.login.protocol.visual.PlayerVisuals import world.gregs.voidps.type.Tile @@ -84,7 +85,7 @@ class Player( // val area: AreaQueue = AreaQueue(this) val networked: Boolean - get() = client != null && viewport != null + get() = client !is DummyClient && viewport != null override var suspension: Suspension? = null diff --git a/game/src/main/kotlin/content/entity/world/RegionLoading.kt b/game/src/main/kotlin/content/entity/world/RegionLoading.kt index 6f2d51ae90..723c14beb9 100644 --- a/game/src/main/kotlin/content/entity/world/RegionLoading.kt +++ b/game/src/main/kotlin/content/entity/world/RegionLoading.kt @@ -112,7 +112,7 @@ class RegionLoading(val dynamicZones: DynamicZones) : Script { if ((dynamic || wasDynamic) && !initial) { viewport.npcs.clear() } - if (!player.isBot) { + if (player.networked) { viewport.loaded = false } viewport.lastLoadZone = player.tile.zone diff --git a/game/src/test/kotlin/WorldTest.kt b/game/src/test/kotlin/WorldTest.kt index 063ba30cd8..a9af84f589 100644 --- a/game/src/test/kotlin/WorldTest.kt +++ b/game/src/test/kotlin/WorldTest.kt @@ -100,7 +100,7 @@ abstract class WorldTest : KoinTest { fun createPlayer(tile: Tile = Tile.EMPTY, name: String = "player"): Player { val player = Player(tile = tile, accountName = name, passwordHash = "") - assertTrue(accounts.setup(player, DummyClient(), 0, false)) + assertTrue(accounts.setup(player, DummyClient(), 0, viewport = true)) accountDefs.add(player) tick() player["creation"] = -1 @@ -109,7 +109,6 @@ abstract class WorldTest : KoinTest { player.softTimers.clear("restore_stats") player.softTimers.clear("restore_hitpoints") tick() - player.viewport = Viewport() player.viewport?.loaded = true return player } From 87288fcf4feea385010ce2270b9ece790b52095d Mon Sep 17 00:00:00 2001 From: GregHib Date: Mon, 16 Feb 2026 23:49:00 +0000 Subject: [PATCH 099/101] Fix potion consumption insert order --- .../main/kotlin/content/skill/constitution/drink/Potions.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/game/src/main/kotlin/content/skill/constitution/drink/Potions.kt b/game/src/main/kotlin/content/skill/constitution/drink/Potions.kt index ad8a7b8562..ab08148e95 100644 --- a/game/src/main/kotlin/content/skill/constitution/drink/Potions.kt +++ b/game/src/main/kotlin/content/skill/constitution/drink/Potions.kt @@ -25,7 +25,10 @@ import java.util.concurrent.TimeUnit class Potions : Script { init { - consumed("*_4,*_3,*_2,*_1") { item, slot -> + consumed("*") { item, slot -> + if (!item.id.endsWith("_1") && !item.id.endsWith("_2") && !item.id.endsWith("_3") && !item.id.endsWith("_4")) { + return@consumed + } val doses = item.id.last().digitToInt() if (doses != 1) { message("You have ${doses - 1} ${"dose".plural(doses - 1)} of the potion left.") From 634b28821a3a0df30cecfff3dacb7f2d34fcbbaf Mon Sep 17 00:00:00 2001 From: GregHib Date: Mon, 16 Feb 2026 23:51:08 +0000 Subject: [PATCH 100/101] Formatting --- game/src/main/kotlin/content/bot/BotCommands.kt | 2 +- game/src/main/kotlin/content/entity/world/RegionLoading.kt | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/game/src/main/kotlin/content/bot/BotCommands.kt b/game/src/main/kotlin/content/bot/BotCommands.kt index 2904607127..0013d2341e 100644 --- a/game/src/main/kotlin/content/bot/BotCommands.kt +++ b/game/src/main/kotlin/content/bot/BotCommands.kt @@ -41,7 +41,7 @@ class BotCommands( val loader: PlayerAccountLoader, val manager: BotManager, val accounts: AccountManager, - val accountDefinitions: AccountDefinitions, + accountDefinitions: AccountDefinitions, ) : Script { val bots = mutableListOf() diff --git a/game/src/main/kotlin/content/entity/world/RegionLoading.kt b/game/src/main/kotlin/content/entity/world/RegionLoading.kt index 723c14beb9..e9759c6a8d 100644 --- a/game/src/main/kotlin/content/entity/world/RegionLoading.kt +++ b/game/src/main/kotlin/content/entity/world/RegionLoading.kt @@ -1,6 +1,5 @@ package content.entity.world -import content.bot.isBot import world.gregs.voidps.engine.Script import world.gregs.voidps.engine.client.instruction.instruction import world.gregs.voidps.engine.client.update.view.Viewport From 95a34bdb5fd912d4a9a63bc0ffbeb3aee160589c Mon Sep 17 00:00:00 2001 From: GregHib Date: Tue, 17 Feb 2026 11:14:21 +0000 Subject: [PATCH 101/101] Better viewport fix --- .../kotlin/world/gregs/voidps/engine/data/AccountManager.kt | 2 +- .../gregs/voidps/engine/entity/character/player/Player.kt | 3 +-- game/src/test/kotlin/WorldTest.kt | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/data/AccountManager.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/data/AccountManager.kt index fea091feb9..9633afbe3f 100644 --- a/engine/src/main/kotlin/world/gregs/voidps/engine/data/AccountManager.kt +++ b/engine/src/main/kotlin/world/gregs/voidps/engine/data/AccountManager.kt @@ -49,7 +49,7 @@ class AccountManager( this["new_player"] = true } - fun setup(player: Player, client: Client, displayMode: Int, viewport: Boolean = true): Boolean { + fun setup(player: Player, client: Client?, displayMode: Int, viewport: Boolean = true): Boolean { player.index = Players.index() ?: return false player.visuals.hits.self = player.index player.interfaces = Interfaces(player, interfaceDefinitions) diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/player/Player.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/player/Player.kt index c047ca5687..af834e9ae7 100644 --- a/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/player/Player.kt +++ b/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/player/Player.kt @@ -27,7 +27,6 @@ import world.gregs.voidps.engine.suspend.Suspension import world.gregs.voidps.engine.timer.TimerQueue import world.gregs.voidps.engine.timer.Timers import world.gregs.voidps.network.client.Client -import world.gregs.voidps.network.client.DummyClient import world.gregs.voidps.network.client.Instruction import world.gregs.voidps.network.login.protocol.visual.PlayerVisuals import world.gregs.voidps.type.Tile @@ -85,7 +84,7 @@ class Player( // val area: AreaQueue = AreaQueue(this) val networked: Boolean - get() = client !is DummyClient && viewport != null + get() = client != null && viewport != null override var suspension: Suspension? = null diff --git a/game/src/test/kotlin/WorldTest.kt b/game/src/test/kotlin/WorldTest.kt index a9af84f589..4533b93356 100644 --- a/game/src/test/kotlin/WorldTest.kt +++ b/game/src/test/kotlin/WorldTest.kt @@ -100,7 +100,7 @@ abstract class WorldTest : KoinTest { fun createPlayer(tile: Tile = Tile.EMPTY, name: String = "player"): Player { val player = Player(tile = tile, accountName = name, passwordHash = "") - assertTrue(accounts.setup(player, DummyClient(), 0, viewport = true)) + assertTrue(accounts.setup(player, null, 0, viewport = true)) accountDefs.add(player) tick() player["creation"] = -1