From f49d426ccfdd91e42f0b25b6e79ccf194fe6f38a Mon Sep 17 00:00:00 2001 From: ccubed Date: Mon, 17 Jun 2024 01:36:51 -0400 Subject: [PATCH] Initial Beta of Bashmatic --- .woodpecker.yaml | 42 + readme.md | 27 +- src/aliases/bashmatic/BM_Addtarget.lua | 23 +- src/aliases/bashmatic/BM_Clear.lua | 26 +- src/aliases/bashmatic/BM_Config.lua | 26 +- src/aliases/bashmatic/BM_Configset.lua | 43 +- src/aliases/bashmatic/BM_Delete.lua | 28 + src/aliases/bashmatic/BM_Help.lua | 34 +- src/aliases/bashmatic/BM_Listtargets.lua | 40 + .../bashmatic/BM_Listtargets_Pager.lua | 54 + src/aliases/bashmatic/BM_Toggle.lua | 8 +- src/aliases/bashmatic/aliases.json | 18 +- src/resources/LICENSE_MDK.lua | 48 + src/resources/ftext.lua | 1697 +++++++++++++++++ src/resources/ftext_spec.lua | 447 +++++ src/scripts/bashmatic/bashmatic_init.lua | 316 +-- 16 files changed, 2651 insertions(+), 226 deletions(-) create mode 100644 .woodpecker.yaml create mode 100644 src/aliases/bashmatic/BM_Delete.lua create mode 100644 src/aliases/bashmatic/BM_Listtargets_Pager.lua create mode 100755 src/resources/LICENSE_MDK.lua create mode 100755 src/resources/ftext.lua create mode 100755 src/resources/ftext_spec.lua diff --git a/.woodpecker.yaml b/.woodpecker.yaml new file mode 100644 index 0000000..2d2d673 --- /dev/null +++ b/.woodpecker.yaml @@ -0,0 +1,42 @@ +steps: + - name: build-release + image: alpine + when: + - event: tag + commands: + - apk add openjdk21 + - apk add wget + - apk add tar + - wget https://github.com/demonnic/muddler/releases/download/0.13/muddle-shadow-0.13.tar + - tar -xvf muddle-shadow-0.13.tar + - mv muddle-shadow-0.13/lib/muddle-0.13-all.jar muddle.jar + - java -jar muddle.jar + + - name: deploy-build + image: alpine + when: + - event: tag + environment: + GITEATKN: + from_secret: GITEA_TOKEN + GOTOKEN: + from_secret: GOTIFY_TOKEN + depends_on: build-release + commands: + - apk add tea + - apk add wget + - tea login add --name=temp --url=https://git.vertinext.com/ --token=$GITEATKN + - tea releases create --asset "build/Bashmatic.mpackage" --asset "build/Bashmatic.xml" --tag ${CI_COMMIT_TAG} --title "Latest Compiled Release" --note "Use .mpackage for Mudlet and .xml for Mushclient" + - wget "https://gotify.vertinext.com/message?token=$GOTOKEN" --post-data "title=${CI} - ${CI_REPO_NAME}&message=New release ${CI_COMMIT_TAG} built and deployed.&priority=0" -O /dev/null + + - name: notify-on-branch-push + image: alpine + when: + - event: [push, pull_request, tag, release] + - status: [success, failure] + environment: + GOTOKEN: + from_secret: GOTIFY_TOKEN + commands: + - apk add wget + - wget "https://gotify.vertinext.com/message?token=$GOTOKEN" --post-data "title=${CI} - ${CI_REPO_NAME}&message=New ${CI_PIPELINE_EVENT} to ${CI_REPO_NAME}/${CI_COMMIT_BRANCH}.&priority=0" -O /dev/null \ No newline at end of file diff --git a/readme.md b/readme.md index 116c1a2..7917b9a 100644 --- a/readme.md +++ b/readme.md @@ -2,15 +2,15 @@ Bashmatic is a generic script package for mudlet. Many muds have a character visit areas and attack various denizens to gain experience to level up. This process is colloquially called bashing. Bashmatic is a very generic script package that automates this process. # What does it support? -Again, it's very generic. Because of that it supports pretty much anywhere that you can bash using a one line command to do so. However, because of the generic nature of the script it also doesn't support more advanced features games might have. It does however support sending commands to the game before and after fights. It also supports grouping bashing targets by areas if you use the mudlet mapper or your game supports gmcp. It also supports finding enemies by gmcp or general input scraping. So overall: +Again, it's very generic. Because of that it supports pretty much anywhere that you can bash using a one line command to do so. However, because of the generic nature of the script it also doesn't support more advanced features games might have. It does however support sending commands to the game before and after fights. It also supports grouping bashing targets by areas if you use the mudlet mapper. Overall it supports: * Supports sending a customized pre-bashing command * Supports sending a customized bashing command * Supports sending a customized post-bashing command -* Supports specifying a custom mob death phrase +* Supports specifying a custom mob death trigger phrase per game * Supports grouping bashing targets by area -* Supports detecting areas using the mudlet mapper or by GMCP -* Supports detecting enemies by GMCP or general input scraping +* Supports detecting areas using the mudlet mapper +* Supports detecting enemies by creating temp triggers based on target names * Provides a function `bmDisable()` that will disable the basher so you can rig a flee/wimpy trigger/alias # Can I use this on my mud? @@ -22,25 +22,24 @@ A few but none that are too bad. ## Switching To Area Group Mode By default, Bashmatic does not group by areas. This means if you switch the target grouping mode to areas you will erase your target list. This is by necessity. If area grouping is turned off, no area data is stored for targets. When you turn on area grouping, we have no way of finding out what area they should be in. So you'll have to readd them to get them in the right area. This is not true of the other way. If you are grouping by area and turn that off, then we will simply collate all targets into a giant list. You can then clear your target list using `bm clear` if you want. -## GMCP Usage -GMCP requires you to tell the script where the variables are located. This isn't hard. You can type `lua gmcp` into mudlet to get back a listing of the gmcp data your mud sends out. If the list of enemies on your mud was at `Room.Info.Enemies` you could give me that information using `bm config egmcp=Room.Info.Enemies` - -## Grouping Targets By Areas -If grouping by GMCP areas, also see above. -We can group targets by areas if you prefer as long as we can tell which area we are in. This can happen by the mud sending the area name by GMCP or by mudlet returning an area through `GetRoomArea(GetPlayerRoom())`. This can be useful for keeping your target lists manageable assuming you want to be able to look at them. +## Grouping Targets By Areas +We can group targets by areas if you prefer as long as we can tell which area we are in. This can happen by mudlet returning an area through `GetRoomAreaName(GetRoomArea(GetPlayerRoom())`. This requires you to have a map made in mudlet. Functionally, the real benefit to this is that we can reduce the number of tmeporary triggers made since we can only make ones related to the current area. However, unless you have thousands upon thousands of targets built into your list you probably won't notice the difference. ## Auto target switching Bashmatic doesn't technically support auto-target switching, but we have a jury rigged solution. Upon detecting an enemy's death we will look at the room again. ## Before, During, and After Bashing commands -You should know that the only time we append the target name is bashing start command. Before, During, and After bashing commands do not receive the target name. +You should know that the only time we append the target name is bashing start command. Before, During, and After bashing commands do not receive the target name. We also kill all triggers related to finding targets once we start bashing a target. # Required Setup -At the very least, you'll need to setup a bashing command. This can be as simply as just `kill` and we'll fill in the target. And you'll need some kind of death trigger. On a lot of muds this could just be the word `died`. A lot of muds just use some variation of the phrase `has died` and this script will match `.*word.*`. Then at least one target added using bm add target. So `bm add bunny` maybe. +At the very least, you'll need to setup a bashing command. This can be as simple as just `kill` and we'll fill in the target so it late becomes `kill target`. And you'll need some kind of phrase that allows us to determine a mob has died: The death trigger phrase. On a lot of muds this could just be the word `died` or `has been slain` or `have slain` or some other combination. A lot of muds just use some variation of the phrase `has died` and this script will match `.*word.*`. Then at least one target added using bma target. So `bma bunny` maybe. * A bash command, Possibly just: `kill` * A death trigger, Possibly just: `died` -* At least one target, Possibly just: `bm add bunny` +* At least one target, Possibly just: `bma bunny` # How does this work? -It depends. Under the hood, it's much more efficient if we can get enemy data from GMCP. Using GMCP, we can simply scan GMCP data for changes to enemy data and react to that. Without GMCP, we create temporary triggers and react to those as they fire. \ No newline at end of file +In general, we create temporary triggers that only fire once that look for matches against the targets given. So for our bunny example, we would create a temp trigger that will only ever fire once against the word bunny. This is not perfect, for obvious reasons. If the room description contains the word bunny or if someone is wearing an item with the word bunny in it, we're going to react to that word. Still, it's a simple enough solution that works most of the time for most situations especially given that the system has to be manually turned on and is likely to only even be turned on in situations where we are less likely to run into those odd situations. IE: If you turn on the autobasher, the bunny you run into is most likely to be the one you want to kill for gaining experience, not the random guy with his bunny cloak. Even if it does mess up, just kill something normally once and it will reset. Or simply toggle it off and on. You'll be ready to go again. + +# In Game Help +There is extensive and easy to understand help built in. Once installed in mudlet, see `bm help` and `bmc` for an indepth explanation of settings and features. \ No newline at end of file diff --git a/src/aliases/bashmatic/BM_Addtarget.lua b/src/aliases/bashmatic/BM_Addtarget.lua index 89719dd..dbc8e8b 100644 --- a/src/aliases/bashmatic/BM_Addtarget.lua +++ b/src/aliases/bashmatic/BM_Addtarget.lua @@ -1,17 +1,16 @@ -if not bashmatic.configs.death.event then +if not bashmatic.death.event then cecho("BASHMATIC: You must configure the death trigger first. Please see bm help for more info.") else - if bashmatic.configs.enemies.detection == "inline" then - -- using trigger detection - local area = nil - if bashmatic.configs.areas.detection == "mapper" then area = GetRoomAreaName(GetRoomArea(GetPlayerRoom())) end - if bashmatic.configs.areas.detection == "gmcp" and bashmatic.configs.areas.data ~= nil then area = bashmatic.configs.areas.data end - inlineAddTarget(matches[2], area) - elseif bashmatic.configs.enemies.detection == "gmcp" then - -- using gmcp detection - cecho("BASHMATIC: GMCP not implemented yet.") + if bashmatic.enemies.group then + -- grouping enemies by area + local area = GetRoomAreaName(GetRoomArea(GetPlayerRoom())) or nil + if area == nil then + cecho("BASHMATIC: We tried to add that enemy but got a bad area.\nGot: " .. area .. "") + else + inlineAddTarget(matches[2], area) + end else - -- well this is broken - cecho("BASHMATIC: Invalid enemy detection method.") + -- not grouping by area + inlineAddTarget(matches[2], nil) end end \ No newline at end of file diff --git a/src/aliases/bashmatic/BM_Clear.lua b/src/aliases/bashmatic/BM_Clear.lua index 24ceae1..d45f237 100644 --- a/src/aliases/bashmatic/BM_Clear.lua +++ b/src/aliases/bashmatic/BM_Clear.lua @@ -1,10 +1,20 @@ -if table.size(bashmatic.hunting.events) > 0 then - for _, tid in ipairs(bashmatic.hunting.events) do - killTrigger(tid) - end -end +killBashingTriggers() toggleBashing(false) bmDisable() -bashmatic.hunting.events = {} -bashmatic.hunting.targets = {} -cecho("BASHMATIC: Cleared target list.") \ No newline at end of file + +if Bashmatic.enemies.group then + local area = GetRoomAreaName(GetRoomArea(GetPlayerRoom())) or nil + if area == nil then + cecho("Bashmatic: Attempted to clear area targets but got a bad area.\nGot: " .. area .. "") + return + end + if not table.contains(Bashmatic.enemies.targets, area) then + cecho("Bashmatic: You don't have any enemies for the area " .. area .. "") + return + end + Bashmatic.enemies.targets.area = nil + cecho("Bashmatic: Cleared target list for area " .. area .. "") +else + Bashmatic.enemies.targets = {} + cecho("Bashmatic: Cleared target list.") +end \ No newline at end of file diff --git a/src/aliases/bashmatic/BM_Config.lua b/src/aliases/bashmatic/BM_Config.lua index 9ce87b2..250f0e6 100644 --- a/src/aliases/bashmatic/BM_Config.lua +++ b/src/aliases/bashmatic/BM_Config.lua @@ -3,22 +3,12 @@ cecho("---------------------------------") cecho("See bm help for information on configuration.") cecho("") cecho("\nBattle Commands:") -cecho("-- Start : " .. bashmatic.commands.start or "None") -cecho("-- Before: " .. bashmatic.commands.before or "None") -cecho("-- During: " .. bashmatic.commands.during or "None" .. "(Every " .. bashmatic.commands.duringTime .. "s)") -cecho("-- After : " .. bashmatic.commands.after or "None") +cecho("-- Start : " .. Bashmatic.commands.start or "None") +cecho("-- Before: " .. Bashmatic.commands.before or "None") +cecho("-- During: " .. Bashmatic.commands.during or "None" .. "(Every " .. Bashmatic.commands.duringTime .. "s)") +cecho("-- After : " .. Bashmatic.commands.after or "None") cecho("\n\nDeath Trigger:") -cecho("-- Phrase : " .. bashmatic.death.phrase or "None") -cecho("-- Activated?: " .. bashmatic.death.event or "No") -cecho("\n\nDetection:") -cecho("-- Detecting enemies using " .. bashmatic.configs.enemies.detection) -if bashmatic.configs.areas.detection == nil then - cecho("-- Not currently grouping enemies by areas.") -else - cecho("-- Currently grouping enemies by areas using " .. bashmatic.configs.areas.detection) -end -if bashmatic.configs.enemies.detection == "gmcp" or bashmatic.configs.areas.detection == "gmcp" then - cecho("\n\nGMCP Detection Settings:") - if bashmatic.configs.enemies.detection == "gmcp" then cecho("-- Enemy data located at " .. bashmatic.configs.enemies.location) end - if bashmatic.configs.areas.detection == "gmcp" then cecho("-- Area data located at " .. bashmatic.configs.areas.location) end -end \ No newline at end of file +cecho("-- Phrase : " .. Bashmatic.death.phrase or "None") +cecho("-- Activated?: " .. Bashmatic.death.event or "No") +cecho("\n\nEnemies:") +cecho("-- Grouping by Areas: " .. Bashmatic.enemies.group or "No") \ No newline at end of file diff --git a/src/aliases/bashmatic/BM_Configset.lua b/src/aliases/bashmatic/BM_Configset.lua index 3b90d7e..e138251 100644 --- a/src/aliases/bashmatic/BM_Configset.lua +++ b/src/aliases/bashmatic/BM_Configset.lua @@ -2,24 +2,49 @@ local which = matches[2]:lower() if which == "phrase" then -- death phrase + if Bashmatic.death.event ~= nil then + killTrigger(Bashmatic.death.event) + end + Bashmatic.death.phrase = matches[2] + Bashmatic.death.event = tempTrigger(Bashmatic.death.phrase,handleMobDeath) + cecho("Bashmatic: Reset the death event trigger phrase to " .. matches[2] .. ".") elseif which == "start" then -- starting battle command + Bashmatic.commands.start = matches[2] + cecho("Bashmatic: Set battle start command to " .. matches[2] .. " target.") elseif which == "before" then -- before battle command + Bashmatic.commands.before = matches[2] + cecho("Bashmatic: Will send " .. matches[2] .. " before battle start.") elseif which == "during" then -- during battle command + Bashmatic.comands.during = matches[2] + cecho("Bashmatic: Will send " .. matches[2] .. " during battle every " .. Bashmatic.commands.duringTime .. " seconds.") elseif which == "duringTime" then -- during battle command time frame + Bashmatic.commands.duringTime = tonumber(mathces[2]) + cecho("Bashmatic: Will send " .. Bashmatic.commands.during .. " during battle every " .. tonumber(matches[2]) .. " seconds.") elseif which == "after" then -- after battle command -elseif which == "enemy" then - -- enemy detection method -elseif which == "egmcp" then - -- enemy gmcp path -elseif which == "area" then - -- area detection method -elseif which == "agmcp" then - -- area gmcp path + Bashmatic.commands.after = matches[2] + cecho("Bashmatic: Will send " .. matches[2] .. " after combat.") +elseif which == "group" then + -- group by area + if #getRooms() == 0 then + Bashmatic.enemies.group = false + cecho("Bashmatic: No map detected. Did you make one? You need at least one room to use this feature.") + else + Bashmatic.enemies.group = not Bashmatic.enemies.group + if Bashmatic.enemies.group then + -- we've just turned on enemy grouping + Bashmatic.enemies.targets = {} + cecho("Bashmatic: Now grouping enemies by areas. Cleared previous enemies.") + else + -- we've just turned off enemy grouping + squashTargetGroups() + cecho("Bashmatic: No longer grouping enemies by areas. Squashed previous enemies into one large table.\nYou can bm clear to erase them.") + end + end else - cecho("BASHMATIC: Unrecognized option " .. matches[2] .. ". See bm help for a list of options.") + cecho("Bashmatic: Unrecognized option " .. matches[2] .. ". See bm help for a list of options.") end \ No newline at end of file diff --git a/src/aliases/bashmatic/BM_Delete.lua b/src/aliases/bashmatic/BM_Delete.lua new file mode 100644 index 0000000..82d416d --- /dev/null +++ b/src/aliases/bashmatic/BM_Delete.lua @@ -0,0 +1,28 @@ +local ourIndex = tonumber(matches[2]) + +if Bashmatic.enemies.group then + local area = GetRoomAreaName(GetRoomArea(GetPlayerRoom())) or nil + if area == nil then + cecho("Bashmatic: Attempted to clear area targets but got a bad area.\nGot: " .. area .. "") + return + end + if not table.contains(Bashmatic.enemies.targets, area) then + cecho("Bashmatic: You don't have any enemies for the area " .. area .. "") + return + end + local size = table.size(Bashmatic.enemies.targets.area) + if ourIndex > size then + cecho("Bashmatic: Index out of range for this area. There are only " .. size .. " targets stored.") + return + end + table.remove(Bashmatic.enemies.targets.area, ourIndex) + cecho("Bashmatic: Removed target from the area list.") +else + local size = table.size(Bashmatic.enemies.targets) + if ourIndex > size then + cecho("Bashmatic: Index out of range. There are only " .. size .. " targets stored.") + return + end + table.remove(Bashmatic.enemies.targets, ourIndex) + cecho("Bashmatic: Removed target from the list. - add a new target to the hunt list +bma - add a new target to the hunt list bmc - show the current configuration -bm config option=value - change the configuration -bmt - List all current targets +bmc option=value - change the configuration +bmt - List all current targets (if grouped by areas this will be for the current area) bm - toggle the bashing system on and off -bm clear - clear all targets +bm clear - clear all targets (All Targets. For All Areas.) +bmd # - Delete the target with the given ID from bmt bm help - this screen ]]) \ No newline at end of file diff --git a/src/aliases/bashmatic/BM_Listtargets.lua b/src/aliases/bashmatic/BM_Listtargets.lua index e69de29..cb6010b 100644 --- a/src/aliases/bashmatic/BM_Listtargets.lua +++ b/src/aliases/bashmatic/BM_Listtargets.lua @@ -0,0 +1,40 @@ +if Bashmatic.enemies.group then + -- grouped by areas + local area = GetRoomAreaName(GetRoomArea(GetPlayerRoom())) or nil + if area == nil then + cecho("Bashmatic: We got an invalid area.\nWe Got: " .. area .. "") + return + end + if not table.contains(Bashmatic.enemies.targets, area) then + cecho("Bashmatic: No enemies added from the area " .. area .. "") + return + end + local size = table.size(Bashmatic.enemies.targets.area) + if size == 0 then + cecho("Bashmatic: No enemies added from the area " .. area .. "") + return + end + if size <= 15 then + -- fits on one page + displayAreaTargetList(area, 1, size, nil, nil) + else + -- paginator + local pages = math.floor(size/15)+1 + displayAreaTargetList(area, 1, 15, 1, pages) + end +else + -- not grouped by area + local size = table.size(Bashmatic.enemies.targets) + if size == 0 then + cecho("Bashmatic: No enemies added.") + return + end + if size <= 15 then + -- fits on one page + displayTargetList(1, size, nil, nil) + else + -- paginator + local pages = math.floor(size/15)+1 + displayTargetList(1, 15, 1, pages) + end +end \ No newline at end of file diff --git a/src/aliases/bashmatic/BM_Listtargets_Pager.lua b/src/aliases/bashmatic/BM_Listtargets_Pager.lua new file mode 100644 index 0000000..e9160f4 --- /dev/null +++ b/src/aliases/bashmatic/BM_Listtargets_Pager.lua @@ -0,0 +1,54 @@ +if Bashmatic.enemies.group then + -- grouped by areas + local area = GetRoomAreaName(GetRoomArea(GetPlayerRoom())) or nil + if area == nil then + cecho("Bashmatic: We got an invalid area.\nWe Got: " .. area .. "") + return + end + if not table.contains(Bashmatic.enemies.targets, area) then + cecho("Bashmatic: No enemies added from the area " .. area .. "") + return + end + local size = table.size(Bashmatic.enemies.targets.area) + if size == 0 then + cecho("Bashmatic: No enemies added from the area " .. area .. "") + return + end + if size <= 15 then + -- fits on one page + displayAreaTargetList(area, 1, size, nil, nil) + else + -- paginator + local pages = math.floor(size/15)+1 + if tonumber(matches[2]) > pages then + cecho("Bashmatic: There aren't that many pages. There are only " .. pages .. " pages.") + return + end + local startIndex = ((tonumber(matches[2]) - 1)*15)+1 + local endIndex = startIndex + 14 + if endIndex > size then endIndex = size end + displayAreaTargetList(area, startIndex, endIndex, tonumber(matches[2]), pages) + end +else + -- not grouped by area + local size = table.size(Bashmatic.enemies.targets) + if size == 0 then + cecho("Bashmatic: No enemies added.") + return + end + if size <= 15 then + -- fits on one page + displayTargetList(1, size, nil, nil) + else + -- paginator + local pages = math.floor(size/15)+1 + if tonumber(matches[2]) > pages then + cecho("Bashmatic: There aren't that many pages. There are only " .. pages .. " pages.") + return + end + local startIndex = ((tonumber(matches[2]) - 1)*15)+1 + local endIndex = startIndex + 14 + if endIndex > size then endIndex = size end + displayTargetList(startIndex, endIndex, tonumber(matches[2]), pages) + end +end \ No newline at end of file diff --git a/src/aliases/bashmatic/BM_Toggle.lua b/src/aliases/bashmatic/BM_Toggle.lua index baf95db..df94b7a 100644 --- a/src/aliases/bashmatic/BM_Toggle.lua +++ b/src/aliases/bashmatic/BM_Toggle.lua @@ -1 +1,7 @@ -bashmatic.configs.enabled = not bashmatic.configs.enabled \ No newline at end of file +Bashmatic.enabled = not Bashmatic.enabled + +if Bashmatic.enabled and table.size(Bashmatic.hunting.events) == 0 and table.size(Bashmatic.enemies.targets) > 1 then + -- we just turned ourself on, we have targets, and no triggers are running + recreateTriggers() + cecho("Bashmatic: Bashmatic turned on. Let's go bash some mobs.") +end \ No newline at end of file diff --git a/src/aliases/bashmatic/aliases.json b/src/aliases/bashmatic/aliases.json index ddcf0f0..5ee517b 100644 --- a/src/aliases/bashmatic/aliases.json +++ b/src/aliases/bashmatic/aliases.json @@ -3,7 +3,7 @@ "isActive": "yes", "isFolder": "no", "name": "BM Addtarget", - "regex": "^bm add (.*)$", + "regex": "^bma (.*)$", "script": "" }, { @@ -17,7 +17,7 @@ "isActive": "yes", "isFolder": "no", "name": "BM Configset", - "regex": "^bm config (.*)=(.*)$", + "regex": "^bmc (.*)=(.*)$", "script": "" }, { @@ -27,6 +27,13 @@ "regex": "^bmt$", "script": "" }, + { + "isActive": "yes", + "isFolder": "no", + "name": "BM Listtargets Pager", + "regex": "^bmt (\\d+)$", + "script": "" + }, { "isActive": "yes", "isFolder": "no", @@ -47,5 +54,12 @@ "name": "BM Help", "regex": "^bm help$", "script": "" + }, + { + "isActive": "yes", + "isFolder": "no", + "name": "BM Delete", + "regex": "^bmd (\\d+)", + "script": "" } ] \ No newline at end of file diff --git a/src/resources/LICENSE_MDK.lua b/src/resources/LICENSE_MDK.lua new file mode 100755 index 0000000..1812afa --- /dev/null +++ b/src/resources/LICENSE_MDK.lua @@ -0,0 +1,48 @@ +--[===[ +The MIT License (MIT) + +Copyright (c) 2020 Damian Monogue + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +--]===] + +-- schema validation provided by schema.lua, license below +--[[ +The MIT License (MIT) + +Copyright (c) 2014 Sebastian Schoener + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +]] diff --git a/src/resources/ftext.lua b/src/resources/ftext.lua new file mode 100755 index 0000000..6a1cbe8 --- /dev/null +++ b/src/resources/ftext.lua @@ -0,0 +1,1697 @@ +--- ftext +-- functions to format and print text, and the objects which use them +-- @module ftext +-- @author Damian Monogue +-- @copyright 2020 Damian Monogue +-- @copyright 2021 Damian Monogue +-- @copyright 2022 Damian Monogue +-- @license MIT, see LICENSE.lua +local ftext = {} +local dec = {"d", "decimal", "dec"} +local hex = {"h", "hexidecimal", "hex"} +local col = {"c", "color", "colour", "col", "name"} + +--- Performs wordwrapping on a string, given a length limit. Does not understand colour tags and will count them as characters in the string +-- @tparam string str the string to wordwrap +-- @tparam number limit the line length to wrap at +function ftext.wordWrap(str, limit, indent, indent1) + -- pulled from http://lua-users.org/wiki/StringRecipes + indent = indent or "" + indent1 = indent1 or indent + limit = limit or 72 + local here = 1 - #indent1 + local function check(sp, st, word, fi) + if fi - here > limit then + here = st - #indent + return "\n" .. indent .. word + end + end + return indent1 .. str:gsub("(%s+)()(%S+)()", check) +end + +--- Performs wordwrapping on a string, while ignoring color tags of a given type. +-- @tparam string text the string you are wordwrapping +-- @tparam number limit the line length to wrap at +-- @tparam string type What type of color codes to ignore. 'c' for cecho, 'd' for decho, 'h' for hecho, and anything else or nil to pass the string on to wordWrap +function ftext.xwrap(text, limit, type) + local colorPattern + if table.contains(dec, type) then + colorPattern = _Echos.Patterns.Decimal[1] + elseif table.contains(hex, type) then + colorPattern = _Echos.Patterns.Hex[1] + elseif table.contains(col, type) then + colorPattern = _Echos.Patterns.Color[1] + else + return ftext.wordWrap(text, limit) + end + local strippedString = rex.gsub(text, colorPattern, "") + local strippedLines = ftext.wordWrap(strippedString, limit):split("\n") + local lineIndex = 1 + local line = "" + local strLine = "" + local lines = {} + local strLines = {} + local workingLine = strippedLines[lineIndex]:split("") + local workingLineLength = #workingLine + local lineColumn = 0 + for str, color, res in rex.split(text, colorPattern) do + if res then + if type == "Hex" then + color = "#r" + elseif type == "Dec" then + color = "" + elseif type == "Color" then + color = "" + end + end + color = color or "" + local strLen = str:len() + if lineColumn + strLen <= workingLineLength then + strLine = strLine .. str + line = line .. str .. color + lineColumn = lineColumn + strLen + else + local neededChars = workingLineLength - lineColumn + local take = str:sub(1, neededChars) + local leave = str:sub(neededChars + 1, -1) + strLine = strLine .. take + line = line .. take + table.insert(lines, line) + table.insert(strLines, strLine) + line = "" + strLine = "" + lineIndex = lineIndex + 1 + workingLine = strippedLines[lineIndex]:split("") + workingLineLength = #workingLine + lineColumn = 0 + if leave:sub(1, 1) == " " then + leave = leave:sub(2, -1) + end + while leave ~= "" do + take = leave:sub(1, workingLineLength) + leave = leave:sub(workingLineLength + 1, -1) + if leave:sub(1, 1) == " " then + leave = leave:sub(2, -1) + end + if take:len() < workingLineLength then + lineColumn = take:len() + line = line .. take .. color + strLine = strLine .. take + else + lineIndex = lineIndex + 1 + workingLine = strippedLines[lineIndex] + if workingLine then + workingLine = strippedLines[lineIndex]:split("") + workingLineLength = #workingLine + end + table.insert(lines, take) + table.insert(strLines, take) + end + if leave == "\n" then + table.insert(lines, leave) + table.insert(strLines, leave) + leave = "" + end + end + end + end + if line ~= "" then + table.insert(lines, line) + end + return table.concat(lines, "\n") +end + +--- The main course, this function returns a formatted string, based on a table of options +-- @tparam string str the string to format +-- @tparam table opts the table of options which control the formatting +--

