commit 0e25cc9a738da3c3dd75cf2384ea07302e05c2c0 Author: Rauru <38548565+AardRauru@users.noreply.github.com> Date: Sat Nov 17 15:08:27 2018 -0700 reupload diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a43a58b --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.db +/db_backups/ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..1f1da25 --- /dev/null +++ b/README.md @@ -0,0 +1,78 @@ +# Mobber + +A mob database plugin for campaigns and global quests. + +## Instructions + +### Prerequisites + +``` +Mushclient version 5.06+ +Aardmush r1916 snapshot+ +``` + +### Installing + +- [x] File -> Plugins -> Add -> Rauru_Mobber.xml +- [ ] File -> Plugins -> Add -> Rauru_Mobber_Miniwindow.xml (optional) + +## New Installation Setup + +A few commands are suggested to start on the right foot. + +``` +1. Enter 'mobber developer' to enable developer mode. +2. Enter 'mobber update areas' to add the default areas to mobber. +3. Enter 'mobber update rooms' to import your rooms from Aardwolf.db to mobber. +4. Enter 'mobber developer' to disable developer mode. +5. Enter 'mobber show areas' to verify areas and rooms were added. +6. Check 'mobber help' for all of the commands and to start logging mobs. +``` + +## Frequently Asked Questions + +1. How do I upgrade to a new version of mobber? + - Download the new version of mobber. + - Drag and drop the mobber database and backups folder into the new version folder. + - Delete the old folder. + - Put the new version folder into your plugins folder. + +2. Why are some mobs in cp/gq check different colors? + - Red: The mob is currently dead. + - Yellow: The area or room has not been logged. + - Gray: The matching mob or room is outside of your level range. + - Blue: A matching area was found. + - Light Blue: A matching mob or room was found within your level range. + +3. Why am I getting error prompts when trying to add a mob to a room? + - You are likely trying to add a mob to a room or zone that you have not + added to the mobber database yet. + +## Developer Commands - Further Explained + +1. mobber backup + - Forces a manual backup of the database into /mobber/db_backups/ +2. mobber vacuum + - Defragments and frees up unused space from the database. +3. mobber remove zone + - Removes the current zone and any rooms and mobs that belong to it. + - Usually 'mobber removemob zone' command is sufficient enough. +4. mobber update keywords + - Explicitly generates and adds mob keywords to the database. + - This command must be used before manually assigning keywords. + +# Author + +* **Rauru** - *Plugin Author* + +## Acknowledgments + +* Anymouse - *Tester* +* Castiel - *Tester* +* Crowley - *Tester* +* Gizmmo - *Tester* + +![help](https://i.imgur.com/IywOEBw.png) +![menu1](https://i.imgur.com/AlbjeXo.png) +![menu2](https://i.imgur.com/GEf13X8.png) +![areas](https://i.imgur.com/VG0sAaZ.png) \ No newline at end of file diff --git a/Rauru_Mobber.xml b/Rauru_Mobber.xml new file mode 100644 index 0000000..f4fcbe0 --- /dev/null +++ b/Rauru_Mobber.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/Rauru_Mobber_Miniwindow.xml b/Rauru_Mobber_Miniwindow.xml new file mode 100644 index 0000000..df97c44 --- /dev/null +++ b/Rauru_Mobber_Miniwindow.xml @@ -0,0 +1,772 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/changelog.txt b/changelog.txt new file mode 100644 index 0000000..19bb44d --- /dev/null +++ b/changelog.txt @@ -0,0 +1,71 @@ +Version 2.4.5: +-------------- +*Miniwindow buttons will now default to on +*Autokill can now have stacked commands delimited by a double semicolon ;; + +Version 2.4.4: +-------------- +*Fixed a bug where mw was defaulting to collapsed (Castiel) + +Version 2.4.3: +-------------- +*Added 'mobber quest' command to the help file +*Added 'mobber noexp ' command to auto-handle noexp on campaigns + +Version 2.4.2: +-------------- +*Toggle ak state will now save. (Crowley/Zexe) +*Added newline to end of scan highlighting +*Merged addmob redundant functions +*Added 'mobber quest' command to show quest target/rooms (Crowley) + +Version 2.4.1: +-------------- +*Miniwindow fixed resize issue when autoresize was enabled and the height of window increased +*Miniwindow fixed save state of buttons enabled/disabled + +Version 2.4: +-------------- +*Plugin ids were changed to wipe state files for compatibility +*Mobber find commands (mf, mf , mfa ) now have condensed outputs +*Added lookups for out-of-level-range campaigns +**If a mob or room match is found out-of-level-range the mob will be darkgray +**Changed lookup order for matching mobs, should be slightly more accurate +*Re-wrote miniwindow plugin for better control +**Miniwindow now has more descriptive messages for campaigns and global quest status +**Miniwindow now has a countdown timer for next campaign available +**Miniwindow now has buttons for sending cp/gq commands +**Miniwindow button colors can be changed with 'mobber btn mw' command +**Miniwindow right-click options now include: collapse/expand, autoresize, hide/show btns + +Version 2.3.3: +-------------- +*Global quest alert sound will only play if 'mobber mute quest' is set to off. (Castiel) + +Version 2.3.2: +-------------- +*Fixed global quest complete triggers (Gizmmo) +*Added immhomes to default zone list and changed duplicate roomid of uplanes/lplanes + +Version 2.3.1: +-------------- +*Fixed quest fail message (Castiel) + +Version 2.3: +------------ +*Fixed an nil error in miniwin uncaught due to state files +*Changed miniwin bg +*Changed a column in area table to unique, should have no affect on existing db's + +Version 2.2: +------------ +*Added default target to quick-where +*Right-click popup on miniwindow targets +*Fixed miniwindow reset/hotspot deletion + +Version 2.1: +------------ +*Added hyperlinks to most outputs +*Added "s&d" alias names +*Added a miniwindow plugin +*Fixed level range searches \ No newline at end of file diff --git a/lua/mobber.lua b/lua/mobber.lua new file mode 100644 index 0000000..09c8b42 --- /dev/null +++ b/lua/mobber.lua @@ -0,0 +1,3662 @@ +---------------------------------- +-- Mobber +---------------------------------- + +require("gmcphelper"); +require("sqlitedb"); +require("checkplugin"); +require("stringutils"); +require("tprint"); +require("serialize"); +require("var"); + +local next = next; + +---------------------------------- +-- Campaigns +---------------------------------- + +campaign = {}; + +function campaign.initialize() + AddAlias("alias_campaign_check", "^cp (?:c|ch|che|chec|check)$", "", + alias_flag.Enabled + alias_flag.IgnoreAliasCase + alias_flag.RegularExpression + alias_flag.Temporary, + "campaign.startTargetCapture" + ); + + AddTriggerEx("trigger_campaign_store_target", + "^You still have to kill \\* (?.+?) \\((?.+?)(? - Dead)?\\)$", + "", trigger_flag.RegularExpression + trigger_flag.OmitFromOutput + trigger_flag.Temporary, custom_colour.NoChange, 0, "", + "campaign.storeTarget", sendto.script + ); + + AddTriggerEx("trigger_campaign_inactive", + "^You are not currently on a campaign\\.$", + "", trigger_flag.RegularExpression + trigger_flag.Temporary, custom_colour.NoChange, 0, "", + "campaign.endInactive", sendto.script + ); + + AddTriggerEx("trigger_campaign_end", + "^(?!You)", + "", trigger_flag.RegularExpression + trigger_flag.Temporary + trigger_flag.OmitFromOutput, custom_colour.NoChange, 0, "", + "campaign.endTargetCapture", sendto.script + ); + + AddTriggerEx("trigger_campaign_update", + "^Congratulations, that was one of your CAMPAIGN mobs!$", + "", trigger_flag.Enabled + trigger_flag.RegularExpression + trigger_flag.Temporary, custom_colour.NoChange, 0, "", + "campaign.update", sendto.script + ); + + AddTriggerEx("trigger_campaign_reset", + "^CONGRATULATIONS! You have completed your campaign\\.$", + "", trigger_flag.Enabled + trigger_flag.RegularExpression + trigger_flag.Temporary, custom_colour.NoChange, 0, "", + "campaign.reset", sendto.script + ); + + campaign.targets = {}; + campaign.currentTargetIndex = nil; +end + +function campaign.startTargetCapture() + campaign.reset(); + + EnableTrigger("trigger_campaign_store_target", true); + EnableTrigger("trigger_campaign_inactive", true); + + Execute("campaign check"); +end + +function campaign.storeTarget(name, line, wildcards) + EnableTrigger("trigger_campaign_end", true); + EnableTrigger("trigger_campaign_inactive", false); + + local target = mobTarget:new{ + name = wildcards.mob_name:lower(), + location = wildcards.location, + isDead = wildcards.is_dead ~= "" and true or false, + amount = 1 + }; + + target:initialize(); + + table.insert(campaign.targets, target); +end + +function campaign.endTargetCapture() + EnableTrigger("trigger_campaign_store_target", false); + EnableTrigger("trigger_campaign_end", false); + + table.sort(campaign.targets, campaign.sort); + display.printTargets(campaign.targets, "CP"); +end + +function campaign.endInactive() + EnableTrigger("trigger_campaign_store_target", false); + EnableTrigger("trigger_campaign_inactive", false); + campaign.reset(); + display.printTargets(nil, "CP"); +end + +function campaign.update() + local targets = campaign.targets; + local index = campaign.currentTargetIndex; + + if (next(targets)) then + if (index and targets[index]) then + local target = targets[index]; + + local playerEnemy = utility.gmcp.getPlayerEnemy(); + local playerZone = utility.gmcp.getPlayerZone(); + + if (target.name == playerEnemy or target.rooms[1].zone_name == playerZone) then + table.remove(targets, index); + + roomHandler.reset(); + display.printTargets(targets, "CP"); + else + campaign.startTargetCapture(); + end + else + campaign.startTargetCapture(); + end + end +end + +function campaign.sort(mob1, mob2) + local isMob1Dead = mob1.isDead and 1 or 0; + local isMob2Dead = mob2.isDead and 1 or 0; + + local mob1Zone = mob1.rooms[1].zone_name:lower(); + local mob2Zone = mob2.rooms[1].zone_name:lower(); + + if (isMob1Dead == isMob2Dead and mob1Zone == mob2Zone) then + return mob1.name < mob2.name; + elseif (isMob1Dead == isMob2Dead) then + return mob1Zone < mob2Zone; + else + return isMob1Dead < isMob2Dead; + end +end + +function campaign.reset() + campaign.targets = {}; + campaign.currentTargetIndex = nil; + roomHandler.reset(); +end + +---------------------------------- +-- Global Quests +---------------------------------- + +globalQuest = {}; + +function globalQuest.initialize() + AddAlias("alias_global_quest_check", "^gq (?:c|ch|che|chec|check)$", "", + alias_flag.Enabled + alias_flag.IgnoreAliasCase + alias_flag.RegularExpression + alias_flag.Temporary, + "globalQuest.startTargetCapture" + ); + + AddTriggerEx("trigger_global_quest_store_target", + "^You still have to kill (?\\d+) \\* (?.+?) \\((?.+?)\\)$", + "", trigger_flag.RegularExpression + trigger_flag.OmitFromOutput + trigger_flag.Temporary, custom_colour.NoChange, 0, "", + "globalQuest.storeTarget", sendto.script + ); + + AddTriggerEx("trigger_global_quest_end", + "^(?!You)", + "", trigger_flag.RegularExpression + trigger_flag.OmitFromOutput + trigger_flag.Temporary, custom_colour.NoChange, 0, "", + "globalQuest.endTargetCapture", sendto.script + ); + + AddTriggerEx("trigger_global_quest_inactive", + "^(?:" .. + "You are not (in|on) a global quest\\." .. + "|" .. + "The global quest has not yet started\\." .. + "|" .. + "No global quest is currently being run\\." .. + ")$", + "", trigger_flag.RegularExpression + trigger_flag.Temporary, custom_colour.NoChange, 0, "", + "globalQuest.endInactive", sendto.script + ); + + AddTriggerEx("trigger_global_quest_update", + "^Congratulations, that was one of the GLOBAL QUEST mobs!$", + "", trigger_flag.Enabled + trigger_flag.RegularExpression + trigger_flag.Temporary, custom_colour.NoChange, 0, "", + "globalQuest.update", sendto.script + ); + + AddTriggerEx("trigger_global_quest_reset1", + "^Global Quest: Global quest # \\d+? \\(extended\\) is now over\\.$", + "", trigger_flag.Enabled + trigger_flag.RegularExpression + trigger_flag.Temporary, custom_colour.NoChange, 0, "", + "globalQuest.reset", sendto.script + ); + + AddTriggerEx("trigger_global_quest_reset2", + "^Global Quest: (?.+?) has completed global quest # \\d+?\\.$", + "", trigger_flag.Enabled + trigger_flag.RegularExpression + trigger_flag.Temporary, custom_colour.NoChange, 0, "", + "globalQuest.reset", sendto.script + ); + + AddTriggerEx("trigger_global_quest_reset3", + "^Global Quest: Global quest # \\d+? has been won by (?.+?) \\- \\d+?(?:nd|st|rd|th) win\\.$", + "", trigger_flag.Enabled + trigger_flag.RegularExpression + trigger_flag.Temporary, custom_colour.NoChange, 0, "", + "globalQuest.reset", sendto.script + ); + + AddTriggerEx("trigger_global_quest_started", + "^Global Quest: Global quest # \\d+? for levels \\d+? to \\d+? has now started\\.$", + "", trigger_flag.Enabled + trigger_flag.RegularExpression + trigger_flag.Temporary, custom_colour.NoChange, 0, "", + "globalQuest.startTargetCapture", sendto.script + ); + + AddTriggerEx("trigger_global_quest_alert", + "^Global Quest: Global quest # \\d+? has been declared for levels (?\\d+?) to (?\\d+?)\\.$", + "", trigger_flag.Enabled + trigger_flag.RegularExpression + trigger_flag.Temporary, custom_colour.NoChange, 0, "", + "globalQuest.alert", sendto.script + ); + + globalQuest.targets = {}; + globalQuest.currentTargetIndex = nil; +end + +function globalQuest.startTargetCapture() + globalQuest.reset(); + + EnableTrigger("trigger_global_quest_store_target", true); + EnableTrigger("trigger_global_quest_inactive", true); + + Execute("gquest check"); +end + +function globalQuest.storeTarget(name, line, wildcards) + EnableTrigger("trigger_global_quest_end", true); + EnableTrigger("trigger_global_quest_inactive", false); + + local target = mobTarget:new{ + name = wildcards.mob_name:lower(), + location = wildcards.location, + amount = tonumber(wildcards.amount); + }; + + target:initialize(); + + table.insert(globalQuest.targets, target); +end + +function globalQuest.endTargetCapture() + EnableTrigger("trigger_global_quest_store_target", false); + EnableTrigger("trigger_global_quest_end", false); + + table.sort(globalQuest.targets, globalQuest.sort); + display.printTargets(globalQuest.targets, "GQ"); +end + +function globalQuest.endInactive() + EnableTrigger("trigger_global_quest_store_target", false); + EnableTrigger("trigger_global_quest_inactive", false); + globalQuest.reset(); + display.printTargets(nil, "GQ"); +end + +function globalQuest.update() + local targets = globalQuest.targets; + local index = globalQuest.currentTargetIndex; + + if (next(globalQuest.targets)) then + if (index and targets[index]) then + local target = targets[index]; + + local playerEnemy = utility.gmcp.getPlayerEnemy(); + local playerZone = utility.gmcp.getPlayerZone(); + + if (target.name == playerEnemy or target.rooms[1].zone_name == playerZone) then + if (target.amount > 1) then + target.amount = target.amount - 1; + else + table.remove(targets, index); + end + + roomHandler.reset(); + display.printTargets(targets, "GQ"); + else + globalQuest.startTargetCapture(); + end + else + globalQuest.startTargetCapture(); + end + end +end + +function globalQuest.sort(mob1, mob2) + local mob1Priority = mob1.rooms[1].mob_priority or 0; + local mob2Priority = mob2.rooms[1].mob_priority or 0; + + local mob1Zone = mob1.rooms[1].zone_name:lower(); + local mob2Zone = mob2.rooms[1].zone_name:lower(); + + if (mob1Priority == mob2Priority) then + return mob1Zone < mob2Zone; + else + return mob1Priority > mob2Priority; + end +end + +function globalQuest.reset(name, line, wildcards) + local playerName = utility.gmcp.getPlayerName(); + + if (wildcards and wildcards.player_name and wildcards.player_name ~= playerName) then + return; + end + + globalQuest.targets = {}; + globalQuest.currentTargetIndex = nil; + roomHandler.reset(); +end + +function globalQuest.alert(name, line, wildcards) + local minLvl = tonumber(wildcards.minLvl); + local maxLvl = tonumber(wildcards.maxLvl); + local playerLevel = utility.gmcp.getPlayerLevel(); + + if (not quest.isMuted and playerLevel >= minLvl and playerLevel <= maxLvl) then + Sound(GetInfo(66) .. "\\sounds\\global_quest.wav"); + end +end + +---------------------------------- +-- Room Handler +---------------------------------- + +roomHandler = {}; + +function roomHandler.initialize() + AddAlias("alias_room_handler_run_to_area", "^xrt (?[a-zA-Z\\d ]+?)$", "", + alias_flag.Enabled + alias_flag.IgnoreAliasCase + alias_flag.RegularExpression + alias_flag.Temporary, + "roomHandler.runToArea" + ); + + AddAlias("alias_room_handler_select_cp_target", "^x?cp (?\\d+)$", "", + alias_flag.Enabled + alias_flag.IgnoreAliasCase + alias_flag.RegularExpression + alias_flag.Temporary, + "roomHandler.selectCampaignTarget" + ); + + AddAlias("alias_room_handler_select_gq_target", "^x?gq (?\\d+)$", "", + alias_flag.Enabled + alias_flag.IgnoreAliasCase + alias_flag.RegularExpression + alias_flag.Temporary, + "roomHandler.selectGlobalQuestTarget" + ); + + AddAlias("alias_room_handler_select_room", "^x?go(?: (?\\d+?))?$", "", + alias_flag.Enabled + alias_flag.IgnoreAliasCase + alias_flag.RegularExpression + alias_flag.Temporary, + "roomHandler.selectRoom" + ); + + AddAlias("alias_room_handler_select_next_room", "^x?(?:next|nex|nx)$", "", + alias_flag.Enabled + alias_flag.IgnoreAliasCase + alias_flag.RegularExpression + alias_flag.Temporary, + "roomHandler.selectNextRoom" + ); + + AddAlias("alias_room_handler_select_previous_room", "^x?(?:prev|pre|pr|nx-)$", "", + alias_flag.Enabled + alias_flag.IgnoreAliasCase + alias_flag.RegularExpression + alias_flag.Temporary, + "roomHandler.selectPreviousRoom" + ); + + roomHandler.rooms = {}; + roomHandler.index = nil; +end + +function roomHandler.runToArea(name, line, wildcards) + local location = wildcards.location; + + local sql = [[ + SELECT room_id + FROM area + WHERE zone_name = %s + LIMIT 1; + ]]; + + sql = sql:format(fixsql(location)); + + local room = database.mobber:gettable(sql); + + if (not next(room)) then + sql = [[ + SELECT room_id + FROM area + WHERE zone_name LIKE %s + OR area_name LIKE %s + LIMIT 1; + ]]; + + local wildcardLocation = fixsql(location, "both"); + + sql = sql:format(wildcardLocation, wildcardLocation); + + room = database.mobber:gettable(sql); + end + + if (next(room)) then + Execute("mapper goto " .. room[1].room_id); + else + local AYLOR_ROOM_ID = 32418; + Execute("mapper goto " .. AYLOR_ROOM_ID .. ";runto " .. location); + end +end + +function roomHandler.selectCampaignTarget(name, line, wildcards) + local targets = campaign.targets; + + if (next(targets)) then + local index = tonumber(wildcards.index); + local target = targets[index]; + + if (target) then + campaign.currentTargetIndex = index; + + roomHandler.processTarget(target) + else + utility.print("That campaign target does not exist!"); + end + else + utility.print("No campaign targets processed!"); + end +end + +function roomHandler.selectGlobalQuestTarget(name, line, wildcards) + local targets = globalQuest.targets; + + if (next(targets)) then + local index = tonumber(wildcards.index); + local target = targets[index]; + + if (target) then + globalQuest.currentTargetIndex = index; + + roomHandler.processTarget(target) + else + utility.print("That global quest target does not exist!"); + end + else + utility.print("No global quest targets processed!"); + end +end + +function roomHandler.processTarget(target) + utility.setTarget(target.name, target.keyword); + + roomHandler.rooms = {}; + + if (target.lookupType == "mobMatchInLvlRange" or target.lookupType == "roomMatchInLvlRange" + or target.lookupType == "mobMatchOutOfLvlRange" or target.lookupType == "roomMatchOutOfLvlRange") then + roomHandler.rooms = target.rooms; + display.printRooms(roomHandler.rooms); + elseif (target.lookupType == "areaMatch") then + local zoneName = target.rooms[1].zone_name; + local playerZone = utility.gmcp.getPlayerZone(); + + if (playerZone ~= zoneName) then + local roomId = target.rooms[1].room_id; + + if (utility.runToRoomId(roomId)) then + Execute("ht " .. target.keyword); + end + else + Execute("ht " .. target.keyword); + end + else + local AYLOR_RECALL_ROOM_ID = 32418; + + if (utility.runToRoomId(AYLOR_RECALL_ROOM_ID)) then + local zoneName = target.rooms[1].zone_name; + + Execute("runto " .. zoneName); + end + end +end + +function roomHandler.selectRoom(name, line, wildcards) + local rooms = roomHandler.rooms; + + if (next(rooms)) then + local index = tonumber(wildcards.index) or 1; + local room = rooms[index]; + + if (room) then + roomHandler.index = index; + utility.runToRoomId(room.room_id); + quickScan.scan("", "", {mob_name = ""}); + else + utility.print("No more rooms!"); + end + else + utility.print("No room list exists."); + end +end + +function roomHandler.selectNextRoom() + local rooms = roomHandler.rooms; + + if (next(rooms)) then + local index = roomHandler.index or 1; + local room = rooms[index]; + + if (room and index < #rooms) then + if (room.room_id == utility.gmcp.getPlayerRoomId()) then + index = index + 1; + roomHandler.index = index; + room = rooms[index]; + end + + utility.runToRoomId(room.room_id); + quickScan.scan("", "", {mob_name = ""}); + else + utility.print("No more rooms!"); + end + else + utility.print("No room list exists."); + end +end + +function roomHandler.selectPreviousRoom() + local rooms = roomHandler.rooms; + + if (next(rooms)) then + local index = roomHandler.index or 1; + local room = rooms[index]; + + if (room and index > 1 and index <= #rooms) then + if (room.room_id == utility.gmcp.getPlayerRoomId()) then + index = index - 1; + roomHandler.index = index; + room = rooms[index]; + end + + utility.runToRoomId(room.room_id); + quickScan.scan("", "", {mob_name = ""}); + else + utility.print("No more rooms!"); + end + else + utility.print("No room list exists."); + end +end + +function roomHandler.reset() + roomHandler.index = nil; + roomHandler.rooms = {}; +end + +---------------------------------- +-- Mob Search +---------------------------------- + +mobSearch = {}; + +function mobSearch.initialize() + AddAlias("alias_mob_search_find", "^mf(?: (?.+?)(?: (?:zone|area) (?.+?))?)?$", "", + alias_flag.Enabled + alias_flag.IgnoreAliasCase + alias_flag.RegularExpression + alias_flag.Temporary, + "mobSearch.findMob" + ); + + AddAlias("alias_mob_search_find_all", "^mfa (?.+?)(?: (?:lvl|level) (?\\d+?) (?\\d+?))?$", "", + alias_flag.Enabled + alias_flag.IgnoreAliasCase + alias_flag.RegularExpression + alias_flag.Temporary, + "mobSearch.findAllMob" + ); + + mobSearch.MIN_LVL = 1; + mobSearch.MAX_LVL = 220; +end + +function mobSearch.findMob(name, line, wildcards) + local mobName = wildcards.mob_name ~= "" and wildcards.mob_name or "%"; + local zoneName = wildcards.zone_name ~= "" and wildcards.zone_name or utility.gmcp.getPlayerZone(); + + roomHandler.reset(); + + if (zoneName ~= "") then + local sql; + + if (mobName ~= "%") then + sql = [[ + SELECT * + FROM mob + WHERE zone_name = %s + AND mob_name LIKE %s + ORDER BY mob_name ASC, room_id ASC; + ]]; + else + sql = [[ + SELECT DISTINCT mob.mob_name, COUNT(mob_name) AS room_count, mob.mob_priority, mob.zone_name + FROM mob + INNER JOIN area ON mob.zone_name = area.zone_name + WHERE mob.zone_name = %s + AND mob.mob_name LIKE %s + GROUP BY mob_name, mob.zone_name + ORDER BY mob.mob_name; + ]]; + + end + + sql = sql:format(fixsql(zoneName), fixsql(mobName, "both")); + + local results = database.mobber:gettable(sql); + + utility.setTarget(mobName, mobName); + + if (next(results)) then + if (mobName ~= "%") then + roomHandler.rooms = results; + display.printMobRooms(roomHandler.rooms); + else + display.printMobs(results); + end + else + utility.print("Found 0 mobs in " .. zoneName .. " with the name: " .. mobName, "yellow"); + end + else + utility.print("GMCP error retrieving zone."); + end +end + +function mobSearch.findAllMob(name, line, wildcards) + local mobName = wildcards.mob_name; + + local minLvl = tonumber(wildcards.min_level) or mobSearch.MIN_LVL; + local maxLvl = tonumber(wildcards.max_level) or mobSearch.MAX_LVL; + + roomHandler.reset(); + + local sql = [[ + SELECT DISTINCT mob.mob_name, COUNT(mob_name) AS room_count, mob.mob_priority, mob.zone_name + FROM mob + INNER JOIN area ON mob.zone_name = area.zone_name + WHERE area.min_lvl >= %d + AND area.max_lvl <= %d + AND mob.mob_name LIKE %s + GROUP BY mob_name, mob.zone_name + ORDER BY mob.zone_name; + ]]; + + sql = sql:format(minLvl, maxLvl, fixsql(mobName, "both")); + + local mobs = database.mobber:gettable(sql); + + utility.setTarget(mobName, mobName); + + if (next(mobs)) then + display.printMobs(mobs); + else + utility.print("Found 0 mobs (L" .. minLvl .. "-" .. maxLvl .. ") with the name: " .. mobName, "yellow"); + end +end + +---------------------------------- +-- Room Search +---------------------------------- + +roomSearch = {}; + +function roomSearch.initialize() + AddAlias("alias_room_search_zone", "^(?:rf|xm)(?: (?.+?)(?: (?:zone|area) (?.+?))?)?$", "", + alias_flag.Enabled + alias_flag.IgnoreAliasCase + alias_flag.RegularExpression + alias_flag.Temporary, + "roomSearch.searchZone" + ); + + AddAlias("alias_room_search_all", "^(?:rfa|xma) (?.+?)(?: (?:lvl|level) (?\\d+?) (?\\d+?))?$", "", + alias_flag.Enabled + alias_flag.IgnoreAliasCase + alias_flag.RegularExpression + alias_flag.Temporary, + "roomSearch.searchAll" + ); + + roomSearch.MIN_LVL = 1; + roomSearch.MAX_LVL = 220; +end + +function roomSearch.searchZone(name, line, wildcards) + local roomName = wildcards.room_name; + local zoneName = wildcards.zone_name ~= "" and wildcards.zone_name or utility.gmcp.getPlayerZone(); + + roomHandler.reset(); + + if (zoneName ~= "") then + local sql = [[ + SELECT room_name, room_id, zone_name + FROM room + WHERE zone_name = %s + AND room_name LIKE %s + ORDER BY room_id ASC; + ]]; + + sql = sql:format(fixsql(zoneName), fixsql(roomName, "both")); + + local rooms = database.mobber:gettable(sql); + + if (next(rooms)) then + roomHandler.rooms = rooms; + display.printRooms(rooms); + else + utility.print("Found 0 rooms in " .. zoneName .. " with the name: " .. roomName, "yellow"); + end + else + utility.print("GMCP error retrieving zone."); + end +end + +function roomSearch.searchAll(name, line, wildcards) + local roomName = wildcards.room_name; + + local minLvl = tonumber(wildcards.min_level) or roomSearch.MIN_LVL; + local maxLvl = tonumber(wildcards.max_level) or roomSearch.MAX_LVL; + + roomHandler.reset(); + + local sql = [[ + SELECT room.room_name, room.room_id, room.zone_name + FROM room + INNER JOIN area ON room.zone_name = area.zone_name + WHERE area.min_lvl >= %d + AND area.max_lvl <= %d + AND room.room_name LIKE %s + ORDER BY room.zone_name ASC, room.room_id ASC; + ]]; + + sql = sql:format(minLvl, maxLvl, fixsql(roomName, "both")); + + local rooms = database.mobber:gettable(sql); + + if (next(rooms)) then + roomHandler.rooms = rooms; + display.printRooms(rooms); + else + utility.print("Found 0 rooms (L" .. minLvl .. "-" .. maxLvl .. ") with the name: " .. roomName, "yellow"); + end +end + +---------------------------------- +-- Display +---------------------------------- + +display = {}; + +function display.printTargets(targets, questType) + targets = targets or {}; + + local msg = questType == "CP" and 420 or 69; + BroadcastPlugin(msg, serialize.save_simple(targets)); + + ColourNote(pluginPalette.border, "", string.format("\r\n+%s+", string.rep("-", 49))); + + ColourNote( + pluginPalette.border, "", "|", + pluginPalette.header, "", string.format("%-19s%s%-19s", " ", "Target List", " "), + pluginPalette.border, "", "|" + ); + + ColourNote(pluginPalette.border, "", string.format("+%s+", string.rep("-", 49))); + + if (next(targets)) then + for i = 1, #targets do + local target = targets[i]; + + local mobName = toPascalCase(target.name); + local zoneName = toPascalCase(target.rooms[1].zone_name); + local amount = target.amount > 1 and target.amount or " "; + local lookupType = target.lookupType; + + local color; + + if (target.isDead) then + color = pluginPalette.deadMob; + elseif (lookupType == "mobMatchInLvlRange" or lookupType == "roomMatchInLvlRange") then + color = pluginPalette.foundMobRoom; + elseif (lookupType == "areaMatch") then + color = pluginPalette.foundArea; + elseif (lookupType == "mobMatchOutOfLvlRange" or lookupType == "roomMatchOutOfLvlRange") then + color = pluginPalette.roomOrMobAll; + else + color = pluginPalette.missingAreaRoom; + end + + local idxLine = string.format("%3.3s", i .. "."); + local mobLine; + local zoneLine; + + if (color ~= pluginPalette.missingAreaRoom) then + mobLine = string.format(" %-29.28s%s ", mobName, amount); + zoneLine = string.format(" %-13.12s", "("..zoneName..") "); + else + mobLine = string.format(" %-31.30s%14s", zoneName, "Missing "); + zoneLine = ""; + end + + local action = questType == "CP" and "xcp" or "xgq"; + + ColourTell(pluginPalette.border, "", "|", pluginPalette.indexNumbering, "", idxLine); + Hyperlink(action .. " " .. i, mobLine .. zoneLine, mobName .. " (" .. zoneName .. ")", color, "", false, true); + ColourTell(pluginPalette.border, "", "|\r\n"); + end + else + ColourNote( + pluginPalette.border, "", "|", + pluginPalette.header, "", string.format("%-17s%s%-17s", " ", "No " .. questType .. " Available", " "), + pluginPalette.border, "", "|" + ); + end + + ColourNote(pluginPalette.border, "", string.format("+%s+", string.rep("-", 49))); +end + +function display.printRooms(rooms) + ColourNote(pluginPalette.border, "", string.format("\r\n+%s+", string.rep("-", 48))); + + ColourNote( + pluginPalette.border, "", "|", + pluginPalette.header, "", string.format("%-19s%s%-19s", " ", "Rooms List", " "), + pluginPalette.border, "", "|" + ); + + ColourNote(pluginPalette.border, "", string.format("+%s+", string.rep("-", 48))); + + if (next(rooms)) then + local zone; + + for i = 1, #rooms do + local zoneName = toPascalCase(rooms[i].zone_name); + local roomName = toPascalCase(rooms[i].room_name); + local roomId = rooms[i].room_id; + + if (zone ~= zoneName) then + zone = zoneName; + + if (i ~= 1) then + ColourNote(pluginPalette.border, "", string.format("+%s+", string.rep("-", 48))); + end + + ColourNote( + pluginPalette.border, "", "|", + "silver", "", ">> ", + "darkgray", "", string.format("%-45s", zoneName), + pluginPalette.border, "", "|" + ); + end + + ColourTell(pluginPalette.border, "", "|", pluginPalette.indexNumbering, "", string.format("%3.3s", i .. ".")); + Hyperlink("mapper goto " .. roomId, string.format(" %-36.35s", roomName), roomName, pluginPalette.rooms, "", false, true); + Hyperlink("mapper goto " .. roomId, string.format(" %-7.7s", "("..roomId..")"), roomName, "silver", "", false, true); + ColourTell(pluginPalette.border, "", "|\r\n"); + end + else + ColourNote( + pluginPalette.border, "", "|", + pluginPalette.header, "", string.format("%-17s%s%-17s", " ", "No Rooms Found", " "), + pluginPalette.border, "", "|" + ); + end + + ColourNote(pluginPalette.border, "", string.format("+%s+", string.rep("-", 48))); +end + +function display.printMobs(mobs) + ColourNote( + pluginPalette.border, "", + string.format( + "+%s+%s+%s+%s+", + string.rep("-", 10), + string.rep("-", 26), + string.rep("-", 2), + string.rep("-", 2) + ) + ); + + ColourNote( + pluginPalette.border, "", "|", + pluginPalette.header, "", string.format("%-10s", "Zone"), + pluginPalette.border, "", "|", + pluginPalette.header, "", string.format(" %-25s", "Mob Name"), + pluginPalette.border, "", "|", + pluginPalette.header, "", string.format("%-2s", "Rm"), + pluginPalette.border, "", "|", + pluginPalette.header, "", string.format("%-2s", "Pr"), + pluginPalette.border, "", "|" + ); + + ColourNote( + pluginPalette.border, "", + string.format( + "+%s+%s+%s+%s+", + string.rep("-", 10), + string.rep("-", 26), + string.rep("-", 2), + string.rep("-", 2) + ) + ); + + local zone; + + for k,v in ipairs(mobs) do + ColourTell(pluginPalette.border, "", "|"); + + Hyperlink("xrt " .. v.zone_name, string.format("%-10.10s", v.zone_name), "Goto: " .. v.zone_name, "teal", "", false, true); + + ColourTell(pluginPalette.border, "", "|"); + + Hyperlink("mf " .. v.mob_name .. " zone " .. v.zone_name, string.format(" %-25.24s", v.mob_name), "Show rooms for: " .. v.mob_name, "paleturquoise", "", false, true); + + ColourTell( + pluginPalette.border, "", "|", + "yellow", "", string.format("%-2.2s", v.room_count), + pluginPalette.border, "", "|", + "green", "", string.format("%-2.2s", v.mob_priority), + pluginPalette.border, "", "|\r\n" + ); + end + + ColourNote( + pluginPalette.border, "", + string.format( + "+%s+%s+%s+%s+", + string.rep("-", 10), + string.rep("-", 26), + string.rep("-", 2), + string.rep("-", 2) + ) + ); + + utility.print("Click a mob to see its rooms.", "silver"); +end + +function display.printMobRooms(mobRooms) + ColourNote( + pluginPalette.border, "", + string.format( + "+%s+%s+%s+%s+%s+", + string.rep("-", 24), + string.rep("-", 29), + string.rep("-", 2), + string.rep("-", 5), + string.rep("-", 10) + ) + ); + + ColourNote( + pluginPalette.border, "", "|", + pluginPalette.header, "", string.format("%-24s", "Mob Name"), + pluginPalette.border, "", "|", + pluginPalette.header, "", string.format(" %-28s", "Room Name"), + pluginPalette.border, "", "|", + pluginPalette.header, "", string.format("%-2s", "Pr"), + pluginPalette.border, "", "|", + pluginPalette.header, "", string.format("%-5s", "Room"), + pluginPalette.border, "", "|", + pluginPalette.header, "", string.format("%-10s", "Zone"), + pluginPalette.border, "", "|" + ); + + ColourNote( + pluginPalette.border, "", + string.format( + "+%s+%s+%s+%s+%s+", + string.rep("-", 24), + string.rep("-", 29), + string.rep("-", 2), + string.rep("-", 5), + string.rep("-", 10) + ) + ); + + local zone; + + for k,v in ipairs(mobRooms) do + ColourTell(pluginPalette.border, "", "|", pluginPalette.indexNumbering, "", string.format("%-3.3s", k .. ".")); + + Hyperlink("mapper goto " .. v.room_id, string.format(" %-20.19s", v.mob_name), "Goto: " .. v.mob_name, "lightblue", "", false, true); + + ColourTell(pluginPalette.border, "", "|"); + + Hyperlink("mapper goto " .. v.room_id, string.format(" %-28.27s", v.room_name), "Goto: " .. v.room_name, "silver", "", false, true); + + ColourTell( + pluginPalette.border, "", "|", + "green", "", string.format("%-2.2s", v.mob_priority), + pluginPalette.border, "", "|" + ); + + Hyperlink("mapper goto " .. v.room_id, string.format("%-5.5s", v.room_id), "Goto: " .. v.room_id, "orange", "", false, true); + + ColourTell(pluginPalette.border, "", "|"); + + Hyperlink("xrt " .. v.zone_name, string.format("%-10.10s", v.zone_name), "Goto: " .. v.zone_name, "teal", "", false, true); + + ColourTell( + pluginPalette.border, "", "|\r\n" + ); + end + + ColourNote( + pluginPalette.border, "", + string.format( + "+%s+%s+%s+%s+%s+", + string.rep("-", 24), + string.rep("-", 29), + string.rep("-", 2), + string.rep("-", 5), + string.rep("-", 10) + ) + ); + + utility.print("Click a mob to go to its room.", "silver"); +end + +function display.printAreas(areas) + ColourNote( + pluginPalette.border, "", + string.format( + "+%s+%s+%s+%s+%s+%s+%s+", + string.rep("-", 10), + string.rep("-", 34), + string.rep("-", 3), + string.rep("-", 3), + string.rep("-", 5), + string.rep("-", 4), + string.rep("-", 3) + ) + ); + + ColourNote( + pluginPalette.border, "", "|", + pluginPalette.header, "", string.format("%-10s", "Zone Name"), + pluginPalette.border, "", "|", + pluginPalette.header, "", string.format("%-34s", "Area Name"), + pluginPalette.border, "", "|", + pluginPalette.header, "", string.format("%-3s", "Min"), + pluginPalette.border, "", "|", + pluginPalette.header, "", string.format("%-3s", "Max"), + pluginPalette.border, "", "|", + pluginPalette.header, "", string.format("%-5s", "Rm Id"), + pluginPalette.border, "", "|", + pluginPalette.header, "", string.format("%-4s", "#Rm"), + pluginPalette.border, "", "|", + pluginPalette.header, "", string.format("%-3s", "#Mb"), + pluginPalette.border, "", "|" + ); + + ColourNote( + pluginPalette.border, "", + string.format( + "+%s+%s+%s+%s+%s+%s+%s+", + string.rep("-", 10), + string.rep("-", 34), + string.rep("-", 3), + string.rep("-", 3), + string.rep("-", 5), + string.rep("-", 4), + string.rep("-", 3) + ) + ); + + for k,v in ipairs(areas) do + ColourNote( + pluginPalette.border, "", "|", + "teal", "", string.format("%-10.10s", v.zone_name), + pluginPalette.border, "", "|", + "white", "", string.format("%-34.34s", v.area_name), + pluginPalette.border, "", "|", + "darkgray", "", string.format("%-3.3s", v.min_lvl), + pluginPalette.border, "", "|", + "darkgray", "", string.format("%-3.3s", v.max_lvl), + pluginPalette.border, "", "|", + "orange", "", string.format("%-5.5s", v.room_id), + pluginPalette.border, "", "|", + "yellow", "", string.format("%-4.4s", v.room_count), + pluginPalette.border, "", "|", + "green", "", string.format("%-3.3s", v.mob_count), + pluginPalette.border, "", "|" + ); + end + + ColourNote( + pluginPalette.border, "", + string.format( + "+%s+%s+%s+%s+%s+%s+%s+", + string.rep("-", 10), + string.rep("-", 34), + string.rep("-", 3), + string.rep("-", 3), + string.rep("-", 5), + string.rep("-", 4), + string.rep("-", 3) + ) + ); + + ColourNote("yellow", "", "* Excludes nomap rooms\r\n", "green", "", "* Unique mob names"); +end + +---------------------------------- +-- Quick Scan +---------------------------------- + +quickScan = {}; + +function quickScan.initialize() + AddAlias("alias_quick_scan", "^qs(?: (?.+?))?$", "", + alias_flag.Enabled + alias_flag.IgnoreAliasCase + alias_flag.RegularExpression + alias_flag.Temporary, + "quickScan.scan" + ); + + AddTriggerEx("trigger_quick_scan_tag_start", + "^\\{scan\\}$", + "", trigger_flag.RegularExpression + trigger_flag.Temporary, custom_colour.NoChange, 0, "", + "quickScan.enableCapture", sendto.script, 100 + ); + + AddTriggerEx("trigger_quick_scan_capture", + "^\\s{5}-\\s(?:\\([A-Za-z ]+\\)\\s?)*(?.+?)$", + "", trigger_flag.RegularExpression + trigger_flag.OmitFromOutput + trigger_flag.Temporary, custom_colour.NoChange, 0, "", + "quickScan.highlight", sendto.script, 100 + ); + + AddTriggerEx("trigger_quick_scan_tag_end", + "^\\{\\/scan\\}$", + "", trigger_flag.RegularExpression + trigger_flag.Temporary, custom_colour.NoChange, 0, "", + "quickScan.disableCapture", sendto.script, 100 + ); + + AddTriggerEx("trigger_quick_scan_tag_start_gag", + "^\\{scan\\}$", + "", trigger_flag.Enabled + trigger_flag.RegularExpression + trigger_flag.KeepEvaluating + trigger_flag.OmitFromOutput + trigger_flag.Temporary, custom_colour.NoChange, 0, "", + "", sendto.script, 99 + ); + + AddTriggerEx("trigger_quick_scan_tag_end_gag", + "^\\{\\/scan\\}$", + "", trigger_flag.Enabled + trigger_flag.RegularExpression + trigger_flag.KeepEvaluating + trigger_flag.OmitFromOutput + trigger_flag.Temporary, custom_colour.NoChange, 0, "", + "", sendto.script, 99 + ); +end + +function quickScan.scan(name, line, wildcards) + if (wildcards.mob_name ~= "") then + utility.targetName = wildcards.mob_name; + utility.targetKeyword = utility.getMobKeyword(wildcards.mob_name); + EnableTrigger("trigger_quick_scan_tag_start", true); + Execute("scan"); + else + local target = utility.targetKeyword; + + if (target) then + EnableTrigger("trigger_quick_scan_tag_start", true); + Execute("scan " .. target); + else + Execute("scan"); + end + end +end + +function quickScan.enableCapture() + EnableTrigger("trigger_quick_scan_tag_start", false); + EnableTrigger("trigger_quick_scan_capture", true); + EnableTrigger("trigger_quick_scan_tag_end", true); +end + +function quickScan.highlight(name, line, wildcards, styles) + local mobName = wildcards.mob_name:lower(); + local target = utility.targetName:lower(); + + if (mobName == target) then + ColourTell(pluginPalette.highlightTargetParentheses, "", "("); + ColourTell(pluginPalette.highlightTarget, "", "TARGET"); + ColourTell(pluginPalette.highlightTargetParentheses, "", ") "); + ColourTell("white", "", toPascalCase(target) .. "\r\n"); + else + for k,v in ipairs(styles) do + ColourTell(RGBColourToName(v.textcolour), v.backcolour, v.text); + end + + print(); + end +end + +function quickScan.disableCapture() + EnableTrigger("trigger_quick_scan_capture", false); + EnableTrigger("trigger_quick_scan_tag_end", false); +end + +---------------------------------- +-- Quick Where +---------------------------------- + +quickWhere = {}; + +function quickWhere.initialize() + AddAlias("alias_quick_where", "^qw(?: ((?\\d+?)\\.)?(?.+?))?$", "", + alias_flag.Enabled + alias_flag.IgnoreAliasCase + alias_flag.RegularExpression + alias_flag.Temporary, + "quickWhere.doWhere" + ); + + AddTriggerEx("trigger_quick_where", + "^(?.{30}) (?.+?)$", + "", trigger_flag.RegularExpression + trigger_flag.KeepEvaluating + trigger_flag.Temporary, custom_colour.NoChange, 0, "", + "quickWhere.matchTarget", sendto.script, 100 + ); + + AddTriggerEx("trigger_quick_where_end", + "^There is no .+? around here\\.$", + "", trigger_flag.RegularExpression + trigger_flag.Temporary, custom_colour.NoChange, 0, "", + "EnableTriggerGroup('trigger_group_quick_where', false)", sendto.script, 99 + ); + + SetTriggerOption("trigger_quick_where", "group", "trigger_group_quick_where"); + SetTriggerOption("trigger_quick_where_end", "group", "trigger_group_quick_where"); +end + +function quickWhere.doWhere(name, line, wildcards) + local index = tonumber(wildcards.index) or 1; + local targetName = wildcards.target_name:lower(); + + if (targetName ~= "") then + utility.setTarget(targetName, targetName); + else + if (utility.targetKeyword) then + targetName = utility.targetKeyword; + else + return utility.print("No target mob has been set to quick where.", "red"); + end + end + + EnableTriggerGroup("trigger_group_quick_where", true); + + Execute("where " .. index .. "." .. targetName); + DoAfterSpecial(10, "EnableTriggerGroup('trigger_group_quick_where', false)", 12); +end + +function quickWhere.matchTarget(name, line, wildcards) + if (utility.targetName) then + local targetName = Trim(wildcards.target_name):lower(); + + for token in utility.targetName:gmatch("[^ ]+") do + if (string.find(targetName, token, 1, true)) then + local roomName = wildcards.room_name; + local zoneName = utility.gmcp.getPlayerZone(); + + if (zoneName ~= "") then + local sql = [[ + SELECT room_name, room_id, zone_name + FROM room + WHERE room_name = %s + AND zone_name = %s + ORDER BY room_id; + ]]; + + sql = sql:format(fixsql(roomName), fixsql(zoneName)); + + local rooms = database.mobber:gettable(sql); + + if (next(rooms)) then + roomHandler.rooms = rooms; + end + + display.printRooms(rooms); + else + utility.print("GMCP error retrieving zone."); + end + + EnableTriggerGroup("trigger_group_quick_where", false); + + break; + end + end + end +end + +---------------------------------- +-- Hunt Trick +---------------------------------- + +huntTrick = {}; + +function huntTrick.initialize() + -- Aliases + + AddAlias("alias_hunt_trick_start", "^ht (?:(?\\d+?)\\.)?(?.+)$", "", + alias_flag.Enabled + alias_flag.IgnoreAliasCase + alias_flag.RegularExpression + alias_flag.Temporary, + "huntTrick.startHuntTrick" + ); + + AddAlias("alias_hunt_trick_stop", "^ht$", "", + alias_flag.Enabled + alias_flag.IgnoreAliasCase + alias_flag.RegularExpression + alias_flag.Temporary, + "huntTrick.stopHuntTrick" + ); + + -- Triggers + + AddTriggerEx("trigger_hunt_trick_continue", + "^(?:" .. + "You are certain that .+? is .+?\\." .. + "|" .. + "You are almost certain that .+? is (?:north|east|south|west|up|down) from here\\." .. + "|" .. + "You are confident that .+? passed through here, heading (north|east|south|west|up|down)\\." .. + "|" .. + "The trail of .+? is confusing, but you're reasonably sure .+? headed (?:north|east|south|west|up|down)\\." .. + "|" .. + "There are traces of .+? having been here\\. Perhaps they lead (?:north|east|south|west|up|down)\\?" .. + "|" .. + "You have no idea what you're doing, but maybe .+? left (?:north|east|south|west|up|down)\\?" .. + "|" .. + "You couldn't find a path to .+? from here\\." .. + "|" .. + ".+? is here!" .. + "|" .. + "You have no idea what you're doing, but maybe (.+?) is (north|east|south|west|up|down)\\?" .. + ")$", + "", trigger_flag.RegularExpression + trigger_flag.Temporary, custom_colour.NoChange, 0, "", + "huntTrick.continueHuntTrick", sendto.script + ); + + AddTriggerEx("trigger_hunt_trick_complete", + "^You seem unable to hunt that target for some reason\\.$", + "", trigger_flag.RegularExpression + trigger_flag.Temporary, custom_colour.NoChange, 0, "", + "huntTrick.completeHuntTrick", sendto.script + ); + + AddTriggerEx("trigger_hunt_trick_stop", + "^(?:" .. + "You couldn't find a path to .+? from here\\." .. + "|" .. + "No one in this area by that name\\." .. + "|" .. + "No one in this area by the name '.+?'\\." .. + "|" .. + "There is no .+? around here\\." .. + "|" .. + "Not while you are fighting!" .. + ")$", + "", trigger_flag.RegularExpression + trigger_flag.Temporary, custom_colour.NoChange, 0, "", + "huntTrick.stopHuntTrick", sendto.script + ); + + SetTriggerOption("trigger_hunt_trick_continue", "group", "trigger_group_hunt_trick"); + SetTriggerOption("trigger_hunt_trick_complete", "group", "trigger_group_hunt_trick"); + SetTriggerOption("trigger_hunt_trick_stop", "group", "trigger_group_hunt_trick"); + + huntTrick.index = nil; + huntTrick.targetName = nil; +end + +function huntTrick.startHuntTrick(name, line, wildcards) + local STATE_ACTIVE = 3; + + if (tonumber(gmcp("char.status.state")) == STATE_ACTIVE) then + EnableTriggerGroup("trigger_group_hunt_trick", true); + + huntTrick.index = tonumber(wildcards.index) or 1; + + huntTrick.targetName = wildcards.targetName; + + huntTrick.huntTarget(huntTrick.index, huntTrick.targetName); + else + ColourNote("white", "", "(", "maroon", "", "Mobber", "white", "", ") You are too busy to hunt."); + end +end + +function huntTrick.continueHuntTrick() + if (huntTrick.index and huntTrick.targetName) then + huntTrick.index = huntTrick.index + 1; + + huntTrick.huntTarget(huntTrick.index, huntTrick.targetName); + else + huntTrick.stopHuntTrick(); + end +end + +function huntTrick.completeHuntTrick() + huntTrick.stopHuntTrick(); + Execute("qw " .. huntTrick.index .. "." .. huntTrick.targetName); +end + +function huntTrick.stopHuntTrick() + EnableTriggerGroup("trigger_group_hunt_trick", false); + + ColourNote("white", "", "(", "maroon", "", "Mobber", "white", "", ") Hunt-tricking stopped."); +end + +function huntTrick.huntTarget(index, targetName) + Execute("hunt " .. index .. "." .. targetName); +end + +---------------------------------- +-- Auto Hunt +---------------------------------- + +autoHunt = {}; + +function autoHunt.initialize() + -- Aliases + + AddAlias("alias_auto_hunt_start", "^ah (?.+)$", "", + alias_flag.Enabled + alias_flag.IgnoreAliasCase + alias_flag.RegularExpression + alias_flag.Temporary, + "autoHunt.startHunt" + ); + + AddAlias("alias_auto_hunt_abort", "^ah$", "", + alias_flag.Enabled + alias_flag.IgnoreAliasCase + alias_flag.RegularExpression + alias_flag.Temporary, + "autoHunt.stopHunt" + ); + + -- Triggers + + AddTriggerEx("trigger_auto_hunt_door", + "^Magical wards .+? bounce you back\\.$", + "", trigger_flag.RegularExpression + trigger_flag.Temporary + trigger_flag.KeepEvaluating, custom_colour.NoChange, 0, "", + "autoHunt.hunt", sendto.script, 99 + ); + + AddTriggerEx("trigger_auto_hunt_continue1", + "^You are confident that .+? passed through here, heading (north|east|south|west|up|down)\\.$", + "", trigger_flag.RegularExpression + trigger_flag.Temporary, custom_colour.NoChange, 0, "", + "autoHunt.hunt", sendto.script + ); + + AddTriggerEx("trigger_auto_hunt_continue2", + "^The trail of .+? is confusing, but you're reasonably sure .+? headed (north|east|south|west|up|down)\\.$", + "", trigger_flag.RegularExpression + trigger_flag.Temporary, custom_colour.NoChange, 0, "", + "autoHunt.hunt", sendto.script + ); + + AddTriggerEx("trigger_auto_hunt_continue3", + "^You are certain that .+? is (north|east|south|west|up|down) from here\\.$", + "", trigger_flag.RegularExpression + trigger_flag.Temporary, custom_colour.NoChange, 0, "", + "autoHunt.hunt", sendto.script + ); + + AddTriggerEx("trigger_auto_hunt_continue4", + "^There are traces of .+? having been here\\. Perhaps they lead (north|east|south|west|up|down)\\?", + "", trigger_flag.RegularExpression + trigger_flag.Temporary, custom_colour.NoChange, 0, "", + "autoHunt.hunt", sendto.script + ); + + AddTriggerEx("trigger_auto_hunt_continue5", + "^The trail of .+? is confusing, but you're reasonably sure .+? is (north|east|south|west|up|down)\\.", + "", trigger_flag.RegularExpression + trigger_flag.Temporary, custom_colour.NoChange, 0, "", + "autoHunt.hunt", sendto.script + ); + + AddTriggerEx("trigger_auto_hunt_continue6", + "^You are almost certain that .+? is (north|east|south|west|up|down) from here\\.$", + "", trigger_flag.RegularExpression + trigger_flag.Temporary, custom_colour.NoChange, 0, "", + "autoHunt.hunt", sendto.script + ); + + AddTriggerEx("trigger_auto_hunt_abort", + "^(?:" .. + "You seem unable to hunt that target for some reason\\." .. + "|" .. + "You couldn't find a path to .+? from here\\." .. + "|" .. + ".+? is here!" .. + "|" .. + "Not while you are fighting!" .. + "|" .. + "No one in this area by the name '.+?'\\." .. + ")$", + "", trigger_flag.RegularExpression + trigger_flag.Temporary, custom_colour.NoChange, 0, "", + "autoHunt.stopHunt", sendto.script + ); + + SetTriggerOption("trigger_auto_hunt_door", "group", "trigger_group_auto_hunt"); + SetTriggerOption("trigger_auto_hunt_abort", "group", "trigger_group_auto_hunt"); + + for i = 1, 6 do + SetTriggerOption("trigger_auto_hunt_continue" .. i, "group", "trigger_group_auto_hunt"); + end + + autoHunt.targetName = nil; + autoHunt.lastDirection = nil; +end + +function autoHunt.startHunt(name, line, wildcards) + local STATE_ACTIVE = 3; + + if (tonumber(gmcp("char.status.state")) == STATE_ACTIVE) then + EnableTriggerGroup("trigger_group_auto_hunt", true); + + autoHunt.targetName = wildcards.targetName; + + SendNoEcho("hunt " .. autoHunt.targetName); + else + ColourNote("white", "", "(", "maroon", "", "Mobber", "white", "", ") You are too busy to auto-hunt."); + end +end + +function autoHunt.hunt(name, line, wildcards) + if (name == "trigger_auto_hunt_door") then + if (autoHunt.lastDirection) then + Execute("open " .. autoHunt.lastDirection); + else + Execute("hunt " .. autoHunt.targetName); + end + else + autoHunt.lastDirection = wildcards[1]; + + Execute(wildcards[1]); + SendNoEcho("hunt " .. autoHunt.targetName); + end +end + +function autoHunt.stopHunt() + EnableTriggerGroup("trigger_group_auto_hunt", false); + autoHunt.targetName = nil; + autoHunt.lastDirection = nil; + ColourNote("white", "", "(", "maroon", "", "Mobber", "white", "", ") Auto-hunt stopped."); +end + +---------------------------------- +-- Autokill +---------------------------------- + +autoKill = {}; + +function autoKill.initialize() + AddAlias("alias_auto_kill", "^(?:ak|kk)$", "", + alias_flag.Enabled + alias_flag.IgnoreAliasCase + alias_flag.RegularExpression + alias_flag.Temporary, + "autoKill.kill" + ); + + AddAlias("alias_auto_kill_set_skill", "^(?:ak|kk) (?.+?)$", "", + alias_flag.Enabled + alias_flag.IgnoreAliasCase + alias_flag.RegularExpression + alias_flag.Temporary, + "autoKill.setSkill" + ); + + AddAlias("alias_auto_kill_toggle", "^toggle (?:ak|kk)$", "", + alias_flag.Enabled + alias_flag.IgnoreAliasCase + alias_flag.RegularExpression + alias_flag.Temporary, + "autoKill.toggle" + ); + + autoKill.skill = var.akSkill or "kill"; + autoKill.isTargetting = var.isTargetting == "true" and true or false; +end + +function autoKill.kill() + local target = utility.targetKeyword; + local skill = autoKill.skill; + + if (autoKill.isTargetting and target) then + skill = skill:gsub("[^;]+", "%1 '" .. target .. "'"); + end + + Execute(skill); +end + +function autoKill.setSkill(name, line, wildcards) + autoKill.skill = wildcards.skill; + var.akSkill = wildcards.skill; + utility.print("Autokill skill/spell: " .. wildcards.skill); +end + +function autoKill.toggle() + if (autoKill.isTargetting) then + utility.print("Target Mode: OFF"); + else + utility.print("Target Mode: ON"); + end + + autoKill.isTargetting = not autoKill.isTargetting; + var.isTargetting = autoKill.isTargetting; +end + +---------------------------------- +-- Noexp +---------------------------------- + +noexp = {}; + +function noexp.initialize() + AddAlias("alias_noexp_toggle", "^mobber noexp(?: (?\\d+?))?$", "", + alias_flag.Enabled + alias_flag.IgnoreAliasCase + alias_flag.RegularExpression + alias_flag.Temporary, + "noexp.toggle" + ); + + AddTriggerEx("trigger_noexp_1", + "^(?:You may take a campaign at this level\\.|You raise a level! You are now level \\d+?\\.)$", + "", trigger_flag.RegularExpression + trigger_flag.Temporary, custom_colour.NoChange, 0, "", + "noexp.enableCampaignAvailableAndCheckTnl", sendto.script, 100 + ); + + AddTriggerEx("trigger_noexp_2", + "^(?:You will have to level before you can go on another campaign\\.|.+?'Good luck in your campaign!')$", + "", trigger_flag.RegularExpression + trigger_flag.Temporary, custom_colour.NoChange, 0, "", + "noexp.disableCampaignAvailableAndTurnOffNoExp", sendto.script, 100 + ); + + AddTriggerEx("trigger_noexp_3", + "^You receive \\d+? experience points?\\.$", + "", trigger_flag.RegularExpression + trigger_flag.Temporary, custom_colour.NoChange, 0, "", + "noexp.checkCampaignTnl", sendto.script, 100 + ); + + AddTriggerEx("trigger_noexp_4", + "^You will no longer receive experience\\. Happy questing!$", + "", trigger_flag.RegularExpression + trigger_flag.Temporary, custom_colour.NoChange, 0, "", + "noexp.enableIsNoExp", sendto.script, 100 + ); + + AddTriggerEx("trigger_noexp_5", + "^You will now receive experience\\. Happy leveling!$", + "", trigger_flag.RegularExpression + trigger_flag.Temporary, custom_colour.NoChange, 0, "", + "noexp.disableIsNoExp", sendto.script, 100 + ); + + for i = 1, 5 do + SetTriggerOption("trigger_noexp_" .. i, "group", "trigger_group_noexp"); + end + + noexp.isNoExpMode = false; + noexp.isNoExp = false; + noexp.isCampaignAvailable = false; + noexp.threshold = tonumber(var.threshold) or 1000; +end + +function noexp.showVars() + print("noexp.isNoExpMode", noexp.isNoExpMode); + print("noexp.isNoExp", noexp.isNoExp); + print("noexp.isCampaignAvailable", noexp.isCampaignAvailable); + print("noexp.threshold", noexp.threshold); +end + +function noexp.toggle(name, line, wildcards) + noexp.threshold = tonumber(wildcards.threshold) or noexp.threshold; + var.threshold = noexp.threshold; + + if (noexp.isNoExpMode) then + EnableTriggerGroup("trigger_group_noexp", false); + utility.print("NOEXP AUTOMODE OFF", "red"); + else + EnableTriggerGroup("trigger_group_noexp", true); + utility.print("NOEXP AUTOMODE ON (THRESHOLD = " .. noexp.threshold .. "xp)", "lime"); + Execute("cp check"); + end + + noexp.setNoExp(false); + + noexp.isNoExpMode = not noexp.isNoExpMode; +end + +function noexp.setNoExp(state) + noexp.isNoExp = state; + + state = state and "on" or "off"; + + Send_GMCP_Packet("config noexp " .. state); + utility.print("Setting noexp " .. state, "orange"); +end + +function noexp.checkTnl() + local tnl = utility.gmcp.getPlayerTnl(); + + if (noexp.isNoExp and tnl > noexp.threshold) then + noexp.setNoExp(false); + elseif (not noexp.isNoExp and tnl < noexp.threshold) then + noexp.setNoExp(true); + end +end + +function noexp.turnOffNoExp() + if (noexp.isNoExp) then + noexp.setNoExp(false); + end +end + +function noexp.enableCampaignAvailableAndCheckTnl() + noexp.isCampaignAvailable = true; + DoAfterSpecial(0.2, "noexp.checkTnl()", 12); +end + +function noexp.disableCampaignAvailableAndTurnOffNoExp() + noexp.isCampaignAvailable = false; + noexp.turnOffNoExp(); +end + +function noexp.checkCampaignTnl() + if (noexp.isCampaignAvailable) then + DoAfterSpecial(0.2, "noexp.checkTnl()", 12); + else + noexp.turnOffNoExp(); + end +end + +function noexp.enableIsNoExp() + noexp.isNoExp = true; +end + +function noexp.disableIsNoExp() + noexp.isNoExp = false; +end + +---------------------------------- +-- Quest +---------------------------------- + +quest = {}; + +function quest.initialize() + AddAlias("alias_quest_mute", "^mobber (?mute|unmute) quest$", "", + alias_flag.Enabled + alias_flag.IgnoreAliasCase + alias_flag.RegularExpression + alias_flag.Temporary, + "quest.toggleMute" + ); + + AddAlias("alias_quest_request", "^mobber quest$", "", + alias_flag.Enabled + alias_flag.IgnoreAliasCase + alias_flag.RegularExpression + alias_flag.Temporary, + "quest.request" + ); + + quest.commPrefix = "@r(@WQuest@r)@w"; + quest.timer = nil; + quest.isMuted = false; +end + +function quest.start(newQuest) + if (not quest.isMuted) then + local commMsg = quest.commPrefix .. " @Y" .. newQuest.targ .. " @win @R" .. newQuest.room .. " @r(@w" .. newQuest.area .. "@r)@w"; + CallPlugin("b555825a4a5700c35fa80780", "storeFromOutside", commMsg); + + AddTimer("questReminder", 0, 1, 0, "", timer_flag.Enabled + timer_flag.Temporary, "quest.playSound"); + quest.timer = os.time(); + end + + quest.process(newQuest); +end + +function quest.showRequest(newQuest) + local mobName = newQuest.targ; + local roomName = stripColors(newQuest.room or ""); + local areaName = newQuest.area; + + if (mobName == "missing") then + ColourNote("maroon", "", "(", "white", "", "Quest", "maroon", "", ") ", "red", "", "Quest mob is missing."); + elseif (mobName == "killed") then + ColourNote("maroon", "", "(", "white", "", "Quest", "maroon", "", ") ", "red", "", "Quest mob is killed."); + elseif (mobName and roomName and areaName) then + ColourNote("maroon", "", "(", "white", "", "Quest", "maroon", "", ") ", "red", "", "Mob: " .. mobName); + ColourNote("maroon", "", "(", "white", "", "Quest", "maroon", "", ") ", "red", "", "Room: " .. roomName); + ColourNote("maroon", "", "(", "white", "", "Quest", "maroon", "", ") ", "red", "", "Area: " .. areaName); + quest.process(newQuest); + else + ColourNote("maroon", "", "(", "white", "", "Quest", "maroon", "", ") ", "red", "", "Quest mob not found."); + end +end + +function quest.request() + Send_GMCP_Packet("request quest"); +end + +function quest.fail() + if (not quest.isMuted) then + local commMsg = quest.commPrefix .. " @WFailed!@w"; + CallPlugin("b555825a4a5700c35fa80780", "storeFromOutside", commMsg); + + quest.timer = nil; + DeleteTimer("questReminder"); + ColourNote( + "maroon", "", "(", + "white", "", "Quest", + "maroon", "", ") ", + "red", "", "You failed the quest!\r\n" + ); + end +end + +function quest.complete(newQuest) + if (not quest.isMuted) then + local commMsg = quest.commPrefix .. " @WComplete!@w"; + CallPlugin("b555825a4a5700c35fa80780", "storeFromOutside", commMsg); + + local duration = 0; + + if (quest.timer) then + duration = os.difftime(os.time(), quest.timer); + duration = formatSeconds(duration); + quest.timer = nil; + end + + local numQuestPoints = tonumber(newQuest.totqp); + local numTriviaPoints = tonumber(newQuest.tp); + + ColourTell( + "maroon", "", "(", + "white", "", "Quest", + "maroon", "", ") ", + "cyan", "", numQuestPoints, + "red", "", " quest points. Completed in: ", + "cyan", "", duration + ); + + if (numTriviaPoints > 0) then + ColourTell( + "red", "", " [", + "lime", "", numTriviaPoints .. " tp", + "red", "", "]\r\n\r\n" + ); + else + ColourTell("red", "", "\r\n\r\n"); + end + + DeleteTimer("questReminder"); + end +end + +function quest.process(newQuest) + local mobName = newQuest.targ; + local roomName = stripColors(newQuest.room); + local areaName = newQuest.area; + + local mobKeyword = database.lookupMobKeyword(mobName) or utility.getMobKeyword(mobName); + + utility.setTarget(mobName, mobKeyword); + + local sql = [[ + SELECT mob.room_name, mob.room_id, mob.zone_name + FROM mob + INNER JOIN area ON mob.zone_name = area.zone_name + WHERE mob.mob_name = %s + AND mob.room_name = %s + AND area.area_name = %s; + ]]; + + sql = sql:format(fixsql(mobName), fixsql(roomName), fixsql(areaName)); + + local rooms = database.mobber:gettable(sql); + + if (not next(rooms)) then + sql = [[ + SELECT room.room_name, room.room_id, room.zone_name + FROM room + INNER JOIN area ON room.zone_name = area.zone_name + WHERE room.room_name = %s + AND area.area_name = %s; + ]]; + + sql = sql:format(fixsql(roomName), fixsql(areaName)); + + rooms = database.mobber:gettable(sql); + end + + roomHandler.rooms = rooms; + + display.printRooms(rooms); +end + +function quest.playSound() + Sound(GetInfo(66) .. "\\sounds\\quest_warning.wav"); +end + +function quest.toggleMute(name, line, wildcards) + if (wildcards.toggle == "unmute") then + quest.isMuted = false; + utility.print("Quest messages are now on.", "yellowgreen"); + else + quest.isMuted = true; + utility.print("Quest messages are now off.", "indianred"); + end + + var.isQuestMuted = quest.isMuted; +end + +---------------------------------- +-- Database +---------------------------------- + +database = {}; + +function database.initialize() + AddAlias("alias_database_developer", "^mobber dev(?:eloper)?(?: mode)?$", "", + alias_flag.Enabled + alias_flag.RegularExpression + alias_flag.Temporary, + "database.toggleDeveloperMode" + ); + + AddAlias("alias_database_backup", "^mobber backup$", "", + alias_flag.Enabled + alias_flag.RegularExpression + alias_flag.Temporary, + "database.manualBackup" + ); + + AddAlias("alias_database_vacuum", "^mobber vacuum$", "", + alias_flag.Enabled + alias_flag.RegularExpression + alias_flag.Temporary, + "database.vacuum" + ); + + AddAlias("alias_database_consider", "^mobber consider$", "", + alias_flag.Enabled + alias_flag.RegularExpression + alias_flag.Temporary, + "database.toggleConsiderMobLogging" + ); + + AddAlias("alias_database_add_area", "^mobber add(?:area|zone) (?\\d+) (?\\d+)$", "", + alias_flag.Enabled + alias_flag.RegularExpression + alias_flag.Temporary, + "database.startAddArea" + ); + + AddAlias("alias_database_remove_area", "^mobber (?:remove|delete) (?:area|zone)$", "", + alias_flag.Enabled + alias_flag.RegularExpression + alias_flag.Temporary, + "database.removeArea" + ); + + AddAlias("alias_database_show_areas", "^mobber show (?:areas|zones)(?: sort (?mobs?|rooms?|level))?$", "", + alias_flag.Enabled + alias_flag.RegularExpression + alias_flag.Temporary, + "database.showAreas" + ); + + AddAlias("alias_database_show_area", "^mobber show (?:area|zone)(?: (?.+?))?$", "", + alias_flag.Enabled + alias_flag.RegularExpression + alias_flag.Temporary, + "database.showArea" + ); + + AddAlias("alias_database_update_rooms", "^mobber update rooms$", "", + alias_flag.Enabled + alias_flag.RegularExpression + alias_flag.Temporary, + "database.updateRooms" + ); + + AddAlias("alias_database_update_areas", "^mobber update (?:areas|zones)$", "", + alias_flag.Enabled + alias_flag.RegularExpression + alias_flag.Temporary, + "database.updateAreas" + ); + + AddAlias("alias_database_update_area_id", "^(?:mobber xset|xset mark)$", "", + alias_flag.Enabled + alias_flag.RegularExpression + alias_flag.Temporary, + "database.updateAreaId" + ); + + AddAlias("alias_database_update_area_level_range", "^mobber update (?:level|lvl)range (?\\d+) (?\\d+)$", "", + alias_flag.Enabled + alias_flag.RegularExpression + alias_flag.Temporary, + "database.updateAreaLevelRange" + ); + + AddAlias("alias_database_add_mob", "^mobber addmob (?.+?)(?: priority (?\\d+))?$", "", + alias_flag.Enabled + alias_flag.RegularExpression + alias_flag.Temporary, + "database.addMob" + ); + + AddAlias("alias_database_remove_mob_room", "^mobber (?:remove|delete)mob (?:room|here)$", "", + alias_flag.Enabled + alias_flag.RegularExpression + alias_flag.Temporary, + "database.removeMobRoom" + ); + + AddAlias("alias_database_remove_mob_zone", "^mobber (?:remove|delete)mob (?:zone|area)$", "", + alias_flag.Enabled + alias_flag.RegularExpression + alias_flag.Temporary, + "database.removeMobZone" + ); + + AddAlias("alias_database_remove_mob_single", "^mobber (?:remove|delete)mob single (?.+?)$", "", + alias_flag.Enabled + alias_flag.RegularExpression + alias_flag.Temporary, + "database.removeMobSingle" + ); + + AddAlias("alias_database_update_mob_keyword", "^mobber keyword (?.+?) mob (?.+?)$", "", + alias_flag.Enabled + alias_flag.RegularExpression + alias_flag.Temporary, + "database.updateMobKeyword" + ); + + AddAlias("alias_database_update_mob_keywords", "^mobber update keywords?$", "", + alias_flag.Enabled + alias_flag.RegularExpression + alias_flag.Temporary, + "database.updateMobKeywords" + ); + + AddAlias("alias_database_clean_keywords", "^mobber clean keywords?$", "", + alias_flag.Enabled + alias_flag.RegularExpression + alias_flag.Temporary, + "database.cleanKeywords" + ); + + AddAlias("alias_database_set_mob_priority", "^mobber priority (?-?\\d+) mob (?.+?)$", "", + alias_flag.Enabled + alias_flag.RegularExpression + alias_flag.Temporary, + "database.setMobPriority" + ); + + -- Consider Triggers + + AddTriggerEx("trigger_database_developer_mob_consider1", + "^You get .+? gold coins? from the .*?corpse of (?.+?)\\.$", + "", trigger_flag.RegularExpression + trigger_flag.Temporary, custom_colour.NoChange, 0, "", + "database.addMob", sendto.script, 100 + ); + + AddTriggerEx("trigger_database_developer_mob_consider2", + "^(?:\\([A-Za-z ]+\\)\\s?)*You would be completely annihilated by (?.+?)$", + "", trigger_flag.RegularExpression + trigger_flag.Temporary, custom_colour.NoChange, 0, "", + "database.addMob", sendto.script, 100 + ); + + AddTriggerEx("trigger_database_developer_mob_consider3", + "^(?:\\([A-Za-z ]+\\)\\s?)*You would stomp (?.+?) into the ground\\.$", + "", trigger_flag.RegularExpression + trigger_flag.Temporary, custom_colour.NoChange, 0, "", + "database.addMob", sendto.script, 100 + ); + + AddTriggerEx("trigger_database_developer_mob_consider4", + "^(?:\\([A-Za-z ]+\\)\\s?)*No Problem! (?.+?) is weak compared to you\\.$", + "", trigger_flag.RegularExpression + trigger_flag.Temporary, custom_colour.NoChange, 0, "", + "database.addMob", sendto.script, 100 + ); + + AddTriggerEx("trigger_database_developer_mob_consider5", + "^(?:\\([A-Za-z ]+\\)\\s?)*Best run away from (?.+?) while you can!$", + "", trigger_flag.RegularExpression + trigger_flag.Temporary, custom_colour.NoChange, 0, "", + "database.addMob", sendto.script, 100 + ); + + AddTriggerEx("trigger_database_developer_mob_consider6", + "^(?:\\([A-Za-z ]+\\)\\s?)*Challenging (?.+?) would be either very brave or very stupid\\.$", + "", trigger_flag.RegularExpression + trigger_flag.Temporary, custom_colour.NoChange, 0, "", + "database.addMob", sendto.script, 100 + ); + + AddTriggerEx("trigger_database_developer_mob_consider7", + "^(?:\\([A-Za-z ]+\\)\\s?)*(?.+?) " .. + "(?:" .. + "would be easy, but is it even worth the work out\\?" .. + "|" .. + "looks a little worried about the idea\\." .. + "|" .. + "should be a fair fight!" .. + "|" .. + "snickers nervously\\." .. + "|" .. + "chuckles at the thought of you fighting (?:him|her|it)\\." .. + "|" .. + "would crush you like a bug!" .. + "|" .. + "would dance on your grave!" .. + "|" .. + "says 'BEGONE FROM MY SIGHT unworthy!'" .. + "|" .. + "has divine protection\\." .. + ")$", + "", trigger_flag.RegularExpression + trigger_flag.Temporary, custom_colour.NoChange, 0, "", + "database.addMob", sendto.script, 100 + ); + + for i = 1, 7 do + local triggerName = "trigger_database_developer_mob_consider" .. i; + SetTriggerOption(triggerName, "group", "trigger_group_add_mob_consider"); + end + + database.isDeveloperMode = false; + database.isConsiderMobLogging = false; +end + +function database.openDatabase() + if (not database.mobber) then + database.mobber = sqlitedb:new{name = "mobber.db"}; + end + database.mobber:open(); + database.checkCreateTables(); + database.checkBackup(); +end + +function database.closeDatabase() + database.mobber:close(); + database.mobber = nil; +end + +function database.checkBackup() + if (var.dbBackupTimer) then + local NUM_SECONDS_DAY = 86400; + local duration = math.abs(GetInfo(232) - var.dbBackupTimer); + + if (duration > NUM_SECONDS_DAY) then + database.mobber:backup(); + var.dbBackupTimer = GetInfo(232); + else + local timeRemaining = formatSeconds(NUM_SECONDS_DAY - duration); + utility.print("Next database backup will occur in: " .. timeRemaining, "darkgray"); + end + else + var.dbBackupTimer = GetInfo(232); + end +end + +function database.manualBackup() + if (database.isDeveloperModeEnabled()) then + database.mobber:backup(); + end +end + +function database.vacuum() + if (database.isDeveloperModeEnabled()) then + database.mobber:vacuum(); + end +end + +function database.toggleDeveloperMode() + if (database.isDeveloperMode) then + utility.print("Developer mode disabled."); + else + utility.print("Developer mode enabled."); + end + + database.isDeveloperMode = not database.isDeveloperMode; +end + +function database.isDeveloperModeEnabled() + if (not database.isDeveloperMode) then + utility.print("Developer mode is disabled.", "red"); + end + + return database.isDeveloperMode; +end + +function database.toggleConsiderMobLogging() + if (database.isConsiderMobLogging) then + utility.print("Developer consider mob logging disabled."); + EnableTriggerGroup("trigger_group_add_mob_consider", false); + else + utility.print("Developer consider mob logging enabled."); + EnableTriggerGroup("trigger_group_add_mob_consider", true); + end + + database.isConsiderMobLogging = not database.isConsiderMobLogging; +end + +function database.lookupMobKeyword(mobName) + local mobKeyword; + + local sql = [[ + SELECT mob_keyword + FROM keyword + WHERE mob_name = %s + LIMIT 1; + ]]; + + sql = sql:format(fixsql(mobName)); + + local results = database.mobber:gettable(sql); + + mobKeyword = next(results) and results[1].mob_keyword or nil; + + return mobKeyword; +end + +function database.lookupMobRoomOrAreaByLvl(location, mobName) + local playerLevel = utility.gmcp.getPlayerLevel(); + + local sql = [[ + SELECT mob.room_name, mob.room_id, mob.zone_name, mob.mob_priority + FROM mob + INNER JOIN area ON mob.zone_name = area.zone_name + WHERE mob.mob_name = %s + AND (mob.room_name = %s OR area.area_name = %s) + AND area.min_lvl - 10 <= %d + AND area.max_lvl + 15 >= %d; + ]]; + + sql = sql:format(fixsql(mobName), fixsql(location), fixsql(location), playerLevel, playerLevel); + + local results = database.mobber:gettable(sql); + + local lookupType = "mobMatchInLvlRange"; + + return results, lookupType; +end + +function database.lookupMobRoomOrArea(location, mobName) + local playerLevel = utility.gmcp.getPlayerLevel(); + + local sql = [[ + SELECT mob.room_name, mob.room_id, mob.zone_name, mob.mob_priority + FROM mob + INNER JOIN area ON mob.zone_name = area.zone_name + WHERE mob.mob_name = %s + AND (mob.room_name = %s OR area.area_name = %s); + ]]; + + sql = sql:format(fixsql(mobName), fixsql(location), fixsql(location), playerLevel, playerLevel); + + local results = database.mobber:gettable(sql); + + local lookupType = "mobMatchOutOfLvlRange"; + + return results, lookupType; +end + +function database.lookupArea(location) + local sql = [[ + SELECT zone_name, room_id + FROM area + WHERE area_name = %s + LIMIT 1; + ]]; + + sql = sql:format(fixsql(location)); + + local results = database.mobber:gettable(sql); + + local lookupType = "areaMatch"; + + return results, lookupType; +end + +function database.lookupRoomAreaByLvl(location) + local playerLevel = utility.gmcp.getPlayerLevel(); + + local sql = [[ + SELECT room.room_name, room.room_id, room.zone_name + FROM room + INNER JOIN area ON room.zone_name = area.zone_name + WHERE room.room_name = %s + AND area.min_lvl - 10 <= %d + AND area.max_lvl + 15 >= %d; + ]]; + + sql = sql:format(fixsql(location), playerLevel, playerLevel); + + local results = database.mobber:gettable(sql); + + local lookupType = "roomMatchInLvlRange"; + + return results, lookupType; +end + +function database.lookupRoomArea(location) + local sql = [[ + SELECT room.room_name, room.room_id, room.zone_name + FROM room + INNER JOIN area ON room.zone_name = area.zone_name + WHERE room.room_name = %s; + ]]; + + sql = sql:format(fixsql(location)); + + local results = database.mobber:gettable(sql); + + local lookupType = "roomMatchOutOfLvlRange"; + + return results, lookupType; +end + +function database.setMobPriority(name, line, wildcards) + if (database.isDeveloperModeEnabled()) then + local mobName = wildcards.mob_name; + local priority = tonumber(wildcards.priority); + local zoneName = utility.gmcp.getPlayerZone(); + + if (zoneName ~= "") then + local sql = [[ + UPDATE mob + SET mob_priority = %d + WHERE zone_name = %s + AND mob_name = %s; + ]]; + + sql = sql:format(priority, fixsql(zoneName), fixsql(mobName)); + + database.mobber:exec(sql); + + local numChanges = database.mobber:changes(); + + if (numChanges > 0) then + utility.print(numChanges .. " instance(s) of " .. mobName .. " had their GQ priority set to: " .. priority, "lime"); + elseif (numChanges == 0) then + utility.print("Mob " .. mobName .. " was not found in " .. zoneName .. ".", "yellow"); + end + else + utility.print("GMCP error retrieving zone."); + end + end +end + +function database.updateMobKeyword(name, line, wildcards) + if (database.isDeveloperModeEnabled()) then + local mobName = wildcards.mob_name; + local mobKeyword = wildcards.mobKeyword; + + local sql = [[ + UPDATE keyword + SET mob_keyword = %s + WHERE mob_name = %s; + ]]; + + sql = sql:format(fixsql(mobKeyword), fixsql(mobName)); + + database.mobber:exec(sql, true); + + local numChanges = database.mobber:changes(); + + if (numChanges ~= 0) then + utility.print("The mob " .. mobName .. " was updated with the keyword " .. mobKeyword .. ".", "lime"); + else + utility.print("The mob " .. mobName .. " was not found and therefore its keyword could not be updated.", "yellow"); + ColourNote( + "yellow", "", + "Possible reasons:\r\n\r\n" .. + "mob name mispelled\r\n" .. + "mob not previously added via the 'mob update keywords' command\r\n" .. + "mob not logged to the database" + ); + end + end +end + +function database.updateMobKeywords() + if (database.isDeveloperModeEnabled()) then + local start = GetInfo(232); + + local sql = [[ + SELECT mob_name FROM mob WHERE mob_name NOT IN (SELECT mob_name FROM keyword); + ]]; + + local mobNames = database.mobber:gettable(sql); + + if (next(mobNames)) then + local sql = [[ + INSERT OR IGNORE INTO keyword(mob_name, mob_keyword) VALUES(%s, %s); + ]]; + + database.mobber:exec("BEGIN TRANSACTION;", true); + + local numChanges = 0; + + for i = 1, #mobNames do + local mobKeyword = utility.getMobKeyword(mobNames[i].mob_name); + local stmt = sql:format(fixsql(mobNames[i].mob_name), fixsql(mobKeyword)); + + database.mobber:exec(stmt, true); + + numChanges = numChanges + database.mobber:changes(); + end + + database.mobber:exec("END TRANSACTION;", true); + + local dif = GetInfo(232) - start; + + utility.print(numChanges .. " mob keyword(s) created.\r\nKeyword update completed in: " .. formatSeconds(dif), "lime"); + else + utility.print("All mob keywords are up-to-date.", "greenyellow"); + end + end +end + +function database.cleanKeywords() + if (database.isDeveloperModeEnabled()) then + local sql = [[ + DELETE FROM keyword WHERE mob_name NOT IN (SELECT mob_name FROM mob); + ]]; + + database.mobber:exec(sql, true); + + local numChanges = database.mobber:changes(); + + if (numChanges ~= 0) then + utility.print(numChanges .. " unmatched keyword(s) deleted from the mobber database.", "lime"); + else + utility.print("There are no unmatched keywords in the mobber database!", "greenyellow"); + end + end +end + +function database.removeMobRoom() + if (database.isDeveloperModeEnabled()) then + local roomId = utility.gmcp.getPlayerRoomId(); + + if (roomId) then + local sql = [[ + DELETE FROM mob WHERE room_id = %d; + ]]; + + sql = sql:format(roomId); + + database.mobber:exec(sql, true); + + local numChanges = database.mobber:changes(); + + utility.print(numChanges .. " mob(s) removed from roomid: " .. roomId, "lime"); + else + utility.print("GMCP error retrieving room id."); + end + end +end + +function database.removeMobZone() + if (database.isDeveloperModeEnabled()) then + local zoneName = utility.gmcp.getPlayerZone(); + + if (zoneName ~= "") then + local sql = [[ + DELETE FROM mob WHERE zone_name = %s; + ]]; + + sql = sql:format(fixsql(zoneName)); + + database.mobber:exec(sql, true); + + local numChanges = database.mobber:changes(); + + utility.print(numChanges .. " mob(s) removed from zone: " .. zoneName, "lime"); + else + utility.print("GMCP error retrieving zone."); + end + end +end + +function database.removeMobSingle(name, line, wildcards) + if (database.isDeveloperModeEnabled()) then + local mobName = wildcards.mob_name; + local zoneName = utility.gmcp.getPlayerZone(); + + if (zoneName ~= "") then + local sql = [[ + DELETE FROM mob WHERE mob_name = %s AND zone_name = %s; + ]]; + + sql = sql:format(fixsql(mobName), fixsql(zoneName)); + + database.mobber:exec(sql, true); + + local numChanges = database.mobber:changes(); + + utility.print(numChanges .. " instance(s) of " .. mobName .. " removed from zone: " .. zoneName, "lime"); + else + utility.print("GMCP error retrieving zone."); + end + end +end + +function database.addMob(name, line, wildcards) + if (name ~= "alias_database_add_mob" or database.isDeveloperModeEnabled()) then + local mobName = wildcards.mob_name; + local mobPriority = tonumber(wildcards.mob_priority) or 0; + + local roomName = utility.gmcp.getPlayerRoomName(); + local zoneName = utility.gmcp.getPlayerZone(); + local roomId = utility.gmcp.getPlayerRoomId(); + + if (roomName ~= "" and zoneName ~= "" and roomId) then + roomName = stripColors(roomName); + + local sql = [[ + INSERT OR IGNORE INTO mob VALUES(%s, %s, %d, %s, %d); + ]]; + + sql = sql:format(fixsql(mobName), fixsql(roomName), roomId, fixsql(zoneName), mobPriority); + + local code = database.mobber:exec(sql, false); + + if (code == sqlite3.OK) then + local numChanges = database.mobber:changes(); + + if (numChanges == 1) then + if (name == "alias_database_add_mob") then + utility.print( + numChanges .. " mob was added to the room.\r\n\r\n" .. + "Mob: " .. mobName .. "\r\n" .. + "Room: " .. roomName .. "\r\n" .. + "RoomId: " .. roomId .. "\r\n" .. + "Zone: " .. zoneName .. "\r\n" .. + "Priority: " .. mobPriority, + "lime" + ); + else + utility.print(mobName .. " was logged to the room." ,"lime"); + end + else + utility.print(mobName .. " is already logged to room id: " .. roomId, "yellow"); + end + elseif (code == sqlite3.CONSTRAINT) then + utility.print("Error: This room and/or zone is missing from the mobber database.", "red"); + else + local err = self.db:errmsg(); + database.mobber.db:execute("ROLLBACK;"); + error(err); + end + else + utility.print("GMCP error adding mob."); + end + end +end + +function database.startAddArea(name, line, wildcards) + if (database.isDeveloperModeEnabled()) then + if (tonumber(wildcards.min_lvl) > tonumber(wildcards.max_lvl)) then + return utility.print("Max level must be greater than or equal to min level.", "yellow"); + end + + database.area = {}; + database.area.minLvl = tonumber(wildcards.min_lvl); + database.area.maxLvl = tonumber(wildcards.max_lvl); + database.area.roomId = utility.gmcp.getPlayerRoomId(); + database.area.isAddingArea = true; + + utility.gmcp.requestPlayerArea(); + end +end + +function database.addArea(zoneName, areaName) + if (database.area and zoneName ~= "" and areaName ~= "") then + local sql = [[ + INSERT OR IGNORE INTO area VALUES(%s, %s, %d, %d, %d); + ]]; + + sql = sql:format(fixsql(zoneName), fixsql(areaName), database.area.minLvl, database.area.maxLvl, database.area.roomId); + + database.mobber:exec(sql, true); + + local numChanges = database.mobber:changes(); + + if (numChanges == 1) then + utility.print("Added to database: " .. areaName .. " (" .. zoneName .. ") " .. database.area.minLvl .. " to " .. database.area.maxLvl, "lime"); + elseif (numChanges == 0) then + utility.print("This area already exists in the mobber database.", "yellow"); + end + else + utility.print("GMCP error adding area."); + end + + database.area = nil; +end + +function database.removeArea() + if (database.isDeveloperModeEnabled()) then + local zoneName = utility.gmcp.getPlayerZone(); + + if (zoneName ~= "") then + local start = GetInfo(232); + + local sql = [[ + DELETE FROM area WHERE zone_name = %s; + ]]; + + sql = sql:format(fixsql(zoneName)); + + database.mobber:exec(sql, true); + + local dif = GetInfo(232) - start; + + local numChanges = database.mobber:changes(); + + if (numChanges == 1) then + utility.print(zoneName .. " was removed from the mobber database.\r\nDelete operation completed in: " .. formatSeconds(dif), "lime"); + elseif (numChanges == 0) then + utility.print(zoneName .. " does not exist in the mobber database!", "yellow"); + end + else + utility.print("GMCP error retrieving zone."); + end + end +end + +function database.showAreas(name, line, wildcards) + local order = wildcards.order; + + if (order == "mob" or order == "mobs") then + order = "mob_count"; + elseif (order == "room" or order == "rooms") then + order = "room_count"; + elseif (order == "level") then + order = "min_lvl ASC, max_lvl"; + else + order = "zone_name"; + end + + local sql = [[ + SELECT * FROM + ( + SELECT area.zone_name, area.area_name, area.min_lvl, area.max_lvl, area.room_id, COUNT(room.room_id) AS room_count + FROM area + LEFT JOIN room USING(zone_name) + GROUP BY zone_name + ) + JOIN + ( + SELECT area.zone_name, COUNT(DISTINCT mob.mob_name) AS mob_count + FROM area + LEFT JOIN mob USING(zone_name) + GROUP BY zone_name + ) + USING (zone_name) + ORDER BY %s ASC; + ]]; + + sql = sql:format(order); + + local areas = database.mobber:gettable(sql); + + if (next(areas)) then + display.printAreas(areas); + end + + utility.print("Found " .. #areas .. " area(s) in the mobber database.", "lime"); +end + +function database.showArea(name, line, wildcards) + local zoneName = wildcards.zone_name ~= "" and wildcards.zone_name or utility.gmcp.getPlayerZone(); + + if (zoneName ~= "") then + local sql = [[ + SELECT * FROM + ( + SELECT area.zone_name, area.area_name, area.min_lvl, area.max_lvl, area.room_id, COUNT(room.room_id) AS room_count + FROM area + LEFT JOIN room USING(zone_name) + WHERE zone_name = %s + GROUP BY zone_name + ) + JOIN + ( + SELECT area.zone_name, COUNT(DISTINCT mob.mob_name) AS mob_count + FROM area + LEFT JOIN mob USING(zone_name) + WHERE zone_name = %s + GROUP BY zone_name + ) + USING (zone_name); + ]]; + + sql = sql:format(fixsql(zoneName), fixsql(zoneName)); + + local areas = database.mobber:gettable(sql); + + if (next(areas)) then + display.printAreas(areas); + end + + utility.print("Found " .. #areas .. " area(s) in the mobber database with the name: " .. zoneName, "lime"); + else + utility.print("GMCP error retrieving zone."); + end +end + +function database.updateAreaId() + if (database.isDeveloperModeEnabled()) then + local roomId = utility.gmcp.getPlayerRoomId(); + local zoneName = utility.gmcp.getPlayerZone(); + + if (roomId and zoneName ~= "") then + local sql = [[ + UPDATE area + SET room_id = %d + WHERE zone_name = %s; + ]]; + + sql = sql:format(roomId, fixsql(zoneName)); + + database.mobber:exec(sql, true); + + local numChanges = database.mobber:changes(); + + if (numChanges == 1) then + utility.print(zoneName .. " roomid set to: " .. roomId, "lime"); + elseif (numChanges == 0) then + utility.print( + zoneName .. " was not found in the mobber database.\r\n" .. + "Possible solutions:\r\n" .. + "Add area to the database with the 'mobber addarea ' command.", + "red" + ); + end + else + utility.print("GMCP error updating zone id."); + end + end +end + +function database.updateAreaLevelRange(name, line, wildcards) + if (database.isDeveloperModeEnabled()) then + local minLvl = tonumber(wildcards.min_lvl); + local maxLvl = tonumber(wildcards.max_lvl); + + if (minLvl > maxLvl) then + return utility.print("Max level must be greater than or equal to min level.", "yellow"); + end + + local zoneName = utility.gmcp.getPlayerZone(); + + if (zoneName ~= "") then + local sql = [[ + UPDATE area + SET min_lvl = %d, max_lvl = %d + WHERE zone_name = %s; + ]]; + + sql = sql:format(minLvl, maxLvl, fixsql(zoneName)); + + database.mobber:exec(sql, true); + + local numChanges = database.mobber:changes(); + + if (numChanges == 1) then + utility.print(zoneName .. " level range set to: " .. minLvl .. " - " .. maxLvl .. ".", "lime"); + elseif (numChanges == 0) then + utility.print("The zone " .. zoneName .. " was not found in the mobber database.", "yellow"); + end + else + utility.print("GMCP error retrieving zone."); + end + end +end + +function database.updateRooms() + if (database.isDeveloperModeEnabled()) then + local start = GetInfo(232); + + local sql = [[ + ATTACH DATABASE %s AS 'aardwolf'; + ]]; + + sql = sql:format(fixsql(GetInfo(68) .. "Aardwolf.db")); + + database.mobber:exec(sql, true); + + sql = [[ + INSERT OR IGNORE INTO room('room_name', 'room_id', 'zone_name') + SELECT name, CAST(uid AS INTEGER), area + FROM aardwolf.rooms + WHERE EXISTS (SELECT * FROM area WHERE aardwolf.rooms.area = area.zone_name); + ]]; + + database.mobber:exec(sql, true); + + local numChanges = database.mobber:changes(); + + sql = [[ + DETACH DATABASE 'aardwolf'; + ]]; + + database.mobber:exec(sql, true); + + local dif = GetInfo(232) - start; + + utility.print(numChanges .. " room(s) imported from Aardwolf.db\r\nRoom update completed in: " .. formatSeconds(dif), "lime"); + end +end + +function database.checkCreateTables() + local sql = [[ + CREATE TABLE IF NOT EXISTS `area` ( + `zone_name` TEXT NOT NULL UNIQUE, + `area_name` TEXT NOT NULL, + `min_lvl` INTEGER NOT NULL, + `max_lvl` INTEGER NOT NULL, + `room_id` INTEGER NOT NULL UNIQUE, + PRIMARY KEY(`zone_name`) + ); + + CREATE TABLE IF NOT EXISTS `room` ( + `room_name` TEXT NOT NULL, + `room_id` INTEGER NOT NULL UNIQUE, + `zone_name` TEXT NOT NULL, + FOREIGN KEY(`zone_name`) REFERENCES `area`(`zone_name`) ON DELETE CASCADE, + PRIMARY KEY(`room_id`) + ); + + CREATE TABLE IF NOT EXISTS `mob` ( + `mob_name` TEXT NOT NULL COLLATE NOCASE, + `room_name` TEXT NOT NULL, + `room_id` INTEGER NOT NULL, + `zone_name` TEXT NOT NULL, + `mob_priority` INTEGER NOT NULL, + FOREIGN KEY(`room_id`) REFERENCES `room`(`room_id`) ON DELETE CASCADE, + FOREIGN KEY(`zone_name`) REFERENCES `area`(`zone_name`) ON DELETE CASCADE, + PRIMARY KEY(`mob_name`,`room_id`) + ); + + CREATE TABLE IF NOT EXISTS `keyword` ( + `mob_name` TEXT NOT NULL UNIQUE COLLATE NOCASE, + `mob_keyword` TEXT NOT NULL, + PRIMARY KEY(`mob_name`) + ); + + CREATE INDEX IF NOT EXISTS `idx_area_an_zn_rid` ON `area` ( + `area_name`, + `zone_name`, + `room_id` + ); + + CREATE INDEX IF NOT EXISTS `idx_room_rn_zn_rid` ON `room` ( + `room_name`, + `zone_name`, + `room_id` + ); + ]]; + + database.mobber:exec(sql); +end + +function database.updateAreas() + if (database.isDeveloperModeEnabled()) then + local start = GetInfo(232); + + local zones = { + ["aardington"] = { area_name = "Aardington Estate", room_id = 47509, min_lvl = 70, max_lvl = 90 }, + ["abend"] = { area_name = "The Dark Continent, Abend", room_id = 24909, min_lvl = 1, max_lvl = 201 }, + ["academy"] = { area_name = "The Aylorian Academy", room_id = 35233, min_lvl = 1, max_lvl = 15 }, + ["adaldar"] = { area_name = "Battlefields of Adaldar", room_id = 34400, min_lvl = 150, max_lvl = 175 }, + ["afterglow"] = { area_name = "Afterglow", room_id = 38134, min_lvl = 190, max_lvl = 201 }, + ["agroth"] = { area_name = "The Marshlands of Agroth", room_id = 11027, min_lvl = 105, max_lvl = 125 }, + ["ahner"] = { area_name = "Kingdom of Ahner", room_id = 30129, min_lvl = 20, max_lvl = 50 }, + ["alagh"] = { area_name = "Alagh, the Blood Lands", room_id = 3224, min_lvl = 1, max_lvl = 201 }, + ["alehouse"] = { area_name = "Wayward Alehouse", room_id = 885, min_lvl = 75, max_lvl = 90 }, + ["amazon"] = { area_name = "The Amazon Nation", room_id = 1409, min_lvl = 120, max_lvl = 201 }, + ["amazonclan"] = { area_name = "The Ivory City", room_id = 34212, min_lvl = 210, max_lvl = 210 }, + ["amusement"] = { area_name = "The Amusement Park", room_id = 29282, min_lvl = 1, max_lvl = 20 }, + ["andarin"] = { area_name = "The Blighted Tundra of Andarin", room_id = 2399, min_lvl = 35, max_lvl = 60 }, + ["annwn"] = { area_name = "Annwn", room_id = 28963, min_lvl = 160, max_lvl = 186 }, + ["anthrox"] = { area_name = "Anthrox", room_id = 3993, min_lvl = 80, max_lvl = 105 }, + ["arboretum"] = { area_name = "Arboretum", room_id = 39100, min_lvl = 110, max_lvl = 130 }, + ["arena"] = { area_name = "The Gladiator's Arena", room_id = 25768, min_lvl = 90, max_lvl = 105 }, + ["arisian"] = { area_name = "Arisian Realm", room_id = 28144, min_lvl = 150, max_lvl = 170 }, + ["ascent"] = { area_name = "The First Ascent", room_id = 43161, min_lvl = 1, max_lvl = 15 }, + ["astral"] = { area_name = "The Astral Travels", room_id = 27882, min_lvl = 180, max_lvl = 201 }, + ["atlantis"] = { area_name = "Atlantis", room_id = 10573, min_lvl = 20, max_lvl = 40 }, + ["autumn"] = { area_name = "Eternal Autumn", room_id = 13839, min_lvl = 170, max_lvl = 201 }, + ["avian"] = { area_name = "Avian Kingdom", room_id = 4334, min_lvl = 170, max_lvl = 186 }, + ["aylor"] = { area_name = "The Grand City of Aylor", room_id = 32418, min_lvl = 1, max_lvl = 201 }, + ["badtrip"] = { area_name = "A Bad Trip", room_id = 32877, min_lvl = 210, max_lvl = 210 }, + ["bard"] = { area_name = "The Bard Clan", room_id = 30538, min_lvl = 1, max_lvl = 201 }, + ["bazaar"] = { area_name = "Onyx Bazaar", room_id = 34454, min_lvl = 30, max_lvl = 85 }, + ["beer"] = { area_name = "The Land of the Beer Goblins", room_id = 20062, min_lvl = 1, max_lvl = 20 }, + ["believer"] = { area_name = "The Path of the Believer", room_id = 25940, min_lvl = 1, max_lvl = 10 }, + ["birthday"] = { area_name = "Aardwolf Birthday Area", room_id = 10920, min_lvl = 210, max_lvl = 210 }, + ["blackrose"] = { area_name = "Black Rose", room_id = 1817, min_lvl = 175, max_lvl = 201 }, + ["bliss"] = { area_name = "Wedded Bliss", room_id = 29988, min_lvl = 80, max_lvl = 100 }, + ["bonds"] = { area_name = "Unearthly Bonds", room_id = 23411, min_lvl = 140, max_lvl = 170 }, + ["bootcamp"] = { area_name = "The Boot Camp", room_id = 49256, min_lvl = 1, max_lvl = 201 }, + ["cabal"] = { area_name = "Cathedral of the Elements", room_id = 15704, min_lvl = 1, max_lvl = 201 }, + ["caldera"] = { area_name = "The Icy Caldera of Mauldoon", room_id = 26341, min_lvl = 190, max_lvl = 201 }, + ["callhero"] = { area_name = "The Call of Heroes", room_id = 33031, min_lvl = 1, max_lvl = 15 }, + ["camps"] = { area_name = "Tournament Camps", room_id = 4714, min_lvl = 1, max_lvl = 15 }, + ["canyon"] = { area_name = "Canyon Memorial Hospital", room_id = 25551, min_lvl = 1, max_lvl = 30 }, + ["caravan"] = { area_name = "Gypsy Caravan", room_id = 16071, min_lvl = 190, max_lvl = 201 }, + ["cards"] = { area_name = "House of Cards", room_id = 6255, min_lvl = 120, max_lvl = 160 }, + ["carnivale"] = { area_name = "Olde Worlde Carnivale", room_id = 28635, min_lvl = 1, max_lvl = 35 }, + ["cataclysm"] = { area_name = "The Cataclysm", room_id = 19976, min_lvl = 145, max_lvl = 201 }, + ["cathedral"] = { area_name = "The Old Cathedral", room_id = 27497, min_lvl = 50, max_lvl = 80 }, + ["cats"] = { area_name = "Sheila's Cat Sanctuary", room_id = 40900, min_lvl = 1, max_lvl = 35 }, + ["chaos"] = { area_name = "The Realm of Chaos", room_id = 28909, min_lvl = 1, max_lvl = 201 }, + ["chasm"] = { area_name = "The Chasm and The Catacombs", room_id = 29446, min_lvl = 1, max_lvl = 25 }, + ["chessboard"] = { area_name = "The Chessboard", room_id = 25513, min_lvl = 1, max_lvl = 20 }, + ["childsplay"] = { area_name = "Child's Play", room_id = 678, min_lvl = 1, max_lvl = 25 }, + ["cineko"] = { area_name = "Aerial City of Cineko", room_id = 1507, min_lvl = 1, max_lvl = 30 }, + ["citadel"] = { area_name = "The Flying Citadel", room_id = 14963, min_lvl = 40, max_lvl = 65 }, + ["conflict"] = { area_name = "Thandeld's Conflict", room_id = 27711, min_lvl = 1, max_lvl = 50 }, + ["coral"] = { area_name = "The Coral Kingdom", room_id = 4565, min_lvl = 1, max_lvl = 50 }, + ["cougarian"] = { area_name = "The Cougarian Queendom", room_id = 14311, min_lvl = 140, max_lvl = 170 }, + ["cove"] = { area_name = "Kiksaadi Cove", room_id = 49941, min_lvl = 190, max_lvl = 201 }, + ["cradle"] = { area_name = "Cradlebrook", room_id = 11267, min_lvl = 30, max_lvl = 50 }, + ["crimson"] = { area_name = "The Crimson Horde Clan Hall", room_id = 27989, min_lvl = 1, max_lvl = 201 }, + ["crusaders"] = { area_name = "The Crusader Clan", room_id = 31122, min_lvl = 1, max_lvl = 201 }, + ["crynn"] = { area_name = "Crynn's Church", room_id = 43800, min_lvl = 190, max_lvl = 201 }, + ["damned"] = { area_name = "Halls of the Damned", room_id = 10469, min_lvl = 95, max_lvl = 115 }, + ["daoine"] = { area_name = "The Underground Hall", room_id = 30949, min_lvl = 1, max_lvl = 201 }, + ["darklight"] = { area_name = "The DarkLight", room_id = 19642, min_lvl = 60, max_lvl = 120 }, + ["darkside"] = { area_name = "The Darkside of the Fractured Lands", room_id = 15060, min_lvl = 35, max_lvl = 60 }, + ["ddoom"] = { area_name = "Desert Doom", room_id = 4193, min_lvl = 135, max_lvl = 150 }, + ["deadlights"] = { area_name = "The Deadlights", room_id = 16856, min_lvl = 175, max_lvl = 201 }, + ["deathtrap"] = { area_name = "Deathtrap Dungeon", room_id = 1767, min_lvl = 85, max_lvl = 120 }, + ["deneria"] = { area_name = "Realm of Deneria", room_id = 35006, min_lvl = 60, max_lvl = 80 }, + ["desert"] = { area_name = "The Desert Prison", room_id = 20186, min_lvl = 130, max_lvl = 201 }, + ["desolation"] = { area_name = "The Mountains of Desolation", room_id = 19532, min_lvl = 130, max_lvl = 180 }, + ["dhalgora"] = { area_name = "Dhal'Gora Outlands", room_id = 16755, min_lvl = 1, max_lvl = 50 }, + ["diatz"] = { area_name = "The Three Pillars of Diatz", room_id = 1254, min_lvl = 60, max_lvl = 80 }, + ["diner"] = { area_name = "Tumari's Diner", room_id = 36700, min_lvl = 130, max_lvl = 140 }, + ["doh"] = { area_name = "Disciples of Hassan Clan Hall", room_id = 16803, min_lvl = 1, max_lvl = 201 }, + ["dominion"] = { area_name = "Dominion Clan Area", room_id = 5863, min_lvl = 1, max_lvl = 201 }, + ["dortmund"] = { area_name = "Dortmund", room_id = 16577, min_lvl = 1, max_lvl = 25 }, + ["drageran"] = { area_name = "The Drageran Empire", room_id = 25894, min_lvl = 125, max_lvl = 150 }, + ["dragon"] = { area_name = "The White Dragon Clan", room_id = 642, min_lvl = 1, max_lvl = 201 }, + ["dread"] = { area_name = "Dread Tower", room_id = 26075, min_lvl = 160, max_lvl = 201 }, + ["druid"] = { area_name = "Isle of Anglesey", room_id = 29582, min_lvl = 1, max_lvl = 201 }, + ["dsr"] = { area_name = "Diamond Soul Revelation", room_id = 30030, min_lvl = 35, max_lvl = 90 }, + ["dundoom"] = { area_name = "The Dungeon of Doom", room_id = 25661, min_lvl = 190, max_lvl = 201 }, + ["dunoir"] = { area_name = "Mount duNoir", room_id = 14222, min_lvl = 175, max_lvl = 201 }, + ["duskvalley"] = { area_name = "Dusk Valley", room_id = 37301, min_lvl = 100, max_lvl = 120 }, + ["dynasty"] = { area_name = "The Eighteenth Dynasty", room_id = 30799, min_lvl = 120, max_lvl = 140 }, + ["earthlords"] = { area_name = "The Earth Lords", room_id = 42000, min_lvl = 190, max_lvl = 201 }, + ["earthplane"] = { area_name = "Earth Plane 4", room_id = 1354, min_lvl = 50, max_lvl = 80 }, + ["elemental"] = { area_name = "Elemental Chaos", room_id = 41624, min_lvl = 90, max_lvl = 150 }, + ["emerald"] = { area_name = "The Emerald Clan HQ", room_id = 831, min_lvl = 1, max_lvl = 201 }, + ["empire"] = { area_name = "The Empire of Aiighialla", room_id = 32203, min_lvl = 150, max_lvl = 186 }, + ["empyrean"] = { area_name = "Empyrean, Streets of Downfall", room_id = 14042, min_lvl = 170, max_lvl = 201 }, + ["entropy"] = { area_name = "The Archipelago of Entropy", room_id = 29773, min_lvl = 120, max_lvl = 145 }, + ["fantasy"] = { area_name = "Fantasy Fields", room_id = 15205, min_lvl = 1, max_lvl = 30 }, + ["farm"] = { area_name = "Kimr's Farm", room_id = 10676, min_lvl = 1, max_lvl = 10 }, + ["fayke"] = { area_name = "All in a Fayke Day", room_id = 30418, min_lvl = 1, max_lvl = 30 }, + ["fens"] = { area_name = "The Curse of the Midnight Fens", room_id = 16528, min_lvl = 190, max_lvl = 201 }, + ["fields"] = { area_name = "The Killing Fields", room_id = 29232, min_lvl = 60, max_lvl = 80 }, + ["firebird"] = { area_name = "Realm of the Firebird", room_id = 32885, min_lvl = 80, max_lvl = 110 }, + ["firenation"] = { area_name = "Realm of the Sacred Flame", room_id = 41879, min_lvl = 190, max_lvl = 201 }, + ["fireswamp"] = { area_name = "The Fire Swamp", room_id = 34755, min_lvl = 1, max_lvl = 15 }, + ["fortress"] = { area_name = "The Goblin Fortress", room_id = 31835, min_lvl = 60, max_lvl = 80 }, + ["fortune"] = { area_name = "Crossroads of Fortune", room_id = 38561, min_lvl = 201, max_lvl = 201 }, + ["fractured"] = { area_name = "The Fractured Lands", room_id = 17033, min_lvl = 20, max_lvl = 40 }, + ["ft1"] = { area_name = "Faerie Tales", room_id = 1205, min_lvl = 100, max_lvl = 120 }, + ["ftii"] = { area_name = "Faerie Tales II", room_id = 26673, min_lvl = 120, max_lvl = 140 }, + ["gaardian"] = { area_name = "Midgaardian Publishing House", room_id = 20026, min_lvl = 1, max_lvl = 201 }, + ["gallows"] = { area_name = "Gallows Hill", room_id = 4344, min_lvl = 1, max_lvl = 20 }, + ["gathering"] = { area_name = "The Gathering Horde", room_id = 36451, min_lvl = 140, max_lvl = 170 }, + ["gauntlet"] = { area_name = "The Gauntlet", room_id = 31652, min_lvl = 1, max_lvl = 30 }, + ["gelidus"] = { area_name = "Gelidus", room_id = 18780, min_lvl = 1, max_lvl = 201 }, + ["geniewish"] = { area_name = "A Genie's Last Wish", room_id = 38464, min_lvl = 201, max_lvl = 201 }, + ["gilda"] = { area_name = "Gilda And The Dragon", room_id = 4243, min_lvl = 120, max_lvl = 140 }, + ["glamdursil"] = { area_name = "The Glamdursil", room_id = 35055, min_lvl = 170, max_lvl = 201 }, + ["glimmerdim"] = { area_name = "Brightsea and Glimmerdim", room_id = 26252, min_lvl = 1, max_lvl = 40 }, + ["gnomalin"] = { area_name = "Cloud City of Gnomalin", room_id = 34397, min_lvl = 1, max_lvl = 35 }, + ["goldrush"] = { area_name = "Gold Rush", room_id = 15014, min_lvl = 30, max_lvl = 70 }, + ["graveyard"] = { area_name = "The Graveyard", room_id = 28918, min_lvl = 1, max_lvl = 15 }, + ["greece"] = { area_name = "Ancient Greece", room_id = 2089, min_lvl = 20, max_lvl = 55 }, + ["gwillim"] = { area_name = "The Trouble with Gwillimberry", room_id = 25974, min_lvl = 185, max_lvl = 201 }, + ["hades"] = { area_name = "Entrance to Hades", room_id = 29161, min_lvl = 180, max_lvl = 201 }, + ["hatchling"] = { area_name = "Hatchling Aerie", room_id = 34670, min_lvl = 1, max_lvl = 55 }, + ["hawklord"] = { area_name = "The Realm of the Hawklords", room_id = 40550, min_lvl = 80, max_lvl = 100 }, + ["hedge"] = { area_name = "Hedgehogs' Paradise", room_id = 15146, min_lvl = 60, max_lvl = 80 }, + ["helegear"] = { area_name = "Helegear Sea", room_id = 30699, min_lvl = 160, max_lvl = 180 }, + ["hell"] = { area_name = "Descent to Hell", room_id = 30984, min_lvl = 30, max_lvl = 85 }, + ["hoard"] = { area_name = "Swordbreaker's Hoard", room_id = 1675, min_lvl = 1, max_lvl = 60 }, + ["hodgepodge"] = { area_name = "A Magical Hodgepodge", room_id = 30469, min_lvl = 1, max_lvl = 35 }, + ["horath"] = { area_name = "The Broken Halls of Horath", room_id = 91, min_lvl = 120, max_lvl = 175 }, + ["horizon"] = { area_name = "Nebulous Horizon", room_id = 31959, min_lvl = 190, max_lvl = 201 }, + ["icefall"] = { area_name = "Icefall", room_id = 38701, min_lvl = 201, max_lvl = 201 }, + ["illoria"] = { area_name = "The Tournament of Illoria", room_id = 10420, min_lvl = 70, max_lvl = 90 }, + ["imagi"] = { area_name = "Imagi's Nation", room_id = 36800, min_lvl = 140, max_lvl = 160 }, + ["immhomes"] = { area_name = "The Aardwolf Plaza Hotel", room_id = 26151, min_lvl = 0, max_lvl = 220 }, + ["imperial"] = { area_name = "Imperial Nation", room_id = 16966, min_lvl = 40, max_lvl = 201 }, + ["imperium"] = { area_name = "The Stronghold of the Imperium", room_id = 30415, min_lvl = 1, max_lvl = 201 }, + ["infamy"] = { area_name = "The Realm of Infamy", room_id = 26641, min_lvl = 190, max_lvl = 201 }, + ["inferno"] = { area_name = "Journey to the Inferno", room_id = 37213, min_lvl = 200, max_lvl = 201 }, + ["infest"] = { area_name = "The Infestation", room_id = 16165, min_lvl = 1, max_lvl = 35 }, + ["insan"] = { area_name = "Insanitaria", room_id = 6850, min_lvl = 95, max_lvl = 115 }, + ["jenny"] = { area_name = "Jenny's Tavern", room_id = 29637, min_lvl = 55, max_lvl = 100 }, + ["jotun"] = { area_name = "Jotunheim", room_id = 31508, min_lvl = 1, max_lvl = 40 }, + ["kearvek"] = { area_name = "The Keep of Kearvek", room_id = 29722, min_lvl = 180, max_lvl = 201 }, + ["kerofk"] = { area_name = "Kerofk", room_id = 16405, min_lvl = 1, max_lvl = 30 }, + ["ketu"] = { area_name = "Ketu Uplands", room_id = 35114, min_lvl = 190, max_lvl = 201 }, + ["kingsholm"] = { area_name = "Kingsholm", room_id = 27522, min_lvl = 1, max_lvl = 70 }, + ["knossos"] = { area_name = "The Great City of Knossos", room_id = 28193, min_lvl = 60, max_lvl = 80 }, + ["kobaloi"] = { area_name = "Keep of the Kobaloi", room_id = 10691, min_lvl = 1, max_lvl = 201 }, + ["kultiras"] = { area_name = "Kul Tiras", room_id = 31161, min_lvl = 1, max_lvl = 30 }, + ["lab"] = { area_name = "Chaprenula's Laboratory", room_id = 28684, min_lvl = 1, max_lvl = 15 }, + ["labyrinth"] = { area_name = "The Labyrinth", room_id = 31405, min_lvl = 30, max_lvl = 60 }, + ["lagoon"] = { area_name = "Black Lagoon", room_id = 30549, min_lvl = 155, max_lvl = 195 }, + ["landofoz"] = { area_name = "The Land of Oz", room_id = 510, min_lvl = 40, max_lvl = 75 }, + ["laym"] = { area_name = "Tai'rha Laym", room_id = 6005, min_lvl = 50, max_lvl = 120 }, + ["legend"] = { area_name = "Land of Legend", room_id = 16224, min_lvl = 1, max_lvl = 20 }, + ["lemdagor"] = { area_name = "Storm Ships of Lem-Dagor", room_id = 1966, min_lvl = 40, max_lvl = 100 }, + ["lidnesh"] = { area_name = "The Forest of Li'Dnesh", room_id = 27995, min_lvl = 1, max_lvl = 10 }, + ["light"] = { area_name = "The Order of Light", room_id = 2339, min_lvl = 1, max_lvl = 201 }, + ["livingmine"] = { area_name = "Living Mines of Dak'Tai", room_id = 37008, min_lvl = 110, max_lvl = 140 }, + ["longnight"] = { area_name = "Into the Long Night", room_id = 26367, min_lvl = 100, max_lvl = 201 }, + ["loqui"] = { area_name = "Loqui Clan Area", room_id = 28580, min_lvl = 1, max_lvl = 201 }, + ["losttime"] = { area_name = "Island of Lost Time", room_id = 28584, min_lvl = 80, max_lvl = 110 }, + ["lowlands"] = { area_name = "Lowlands Paradise '96", room_id = 28044, min_lvl = 1, max_lvl = 10 }, + ["lplanes"] = { area_name = "The Lower Planes", room_id = 29364, min_lvl = 70, max_lvl = 100 }, + ["maelstrom"] = { area_name = "The Maelstrom", room_id = 38058, min_lvl = 20, max_lvl = 45 }, + ["manor"] = { area_name = "Death's Manor", room_id = 10621, min_lvl = 20, max_lvl = 40 }, + ["manor1"] = { area_name = "The Aardwolf Real Estates", room_id = 14460, min_lvl = 1, max_lvl = 201 }, + ["manor3"] = { area_name = "Aardwolf Estates 2000", room_id = 20836, min_lvl = 1, max_lvl = 201 }, + ["manorisle"] = { area_name = "The Aardwolf Isle Estates", room_id = 6366, min_lvl = 1, max_lvl = 201 }, + ["manormount"] = { area_name = "Mountain View Estates", room_id = 39449, min_lvl = 1, max_lvl = 201 }, + ["manorsea"] = { area_name = "Seaside Height Estates", room_id = 35003, min_lvl = 1, max_lvl = 201 }, + ["manorville"] = { area_name = "Prairie Village Estates", room_id = 35004, min_lvl = 1, max_lvl = 201 }, + ["manorwoods"] = { area_name = "Shady Acres Estates", room_id = 35002, min_lvl = 1, max_lvl = 201 }, + ["masaki"] = { area_name = "Masaki Clan Area", room_id = 15852, min_lvl = 1, max_lvl = 201 }, + ["masq"] = { area_name = "Masquerade Island", room_id = 29840, min_lvl = 100, max_lvl = 130 }, + ["mayhem"] = { area_name = "Artificer's Mayhem", room_id = 1866, min_lvl = 180, max_lvl = 201 }, + ["melody"] = { area_name = "Art of Melody", room_id = 14172, min_lvl = 1, max_lvl = 15 }, + ["mesolar"] = { area_name = "The Continent of Mesolar", room_id = 12664, min_lvl = 1, max_lvl = 201 }, + ["minos"] = { area_name = "The Shadows of Minos", room_id = 20472, min_lvl = 1, max_lvl = 35 }, + ["mistridge"] = { area_name = "The Covenant of Mistridge", room_id = 4491, min_lvl = 160, max_lvl = 186 }, + ["monastery"] = { area_name = "The Monastery", room_id = 15756, min_lvl = 85, max_lvl = 115 }, + ["mudwog"] = { area_name = "Mudwog's Swamp", room_id = 2347, min_lvl = 30, max_lvl = 45 }, + ["nanjiki"] = { area_name = "Nanjiki Ruins", room_id = 11203, min_lvl = 140, max_lvl = 160 }, + ["necro"] = { area_name = "Necromancers' Guild", room_id = 29922, min_lvl = 1, max_lvl = 35 }, + ["nenukon"] = { area_name = "Nenukon and the Far Country", room_id = 31784, min_lvl = 70, max_lvl = 110 }, + ["newthalos"] = { area_name = "New Thalos", room_id = 23853, min_lvl = 1, max_lvl = 35 }, + ["ninehells"] = { area_name = "The Nine Hells", room_id = 4613, min_lvl = 190, max_lvl = 201 }, + ["northstar"] = { area_name = "Northstar", room_id = 11127, min_lvl = 70, max_lvl = 150 }, + ["nottingham"] = { area_name = "Nottingham", room_id = 11077, min_lvl = 190, max_lvl = 201 }, + ["nulan"] = { area_name = "Plains of Nulan'Boar", room_id = 37900, min_lvl = 60, max_lvl = 80 }, + ["nursing"] = { area_name = "Ascension Bluff Nursing Home", room_id = 31977, min_lvl = 100, max_lvl = 130 }, + ["nynewoods"] = { area_name = "The Nyne Woods", room_id = 23562, min_lvl = 190, max_lvl = 201 }, + ["oceanpark"] = { area_name = "Andolor's Ocean Adventure Park", room_id = 39600, min_lvl = 190, max_lvl = 201 }, + ["omentor"] = { area_name = "The Witches of Omen Tor", room_id = 15579, min_lvl = 160, max_lvl = 201 }, + ["ooku"] = { area_name = "Ookushka Garrison", room_id = 39000, min_lvl = 201, max_lvl = 201 }, + ["oradrin"] = { area_name = "Oradrin's Chosen", room_id = 25436, min_lvl = 201, max_lvl = 201 }, + ["origins"] = { area_name = "Tribal Origins", room_id = 35900, min_lvl = 175, max_lvl = 186 }, + ["orlando"] = { area_name = "Hotel Orlando", room_id = 30331, min_lvl = 1, max_lvl = 20 }, + ["paradise"] = { area_name = "Paradise Lost", room_id = 29624, min_lvl = 50, max_lvl = 70 }, + ["partroxis"] = { area_name = "The Partroxis", room_id = 5814, min_lvl = 180, max_lvl = 201 }, + ["peninsula"] = { area_name = "Tairayden Peninsula", room_id = 35701, min_lvl = 115, max_lvl = 125 }, + ["perdition"] = { area_name = "Perdition Clan Area", room_id = 19968, min_lvl = 1, max_lvl = 201 }, + ["petstore"] = { area_name = "Giant's Pet Store", room_id = 995, min_lvl = 1, max_lvl = 20 }, + ["pompeii"] = { area_name = "Pompeii", room_id = 57, min_lvl = 90, max_lvl = 110 }, + ["promises"] = { area_name = "Foolish Promises", room_id = 25819, min_lvl = 130, max_lvl = 140 }, + ["prosper"] = { area_name = "Prosper's Island", room_id = 28268, min_lvl = 100, max_lvl = 201 }, + ["pyre"] = { area_name = "Twilight Hall", room_id = 15141, min_lvl = 1, max_lvl = 201 }, + ["qong"] = { area_name = "Qong", room_id = 16115, min_lvl = 190, max_lvl = 201 }, + ["quarry"] = { area_name = "Gnoll's Quarry", room_id = 23510, min_lvl = 105, max_lvl = 125 }, + ["radiance"] = { area_name = "Radiance Woods", room_id = 19805, min_lvl = 190, max_lvl = 201 }, + ["raga"] = { area_name = "Raganatittu", room_id = 19861, min_lvl = 40, max_lvl = 60 }, + ["raukora"] = { area_name = "The Blood Opal of Rauko'ra", room_id = 6040, min_lvl = 130, max_lvl = 201 }, + ["rebellion"] = { area_name = "Rebellion of the Nix", room_id = 10305, min_lvl = 150, max_lvl = 201 }, + ["remcon"] = { area_name = "The Reman Conspiracy", room_id = 25837, min_lvl = 130, max_lvl = 201 }, + ["reme"] = { area_name = "The Imperial City of Reme", room_id = 32703, min_lvl = 20, max_lvl = 150 }, + ["romani"] = { area_name = "A Clearing in the Woods", room_id = 24180, min_lvl = 1, max_lvl = 201 }, + ["rosewood"] = { area_name = "Rosewood Castle", room_id = 6901, min_lvl = 65, max_lvl = 150 }, + ["ruins"] = { area_name = "The Ruins of Diamond Reach", room_id = 16805, min_lvl = 60, max_lvl = 125 }, + ["sagewood"] = { area_name = "Sagewood Grove", room_id = 28754, min_lvl = 145, max_lvl = 195 }, + ["sahuagin"] = { area_name = "The Abyssal Caverns of Sahuagin", room_id = 34592, min_lvl = 140, max_lvl = 160 }, + ["salt"] = { area_name = "The Great Salt Flats", room_id = 4538, min_lvl = 40, max_lvl = 75 }, + ["sanctity"] = { area_name = "Sanctity of Eternal Damnation", room_id = 10518, min_lvl = 100, max_lvl = 201 }, + ["sanctum"] = { area_name = "The Blood Sanctum", room_id = 15307, min_lvl = 180, max_lvl = 201 }, + ["sandcastle"] = { area_name = "Sho'aram, Castle in the Sand", room_id = 37701, min_lvl = 1, max_lvl = 30 }, + ["sanguine"] = { area_name = "The Sanguine Tavern", room_id = 15436, min_lvl = 120, max_lvl = 150 }, + ["scarred"] = { area_name = "The Scarred Lands", room_id = 34036, min_lvl = 90, max_lvl = 110 }, + ["seaking"] = { area_name = "Sea King's Dominion", room_id = 145, min_lvl = 210, max_lvl = 210 }, + ["seekers"] = { area_name = "The Fortress of Knowledge", room_id = 14165, min_lvl = 1, max_lvl = 201 }, + ["sendhian"] = { area_name = "Adventures in Sendhia", room_id = 20288, min_lvl = 1, max_lvl = 60 }, + ["sennarre"] = { area_name = "Sen'narre Lake", room_id = 15491, min_lvl = 1, max_lvl = 20 }, + ["shadokil"] = { area_name = "The Shadokil Guildhouse", room_id = 32407, min_lvl = 1, max_lvl = 201 }, + ["shouggoth"] = { area_name = "The Temple of Shouggoth", room_id = 34087, min_lvl = 1, max_lvl = 65 }, + ["siege"] = { area_name = "Kobold Siege Camp", room_id = 43265, min_lvl = 80, max_lvl = 100 }, + ["sirens"] = { area_name = "Siren's Oasis Resort", room_id = 16298, min_lvl = 1, max_lvl = 15 }, + ["slaughter"] = { area_name = "The Slaughter House", room_id = 1601, min_lvl = 120, max_lvl = 145 }, + ["snuckles"] = { area_name = "Snuckles Village", room_id = 182, min_lvl = 80, max_lvl = 100 }, + ["soh"] = { area_name = "The School of Horror", room_id = 25611, min_lvl = 125, max_lvl = 201 }, + ["sohtwo"] = { area_name = "The School of Horror", room_id = 30752, min_lvl = 165, max_lvl = 201 }, + ["solan"] = { area_name = "The Town of Solan", room_id = 23713, min_lvl = 1, max_lvl = 35 }, + ["songpalace"] = { area_name = "The Palace of Song", room_id = 47013, min_lvl = 60, max_lvl = 80 }, + ["southern"] = { area_name = "The Southern Ocean", room_id = 5192, min_lvl = 1, max_lvl = 201 }, + ["spyreknow"] = { area_name = "Guardian's Spyre of Knowledge", room_id = 34800, min_lvl = 1, max_lvl = 30 }, + ["stone"] = { area_name = "The Fabled City of Stone", room_id = 11386, min_lvl = 70, max_lvl = 135 }, + ["storm"] = { area_name = "Storm Mountain", room_id = 6304, min_lvl = 1, max_lvl = 40 }, + ["stormhaven"] = { area_name = "The Ruins of Stormhaven", room_id = 20649, min_lvl = 150, max_lvl = 201 }, + ["stronghold"] = { area_name = "Dark Elf Stronghold", room_id = 20572, min_lvl = 70, max_lvl = 125 }, + ["stuff"] = { area_name = "The Stuff of Shadows", room_id = 40400, min_lvl = 110, max_lvl = 130 }, + ["takeda"] = { area_name = "Takeda's Warcamp", room_id = 15952, min_lvl = 140, max_lvl = 201 }, + ["talsa"] = { area_name = "The Empire of Talsa", room_id = 26917, min_lvl = 65, max_lvl = 140 }, + ["tanelorn"] = { area_name = "The Legendary City of Tanelorn", room_id = 31561, min_lvl = 1, max_lvl = 201 }, + ["tanra"] = { area_name = "Tanra'vea", room_id = 46913, min_lvl = 180, max_lvl = 201 }, + ["tao"] = { area_name = "The Collective Mind of Tao", room_id = 29210, min_lvl = 1, max_lvl = 201 }, + ["temple"] = { area_name = "The Temple of Shal'indrael", room_id = 31597, min_lvl = 180, max_lvl = 201 }, + ["terra"] = { area_name = "The Cracks of Terra", room_id = 19679, min_lvl = 190, max_lvl = 201 }, + ["terramire"] = { area_name = "Fort Terramire", room_id = 4493, min_lvl = 1, max_lvl = 35 }, + ["thieves"] = { area_name = "Den of Thieves", room_id = 7, min_lvl = 1, max_lvl = 20 }, + ["times"] = { area_name = "Intrigues of Times Past", room_id = 28463, min_lvl = 180, max_lvl = 201 }, + ["tirna"] = { area_name = "Tir na nOg", room_id = 20136, min_lvl = 130, max_lvl = 150 }, + ["titan"] = { area_name = "The Titans' Keep", room_id = 38234, min_lvl = 190, max_lvl = 201 }, + ["tol"] = { area_name = "The Tree of Life", room_id = 16325, min_lvl = 175, max_lvl = 201 }, + ["tombs"] = { area_name = "The Relinquished Tombs", room_id = 15385, min_lvl = 45, max_lvl = 100 }, + ["touchstone"] = { area_name = "Touchstone Cavern", room_id = 28346, min_lvl = 1, max_lvl = 201 }, + ["twinlobe"] = { area_name = "The Twinlobe Clan HQ", room_id = 15575, min_lvl = 1, max_lvl = 201 }, + ["umari"] = { area_name = "Umari's Castle", room_id = 36601, min_lvl = 190, max_lvl = 201 }, + ["uncharted"] = { area_name = "The Uncharted Oceans", room_id = 7701, min_lvl = 1, max_lvl = 201 }, + ["underdark"] = { area_name = "The UnderDark", room_id = 27341, min_lvl = 1, max_lvl = 50 }, + ["uplanes"] = { area_name = "The Upper Planes", room_id = 29365, min_lvl = 60, max_lvl = 85 }, + ["uprising"] = { area_name = "The Uprising", room_id = 15382, min_lvl = 120, max_lvl = 160 }, + ["vale"] = { area_name = "Sundered Vale", room_id = 1036, min_lvl = 1, max_lvl = 30 }, + ["vanir"] = { area_name = "The Halls of Vanir", room_id = 878, min_lvl = 1, max_lvl = 201 }, + ["verdure"] = { area_name = "Verdure Estate", room_id = 24090, min_lvl = 120, max_lvl = 140 }, + ["verume"] = { area_name = "Jungles of Verume", room_id = 30607, min_lvl = 1, max_lvl = 40 }, + ["vidblain"] = { area_name = "Vidblain, the Ever Dark", room_id = 33570, min_lvl = 1, max_lvl = 201 }, + ["village"] = { area_name = "A Peaceful Giant Village", room_id = 30850, min_lvl = 135, max_lvl = 170 }, + ["vlad"] = { area_name = "Castle Vlad-Shamir", room_id = 15970, min_lvl = 50, max_lvl = 100 }, + ["volcano"] = { area_name = "The Silver Volcano", room_id = 6091, min_lvl = 30, max_lvl = 100 }, + ["watchmen"] = { area_name = "The World of the Watchmen", room_id = 32342, min_lvl = 1, max_lvl = 201 }, + ["weather"] = { area_name = "Weather Observatory", room_id = 40499, min_lvl = 20, max_lvl = 40 }, + ["werewood"] = { area_name = "The Were Wood", room_id = 30956, min_lvl = 190, max_lvl = 201 }, + ["wildwood"] = { area_name = "Wildwood", room_id = 322, min_lvl = 1, max_lvl = 40 }, + ["winds"] = { area_name = "Winds of Fate", room_id = 39900, min_lvl = 201, max_lvl = 201 }, + ["winter"] = { area_name = "Winterlands", room_id = 1306, min_lvl = 150, max_lvl = 170 }, + ["wizards"] = { area_name = "War of the Wizards", room_id = 31316, min_lvl = 1, max_lvl = 35 }, + ["wonders"] = { area_name = "Seven Wonders", room_id = 32981, min_lvl = 100, max_lvl = 120 }, + ["wooble"] = { area_name = "The Wobbly Woes of Woobleville", room_id = 11335, min_lvl = 40, max_lvl = 60 }, + ["woodelves"] = { area_name = "The Wood Elves of Nalondir", room_id = 32199, min_lvl = 1, max_lvl = 30 }, + ["wtc"] = { area_name = "Warrior's Training Camp", room_id = 37895, min_lvl = 1, max_lvl = 15 }, + ["wyrm"] = { area_name = "The Council of the Wyrm", room_id = 28847, min_lvl = 190, max_lvl = 201 }, + ["xmas"] = { area_name = "Christmas Vacation", room_id = 6212, min_lvl = 110, max_lvl = 150 }, + ["xylmos"] = { area_name = "Xyl's Mosaic", room_id = 472, min_lvl = 100, max_lvl = 120 }, + ["yarr"] = { area_name = "The Misty Shores of Yarr", room_id = 30281, min_lvl = 115, max_lvl = 135 }, + ["ygg"] = { area_name = "Yggdrasil: The World Tree", room_id = 24186, min_lvl = 180, max_lvl = 201 }, + ["yurgach"] = { area_name = "The Yurgach Domain", room_id = 29450, min_lvl = 35, max_lvl = 110 }, + ["zangar"] = { area_name = "Zangar's Demonic Grotto", room_id = 6164, min_lvl = 40, max_lvl = 60 }, + ["zodiac"] = { area_name = "Realm of the Zodiac", room_id = 15857, min_lvl = 1, max_lvl = 45 }, + ["zoo"] = { area_name = "Aardwolf Zoological Park", room_id = 5920, min_lvl = 1, max_lvl = 35 }, + ["zyian"] = { area_name = "The Dark Temple of Zyian", room_id = 729, min_lvl = 120, max_lvl = 150 }, + }; + + local sql = [[ + INSERT OR IGNORE INTO area VALUES(%s, %s, %d, %d, %d); + ]]; + + database.mobber:exec("BEGIN TRANSACTION;", true); + + local numChanges = 0; + + for k,v in pairs(zones) do + local stmt = sql:format(fixsql(k), fixsql(v.area_name), v.min_lvl, v.max_lvl, v.room_id); + database.mobber:exec(stmt); + numChanges = numChanges + database.mobber:changes(); + end + + database.mobber:exec("END TRANSACTION;", true); + + local dif = GetInfo(232) - start; + + utility.print(numChanges .. " area(s) added to the mobber database.\r\nAreas update completed in: " .. formatSeconds(dif), "lime"); + end +end + +---------------------------------- +-- Utility +---------------------------------- + +utility = {}; + +function utility.initialize() + AddAlias("alias_utility_help_main", "^(?:mobber help|help mobber)$", "", + alias_flag.Enabled + alias_flag.IgnoreAliasCase + alias_flag.RegularExpression + alias_flag.Temporary, + "utility.help.main" + ); + + AddAlias("alias_utility_help_quests", "^mobber help quests?$", "", + alias_flag.Enabled + alias_flag.IgnoreAliasCase + alias_flag.RegularExpression + alias_flag.Temporary, + "utility.help.quests" + ); + + AddAlias("alias_utility_help_searching", "^mobber help search(?:ing)?$", "", + alias_flag.Enabled + alias_flag.IgnoreAliasCase + alias_flag.RegularExpression + alias_flag.Temporary, + "utility.help.searching" + ); + + AddAlias("alias_utility_help_utils", "^mobber help (?:utils?|utility)$", "", + alias_flag.Enabled + alias_flag.IgnoreAliasCase + alias_flag.RegularExpression + alias_flag.Temporary, + "utility.help.utils" + ); + + AddAlias("alias_utility_help_developer", "^mobber help dev(?:eloper)?$", "", + alias_flag.Enabled + alias_flag.IgnoreAliasCase + alias_flag.RegularExpression + alias_flag.Temporary, + "utility.help.developer" + ); + + local initializers = { + database.initialize, + campaign.initialize, + globalQuest.initialize, + roomHandler.initialize, + roomSearch.initialize, + mobSearch.initialize, + quickScan.initialize, + quickWhere.initialize, + autoHunt.initialize, + huntTrick.initialize, + autoKill.initialize, + noexp.initialize, + quest.initialize, + colorPalette.initialize + }; + + for k,v in ipairs(initializers) do + v(); + end + + if (var.isQuestMuted == "false") then + quest.isMuted = false; + else + quest.isMuted = true; + end + + database.openDatabase(); +end + +function utility.deinitialize() + local aliases = GetAliasList(); + + if (aliases) then + for i = 1, #aliases do + EnableAlias(aliases[i], false); + DeleteAlias(aliases[i]); + end + end + + local triggers = GetTriggerList(); + + if (triggers) then + for i = 1, #triggers do + EnableTrigger(triggers[i], false); + DeleteTrigger(triggers[i]); + end + end + + database.closeDatabase(); +end + +function utility.runToRoomId(roomId) + local STATE_ACTIVE = 3; + local canRun = true; + + if (utility.gmcp.getPlayerRoomId() ~= roomId) then + if (utility.gmcp.getPlayerState() == STATE_ACTIVE) then + Execute("mapper goto " .. roomId); + else + canRun = false; + Execute("mapper where " .. roomId); + end + else + utility.print("You are already in that room."); + end + + return canRun; +end + +function utility.setTarget(target, keyword) + utility.targetName = target; + utility.targetKeyword = keyword; + + var.target = target; +end + +function utility.updateEnemy() + local player_enemy = utility.gmcp.getPlayerEnemy():lower(); + + if (player_enemy ~= "" and utility.playerEnemy ~= player_enemy) then + utility.playerEnemy = player_enemy; + end +end + +function utility.getMobKeyword(mobName) + mobName = mobName:lower(); + + local mobKeyword; + + local splitMobName = {}; + + for token in mobName:gmatch("[^ ]+") do + token = token:gsub("[^%a]", ""); + table.insert(splitMobName, token); + end -- for each non-space word strip any non-letter/"-" + + local keywords = {}; + + for i = 1, #splitMobName do + local token = splitMobName[i]; + + if (#splitMobName == 2 and (token:find("^evil$") == 1 or token:find("^good$") == 1)) then + return splitMobName[1]; + end -- sohtwo mobs + + local prefixPatterns = { + "^a$", + "^an$", + "^the$", + "^of$", + "^some$", + "^whelp$", + "^dragon$", + "^lizardman$", + "^sea$" + }; + + for k,v in ipairs(prefixPatterns) do + token = token:gsub(v, ""); + end -- strip certain non-keyword prefixes + + if (token ~= "") then + table.insert(keywords, token); + end -- if token still exists put into keywords table + end + + if (not next(keywords)) then + mobKeyword = mobName; + else + mobKeyword = #keywords == 1 and keywords[1] or (keywords[1]:sub(1, 4) .. " " .. keywords[#keywords]:sub(1, 4)); + end + + return mobKeyword; +end + +function utility.print(message, color) + color = color or "white"; + + ColourNote( + pluginPalette.mobberParentheses, "", "\r\n(", + pluginPalette.mobber, "", "Mobber", + pluginPalette.mobberParentheses, "", ") ", + color, "", message .. "\r\n\r\n" + ); +end + +---------------------------------- +-- GMCP +---------------------------------- + +utility.gmcp = {}; + +function utility.gmcp.getPlayerName() + return gmcp("char.base.name"); +end + +function utility.gmcp.getPlayerLevel() + return tonumber(gmcp("char.status.level")); +end + +function utility.gmcp.getPlayerTnl() + return tonumber(gmcp("char.status.tnl")); +end + +function utility.gmcp.getPlayerRoomId() + return tonumber(gmcp("room.info.num")); +end + +function utility.gmcp.getPlayerRoomName() + return gmcp("room.info.name"); +end + +function utility.gmcp.getPlayerZone() + return gmcp("room.info.zone"); +end + +function utility.gmcp.getPlayerState() + return tonumber(gmcp("char.status.state")); +end + +function utility.gmcp.getPlayerEnemy() + return gmcp("char.status.enemy"):lower(); +end + +function utility.gmcp.requestPlayerArea() + Send_GMCP_Packet("request area"); +end + +---------------------------------- +-- Help +---------------------------------- + +utility.help = {}; + +function utility.help.main() + print(); + ColourNote(pluginPalette.border, "", string.format('%-15s : ', 'Name'), pluginPalette.helpDescription, "", "Mobber"); + ColourNote(pluginPalette.border, "", string.format('%-15s : ', 'Author'), pluginPalette.helpDescription, "", GetPluginInfo(GetPluginID(), 2)); + ColourNote(pluginPalette.border, "", string.format('%-15s : ', 'Version'), pluginPalette.helpDescription, "", string.format("%.1f", GetPluginInfo(GetPluginID(), 19))); + ColourNote(pluginPalette.border, "", string.format('%-15s : ', 'Purpose'), pluginPalette.helpDescription, "", GetPluginInfo(GetPluginID(), 8)); + ColourNote(pluginPalette.border, "", string.format('%-15s : ', 'Mem Usage'), "yellow", "", string.format('%0d KB', collectgarbage('count'))); + print("\r\n\r\n"); + + ColourNote(pluginPalette.border, "", string.rep("-", 7), pluginPalette.helpTitles, "", " Mobber Topics ", pluginPalette.border, "", string.rep("-", 7)); + print(); + + utility.help.command("mobber help quests", "Campaigns and global quests."); + utility.help.command("mobber help searching", "Searching for mobs and rooms."); + utility.help.command("mobber help utils", "Utility commands."); + utility.help.command("mobber help developer", "Adding areas, rooms and mobs to the database."); + print("\r\n\r\n"); + + collectgarbage("collect"); +end + +function utility.help.quests() + print(); + ColourNote(pluginPalette.border, "", string.rep("-", 7), pluginPalette.helpTitles, "", " Mobber Quests ", pluginPalette.border, "", string.rep("-", 7)); + print(); + + utility.help.command("cp c", "Send 'campaign check' to process cp mobs."); + utility.help.command("xcp <#>", "Select a mob from the campaign list."); + print(); + + utility.help.command("gq c", "Send 'gquest check' to process gq mobs."); + utility.help.command("xgq <#>", "Select a mob from the global quest list."); + print(); + + utility.help.command("xgo <#>", "Go to a room in the current room list."); + utility.help.command("xnext", "Go to the next room in the room list."); + utility.help.command("xprev", "Go to the previous room in the room list."); + print(); + + utility.help.command("mobber quest", "Show quest info for current quest mob."); + utility.help.command("mobber mute|unmute quest", "Mute or unmute quest messages/sounds."); + print("\r\n\r\n"); +end + +function utility.help.searching() + print(); + ColourNote(pluginPalette.border, "", string.rep("-", 7), pluginPalette.helpTitles, "", " Mobber Searching ", pluginPalette.border, "", string.rep("-", 7)); + print(); + + utility.help.command("mf zone ", "Search for a mob in the current zone."); + print(); + + utility.help.command("mfa lvl ", "Search for a mob in all zones."); + print(); + + utility.help.command("rf zone ", "Search for a room in the current zone."); + print(); + utility.help.command("rfa lvl ", "Search for a room in all zones."); + print(); + + utility.help.command("mobber show zone ", "Show information for a single zone."); + utility.help.command("mobber show zones sort ", "Show all of the zones in the database."); + utility.help.command("", " can be 'mobs', 'rooms' or 'level'."); + print("\r\n\r\n"); + + ColourNote("silver", "", "Note: Most arguments/parameters are optional."); + print("\r\n\r\n"); +end + +function utility.help.utils() + print(); + + ColourNote(pluginPalette.border, "", string.rep("-", 7), pluginPalette.helpTitles, "", " Mobber Utilities ", pluginPalette.border, "", string.rep("-", 7)); + print(); + + utility.help.command("xrt ", "Run to the marked room of a zone."); + utility.help.command("xset mark", "Mark the landing room of a zone."); + print(); + + utility.help.command("qs ", "Quick scan for current target."); + utility.help.command("", " is optional. Default is target."); + print(); + + utility.help.command("qw ", "Quick where a mob to get its location."); + utility.help.command("", " is optional. Default is target."); + print(); + + utility.help.command("ak", "Send the set autokill command."); + utility.help.command("ak ", "Set the command to use for autokill."); + utility.help.command("toggle ak", "Toggle appending the current target."); + print(); + + utility.help.command("ah ", "Auto-hunt a mob. No arg will abort hunt."); + utility.help.command("ht ", "Hunt-trick a mob. No arg will abort hunt."); + print(); + + utility.help.command("mobber noexp ", "Auto noexp. Default is 1000 exp threshold."); + print(); + + utility.help.command("mobber color ", "Options: red, blue, green, yellow, purple."); + print(); + + utility.help.command("mobber btncolor window", "Change text color of the window buttons."); + utility.help.command("mobber hide|show window", "Show or hide the window."); + utility.help.command("mobber reset window", "Reset the window if it goes off-screen."); + print("\r\n\r\n"); + + ColourNote("silver", "", "Note: Window commands require the miniwindow plugin to be installed."); + print("\r\n\r\n"); +end + +function utility.help.developer() + print(); + + ColourNote(pluginPalette.border, "", string.rep("-", 7), pluginPalette.helpTitles, "", " Mobber Developer ", pluginPalette.border, "", string.rep("-", 7)); + print(); + + utility.help.command("mobber developer", "Enable or disable developer mode."); + utility.help.command("mobber backup", "Manually force a backup of the database."); + utility.help.command("mobber vacuum", "Defragment and reduce size of the db."); + print(); + + utility.help.command("mobber addzone ", "Add the current zone to the database."); + utility.help.command("mobber update lvlrange <#> <#>", "Update a zone's level range."); + print(); + + utility.help.command("mobber update zones", "Update zones with default values."); + utility.help.command("mobber update rooms", "Update rooms from Aardwolf.db to the database."); + print(); + + utility.help.command("mobber addmob ", "Add a mob to the current room."); + utility.help.command("mobber consider", "Toggle logging mobs with 'consider' command."); + print(); + + utility.help.command("mobber removemob single ", "Remove matching mobs from the current zone."); + utility.help.command("mobber removemob room", "Remove all mobs from the current room."); + utility.help.command("mobber removemob zone", "Remove all mobs from the current zone."); + utility.help.command("mobber remove zone", "Remove the current zone from the database."); + print(); + + utility.help.command("mobber update keywords", "Update keywords for all mobs in the database."); + utility.help.command("mobber keyword mob ", "Update keyword for a single mob."); + utility.help.command("mobber clean keywords", "Remove unused keywords from the database."); + utility.help.command("mobber priority <#> mob ", "Update priority of a mob in the current zone."); + print("\r\n\r\n"); + + + ColourNote("silver", "", "Note: See the README.txt before using developer commands."); + print("\r\n\r\n"); +end + +function utility.help.command(cmd, desc) + ColourNote(pluginPalette.helpCommand, "", string.format('%-31s', cmd), pluginPalette.border, "", " : ", pluginPalette.helpDescription, "", desc); +end + +---------------------------------- +-- Mob Target Prototype +---------------------------------- + +mobTarget = { + name = "", + keyword = "", + location = "", + isDead = false, + lookupType = "", + amount = 1, + rooms = {} +}; + +function mobTarget:new(mob) + mob = mob or {}; + setmetatable(mob, self); + self.__index = self; + return mob; +end + +function mobTarget:initialize() + self.keyword = database.lookupMobKeyword(self.name) or utility.getMobKeyword(self.name); + + local lookups = { + database.lookupMobRoomOrAreaByLvl, + database.lookupMobRoomOrArea, + database.lookupArea, + database.lookupRoomAreaByLvl, + database.lookupRoomArea + }; + + for k,v in ipairs(lookups) do + self.rooms, self.lookupType = v(self.location, self.name); + + if (next(self.rooms)) then + break; + end + end + + if (not next(self.rooms)) then + self.lookupType = "none"; + table.insert(self.rooms, {zone_name = self.location}); + end +end + +---------------------------------- +-- Color Palette +---------------------------------- + +colorPalette = {}; + +function colorPalette.initialize() + AddAlias("alias_color_palette_set_color", "^mobber color (?.+?)$", "", + alias_flag.Enabled + alias_flag.IgnoreAliasCase + alias_flag.RegularExpression + alias_flag.Temporary, + "colorPalette.setColor" + ); + + if (var.pluginPalette) then + pluginPalette = loadstring("return " .. var.pluginPalette)(); + else + pluginPalette = colorPalette.getDefault(); + var.pluginPalette = serialize.save_simple(pluginPalette); + end +end + +function colorPalette.getDefault() + return { + border = "maroon", + header = "white", + indexNumbering = "white", + deadMob = "red", + foundMobRoom = "paleturquoise", + foundArea = "deepskyblue", + roomOrMobAll = "darkgray", + missingAreaRoom = "yellow", + rooms = "paleturquoise", + highlightTarget = "red", + highlightTargetParentheses = "#4D4D4D", + mobber = "white", + mobberParentheses = "maroon", + helpDescription = "white", + helpTitles = "darkgray", + helpCommand = "firebrick" + }; +end + +function colorPalette.setColor(name, line, wildcards) + local color = wildcards.color; + + if (color == "green") then + pluginPalette.border = "forestgreen"; + pluginPalette.highlightTarget = "lime"; + pluginPalette.mobberParentheses = "forestgreen"; + pluginPalette.helpCommand = "chartreuse"; + elseif (color == "blue") then + pluginPalette.border = "steelblue"; + pluginPalette.highlightTarget = "lightskyblue"; + pluginPalette.mobberParentheses = "blue"; + pluginPalette.helpCommand = "mediumturquoise"; + elseif (color == "purple") then + pluginPalette.border = "slateblue"; + pluginPalette.highlightTarget = "blueviolet"; + pluginPalette.mobberParentheses = "darkorchid"; + pluginPalette.helpCommand = "mediumorchid"; + elseif (color == "yellow") then + pluginPalette.border = "#B1B55F"; + pluginPalette.highlightTarget = "yellow"; + pluginPalette.mobberParentheses = "khaki"; + pluginPalette.helpCommand = "#F3FF00"; + else + color = "default"; + pluginPalette = colorPalette.getDefault(); + end + + var.pluginPalette = serialize.save_simple(pluginPalette); + + utility.print("Color scheme set to: " .. color); +end + +---------------------------------- +-- Mushclient Plugin Callbacks +---------------------------------- + +function OnPluginInstall() + utility.initialize(); +end + +function OnPluginConnect() + Send_GMCP_Packet("request char"); + Send_GMCP_Packet("request room"); + Send_GMCP_Packet("request quest"); +end + +function OnPluginClose() + utility.deinitialize(); +end + +function OnPluginEnable() + OnPluginInstall(); + OnPluginConnect(); +end + +function OnPluginDisable() + OnPluginClose(); +end + +function OnPluginBroadcast(msg, id, name, text) + if (id == "3e7dedbe37e44942dd46d264") then + if (text == "char.status") then + utility.updateEnemy(); + elseif (text == "room.area" and database.area and database.area.isAddingArea) then + local zoneName = gmcp("room.area.id"); + local areaName = gmcp("room.area.name"); + + database.addArea(zoneName, areaName); + elseif (text == "comm.quest") then + local newQuest = gmcp("comm.quest"); + + if (newQuest.action == "ready") then + if (not quest.isMuted) then + local commMsg = quest.commPrefix .. " @WAvailable!@w"; + CallPlugin("b555825a4a5700c35fa80780", "storeFromOutside", commMsg); + end + elseif (newQuest.action == "start") then + quest.start(newQuest); + elseif (newQuest.action == "status") then + quest.showRequest(newQuest); + elseif (newQuest.action == "fail") then + quest.fail(newQuest); + elseif (newQuest.action == "comp") then + quest.complete(newQuest); + end + elseif (text == "config") then + noexp.isNoExp = gmcp("config.noexp") == "YES" and true or false; + end + end +end \ No newline at end of file diff --git a/lua/mw_theme_base.lua b/lua/mw_theme_base.lua new file mode 100644 index 0000000..342600e --- /dev/null +++ b/lua/mw_theme_base.lua @@ -0,0 +1,500 @@ +--[[ +This file contains code that that allows many different miniwindow plugins +to maintain the same UI theme, as long as they follow certain guidelines. +We can use this to get a base set of colors, standard display elements, +and title fonts that get used everywhere in order to unify the visual style +and provide a way to customize that style easily. + +Steps for use: (also see https://github.com/fiendish/aardwolfclientpackage/wiki/Miniwindow-Color-Themes ) +1) Inside your plugins, require this file at the very beginning of your script section. +2) Use variable names as spelled out in lua/mw_themes/Charcoal.lua for colorization within the "Theme." namespace, e.g. Theme.BODY_TEXT etc. +3) Use the shared graphics functions defined below for drawing various elements with the current color theme. + Some of the functions are not for drawing things. You can ignore them. +4) Four things make a miniwindow fit the theme: The border, the titlebar, the resize widget, and the general colors being used. + The border, titlebar, resize widget, and 3D boxes all have special draw functions included below. The colors are defined in the theme files. +4) Optional: Make your own themes (clone one of the files in lua/mw_themes and customize your colors). +5) Optional: If your plugin wants to preserve state between theme changes (changing theme reloads the plugin), + you can detect whether the plugin is closing because of a theme change with GetVariable(Theme.reloading_variable). +--]] +require "checkplugin" +require "movewindow" + +dofile(GetInfo(60) .. "aardwolf_colors.lua"); + +module ("Theme", package.seeall) + +function b9315e040989d3f81f4328d6() + -- used for theme system detection + return true +end + +theme_dir = GetInfo(66).."lua\\mw_themes\\" +theme_file = "Charcoal.lua" + +function get_theme() + return theme_file +end + +reloading_variable = "aard_theme_just_reloading" + +function just_reloading() + SetVariable(reloading_variable, 1) +end + +local default_theme = { + LOGO_OPACITY = 0.02, + + PRIMARY_BODY = 0x0c0c0c, + SECONDARY_BODY = 0x777777, + BODY_TEXT = 0xe8e8e8, + + CLICKABLE = 0x666666, + CLICKABLE_HOVER = 0x444444, + CLICKABLE_HOT = 0x40406b, + CLICKABLE_TEXT = 0xc8c8c8, + CLICKABLE_HOVER_TEXT = 0xdddddd, + CLICKABLE_HOT_TEXT = 0xcfc5df, + + TITLE_PADDING = 2, + + THREE_D_HIGHLIGHT = 0xe8e8e8, + + THREE_D_GRADIENT = miniwin.gradient_vertical, + THREE_D_GRADIENT_FIRST = 0xcdced1, + THREE_D_GRADIENT_SECOND = 0x8c8c8c, + THREE_D_GRADIENT_ONLY_IN_TITLE = false, + + THREE_D_SOFTSHADOW = 0x606060, + THREE_D_HARDSHADOW = 0x303030, + THREE_D_SURFACE_DETAIL = 0x050505, + + SCROLL_TRACK_COLOR1 = 0x444444, + SCROLL_TRACK_COLOR2 = 0x888888, + VERTICAL_TRACK_BRUSH = miniwin.brush_hatch_forwards_diagonal, + + DYNAMIC_BUTTON_PADDING = 20, + RESIZER_SIZE = 16 +} + +function load_theme(file) + local file_loaded, data_from_file = pcall(dofile, theme_dir..file) + + -- init defaults + for k,v in pairs(default_theme) do + Theme[k] = v + end + + if file_loaded then + if type(data_from_file) == "table" then + for k,v in pairs(data_from_file) do + Theme[k] = v + end + theme_file = file + else + print("Error loading theme file: ", theme_dir..file) + print("This theme file is invalid. Please delete it and select a different theme.") + print() + end + else + print("Error loading theme file: ", theme_dir..file) + print(data_from_file) -- error message + end +end + +-- junk files that should not have existed in the first place +local bad_shas = { + ["default.lua"] = { + ["d189be39efb49730537c8ada65fbd5382d3e44ad"] = true, + ["39d9b7fc4d571ac62d1005b52f8da43c4b7827e1"] = true, + ["774bdd2f098f1c4fe81d9a6b8eb5bfad6bd79c51"] = true, + ["b7fd919be678f0c7b1c52bea644f183a7b9397f1"] = true, + ["acc1d615901050b2faa50e1b0497ee30b72f5118"] = true, + ["ac993b349691eb51e472ca5b1b0ae18a03943bf2"] = true, + ["0cd5bcca0723a57eabedd08a9cfa6f4ec93ea469"] = true + }, + ["dark_pony.lua"] = { + ["d189be39efb49730537c8ada65fbd5382d3e44ad"] = true + }, + ["Joker.lua"] = { + ["17fa722746df781326b100744dd790c66721b749"] = true, + ["b2b83804ce87eb889127658cb80c9d23d76082b4"] = true + } +} + +require "gitsha" +function theme_has_bad_sha(filename) + return bad_shas[filename] and bad_shas[filename][gitsha(theme_dir..filename)] +end + +local theme_controller_ID = "b9315e040989d3f81f4328d6" +local theme_controller_name = "aard_Theme_Controller" +if (GetPluginID() ~= theme_controller_ID) then + if not IsPluginInstalled(theme_controller_ID) then + local inner_action = [[DoAfterSpecial(0.1, 'require \'checkplugin\';do_plugin_check_now(\']]..theme_controller_ID..[[\', \']]..theme_controller_name..[[\')', sendto.script)]] + + -- execute_in_global_space(inner_action) + local prefix = GetAlphaOption("script_prefix") + local action = [[ + SetAlphaOption("script_prefix", "/") + Execute("/]] .. inner_action:gsub("\\", "\\\\") .. [[") + SetAlphaOption("script_prefix", "]] .. prefix:gsub("\\", "\\\\") .. [[") + ]] + DoAfterSpecial(0.1, action, sendto.script) + end + + local maybe_theme_file = GetPluginVariable(theme_controller_ID, "theme_file") or theme_file + + if not theme_has_bad_sha(maybe_theme_file) then + theme_file = maybe_theme_file + end + + load_theme(theme_file) +end + +-- Replacement for WindowRectOp action 5, which allows for a 3D look while maintaining color theme. +function Draw3DRect (win, left, top, right, bottom, depressed) + local gradient = (not THREE_D_GRADIENT_ONLY_IN_TITLE or __theme_istitle) and THREE_D_GRADIENT or false + __theme_istitle = false + + if right > 0 then + right = right + 1 + end + if bottom > 0 then + bottom = bottom + 1 + end + + if gradient and (THREE_D_GRADIENT_FIRST == THREE_D_GRADIENT_SECOND) then + gradient = false + end + + if (gradient == 1) or (gradient == 2) or (gradient == 3) then + WindowGradient(win, left, top, right, bottom, + THREE_D_GRADIENT_FIRST, + THREE_D_GRADIENT_SECOND, + gradient) + else + WindowRectOp(win, 2, left, top, right, bottom, THREE_D_GRADIENT_FIRST) + end + + if not depressed then + WindowLine(win, left+1, top+1, right, top+1, THREE_D_HIGHLIGHT, 0x0200, 1) + WindowLine(win, left+1, top+1, left+1, bottom, THREE_D_HIGHLIGHT, 0x0200, 1) + + WindowLine(win, left, bottom-2, right, bottom-2, THREE_D_SOFTSHADOW, 0x0200, 1) + WindowLine(win, right-2, top, right-2, bottom-2, THREE_D_SOFTSHADOW, 0x0200, 1) + + WindowLine(win, left, bottom-1, right, bottom-1, THREE_D_HARDSHADOW, 0x0200, 1) + WindowLine(win, right-1, top, right-1, bottom-1, THREE_D_HARDSHADOW, 0x0200, 1) + else + WindowLine(win, left, top+1, right, top+1, THREE_D_HARDSHADOW, 0x0200, 1) + WindowLine(win, left+1, top, left+1, bottom, THREE_D_HARDSHADOW, 0x0200, 1) + + WindowLine(win, left, top, right, top, THREE_D_HARDSHADOW, 0x0200, 1) + WindowLine(win, left, top, left, bottom, THREE_D_HARDSHADOW, 0x0200, 1) + end +end + +function Draw3DTextBox(win, font, left, top, text, utf8) + local right = left + WindowTextWidth(win, font, text) + local bottom = top + TextHeight(win, font) + Draw3DRect(win, left, top, right+3, bottom+3) + WindowText(win, font, text, left+2, top+2, right+1, bottom+1, THREE_D_SURFACE_DETAIL, utf8) + return right-left +end + +function DrawTextBox(win, font, left, top, text, utf8, outlined, bgcolor, textcolor) + if nil == bgcolor then + bgcolor = CLICKABLE + end + if nil == textcolor then + textcolor = CLICKABLE_TEXT + end + local right = left + WindowTextWidth(win, font, text) + 4 + local bottom = top + TextHeight(win, font) + WindowRectOp(win, 2, left, top+1, right, bottom+2, bgcolor) + if outlined then + WindowRectOp(win, 1, left-1, top, right+1, bottom+3, textcolor) + end + WindowText(win, font, text, left+2, top+1, right, bottom+1, textcolor, utf8) + return right-left +end + +function AddResizeTag(win, type, x1, y1, mousedown_callback, dragmove_callback, dragrelease_callback) + local x1, y1 = DrawResizeTag(win, type, x1, y1) + + -- Add handler hotspots + if WindowMoveHotspot(win, "resize", x1, y1, 0, 0) ~= 0 then + WindowAddHotspot(win, "resize", x1, y1, 0, 0, nil, nil, mousedown_callback, nil, nil, "", 6, 0) + WindowDragHandler(win, "resize", dragmove_callback, dragrelease_callback, 0) + end + + return x1, y1 +end + +function DrawResizeTag(win, type, x1, y1) + local x2, y2 + if not (x1 and y1) then + x2 = WindowInfo(win, 3) - 3 + y2 = WindowInfo(win, 4) - 3 + x1 = x2 - RESIZER_SIZE + 1 + y1 = y2 - RESIZER_SIZE + 1 + else + x2 = x1 + RESIZER_SIZE + y2 = y1 + RESIZER_SIZE + end + + local m = 2 + local n = 2 + if (type == 2) or (type == "full") then + Draw3DRect(win, x1, y1, x2, y2, false) + while (x1+m < x2 and y1+n+1 < y2) do + WindowLine(win, x1+m-1, y2-2, x2-1, y1+n-2, THREE_D_HIGHLIGHT, 0, 1) + WindowLine(win, x1+m, y2-2, x2-1, y1+n-1, THREE_D_HARDSHADOW, 0, 1) + WindowLine(win, x1+m+1, y2-2, x2-1, y1+n, THREE_D_GRADIENT_FIRST, 0, 1) + m = m+3 + n = n+3 + end + WindowSetPixel(win, x2-2, y2-2, THREE_D_HIGHLIGHT) + else + while (x1+m < x2 and y1+n+1 < y2) do + WindowLine(win, x1+m, y2-1, x2, y1+n-1, THREE_D_HIGHLIGHT, 0, 1) + WindowLine(win, x1+m+1, y2-1, x2, y1+n, THREE_D_HARDSHADOW, 0, 1) + WindowLine(win, x1+m+2, y2-1, x2, y1+n+1, THREE_D_GRADIENT_FIRST, 0, 1) + m = m+3 + n = n+3 + end + WindowLine(win, x1, y2, x2, y2, THREE_D_HARDSHADOW, 0, 1) + WindowLine(win, x2, y1, x2, y2+1, THREE_D_HARDSHADOW, 0, 1) + WindowSetPixel(win, x2-1, y2-1, THREE_D_HIGHLIGHT) + end + return x1, y1 +end + +function TextHeight(win, font) + return WindowFontInfo(win, font, 1) +end + +-- title_alignment can be "left", "right", or "center" (the default) +function DressWindow(win, font, title, title_alignment) + local l, t, r, b = DrawBorder(win) + + if title and (title ~= "") then + t = DrawTitleBar(win, font, title, title_alignment) + if t > 1 then + movewindow.add_drag_handler(win, 0, 0, 0, t) + end + end + + return l, t, r, b +end + +function DrawTitleBar(win, font, title, text_alignment) + local title_lines + if type(title) == "string" then + title_lines = utils.split(title, "\n") + else + title_lines = title + end + + local line_height = 0 + local title_height = 0 + if font and title_lines then + line_height = TextHeight(win, font) + if line_height == nil then + return (2*TITLE_PADDING) + end + title_height = (2*TITLE_PADDING) + (line_height * #title_lines) + end + + __theme_istitle = true + Draw3DRect( + win, + -1, + -1, + WindowInfo(win, 3)-1, + title_height, + false + ) + + local first_color = nil + local txt = nil + for i,v in ipairs(title_lines) do + if type(v) == "table" then + txt = strip_colours_from_styles(v) + else + txt = v + end + local width = WindowTextWidth(win, font, txt) + + local text_left = (WindowInfo(win, 3) - width) / 2 -- default text align center + if text_alignment == "left" then + text_left = TITLE_PADDING + 2 + elseif text_alignment == "right" then + text_left = WindowInfo(win, 3) - width - TITLE_PADDING + end + local text_right = math.min(text_left + width, WindowInfo(win, 3) - TITLE_PADDING - 2) + + local text_top = (line_height * (i-1)) + TITLE_PADDING + if type(v) == "string" then + WindowText(win, font, v, text_left, text_top, text_right, title_height, THREE_D_SURFACE_DETAIL) + else + -- The colors of all styles matching the first style color get stripped out and replaced with the default title color + for i,w in ipairs(v) do + first_color = first_color or w.textcolour + if w.textcolour == first_color then + w.textcolour = THREE_D_SURFACE_DETAIL + end + end + WindowTextFromStyles(win, font, v, text_left, text_top, text_right, title_height, true) + end + end + return title_height +end + +function DrawBorder(win) + local r = WindowInfo(win, 3)-3 + local b = WindowInfo(win, 4)-3 + WindowRectOp(win, 1, 0, 0, 0, 0, THREE_D_HIGHLIGHT) + WindowRectOp(win, 1, 1, 1, -1, -1, THREE_D_SOFTSHADOW) + return 2, 2, r, b +end + +function OutlinedText(win, font, text, startx, starty, endx, endy, color, outline_color, utf8, thickness) + if thickness == nil then + thickness = 1 + end + if outline_color == nil then + outline_color = THREE_D_HARDSHADOW + end + local right = nil + for xi = -thickness,thickness do + for yi = -thickness,thickness do + right = WindowText(win, font, text, startx+xi, starty+yi, endx+1, endy+1, outline_color, utf8) + end + end + local right = WindowText(win, font, text, startx+1, starty+1, endx, endy, outline_color, utf8) + WindowText(win, font, text, startx, starty, endx, endy, color, utf8) + return right +end + +function WindowTextFromStyles(win, font, styles, left, top, right, bottom, utf8) + for i,v in ipairs(styles) do + left = left + WindowText(win, font, v.text, left, top, right, bottom, v.textcolour or BODY_TEXT, utf8) + end + return left +end + +-- text with a black outline +function OutlinedTextFromStyles(win, font, styles, startx, starty, endx, endy, outline_color, utf8, thickness) + if thickness == nil then + thickness = 1 + end + if outline_color == nil then + outline_color = THREE_D_HARDSHADOW + end + local text = strip_colours_from_styles(styles) + local right = nil + for xi = -thickness,thickness do + for yi = -thickness,thickness do + right = WindowText(win, font, text, startx+xi, starty+yi, endx+1, endy+1, outline_color, utf8) + end + end + WindowTextFromStyles(win, font, styles, startx, starty, endx, endy, utf8) + return right +end + +-- Based on mw.lua's popup function, but with theme colors +function Popup(win, -- window name to use + font_id, -- font to use for each body line + info, -- table of lines to show (plain text or styles) + left, top, -- preferred location + stay_left_of, -- guidance for keeping the popup visible + stay_right_of) -- guidance for keeping the popup visible + + local BORDER_WIDTH = 2 + + assert(WindowInfo (win, 1), "Window " .. win .. " must already exist") + assert(WindowFontInfo (win, font_id, 1), "No font " .. font_id .. " in " .. win) + + local font_height = WindowFontInfo (win, font_id, 1) + local font_leading = WindowFontInfo (win, font_id, 4) + WindowFontInfo (win, font_id, 5) + + -- find text width - minus colour codes + local infowidth = 0 + local infoheight = 0 + + -- calculate remaining width and height + for _, v in ipairs (info) do + if type(v) == "table" then + txt = strip_colours_from_styles(v) + else + txt = strip_colours(v) + end + infowidth = math.max (infowidth, WindowTextWidth (win, font_id, txt)) + infoheight = infoheight + font_height + end -- for + + infowidth = infowidth + (2 * BORDER_WIDTH) + -- leave room for border + WindowFontInfo (win, font_id, 6) -- one character width extra + + infoheight = infoheight + (2 * BORDER_WIDTH) + -- leave room for border + font_leading + -- plus leading below bottom line, + 10 -- and 5 pixels top and bottom + + -- if align_right then + -- left = left - infowidth + -- end -- if align_right + + -- if align_bottom then + -- top = top - infoheight + -- end -- if align_bottom + + top = math.min(top, GetInfo(280) - infoheight) + top = math.max(0, top) + if left < stay_left_of then + if left+infowidth > stay_left_of then + left = stay_left_of - infowidth + end + if left < 0 then + left = stay_right_of + end + else + if left < stay_right_of then + left = stay_right_of + end + if (left + infowidth) > GetInfo(281) then + left = stay_left_of - infowidth + end + end + WindowCreate(win, + left, top, -- where + infowidth, -- width (gap of 5 pixels per side) + infoheight, -- height + miniwin.pos_top_left, -- position mode: can't be 0 to 3 + miniwin.create_absolute_location + miniwin.create_transparent, + SECONDARY_BODY) + + WindowCircleOp(win, miniwin.circle_round_rectangle, + BORDER_WIDTH, BORDER_WIDTH, -BORDER_WIDTH, -BORDER_WIDTH, -- border inset + THREE_D_HIGHLIGHT, miniwin.pen_solid, BORDER_WIDTH, -- line + PRIMARY_BODY, miniwin.brush_solid, -- fill + 5, 5) -- diameter of ellipse + + local x = BORDER_WIDTH + WindowFontInfo (win, font_id, 6) / 2 -- start 1/2 character in + local y = BORDER_WIDTH + 5 -- skip border, and leave 5 pixel gap + + -- show each line + for _, line in ipairs(info) do + if type(line) == "string" then + WindowText(win, font_id, line, x, y, 0, 0, BODY_TEXT) + else + WindowTextFromStyles(win, font_id, line, x, y, 0, 0) + end + + y = y + font_height + end -- for + + -- display popup window + WindowShow(win, true) + +end -- popup diff --git a/lua/sqlitedb.lua b/lua/sqlitedb.lua new file mode 100644 index 0000000..2f0c387 --- /dev/null +++ b/lua/sqlitedb.lua @@ -0,0 +1,177 @@ +---------------------------------- +-- sqlite3 Helper Module +---------------------------------- + +sqlitedb = { + path = GetPluginInfo(GetPluginID(), 20), + name = "sqlite.db", + db = nil +}; + +function sqlitedb:new(db) + db = db or {}; + setmetatable(db, self); + self.__index = self; + return db; +end + +function sqlitedb:open() + if (self.db == nil or not self.db:isopen()) then + self.db = assert(sqlite3.open(self.path .. self.name)); + self:pragma(); + end +end + +function sqlitedb:close() + if (self.db) then + if (self.db:isopen()) then + local code = self.db:close(); + self:check(code); + end + + self.db = nil; + end +end + +function sqlitedb:check(code) + if (code ~= sqlite3.OK and + code ~= sqlite3.ROW and + code ~= sqlite3.DONE) then + + local err = self.db:errmsg(); + self.db:execute("ROLLBACK;"); + error("REPORT THIS ERROR TO RAURU:\r\n" .. err, 2); + end +end + +function sqlitedb:exec(sql, checkCode, callback) + assert(sqlite3.complete(sql), "Not an SQL statement: " .. sql); + + local code = self.db:execute(sql, callback); + + if (checkCode) then + self:check(code); + end + + return code; +end + +function sqlitedb:pragma() + self:exec("PRAGMA foreign_keys=ON;", true); +end + +function sqlitedb:gettable(sql) + assert(sqlite3.complete(sql), "Not an SQL statement:\r\n" .. sql); + + local results = {}; + + for row in self.db:nrows(sql) do + table.insert(results, row); + end + + return results; +end + +function sqlitedb:changes() + return self.db:changes(); +end + +function sqlitedb:backup() + self:open(); + + local isDatabaseBackedUp; + + print("\r\n\r\nPERFORMING", self.name, "DATABASE BACKUP."); + print("CHECKING INTEGRITY..."); + + BroadcastPlugin(999, "repaint"); + + local integrityCheck = true; + + self:exec("PRAGMA wal_checkpoint;", true); + + for row in self.db:nrows("PRAGMA integrity_check;") do + if (row.integrity_check ~= "ok") then + integrityCheck = false; + end + end + + self:close(); + + if (not integrityCheck) then + isDatabaseBackedUp = false; + + print("INTEGRITY CHECK FAILED. CLOSE MUSHCLIENT AND RESTORE A KNOWN GOOD DATABASE."); + print("BACKUP ABORTED.\r\n\r\n"); + else + print("INTEGRITY CHECK PASSED."); + + BroadcastPlugin (999, "repaint"); + + local backupDir = self.path .. "db_backups\\"; + + local makeDirCmd = "mkdir " .. addQuotes(backupDir); + + os.execute(makeDirCmd); + + local copyCmd = "copy /Y " .. addQuotes(self.path .. self.name) .. " " .. addQuotes(backupDir .. self.name .. "." .. "backup"); + + os.execute(copyCmd); + + print("FINISHED DATABASE BACKUP.\r\n\r\n"); + + self:open(); + + isDatabaseBackedUp = true; + end + + return isDatabaseBackedUp; +end + +function sqlitedb:vacuum() + local fileSizeBefore; + local fileSizeAfter; + + self:open(); + + local file = io.open(self.path .. self.name); + + fileSizeBefore = file and fsize(file) or nil; + + print("BEGIN VACUUM ON", self.name); + + local code = self:exec("VACUUM;"); + + if (code ~= sqlite3.OK) then + print("END VACUUM ON", self.name); + local err = self.db:errmsg(); + self.db:execute("ROLLBACK;"); + file:close(); + error(err); + else + fileSizeAfter = file and fsize(file) or nil; + file:close(); + + if (fileSizeBefore and fileSizeAfter) then + fileSizeBefore = fileSizeBefore / 1024; + fileSizeAfter = fileSizeAfter / 1024; + + local fileSizeDif = fileSizeBefore - fileSizeAfter; + + print("DISK SPACE RECOVERED: " .. fileSizeDif .. " KB"); + end + + print("END VACUUM ON", self.name); + end +end + +function fsize(file) + local current = file:seek(); -- get current position + local size = file:seek("end"); -- get file size + file:seek("set", current); -- restore position + return size; +end + +function addQuotes(str) + return "\"" .. str .. "\""; +end \ No newline at end of file diff --git a/lua/stringutils.lua b/lua/stringutils.lua new file mode 100644 index 0000000..d5c21f4 --- /dev/null +++ b/lua/stringutils.lua @@ -0,0 +1,113 @@ +--[[ +String Functions + +functions in this module + +fixsql - usage: fixsql(sql, likeOperator) + change quotes to double single quotes + +stripColors - usage: stripColors(str) + remove color codes from string + +toPascalCase - usage toPascalCase(str) + captialize the beginning of each word + +formatSeconds - usage formatSeconds(seconds) + convert seconds to hrs mins secs +--]] + + +function fixsql(sql, likeOperator) + if (sql) then + if (likeOperator) then + if (likeOperator == "left") then + sql = "'%" .. string.gsub(sql, "'", "''") .. "'"; + elseif (likeOperator == "right") then + sql = "'" .. string.gsub(sql, "'", "''") .. "%'"; + else + sql = "'%" .. string.gsub(sql, "'", "''") .. "%'"; + end + else + sql = "'" .. string.gsub(sql, "'", "''") .. "'"; + end + else + sql = "NULL;" + end + + return sql; +end + +function stripColors(str) + str = str:gsub("@@", "\0"); -- change @@ to 0x00 + str = str:gsub("@%-", "~"); -- fix tildes (historical) + str = str:gsub("@x%d?%d?%d?", ""); -- strip valid and invalid xterm color codes + str = str:gsub("@.([^@]*)", "%1"); -- strip normal color codes and hidden garbage + return (str:gsub("%z", "@")); -- put @ back (has parentheses on purpose) +end + +function toPascalCase(str) + str = string.gsub(str, "(%a)([%w_']*)", + function (first, rest) + return first:upper()..rest:lower(); + end + ); + return str; +end + +function wrap(line, length) + local lines = {}; + + length = length or 10; + + while (#line > length) do + -- find a space not followed by a space, or a , closest to the end of the line + local col = string.find(line:sub(1, length), "[%s,][^%s,]*$"); + + if (col and col > 2) then + -- col = col - 1 -- use the space to indent + else + col = length -- just cut off at wrap_column + end -- if + + table.insert(lines, line:sub(1, col)); + line = line:sub(col + 1); + end + + table.insert(lines, line); + + return lines; +end + +function formatSeconds(seconds) + if (not tonumber(seconds)) then + return seconds; + end + + if (seconds < 1) then + return string.format("%.2fs", seconds); + end + + local hours = math.floor(seconds / 3600); + + seconds = seconds % 3600 + + local mins = math.floor(seconds / 60); + + seconds = math.floor(seconds % 60); + + local duration = ""; + + if (hours > 0) then + duration = hours .. "h "; + end + + if (mins > 0) then + duration = duration .. mins .. "m "; + end + + if (seconds > 0) then + duration = duration .. seconds .. "s"; + end + + return duration; +end \ No newline at end of file diff --git a/resources/README.txt b/resources/README.txt new file mode 100644 index 0000000..c95a178 --- /dev/null +++ b/resources/README.txt @@ -0,0 +1,5 @@ +Customize Background +-------------------- + +Replace the bg.png image with a png of your own, same file name. +Image should be at least 350 x 270px. \ No newline at end of file diff --git a/resources/bg.png b/resources/bg.png new file mode 100644 index 0000000..0368cd0 Binary files /dev/null and b/resources/bg.png differ