Table of options +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +--
option namedescriptiondefault
wrapShould it wordwrap to multiple lines?true
formatTypeDetermines how it formats for color. 'c' for cecho, 'd' for decho, 'h' for hecho, and anything else for no colors""
widthHow wide should we format the text?80
capwhat characters to use for the endcap.""
capColorwhat color to make the endcap?the correct 'white' for your formatType
spacerWhat character to use for empty space. Must be a single character" "
spacerColorwhat color should the spacer be?the correct 'white' for your formatType
textColorwhat color should the text itself be?the correct 'white' for your formatType
alignmentHow should the text be aligned within the width. "center", "left", or "right""center"
nogapShould we put a literal space between the spacer character and the text?false
insidePut the spacers inside the caps?false
mirrorShould the endcap be reversed on the right? IE [[ becomes ]]true
truncateCut the string to width. Is superceded by wrap being true.false
+function ftext.fText(str, opts) + local options = ftext.fixFormatOptions(str, opts) + if options.wrap and (options.strLen > options.effWidth) then + local wrapped = "" + if str:find("\n") then + for _,line in ipairs(str:split("\n")) do + local newline = "\n" + if _ == 1 then newline = "" end + wrapped = wrapped .. newline .. ftext.xwrap(line, options.effWidth, options.formatType) + end + else + wrapped = ftext.xwrap(str, options.effWidth, options.formatType) + end + local lines = wrapped:split("\n") + local formatted = {} + options.fixed = false + for _, line in ipairs(lines) do + table.insert(formatted, ftext.fLine(line, options)) + end + return table.concat(formatted, "\n") + else + return ftext.fLine(str, options) + end +end + +-- internal function, used to set defaults and type correct the options table +function ftext.fixFormatOptions(str, opts) + if opts.fixed then + return table.deepcopy(opts) + end + -- Set up all the things we might call the different echo types + if opts == nil then + opts = {} + end -- don't overwrite options if they passed them + -- but if they passed something other than a table as the options than oopsie! + if type(opts) ~= "table" then + error("Improper argument: options expected to be passed as table") + end + -- now we make a copy of the table, so we don't edit the original during all this + local options = table.deepcopy(opts) + if options.wrap == nil then + options.wrap = true + end -- wrap by default. + if options.truncate == nil then + options.truncate = false + end -- do not truncate by default + options.formatType = options.formatType or "" -- by default, no color formatting. + options.width = options.width or 80 -- default 80 width + options.cap = options.cap or "" -- no cap by default + options.spacer = options.spacer or " " -- default spacer is the space character + options.alignment = options.alignment or "center" -- default alignment is centered + if options.nogap == nil then + options.nogap = false + end + if options.inside == nil then + options.inside = false + end -- by default, we don't put the spacer inside + if not options.mirror == false then + options.mirror = options.mirror or true + end -- by default, we do want to use mirroring for the caps + -- setup default options for colors based on the color formatting type + if table.contains(dec, options.formatType) then + options.capColor = options.capColor or "<255,255,255>" + options.spacerColor = options.spacerColor or "<255,255,255>" + options.textColor = options.textColor or "<255,255,255>" + options.colorReset = "" + options.colorPattern = _Echos.Patterns.Decimal[1] + elseif table.contains(hex, options.formatType) then + options.capColor = options.capColor or "#FFFFFF" + options.spacerColor = options.spacerColor or "#FFFFFF" + options.textColor = options.textColor or "#FFFFFF" + options.colorReset = "#r" + options.colorPattern = _Echos.Patterns.Hex[1] + elseif table.contains(col, options.formatType) then + options.capColor = options.capColor or "" + options.spacerColor = options.spacerColor or "" + options.textColor = options.textColor or "" + options.colorReset = "" + options.colorPattern = _Echos.Patterns.Color[1] + else + options.capColor = "" + options.spacerColor = "" + options.textColor = "" + options.colorReset = "" + options.colorPattern = "" + end + options.originalString = str + options.strippedString = rex.gsub(tostring(str), options.colorPattern, "") + options.strLen = string.len(options.strippedString) + options.leftCap = options.cap + options.rightCap = options.cap + options.capLen = string.len(options.cap) + local gapSpaces = 0 + if not options.nogap then + if options.alignment == "center" then + gapSpaces = 2 + else + gapSpaces = 1 + end + end + options.nontextlength = options.width - options.strLen - gapSpaces + options.leftPadLen = math.floor(options.nontextlength / 2) + options.rightPadLen = options.nontextlength - options.leftPadLen + options.effWidth = options.width - ((options.capLen * gapSpaces) + gapSpaces) + if options.capLen > options.leftPadLen then + options.cap = options.cap:sub(1, options.leftPadLen) + options.capLen = string.len(options.cap) + end + options.fixed = true + return options +end + +-- internal function, processes a single line of the wrapped string. +function ftext.fLine(str, opts) + local options = ftext.fixFormatOptions(str, opts) + local truncate, strLen, width = options.truncate, options.strLen, options.width + if truncate and strLen > width then + local wrapped = ftext.xwrap(str, options.effWidth, options.formatType) + local lines = wrapped:split("\n") + str = lines[1] + end + local leftCap = options.leftCap + local rightCap = options.rightCap + local leftPadLen = options.leftPadLen + local rightPadLen = options.rightPadLen + local capLen = options.capLen + + if options.alignment == "center" then -- we're going to center something + if options.mirror then -- if we're reversing the left cap and the right cap (IE {{[[ turns into ]]}} ) + rightCap = string.gsub(rightCap, "<", ">") + rightCap = string.gsub(rightCap, "%[", "%]") + rightCap = string.gsub(rightCap, "{", "}") + rightCap = string.gsub(rightCap, "%(", "%)") + rightCap = string.reverse(rightCap) + end -- otherwise, they'll be the same, so don't do anything + if not options.nogap then + str = string.format(" %s ", str) + end + + elseif options.alignment == "right" then -- we'll right-align the text + leftPadLen = leftPadLen + rightPadLen + rightPadLen = 0 + rightCap = "" + if not options.nogap then + str = string.format(" %s", str) + end + + else -- Ok, so if it's not center or right, we assume it's left. We don't do justified. Sorry. + rightPadLen = rightPadLen + leftPadLen + leftPadLen = 0 + leftCap = "" + if not options.nogap then + str = string.format("%s ", str) + end + end -- that's it, took care of both left, right, and center formattings, now to output the durn thing. + local fullLeftCap = string.format("%s%s%s", options.capColor, leftCap, options.colorReset) + local fullLeftSpacer = string.format("%s%s%s", options.spacerColor, string.rep(options.spacer, (leftPadLen - capLen)), options.colorReset) + local fullText = string.format("%s%s%s", options.textColor, str, options.colorReset) + local fullRightSpacer = string.format("%s%s%s", options.spacerColor, string.rep(options.spacer, (rightPadLen - capLen)), options.colorReset) + local fullRightCap = string.format("%s%s%s", options.capColor, rightCap, options.colorReset) + + if options.inside then + -- "endcap===== some text =====endcap" + -- "endcap===== some text =====pacdne" + -- "endcap================= some text" + -- "some text =================endcap" + local finalString = string.format("%s%s%s%s%s", fullLeftCap, fullLeftSpacer, fullText, fullRightSpacer, fullRightCap) + return finalString + else + -- "=====endcap some text endcap=====" + -- "=====endcap some text pacdne=====" + -- "=================endcap some text" + -- "some text endcap=================" + + local finalString = string.format("%s%s%s%s%s", fullLeftSpacer, fullLeftCap, fullText, fullRightCap, fullRightSpacer) + return finalString + end +end + +-- Functions below here are honestly for backwards compatibility and subject to removal soon. +-- They just force some options table overrides for the most part. + +-- no colors, no wrap +function ftext.align(str, opts) + local options = {} + if opts == nil then + opts = {} + end + if type(opts) == "table" then + options = table.deepcopy(opts) + options.formatType = "" + options.wrap = false + else + error("Improper argument: options expected to be passed as table") + end + options = ftext.fixFormatOptions(str, options) + return ftext.fLine(str, options) +end + +-- decho formatting, no wrap +function ftext.dalign(str, opts) + local options = {} + if opts == nil then + opts = {} + end + if type(opts) == "table" then + options = table.deepcopy(opts) + options.formatType = "d" + options.wrap = false + else + error("Improper argument: options expected to be passed as table") + end + options = ftext.fixFormatOptions(str, options) + return ftext.fLine(str, options) +end + +-- cecho formatting, no wrap +function ftext.calign(str, opts) + local options = {} + if opts == nil then + opts = {} + end + if type(opts) == "table" then + options = table.deepcopy(opts) + options.formatType = "c" + options.wrap = false + else + error("Improper argument: options expected to be passed as table") + end + options = ftext.fixFormatOptions(str, options) + return ftext.fLine(str, options) +end + +-- hecho formatting, no wrap +function ftext.halign(str, opts) + local options = {} + if opts == nil then + opts = {} + end + if type(opts) == "table" then + options = table.deepcopy(opts) + options.formatType = "h" + options.wrap = false + else + error("Improper argument: options expected to be passed as table") + end + options = ftext.fixFormatOptions(str, options) + return ftext.fLine(str, options) +end + +-- literally just fText but forces cecho formatting +function ftext.cfText(str, opts) + local options = {} + if opts == nil then + opts = {} + end + if type(opts) == "table" then + options = table.deepcopy(opts) + options.formatType = "c" + else + error("Improper argument: options expected to be passed as table") + end + options = ftext.fixFormatOptions(str, options) + return ftext.fText(str, options) +end + +-- fText but forces decho formatting +function ftext.dfText(str, opts) + local options = {} + if opts == nil then + opts = {} + end + if type(opts) == "table" then + options = table.deepcopy(opts) + options.formatType = "d" + else + error("Improper argument: options expected to be passed as table") + end + options = ftext.fixFormatOptions(str, options) + return ftext.fText(str, options) +end + +-- fText but forces hecho formatting +function ftext.hfText(str, opts) + local options = {} + if opts == nil then + opts = {} + end + if type(opts) == "table" then + options = table.deepcopy(opts) + options.formatType = "h" + else + error("Improper argument: options expected to be passed as table") + end + options = ftext.fixFormatOptions(str, options) + return ftext.fText(str, options) +end + +--- Stand alone text formatter object. Remembers the options you set and can be adjusted as needed +-- @type ftext.TextFormatter +-- @author Damian Monogue +-- @copyright 2020 Damian Monogue +-- @license MIT, see LICENSE.lua + +local TextFormatter = {} +TextFormatter.validFormatTypes = {'d', 'dec', 'decimal', 'h', 'hex', 'hexidecimal', 'c', 'color', 'colour', 'col', 'name', 'none', 'e', 'plain', ''} + +--- Set's the formatting type whether it's for cecho, decho, or hecho +-- @tparam string typeToSet What type of formatter is this? Valid options are { 'd', 'dec', 'decimal', 'h', 'hex', 'hexidecimal', 'c', 'color', 'colour', 'col', 'name'} +function TextFormatter:setType(typeToSet) + local isNotValid = not table.contains(self.validFormatTypes, typeToSet) + if isNotValid then + error("TextFormatter:setType: Invalid argument, valid types are:" .. table.concat(self.validFormatTypes, ", ")) + end + self.options.formatType = typeToSet +end + +function TextFormatter:toBoolean(thing) + if type(thing) ~= "boolean" then + if thing == "true" then + thing = true + elseif thing == "false" then + thing = false + else + return nil + end + end + return thing +end + +function TextFormatter:checkString(str) + if type(str) ~= "string" then + if tostring(str) then + str = tostring(str) + else + return nil + end + end + return str +end + +--- Sets whether or not we should do word wrapping. +-- @tparam boolean shouldWrap should we do wordwrapping? +function TextFormatter:setWrap(shouldWrap) + local argumentType = type(shouldWrap) + shouldWrap = self:toBoolean(shouldWrap) + if shouldWrap == nil then + error("TextFormatter:setWrap(shouldWrap) Argument error, boolean expected, got " .. argumentType .. + ", if you want to set the number of characters wide to format for, use setWidth()") + end + self.options.wrap = shouldWrap +end + +--- Sets the width we should format for +-- @tparam number width the width we should format for +function TextFormatter:setWidth(width) + if type(width) ~= "number" then + if tonumber(width) then + width = tonumber(width) + else + error("TextFormatter:setWidth(width): Argument error, number expected, got " .. type(width)) + end + end + self.options.width = width +end + +--- Sets the cap for the formatter +-- @tparam string cap the string to use for capping the formatted string. +function TextFormatter:setCap(cap) + local argumentType = type(cap) + local cap = self:checkString(cap) + if cap == nil then + error("TextFormatter:setCap(cap): Argument error, string expect, got " .. argumentType) + end + self.options.cap = cap +end + +--- Sets the color for the format cap +-- @tparam string capColor Color which can be formatted via Geyser.Color.parse() +function TextFormatter:setCapColor(capColor) + local argumentType = type(capColor) + local capColor = self:checkString(capColor) + if capColor == nil then + error("TextFormatter:setCapColor(capColor): Argument error, string expected, got " .. argumentType) + end + self.options.capColor = capColor +end + +--- Sets the color for spacing character +-- @tparam string spacerColor Color which can be formatted via Geyser.Color.parse() +function TextFormatter:setSpacerColor(spacerColor) + local argumentType = type(spacerColor) + local spacerColor = self:checkString(spacerColor) + if spacerColor == nil then + error("TextFormatter:setSpacerColor(spacerColor): Argument error, string expected, got " .. argumentType) + end + self.options.spacerColor = spacerColor +end + +--- Sets the color for formatted text +-- @tparam string textColor Color which can be formatted via Geyser.Color.parse() +function TextFormatter:setTextColor(textColor) + local argumentType = type(textColor) + local textColor = self:checkString(textColor) + if textColor == nil then + error("TextFormatter:setTextColor(textColor): Argument error, string expected, got " .. argumentType) + end + self.options.textColor = textColor +end + +--- Sets the spacing character to use. Should be a single character +-- @tparam string spacer the character to use for spacing +function TextFormatter:setSpacer(spacer) + local argumentType = type(spacer) + local spacer = self:checkString(spacer) + if spacer == nil then + error("TextFormatter:setSpacer(spacer): Argument error, string expect, got " .. argumentType) + end + self.options.spacer = spacer +end + +--- Set the alignment to format for +-- @tparam string alignment How to align the formatted string. Valid options are 'left', 'right', or 'center' +function TextFormatter:setAlignment(alignment) + local validAlignments = {"left", "right", "center"} + if not table.contains(validAlignments, alignment) then + error("TextFormatter:setAlignment(alignment): Argument error: Only valid arguments for setAlignment are 'left', 'right', or 'center'. You sent" .. + alignment) + end + self.options.alignment = alignment +end + +--- Set whether the the spacer should go inside the the cap or outside of it +-- @tparam boolean spacerInside +function TextFormatter:setInside(spacerInside) + local argumentType = type(spacerInside) + spacerInside = self:toBoolean(spacerInside) + if spacerInside == nil then + error("TextFormatter:setInside(spacerInside) Argument error, boolean expected, got " .. argumentType) + end + self.options.inside = spacerInside +end + +--- Set whether we should mirror/reverse the caps. IE << becomes >> if set to true +-- @tparam boolean shouldMirror +function TextFormatter:setMirror(shouldMirror) + local argumentType = type(shouldMirror) + shouldMirror = self:toBoolean(shouldMirror) + if shouldMirror == nil then + error("TextFormatter:setMirror(shouldMirror): Argument error, boolean expected, got " .. argumentType) + end + self.options.mirror = shouldMirror +end + +--- Set whether we should remove the gap spaces between the text and spacer characters. "===some text===" if set to true, "== some text ==" if set to false +-- @tparam boolean noGap +function TextFormatter:setNoGap(noGap) + local argumentType = type(noGap) + noGap = self:toBoolean(noGap) + if noGap == nil then + error("TextFormatter:setNoGap(noGap): Argument error, boolean expected, got " .. argumentType) + end + self.options.noGap = noGap +end + +--- Enables truncation (cutting to length). You still need to ensure wrap is disabled, as it supercedes. +function TextFormatter:enableTruncate() + self.options.truncate = true +end + +--- Disables truncation (cutting to length). You still need to ensure wrap is enabled if you want it to wrap. +function TextFormatter:disableTruncate() + self.options.truncate = false +end + +--- Format a string based on the stored options +-- @tparam string str The string to format +function TextFormatter:format(str) + return ftext.fText(str, self.options) +end + +--- Creates and returns a new TextFormatter. +-- @tparam table options the options for the text formatter to use when running format() +--

Table of options +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +--
option namedescriptiondefault
wrapShould it wordwrap to multiple lines?true
formatTypeDetermines how it formats for color. 'c' for cecho, 'd' for decho, 'h' for hecho, and anything else for no colors"c"
widthHow wide should we format the text?80
capwhat characters to use for the endcap.""
capColorwhat color to make the endcap?the correct 'white' for your formatType
spacerWhat character to use for empty space. Must be a single character" "
spacerColorwhat color should the spacer be?the correct 'white' for your formatType
textColorwhat color should the text itself be?the correct 'white' for your formatType
alignmentHow should the text be aligned within the width. "center", "left", or "right""center"
nogapShould we put a literal space between the spacer character and the text?false
insidePut the spacers inside the caps?false
mirrorShould the endcap be reversed on the right? IE [[ becomes ]]true
truncateCut the string to width. Is superceded by wrap being true.false
+-- @usage +-- local TextFormatter = require("MDK.ftext").TextFormatter +-- myFormatter = TextFormatter:new( { +-- width = 40, +-- cap = "[CAP]", +-- capColor = "", +-- textColor = "" +-- }) +-- myMessage = "This is a test of the emergency broadcasting system. This is only a test" +-- cecho(myFormatter:format(myMessage)) + +function TextFormatter:new(options) + if options == nil then + options = {} + end + if options and type(options) ~= "table" then + error("TextFormatter:new(options): Argument error, table expected, got " .. type(options)) + end + local me = {} + me.options = {formatType = "c", wrap = true, width = 80, cap = "", spacer = " ", alignment = "center", inside = true, mirror = false} + for option, value in pairs(options) do + me.options[option] = value + end + setmetatable(me, self) + self.__index = self + return me +end +ftext.TextFormatter = TextFormatter + +--- Easy formatting for text tables +-- @type ftext.TableMaker +-- @author Damian Monogue +-- @copyright 2020 Damian Monogue +-- @license MIT, see LICENSE.lua + +local TableMaker = { + headCharacter = "*", + footCharacter = "*", + edgeCharacter = "*", + rowSeparator = "-", + separator = "|", + separateRows = true, + colorReset = "", + formatType = "c", + printHeaders = true, + autoEcho = false, + title = "", + printTitle = false, + headerTitle = false, + forceHeaderSeparator = false, + autoEchoConsole = "main", +} + +function TableMaker:checkPosition(position, func) + if position == nil then + position = 0 + end + if type(position) ~= "number" then + if tonumber(position) then + position = tonumber(position) + else + error(func .. ": Argument error: position expected as number, got " .. type(position)) + end + end + return position +end + +function TableMaker:insert(tbl, pos, item) + if pos ~= 0 then + table.insert(tbl, pos, item) + else + table.insert(tbl, item) + end +end + +--- Get the TextFormatter which defines the format of a specific column +-- @tparam number position The position of the column you're getting, counting from the left. If not provided will return the last column. +function TableMaker:getColumn(position) + position = position or #self.columns + position = self:checkPosition(position, "TableMaker:getColumn(position)") + return self.columns[position] +end + +--- Adds a column definition for the table. +-- @tparam table options Table of options suitable for a TextFormatter object. See https://github.com/demonnic/fText/wiki/fText +-- @tparam number position The position of the column you're adding, counting from the left. If not provided will add it as the last column +function TableMaker:addColumn(options, position) + if options == nil then + options = {} + end + if not type(options) == "table" then + error("TableMaker:addColumn(options, position): Argument error: options expected as table, got " .. type(options)) + end + local options = table.deepcopy(options) + position = self:checkPosition(position, "TableMaker:addColumn(options, position)") + options.width = options.width or 20 + options.name = options.name or "" + local formatter = TextFormatter:new(options) + self:insert(self.columns, position, formatter) +end + +--- Deletes a column at the given position +-- @tparam number position the column you wish to delete +function TableMaker:deleteColumn(position) + if position == nil then + error("TableMaker:deleteColumn(position): Argument Error: position as number expected, got nil") + end + position = self:checkPosition(position) + local maxColumn = #self.columns + if position > maxColumn then + error( + "TableMaker:deleteColumn(position): Argument Error: position provided was larger than number of columns in the table. Number of columns: " .. + #self.columns) + end + table.remove(self.columns, position) +end + +--- Replaces a column at a specific position with the newly provided formatting +-- @tparam table options table of options suitable for a TextFormatter object. See https://github.com/demonnic/fText/wiki/fText +-- @tparam number position which column you are replacing, counting from the left. +function TableMaker:replaceColumn(options, position) + if position == nil then + error("TableMaker:replaceColumn(options, position): Argument error: position as number expected, got nil") + end + position = self:checkPosition(position) + if type(options) ~= "table" then + error("TableMaker:replaceColumn(options, position): Argument error: options as table expected, got " .. type(options)) + end + if #self.columns < position then + error( + "TableMaker:replaceColumn(options, position): you cannot specify a position higher than the number of columns currently in the TableMaker. You sent:" .. + position .. " and there are: " .. #self.columns .. "columns in the TableMaker") + end + options.width = options.width or 20 + options.name = options.name or "" + local formatter = TextFormatter:new(options) + self.columns[position] = formatter +end + +--- Gets the row of output at a specific position +-- @tparam number position The position of the row you want to get, coutning from the top down. If not provided defaults to the last row in the table +-- @return table of entries in the specified row +function TableMaker:getRow(position) + position = position or #self.rows + position = self:checkPosition(position, "TableMaker:getRow(position)") + return self.rows[position] +end + +--- Adds a row of output to the table +-- @tparam table columnEntries This indexed table contains an entry for each column in the table. Entries in the table must be strings, a table of options for insertPopup or insertLink, or a function which returns one of these things +-- @tparam number position position for the row you want to add, counting from the top down. If not provided defaults to the last line in the table. +function TableMaker:addRow(columnEntries, position) + local columnEntriesType = type(columnEntries) + if columnEntriesType ~= "table" then + error("TableMaker:addRow(columnEntries, position): Argument error, columnEntries expected as table, got " .. columnEntriesType) + end + for _, entry in ipairs(columnEntries) do + local entryCheck = self:checkEntry(entry) + if entryCheck == 0 then + if type(entry) == "function" then + error( + "TableMaker:addRow(columnEntries, position): Argument Error, you provided a function for a columnEntry but it does not return a string. We need a string. It was entry number " .. + _ .. "in columnEntries") + else + error("TableMaker:addRow(columnEntries, position): Argument error, columnEntries items expected as string, got:" .. type(entry)) + end + end + end + position = self:checkPosition(position, "TableMaker:addRow(columnEntries, position)") + self:insert(self.rows, position, columnEntries) +end + +--- Deletes the row at the given position +-- @tparam number position the row to delete +function TableMaker:deleteRow(position) + if position == nil then + error("TableMaker:deleteRow(position): Argument Error: position as number expected, got nil") + end + position = self:checkPosition(position, "TableMaker:deleteRow(position)") + local maxRow = #self.rows + if position > maxRow then + error("TableMaker:deleteRow(position): Argument Error: position given was > the number of rows we have # of rows is:" .. maxRow) + end + table.remove(self.rows, position) +end + +--- Replaces a row of output in the table +-- @tparam table columnEntries This indexed table contains an entry for each column in the table. Entries in the table must be strings, a table of options for insertPopup or insertLink, or a function which returns one of these things +-- @tparam number position position for the row you want to add, counting from the top down. +function TableMaker:replaceRow(columnEntries, position) + if position == nil then + error("TableMaker:replaceRow(columnEntries, position): ArgumentError: position expected as number, received nil") + end + position = self:checkPosition(position, "TableMaker:replaceRow(columnEntries, position)") + if #self.rows < position then + error( + "TableMaker:replaceRow(columnEntries, position): position cannot be greater than the number of rows already in the tablemaker. You provided: " .. + position .. " and there are " .. #self.rows .. "rows in the TableMaker") + end + for _, entry in ipairs(columnEntries) do + local entryCheck = self:checkEntry(entry) + if entryCheck == 0 then + if type(entry) == "function" then + error( + "TableMaker:replaceRow(columnEntries, position): Argument Error: you provided a function for a columnEntry but it does not return a string. We need a string. It was entry number " .. + _ .. "in columnEntries") + else + error("TableMaker:replaceRow(columnEntries, position): Argument error: columnEntries items expected as string, got:" .. type(entry)) + end + end + end + self.rows[position] = columnEntries +end + +function TableMaker:checkEntry(entry) + local allowedTypes = {"string"} + if self.allowPopups then + table.insert(allowedTypes, "table") + end + local entryType = type(entry) + if entryType == "function" then + entryType = type(entry()) + end + if table.contains(allowedTypes, entryType) then + return entry + else + return 0 + end +end + +function TableMaker:checkNumber(num) + if num == nil then + num = 0 + end + if not tonumber(num) then + num = 0 + end + return tonumber(num) +end + +--- Get the contents and formatter for a specific cell +-- @tparam number row the row number of the cell, counted top down. +-- @tparam number column the column number of the cell, counted from the left. +-- @return the base text and TextFormatter for the cell at the specific row and column number +function TableMaker:getCell(row, column) + local rowType = type(row) + local columnType = type(column) + local maxRow = #self.rows + local maxColumn = #self.columns + local ae = "TableMaker:getCell(row, column): Argument error:" + row = self:checkNumber(row) + column = self:checkNumber(column) + if row == 0 then + if rowType ~= "number" then + printError(f"{ae} row as number expected, got {rowType}", true, true) + else + printError(f"{ae} rows start at 1, and you asked for row 0", true, true) + end + elseif column == 0 then + if columnType ~= "number" then + printError(f"{ae} column as number expected, got {columnType}", true, true) + else + printError(f"{ae} columns start at 1, and you asked for column 0", true, true) + end + elseif row > maxRow then + printError(f"{ae} row exceeds number of rows in table ({maxRow})") + elseif column > maxColumn then + printError(f"{ae} column exceeds number of columns in table ({maxColumn})", true, true) + end + return self.rows[row][column], self.columns[column] +end + +--- Sets a specific cell's display information +-- @tparam number row the row number of the cell, counted from the top down +-- @tparam number column the column number of the cell, counted from the left +-- @param entry What to set the entry to. Must be a string, or a table of options for insertLink/insertPopup if allowPopups is set. Or a function which returns one of these things +function TableMaker:setCell(row, column, entry) + local maxRow = #self.rows + local maxColumn = #self.columns + local ae = "TableMaker:setCell(row, column, entry): Argument Error:" + row = self:checkNumber(row) + if row == 0 then + error(ae .. " row must be a number, you provided " .. type(row)) + end + column = self:checkNumber(column) + if column == 0 then + error(ae .. " column must be a number, you provided " .. type(column)) + end + if row > maxRow then + error(ae .. " row is higher than the number of rows in the table. Highest row:" .. maxRow) + end + if column > maxColumn then + error(ae .. " column is higher than the number of columns in the table. Highest column:" .. maxColumn) + end + local entryType = type(entry) + entry = self:checkEntry(entry) + if entry == 0 then + if entryType == "function" then + error(ae .. " entry was provided as a function, but does not return a string. We need a string in the end") + else + error("TableMaker:setCell(row, column, entry): Argument Error: entry must be a string, or a function which returns a string. You provided a " .. entryType) + end + end + self.rows[row][column] = entry +end + +function TableMaker:totalWidth() + local width = 0 + local numberOfColumns = #self.columns + local separatorWidth = string.len(self.separator) + local edgeWidth = string.len(self.edgeCharacter) * 2 + for _, column in ipairs(self.columns) do + width = width + column.options.width + end + separatorWidth = separatorWidth * (numberOfColumns - 1) + width = width + edgeWidth + separatorWidth + return width +end + +function TableMaker:getType() + local dec = {"d", "decimal", "dec"} + local hex = {"h", "hexidecimal", "hex"} + local col = {"c", "color", "colour", "col", "name"} + if table.contains(dec, self.formatType) then + return 'd' + elseif table.contains(hex, self.formatType) then + return 'h' + elseif table.contains(col, self.formatType) then + return 'c' + else + return '' + end +end + +function TableMaker:echo(message, echoType, ...) + local fType = self:getType() + local consoleType = type(self.autoEchoConsole) + local console = "" + if echoType == nil then + echoType = "" + end + if consoleType == "string" then + console = self.autoEchoConsole + elseif consoleType == "nil" then + console = "main" + else + console = self.autoEchoConsole.name + end + local functionName = string.format("%secho%s", fType, echoType) + local func = _G[functionName] + if echoType == "" then + func(console, message) + else + func(console, message, ...) + end +end + +function TableMaker:scanRow(rowToScan) + local row = table.deepcopy(rowToScan) + local rowEntries = #row + local numberOfColumns = #self.columns + local columns = {} + local linesInRow = 0 + local rowText = "" + local ec = self.frameColor .. self.edgeCharacter .. self.colorReset + local sep = self.separatorColor .. self.separator .. self.colorReset + + if rowEntries < numberOfColumns then + local entriesNeeded = numberOfColumns - rowEntries + for i = 1, entriesNeeded do + table.insert(row, "") + end + end + for index, formatter in ipairs(self.columns) do + local str = row[index] + local column = "" + if type(str) == "function" then + str = str() + end + column = formatter:format(str) + table.insert(columns, column:split("\n")) + end + for _, rowLines in ipairs(columns) do + if linesInRow < #rowLines then + linesInRow = #rowLines + end + end + for index, rowLines in ipairs(columns) do + if #rowLines < linesInRow then + local neededLines = linesInRow - #rowLines + for i = 1, neededLines do + table.insert(rowLines, self.columns[index]:format("")) + end + end + end + for i = 1, linesInRow do + local thisLine = ec + for index, column in ipairs(columns) do + if index == 1 then + thisLine = string.format("%s%s", thisLine, column[i]) + else + thisLine = string.format("%s%s%s", thisLine, sep, column[i]) + end + end + thisLine = string.format("%s%s", thisLine, ec) + if rowText == "" then + rowText = thisLine + else + rowText = string.format("%s\n%s", rowText, thisLine) + end + end + return rowText +end + +function TableMaker:echoRow(rowToScan) + local row = table.deepcopy(rowToScan) + local rowEntries = #row + local numberOfColumns = #self.columns + local columns = {} + local linesInRow = 0 + local ec = self.frameColor .. self.edgeCharacter .. self.colorReset + local sep = self.separatorColor .. self.separator .. self.colorReset + if rowEntries < numberOfColumns then + local entriesNeeded = numberOfColumns - rowEntries + for i = 1, entriesNeeded do + table.insert(row, "") + end + end + for index, formatter in ipairs(self.columns) do + local str = row[index] + local column = "" + if type(str) == "function" then + str = str() + end + if type(str) == "table" then + str = str[1] + end + column = formatter:format(str) + table.insert(columns, column:split("\n")) + end + for _, rowLines in ipairs(columns) do + if linesInRow < #rowLines then + linesInRow = #rowLines + end + end + for index, rowLines in ipairs(columns) do + if #rowLines < linesInRow then + local neededLines = linesInRow - #rowLines + for i = 1, neededLines do + table.insert(rowLines, self.columns[index]:format("")) + end + end + end + for i = 1, linesInRow do + self:echo(ec) + for index, column in ipairs(columns) do + local message = column[i] + if index ~= 1 then + self:echo(sep) + end + if type(row[index]) == "string" then + self:echo(message) + elseif type(row[index]) == "table" then + local rowEntry = row[index] + local echoType = "" + if type(rowEntry[2]) == "string" then + echoType = "Link" + elseif type(rowEntry[2]) == "table" then + echoType = "Popup" + end + self:echo(message, echoType, rowEntry[2], rowEntry[3], rowEntry[4] or true) + end + end + self:echo(ec) + self:echo("\n") + end +end + +function TableMaker:makeHeader() + local totalWidth = self:totalWidth() + local ec = self.frameColor .. self.edgeCharacter .. self.colorReset + local sep = self.separatorColor .. self.separator .. self.colorReset + local header = self.frameColor .. string.rep(self.headCharacter, totalWidth) .. self.colorReset + local columnHeaders = "" + if self.printHeaders then + local columnEntries = {} + for _, v in ipairs(self.columns) do + table.insert(columnEntries, v:format(v.options.name)) + end + local divWithNewlines = self.headerTitle and header or self:createRowDivider() + divWithNewlines = "\n" .. divWithNewlines + columnHeaders = string.format("\n%s%s%s%s", ec, table.concat(columnEntries, sep), ec, (self.separateRows or self.forceHeaderSeparator) and divWithNewlines or '') + end + local title = self:makeTitle(totalWidth, header) + header = string.format("%s%s%s", header, title, columnHeaders) + return header +end + +function TableMaker:makeTitle(totalWidth, header) + if not self.printTitle then + return "" + end + local title = ftext.fText(self.title, {width = totalWidth, alignment = "center", cap = self.headCharacter, capColor = self.frameColor, inside = true, textColor = self.titleColor, formatType = self.formatType}) + title = string.format("\n%s\n%s", title, header) + return title +end + +function TableMaker:createRowDivider() + local columnPieces = {} + for _, v in ipairs(self.columns) do + local piece = string.format("%s%s%s", self.separatorColor, string.rep(self.rowSeparator, v.options.width), self.colorReset) + table.insert(columnPieces, piece) + end + local ec = self.frameColor .. self.edgeCharacter .. self.colorReset + local sep = self.separatorColor .. self.separator .. self.colorReset + return string.format("%s%s%s", ec, table.concat(columnPieces, sep), ec) +end + +--- set the title of the table +-- @tparam string title The title of the table. +function TableMaker:setTitle(title) + self.title = title + if self.autoEcho then self:assemble() end +end + +--- set the rowSeparator for the table +-- @tparam string char The row separator to use +function TableMaker:setRowSeparator(char) + self.rowSeparator = char + if self.autoEcho then self:assemble() end +end + +--- set the edgeCharacter for the table +-- @tparam string char The edge character to use +function TableMaker:setEdgeCharacter(char) + self.edgeCharacter = char + if self.autoEcho then self:assemble() end +end + +--- set the foot character for the table +-- @tparam string char The foot character to use +function TableMaker:setFootCharacter(char) + self.footCharacter = char + if self.autoEcho then self:assemble() end +end + +--- set the head character for the table +-- @tparam string char The head character to use +function TableMaker:setHeadCharacter(char) + self.headCharacter = char + if self.autoEcho then self:assemble() end +end + +--- set the column separator character for the table +-- @tparam string char The separator character to use +function TableMaker:setSeparator(char) + self.separator = char + if self.autoEcho then self:assemble() end +end + +--- set the title color for the table +-- @tparam string color The title color to use. Should match the color type of the tablemaker (cecho by default) +function TableMaker:setTitleColor(color) + self.titleColor = color + if self.autoEcho then self:assemble() end +end + +--- set the title color for the table +-- @tparam string color The separator color to use. Should match the color type of the tablemaker (cecho by default) +function TableMaker:setSeparatorColor(color) + self.separatorColor = color + if self.autoEcho then self:assemble() end +end + +--- set the title color for the table +-- @tparam string color The frame color to use. Should match the color type of the tablemaker (cecho by default) +function TableMaker:setFrameColor(color) + self.frameColor = color + if self.autoEcho then self:assemble() end +end + +--- Force a separator between the header and first row, even if the row separator is disabled for the overall table +function TableMaker:enableForceHeaderSeparator() + self.forceHeaderSeparator = true + if self.autoEcho then self:assemble() end +end + +--- Do not force a separator between the header and first row, even if the row separator is disabled for the overall table +function TableMaker:disableForceHeaderSeparator() + self.forceHeaderSeparator = false + if self.autoEcho then self:assemble() end +end + +--- Enable using the title separator for the column headers as well +function TableMaker:enableHeaderTitle() + self.headerTitle = true + if self.autoEcho then self:assemble() end +end + +--- Disable using the title separator for the column headers as well +function TableMaker:disableHeaderTitle() + self.headerTitle = false + if self.autoEcho then self:assemble() end +end + +--- enable printing the title of the table +function TableMaker:enablePrintTitle() + self.printTitle = true + if self.autoEcho then self:assemble() end +end + +--- disable printing the title of the table +function TableMaker:disablePrintTitle() + self.printTitle = false + if self.autoEcho then self:assemble() end +end + +--- enable printing of the column headers +function TableMaker:enablePrintHeaders() + self.printHeaders = true + if self.autoEcho then self:assemble() end +end + +--- disable printing of the column headers +function TableMaker:disablePrintHeaders() + self.printHeaders = false + if self.autoEcho then self:assemble() end +end + +--- enable printing the separator line between rows +function TableMaker:enableRowSeparator() + self.separateRows = true + if self.autoEcho then self:assemble() end +end + +--- enable printing the separator line between rows +function TableMaker:disableRowSeparator() + self.separateRows = false + if self.autoEcho then self:assemble() end +end + +--- enables making cells which incorporate insertLink/insertPopup +function TableMaker:enablePopups() + self.autoEcho = true + self.allowPopups = true + if self.autoEcho then self:assemble() end +end + +--- enables autoEcho so that when assemble is called it echos automatically +function TableMaker:enableAutoEcho() + self.autoEcho = true + self:assemble() +end + +--- disables autoecho. Cannot be used if allowPopups is set +function TableMaker:disableAutoEcho() + if self.allowPopups then + error("TableMaker:disableAutoEcho(): you cannot disable autoEcho once you have enabled popups.") + else + self.autoEcho = false + end +end + +--- Enables automatically clearing the miniconsole we echo to +function TableMaker:enableAutoClear() + self.autoClear = true + if self.autoEcho then self:assemble() end +end + +--- Disables automatically clearing the miniconsole we echo to +function TableMaker:disableAutoClear() + self.autoClear = false +end + +--- Set the miniconsole to echo to +-- @param console The miniconsole to autoecho to. Set to "main" or do not pass the parameter to autoecho to the main console. Can be a string name of the console, or a Geyser MiniConsole +function TableMaker:setAutoEchoConsole(console) + local funcName = "TableMaker:setAutoEchoConsole(console)" + if console == nil then + console = "main" + end + local consoleType = type(console) + if consoleType ~= "string" and consoleType ~= "table" then + error(funcName .. " ArgumentError: console as string or a Geyser MiniConsole or UserWindow expected, got " .. consoleType) + elseif consoleType == "table" and not (console.type == "miniConsole" or console.type == "userwindow") then + error(funcName .. " ArgumentError: console received was a table and may be a Geyser object, but console.type is not miniConsole, it is " .. + console.type) + end + self.autoEchoConsole = console + if self.autoEcho then self:assemble() end +end + +--- Assemble the table. If autoEcho is enabled/set to true, will automatically echo. Otherwise, returns the formatted string to echo the table +function TableMaker:assemble() + if self.allowPopups and self.autoEcho then + self:popupAssemble() + else + return self:textAssemble() + end +end + +function TableMaker:popupAssemble() + if self.autoClear then + local console = self.autoEchoConsole + if console and console ~= "main" then + if type(console) == "table" then + console = console.name + end + clearWindow(console) + end + end + local divWithNewLines = string.format("%s\n", self:createRowDivider()) + local header = self:makeHeader() .. "\n" + local footer = string.format("%s%s%s\n", self.frameColor, string.rep(self.footCharacter, self:totalWidth()), self.colorReset) + self:echo(header) + for _, row in ipairs(self.rows) do + if _ ~= 1 and self.separateRows then + self:echo(divWithNewLines) + end + self:echoRow(row) + end + self:echo(footer) +end + +function TableMaker:textAssemble() + local sheet = "" + local rows = {} + for _, row in ipairs(self.rows) do + table.insert(rows, self:scanRow(row)) + end + local divWithNewlines = string.format("\n%s\n", self:createRowDivider()) + local footer = string.format("%s%s%s", self.frameColor, string.rep(self.footCharacter, self:totalWidth()), self.colorReset) + sheet = string.format("%s\n%s\n%s\n", self:makeHeader(), table.concat(rows, self.separateRows and divWithNewlines or "\n"), footer) + if self.autoEcho then + local console = self.autoEchoConsole or "main" + if type(console) == "table" then + console = console.name + end + if self.autoClear and console ~= "main" then + clearWindow(console) + end + self:echo(sheet) + end + return sheet +end + +--- Creates and returns a new TableMaker. +-- see https://github.com/demonnic/MDK/wiki/fText%3A-TableMaker%3A-Examples for usage +-- @tparam table options table of options for the TableMaker object +--

Table of new options +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +-- +--
option namedescriptiondefault
formatTypeDetermines how it formats for color. 'c' for cecho, 'd' for decho, 'h' for hecho, and anything else for no colorsc
printHeadersprint top row as headertrue
headCharacterThe character used to construct the very top of the table. A solid line of these characters is used"*"
footCharacterThe character used to construct the very bottom of the table. A solid line of these characters is used"*"
edgeCharacterthe character used to form the left and right edges of the table. There is one on either side of every line"*"
frameColorThe color to use for the frame. The frame is the border around the outside edge of the table (headCharacter, footCharacter, and edgeCharacters will all be this color).the correct 'white' for your formatType
rowSeparatorthe character used to form the lines between rows of text"-"
separatorCharacter used between columns."|"
separatorColorthe color used for the separators, the things which divide the rows and columns internally. (separator and rowSeparator will be this color)frameColor
autoEchoecho the table automatically in addition to returning the string representation?false
autoEchoConsoleMiniConsole to autoEcho to"main"
autoClearIf autoEchoing, and not echoing to the main console, should we clear the console before outputting?false
allowPopupssetting this to true allows you to make cells in the table clickable, as well as give them right-click menus.
+-- Please see Clickable Tables HERE
false
separateRowsWhen false, will not print the separator line between rowstrue
titleThe overall title of the table. Displayed at the top""
titleColorWhat color to use for the title textformatColor
printTitleShould we print the title of the table at the very tip-top?false
headerTitleUse the same separator for the column headers as the title/top, rather than the row separatorformatColor
forceHeaderSeparatorForce a separator between the column headers and the first row, even if rowSeparator is false.false
+function TableMaker:new(options) + local funcName = "TableMaker:new(options)" + local me = {} + setmetatable(me, self) + self.__index = self + if options == nil then + options = {} + end + if type(options) ~= "table" then + error("TableMaker:new(options): ArgumentError: options expected as table, got " .. type(options)) + end + local options = table.deepcopy(options) + if options.allowPopups == true then + options.autoEcho = true + else + options.allowPopups = false + end + local columns = false + if options.columns then + if type(options.columns) ~= "table" then + error("TableMaker:new(options): option error: You provided an options.columns entry of type " .. type(options.columns) .. + " and columns must a table with entries suitable for TableFormatter:addColumn().") + end + columns = table.deepcopy(options.columns) + options.columns = nil + end + local rows = false + if options.rows then + if type(options.rows) ~= "table" then + error("TableMaker:new(options): option error: You provided an options.rows entry of type " .. type(options.rows) .. + " and rows must be a table with entrys suitable for TableFormatter:addRow()") + end + rows = table.deepcopy(options.rows) + options.rows = nil + end + for option, value in pairs(options) do + me[option] = value + end + local dec = {"d", "decimal", "dec"} + local hex = {"h", "hexidecimal", "hex"} + local col = {"c", "color", "colour", "col", "name"} + if table.contains(dec, me.formatType) then + me.frameColor = me.frameColor or "<255,255,255>" + me.separatorColor = me.separatorColor or me.frameColor + me.titleColor = me.titleColor or me.frameColor + me.colorReset = "" + elseif table.contains(hex, me.formatType) then + me.frameColor = me.frameColor or "#ffffff" + me.separatorColor = me.separatorColor or me.frameColor + me.titleColor = me.titleColor or me.frameColor + me.colorReset = "#r" + elseif table.contains(col, me.formatType) then + me.frameColor = me.frameColor or "" + me.separatorColor = me.separatorColor or me.frameColor + me.titleColor = me.titleColor or me.frameColor + me.colorReset = "" + else + me.frameColor = "" + me.separatorColor = "" + me.titleColor = "" + me.colorReset = "" + end + me.columns = {} + me.rows = {} + if columns then + for _, column in ipairs(columns) do + me:addColumn(column) + end + end + if rows then + for _, row in ipairs(rows) do + me:addRow(row) + end + end + return me +end +ftext.TableMaker = TableMaker + +return ftext diff --git a/src/resources/ftext_spec.lua b/src/resources/ftext_spec.lua new file mode 100755 index 0000000..fdd3c15 --- /dev/null +++ b/src/resources/ftext_spec.lua @@ -0,0 +1,447 @@ +local ftext = require("MDK.ftext") + +describe("ftext:", function() + describe("ftext.fText:", function() + local fText = ftext.fText + + it("Should properly center text", function() + local expected = " some text " + local actual = fText("some text", {width = 20}) + assert.equals(expected, actual) + assert.equals(20, actual:len()) + end) + + it("Should properly pad left aligned text", function() + local expected = "some text " + local actual = fText("some text", {width = 20, alignment = "left"}) + assert.equals(expected, actual) + assert.equals(20, actual:len()) + end) + + it("Should properly pad right aligned text", function() + local expected = " some text" + local actual = fText("some text", {width = 20, alignment = "right"}) + assert.equals(expected, actual) + assert.equals(20, actual:len()) + end) + + it("Should wrap lines to the correct length", function() + local str = "This is a test of the emergency broadcast system. This is only a test" + local options = {width = 10, alignment = "centered"} + local actual = fText(str, options) + for _, line in ipairs(actual:split("\n")) do + assert.equals(line:len(), 10) + end + options.width = 15 + actual = fText(str, options) + for _, line in ipairs(actual:split("\n")) do + assert.equals(line:len(), 15) + end + end) + + describe("non-space spacer character:", function() + local str = "some text" + local options = {width = "20", alignment = "left", spacer = "="} + it("Should work with left align", function() + local expected = "some text ==========" + local actual = fText(str, options) + assert.equals(expected, actual) + assert.equals(20, actual:len()) + end) + + it("Should work with right align", function() + local expected = "========== some text" + options.alignment = "right" + local actual = fText(str, options) + assert.equals(expected, actual) + assert.equals(20, actual:len()) + end) + + it("Should work with center align", function() + local expected = ("==== some text =====") + options.alignment = "center" + local actual = fText(str, options) + assert.equals(expected, actual) + assert.equals(20, actual:len()) + end) + end) + + describe("nogap option:", function() + local str = "some text" + local options = {width = "20", alignment = "left", spacer = "=", nogap = true} + + it("Should work with left align", function() + local expected = "some text===========" + local actual = fText(str, options) + assert.equals(expected, actual) + assert.equals(20, actual:len()) + end) + + it("Should work with right align", function() + local expected = "===========some text" + options.alignment = "right" + local actual = fText(str, options) + assert.equals(expected, actual) + assert.equals(20, actual:len()) + end) + + it("Should work with center align", function() + local expected = "=====some text======" + options.alignment = "center" + local actual = fText(str, options) + assert.equals(expected, actual) + assert.equals(20, actual:len()) + end) + end) + + describe("cap functionality", function() + local str = "some text" + local options = {width = 20, spacer = "=", cap = "|"} + + it("Should place the spacer outside the cap by default", function() + local expected = "===| some text |====" + local actual = fText(str, options) + assert.equals(expected, actual) + assert.equals(20, actual:len()) + end) + + it("Should place it inside the cap if inside option is true", function() + local expected = "|=== some text ====|" + options.inside = true + local actual = fText(str, options) + options.inside = nil + assert.equals(expected, actual) + assert.equals(20, actual:len()) + end) + + it("Should mirror certain characters with their opposites", function() + local expected = "===[ some text ]====" + options.mirror = true + options.cap = "[" + local actual = fText(str, options) + assert.equals(expected, actual) + options.inside = true + expected = "[=== some text ====]" + actual = fText(str, options) + assert.equals(expected, actual) + options.inside = nil + options.cap = "<" + expected = "===< some text >====" + actual = fText(str, options) + assert.equals(expected, actual) + options.cap = "{" + expected = "==={ some text }====" + actual = fText(str, options) + assert.equals(expected, actual) + options.cap = "(" + expected = "===( some text )====" + actual = fText(str, options) + assert.equals(expected, actual) + options.cap = "|" + expected = "===| some text |====" + actual = fText(str, options) + assert.equals(expected, actual) + end) + end) + end) + + describe("ftext.cfText", function() + local cfText = ftext.cfText + local str = "some text" + local options = { + width = 20, + spacer = "=", + cap = "[", + inside = true, + mirror = true, + capColor = "", + spacerColor = "", + textColor = "", + } + it("Should handle cecho colored text", function() + local expectedStripped = "[=== some text ====]" + local expected = "[=== some text ====]" + local actual = cfText(str, options) + local actualStripped = cecho2string(actual) + assert.equals(expected, actual) + assert.equals(expectedStripped, actualStripped) + assert.equals(20, actualStripped:len()) + expectedStripped = "===[ some text ]====" + expected = "===[ some text ]====" + options.inside = false + actual = cfText(str, options) + actualStripped = cecho2string(actual) + assert.equals(expected, actual) + assert.equals(expectedStripped, actualStripped) + assert.equals(20, actualStripped:len()) + end) + + it("Should wrap cecho lines to the correct length", function() + local str = "This is a test of the emergency broadcast system. This is only a test" + local options = {width = 10, alignment = "centered"} + local actual = cfText(str, options) + for _, line in ipairs(actual:split("\n")) do + assert.equals(cecho2string(line):len(), 10) + end + options.width = 15 + actual = cfText(str, options) + for _, line in ipairs(actual:split("\n")) do + assert.equals(cecho2string(line):len(), 15) + end + end) + + end) + + describe("ftext.dfText", function() + local dfText = ftext.dfText + local str = "some text" + local options = { + width = 20, + spacer = "=", + cap = "[", + inside = true, + mirror = true, + capColor = "<160,32,240>", + spacerColor = "<0,255,0>", + textColor = "<255,0,0>", + } + it("Should handle decho colored text", function() + local expectedStripped = "[=== some text ====]" + local expected = "<160,32,240>[<0,255,0>===<255,0,0> some text <0,255,0>====<160,32,240>]" + local actual = dfText(str, options) + local actualStripped = decho2string(actual) + assert.equals(expected, actual) + assert.equals(expectedStripped, actualStripped) + assert.equals(20, actualStripped:len()) + expectedStripped = "===[ some text ]====" + expected = "<0,255,0>===<160,32,240>[<255,0,0> some text <160,32,240>]<0,255,0>====" + options.inside = false + actual = dfText(str, options) + actualStripped = decho2string(actual) + assert.equals(expected, actual) + assert.equals(expectedStripped, actualStripped) + assert.equals(20, actualStripped:len()) + end) + + it("Should wrap decho lines to the correct length", function() + local str = "This is a test of the emergency broadcast system. This is only a test" + local options = {width = 10, alignment = "centered"} + local actual = dfText(str, options) + for _, line in ipairs(actual:split("\n")) do + assert.equals(decho2string(line):len(), 10) + end + options.width = 15 + actual = dfText(str, options) + for _, line in ipairs(actual:split("\n")) do + assert.equals(decho2string(line):len(), 15) + end + end) + end) + + describe("ftext.hfText", function() + local hfText = ftext.hfText + local str = "some text" + local options = { + width = 20, + spacer = "=", + cap = "[", + inside = true, + mirror = true, + capColor = "#a020f0", + spacerColor = "#00ff00", + textColor = "#ff0000", + } + it("Should handle hecho colored text", function() + local expectedStripped = "[=== some text ====]" + local expected = "#a020f0[#r#00ff00===#r#ff0000 some text #r#00ff00====#r#a020f0]#r" + local actual = hfText(str, options) + local actualStripped = hecho2string(actual) + assert.equals(expected, actual) + assert.equals(expectedStripped, actualStripped) + assert.equals(20, actualStripped:len()) + expectedStripped = "===[ some text ]====" + expected = "#00ff00===#r#a020f0[#r#ff0000 some text #r#a020f0]#r#00ff00====#r" + options.inside = false + actual = hfText(str, options) + actualStripped = hecho2string(actual) + assert.equals(expected, actual) + assert.equals(expectedStripped, actualStripped) + assert.equals(20, actualStripped:len()) + end) + + it("Should wrap hecho lines to the correct length", function() + local str = "This is a test of the emergency broadcast system. This is only a test" + local options = {width = 10, alignment = "centered"} + local actual = hfText(str, options) + for _, line in ipairs(actual:split("\n")) do + assert.equals(hecho2string(line):len(), 10) + end + options.width = 15 + actual = hfText(str, options) + for _, line in ipairs(actual:split("\n")) do + assert.equals(hecho2string(line):len(), 15) + end + end) + end) + + describe("ftext.TextFormatter", function() + local tf = ftext.TextFormatter + local str = "some text" + local formatter + + before_each(function() + formatter = tf:new({width = 20}) + end) + + it("Should let you change width using :setWidth", function() + formatter:setWidth(80) + local expected = + " some text " + local actual = formatter:format(str) + assert.equals(expected, actual) + assert.equals(80, cecho2string(actual):len()) + end) + + it("Should format for cecho by default", function() + local expected = " some text " + local expectedStripped = " some text " + local actual = formatter:format(str) + local actualStripped = cecho2string(actual) + assert.equals(expected, actual) + assert.equals(expectedStripped, actualStripped) + assert.equals(20, actualStripped:len()) + end) + + it("Should produce the same line as cfText given the same options", function() + local expected = ftext.cfText(str, formatter.options) + local actual = formatter:format(str) + assert.equals(expected, actual) + end) + + it("Should let you change type using :setType", function() + formatter:setType("h") + local expected = ftext.hfText(str, formatter.options) + local actual = formatter:format(str) + assert.equals(expected, actual) + formatter:setType("d") + expected = ftext.dfText(str, formatter.options) + actual = formatter:format(str) + assert.equals(expected, actual) + formatter:setType("") + expected = ftext.fText(str, formatter.options) + actual = formatter:format(str) + assert.equals(expected, actual) + end) + + it("Should default to word wrapping, and let you change it with :setWrap", function() + formatter:setWidth(10) + local expected = + " some \n text " + local actual = formatter:format(str) + assert.equals(expected, actual) + expected = " some text " + formatter:setWrap(false) + actual = formatter:format(str) + assert.equals(expected, actual) + end) + + it("Should allow you to change the cap using :setCap", function() + formatter:setCap('|') + local expected = "| some text |" + local actual = formatter:format(str) + assert.equals(expected, actual) + end) + + it("Should allow you to change the capColor using :setCapColor", function() + formatter:setCapColor('') + local expected = " some text " + local actual = formatter:format(str) + assert.equals(expected, actual) + end) + + it("Should allow you to change the spacer color using :setSpacerColor", function() + formatter:setSpacerColor("") + local expected = " some text " + local actual = formatter:format(str) + assert.equals(expected, actual) + end) + + it("Should allow you to change the text color using :setTextColor", function() + formatter:setTextColor("") + local expected = " some text " + local actual = formatter:format(str) + assert.equals(expected, actual) + end) + + it("Should allow you to change the spacer using :setSpacer", function() + formatter:setSpacer("=") + -- local expected = " some text " + local expected = "==== some text =====" + local actual = formatter:format(str) + assert.equals(expected, actual) + end) + + it("Should allow you to set the alignment using :setAlignment", function() + formatter:setAlignment("left") + local expected = "some text " + local actual = formatter:format(str) + assert.equals(expected, actual) + formatter:setAlignment("right") + expected = " some text" + actual = formatter:format(str) + assert.equals(expected, actual) + end) + + it("Should allow you to change the 'inside' option using :setInside", function() + formatter:setInside(false) + local expected = " some text " + local actual = formatter:format(str) + assert.equals(expected, actual) + end) + + it("Should allow you to change the mirror option using :setMirror", function() + formatter:setCap('<') + formatter:setMirror(true) + local expected = "< some text >" + local actual = formatter:format(str) + assert.equal(expected, actual) + end) + end) + + describe("ftext.TableMaker", function() + local TableMaker = ftext.TableMaker + local tm + before_each(function() + tm = TableMaker:new() + tm:addColumn({name = "col1", width = 15, textColor = ""}) + tm:addColumn({name = "col2", width = 15, textColor = ""}) + tm:addColumn({name = "col3", width = 15, textColor = ""}) + tm:addRow({"some text", "more text", "other text"}) + tm:addRow({"little text", "bigger text", "text"}) + end) + + it("Should assemble a formatted table given default options", function() + local expected = [[************************************************* +* col1 | col2 | col3 * +*---------------|---------------|---------------* +* some text | more text | other text * +*---------------|---------------|---------------* +* little text | bigger text | text * +************************************************* +]] + local actual = tm:assemble() + assert.equals(expected, actual) + end) + + it("TableMaker:getCell should return the text and formatter for a specific cell", function() + local expectedText = "more text" + local expectedFormatter = tm.columns[2] + local actualText, actualFormatter = tm:getCell(1, 2) + assert.equals(expectedText, actualText) + assert.equals(expectedFormatter, actualFormatter) + local expectedFormatted = " more text " + local actualFormatted = actualFormatter:format(actualText) + assert.equals(expectedFormatted, actualFormatted) + end) + end) +end) diff --git a/src/scripts/bashmatic/bashmatic_init.lua b/src/scripts/bashmatic/bashmatic_init.lua index f1cfcf0..a82df95 100644 --- a/src/scripts/bashmatic/bashmatic_init.lua +++ b/src/scripts/bashmatic/bashmatic_init.lua @@ -1,207 +1,235 @@ -bashmatic = bashmatic or nil +local ourTables = require("Bashmatic.ftext").TableMaker + +Bashmatic = Bashmatic or { + ["commands"] = { + start = nil, + before = nil, + during = nil, + duringTime = nil, + after = nil + }, + ["death"] = { + phrase = nil, + event = nil + }, + ["enemies"] = { + group = false, + targets = {} + }, + ["enabled"] = false, + ["bashing"] = false, + ["hunting"] = { + events = {} + } +} function handleInstall(_, name) - if name:lower() == "bashmatic" then - if bashmatic == nil then - -- it's empty - bashmatic = {} - bashmatic.commands = { - "start" = nil, - "before" = nil, - "during" = nil, - "duringTime" = 2, - "after" = nil - } - bashmatic.death = { - "phrase" = nil, - "event" = nil - } - bashmatic.configs = { - "enemies" = { - "detection" = "inline", - "location" = nil, - "data" = nil - }, - "areas" = { - "detection" = nil, - "location" = nil, - "data" = nil - } - } - bashmatic.enabled = false - bashmatic.bashing = false - bashmatic.hunting = { - "targets" = {}, - "events" = {} - } - else - -- it's not empty, verify install - local corruptions = 0 - if not table.contains(bashmatic, "commands") then - bashmatic.commands = { - "start" = nil, - "before" = nil, - "during" = nil, - "duringTime" = 2, - "after" = nil - } - cecho("BASHMATIC: Potentially corrupt install. Tried to repair bashing commands.") - corruptions += 1 - end - - if not table.contains(bashmatic, "death") then - bashmatic.death = { - "phrase" = nil, - "event" = nil - } - cecho("BASHMATIC: Potentially corrupt install. Tried to repair death trigger.") - corruptions += 1 - end - - if not table.contains(bashmatic, "configs") then - bashmatic.configs = { - "enemies"{ - "detection" = "inline", - "location" = nil, - "data" = nil - }, - "areas" = { - "detection" = nil, - "location" = nil, - "data" = nil - } - } - cecho("BASHMATIC: Potentially corrupt install. Tried to repair configuration.") - corruptions += 1 - end - - if not table.contains(bashmatic, "enabled") then - bashmatic.enabled = false - cecho("BASHMATIC: Potentially corrupt install. Tried to repair status settings.") - corruptions += 1 - end - - if not table.contains(bashmatic, "bashing") then - bashmatic.bashing = false - cecho("BASHMATIC: Potentially corrupt install. Tried to repair status settings.") - corruptions += 1 - end - - if not table.contains(bashmatic, "hunting") then - bashmatic.hunting = { - "targets" = {}, - "events" = {} - } - cecho("BASHMATIC: Potentially corrupt install. Tried to repair target listings.") - corruptions += 1 - end - - if corruptions >= 2 then - cecho("BASHMATIC: Multiple corruptions fixed. You should probably uninstall and reinstall Bashmatic.") - end - end + if name:lower() == "Bashmatic" then + cecho("Bashmatic: Welcome to Bashmatic. See bm help to get started.") end end registerAnonymousEventHandler("sysInstallPackage", handleInstall) function handleShutdown() - if bashmatic.configs.death.event ~= nil then killTrigger(bashmatic.configs.death.event) end - bashmatic.configs.death.event = nil + if Bashmatic.death.event ~= nil then killTrigger(Bashmatic.death.event) end + killBashingTriggers() + bmDisable() + toggleBashing(false) + Bashmatic.death.event = nil - table.save(getMudletHomeDir() .. "/bashmatic.lua", bashmatic) + table.save(getMudletHomeDir() .. "/Bashmatic.lua", Bashmatic) end registerAnonymousEventHandler("sysExitEvent", handleShutdown) function handleLoadEvent() - if io.exists(getMudletHomeDir() .. "/bashmatic.lua") then - table.load(getMudletHomeDir() .. "/bashmatic.lua", bashmatic) + if io.exists(getMudletHomeDir() .. "/Bashmatic.lua") then + table.load(getMudletHomeDir() .. "/Bashmatic.lua", Bashmatic) end - if bashmatic.configs.death.phrase ~= nil then - bashmatic.configs.death.event = tempTrigger(bashmatic.configs.death.phrase,handleMobDeath) + if Bashmatic.death.phrase ~= nil then + Bashmatic.death.event = tempTrigger(Bashmatic.death.phrase,handleMobDeath) end - cecho("BASHMATIC: Loaded settings. Let's bash some mobs.") + cecho("Bashmatic: Loaded settings. Let's bash some mobs.") end registerAnonymousEventHandler("sysLoadEvent", handleLoadEvent) function bmDisable() - bashmatic.enabled = false + Bashmatic.enabled = false end function toggleBashing(status) - bashmatic.bashing = status + Bashmatic.bashing = status end function handleMobDeath() - if bashmatic.configs.commands.after then - send(bashmatic.configs.commands.after) + if Bashmatic.commands.after then + send(Bashmatic.commands.after) end toggleBashing(false) - if bashmatic.configs.enabled then - send("look") + if Bashmatic.enabled then + if table.size(Bashmatic.hunting.events) > 0 then + -- active triggers + send("look") + else + -- recreate triggers and then look + recreateTriggers() + send("look") + end end end function handleDuringTick() - send(bashmatic.configs.commands.during) - if bashmatic.configs.bashing then tempTimer(bashmatic.configs.commands.duringTime,handleDuringTick) end + send(Bashmatic.commands.during) + if Bashmatic.bashing then tempTimer(Bashmatic.commands.duringTime,handleDuringTick) end end -- Functions for handling inline matching function handleBashingMatches(match) - if not bashmatic.configs.enabled then - -- bashing is disabled, so clear the triggers - for _, tid in ipairs(bashmatic.hunting.events) do - killTrigger(tid) - end - bashmatic.hunting.events = {} + if not Bashmatic.enabled or Bashmatic.bashing then + -- bashing is disabled or we're currently bashing something, so clear the triggers + killBashingTriggers() else - if bashmatic.configs.bashing then - -- we're currently bashing something, so reup the trigger and wait - bmCreateTrigger(match) - else - if bashmatic.configs.commands.before then send(bashmatic.configs.commands.before) end - send(bashmatic.configs.commands.start .. match) - if bashmatic.configs.commands.during then tempTimer(bashmatic.configs.commands.duringTime,handleDuringTick) end - end + if Bashmatic.commands.before then send(Bashmatic.commands.before) end + send(Bashmatic.commands.start .. match) + if Bashmatic.commands.during then tempTimer(Bashmatic.commands.duringTime,handleDuringTick) end end end function bmCreateTrigger(keyword) - table.insert(bashmatic.hunting.events, tempTrigger(keyword, handleBashingMatches(keyword), 1)) + table.insert(Bashmatic.hunting.events, tempTrigger(keyword, handleBashingMatches(keyword), 1)) end --- Functions for handling GMCP matching - -- Functions for handling adding targets function inlineAddTarget(target, area) if area == nil then - cecho("BASHMATIC: Added " .. matches[2] .. " to hunting targets.") - if not table.contains(bashmatic.hunting.targets, matches[2]) then - table.insert(bashmatic.hunting.targets, matches[2]) + cecho("Bashmatic: Added " .. target .. " to hunting targets.") + if not table.contains(Bashmatic.enemies.targets, target) then + table.insert(Bashmatic.enemies.targets, target) end end if area ~= nil then - cecho("BASHMATIC: Added " .. matches[2] .. " to hunting targets in " .. area .. ".") - if table.contains(bashmatic.hunting.targets, area) then - table.insert(bashmatic.hunting.targets.area, matches[2]) + cecho("Bashmatic: Added " .. target .. " to hunting targets in " .. area .. ".") + if table.contains(Bashmatic.enemies.targets, area) then + table.insert(Bashmatic.enemies.targets.area, target) else - bashmatic.hunting.targets.area = {} - table.insert(bashmatic.hunting.targets.area, matches[2]) + Bashmatic.enemies.targets.area = {} + table.insert(Bashmatic.enemies.targets.area, target) end end - if not bashmatic.configs.bashing then - if bashmatic.configs.enabled then - bmCreateTrigger(matches[2]) + if not Bashmatic.bashing and Bashmatic.enabled then + bmCreateTrigger(target) + end +end + +function recreateTriggers() + if Bashmatic.enemies.group then + -- Grouping enemies by areas + local thisArea = GetRoomAreaName(GetRoomArea(GetPlayerRoom())) or nil + if thisArea == nil then + cecho("Bashmatic: We attempted to locate your area but failed.\nGot: " .. thisArea .. "") + return + end + if not table.contains(Bashmatic.enemies.targets, thisArea) then + cecho("Bashmatic: You haven't added any enemies for the area " .. thisArea .. "") + return + end + for _, keyword in ipairs(Bashmatic.enemies.targets.thisArea) do + bmCreateTrigger(keyword) + end + else + -- Not grouping enemies by areas + for _, keyword in ipairs(Bashmatic.enemies.targets) do + bmCreateTrigger(keyword) end end +end + +function killBashingTriggers() + if table.size(Bashmatic.hunting.events) > 0 then + for _, tid in ipairs(Bashmatic.hunting.events) do + killTrigger(tid) + end + end + Bashmatic.hunting.events = {} +end + +function displayTargetList(start, endi, page, pages) + local thisTable = ourTables:new({ + title = "Bashmatic Target List", + printTitle = true, + titleColor = "", + printHeaders = false, + separateRows = false + }) + thisTable:addColumn({ + name = "Index", + width = 8, + textColor = "" + }) + thisTable:addColumn({ + name = "Target", + width = 72, + textColor = "" + }) + for i=start,i<=endi,1 do + thisTable:addRow({ + i, + Bashmatic.enemies.targets[i] + }) + end + cecho(thisTable:assemble()) + if page ~= nil then + cecho("Displaying " .. start .. " to " .. endi .. " (Page " .. page .. " of " .. pages .. ")") + end +end + +function displayAreaTargetList(area, start, endi, page, pages) + local thisTable = ourTables:new({ + title = "Bashmatic Target List", + printTitle = true, + titleColor = "", + printHeaders = false, + separateRows = false + }) + thisTable:addColumn({ + name = "Index", + width = 8, + textColor = "" + }) + thisTable:addColumn({ + name = "Target", + width = 72, + textColor = "" + }) + for i=start,i<=endi,1 do + thisTable:addRow({ + i, + Bashmatic.enemies.targets.area[i] + }) + end + cecho("Target Listing for area " .. area .. "") + if page ~= nil then + cecho("Displaying " .. start .. " to " .. endi .. " (Page " .. page .. " of " .. pages .. ")") + end +end + +function squashTargetGroups() + local finalTable = {} + for _, area in ipairs(Bashmatic.enemies.targets) do + if table.size(Bashmatic.enemies.targets.area) > 0 then + for _, enemy in ipairs(Bashmatic.enemies.targets.area) do + table.insert(finalTable,enemy) + end + end + end + Bashmatic.enemies.targets = finalTable end \ No newline at end of file