Because I'm really lazy and dislike having to write more code than I have to, a while ago I started work on a relatively big project. The goal was to allow me to add priority switches into my curing on the fly using an alias - rather than having to write new bits of code - and for the associated logic involved in deciding whether to make the swap to be able to be pretty complex. I'm on, I think, version four of that code now (and there are still some changes I may make eventually), but it's at a point where I figured I'd post it here for anybody who wants to take a look at it and maybe adapt it for their own use.
It's part of my system for working with server-side curing (which is just called SSC) so this is basically going to be a code dump of various bits of code. It's not something that you can just copy into Mudlet and have work unfortunately, but if you can understand the code it should be possible for somebody to adapt it to work with other curing systems such as Svof.
First part is the "affcheck" namespace. This is what does the majority of the work and takes strings and converts them to objects to then use to compare against afflictions, balances, etc. It also considers your class and classes you're fighting against. Here's the code for it:
ssc.affcheck = ssc.affcheck or {} ssc.affcheck.const = ssc.affcheck.const or { affliction = 1, balance = 2, comparison = 3, balances = {"sip", "moss", "smoke", "herb", "salve", "tree", "fitness", "focus", "balance", "rage", "shrugging", "equilibrium"} } ssc.affcheck.create = function(ifaffs, classes, fighting) local checkobject = {} if ifaffs then checkobject.affs = {} for aff in string.gmatch(ifaffs, "%S+") do aff = aff:lower() if not aff:find("|") then table.insert(checkobject.affs, ssc.affcheck.createaff(aff)) else local affs = {} for a in string.gmatch(aff, "[^|]+") do table.insert(affs, ssc.affcheck.createaff(a)) end table.insert(checkobject.affs, affs) end end end if classes then checkobject.classes = {} for class in string.gmatch(classes, "%a+") do class = class:lower() table.insert(checkobject.classes, class) end end if fighting then checkobject.fighting = {} for class in string.gmatch(fighting, "%S+") do class = class:lower() local classobj = {} if string.starts(class, "!") then classobj.negate = true class = string.sub(class, 2) end classobj.class = class table.insert(checkobject.fighting, classobj) end end return checkobject end ssc.affcheck.createaff = function(affstring) local affobject = {} if string.starts(affstring, "!") then affobject.negate = true affstring = string.sub(affstring, 2) else affobject.negate = false end if string.match(affstring, "[<>=]+") then affobject.type = ssc.affcheck.const.comparison local first = affstring:match("^(%a+)") local comp = affstring:match("([<>=]+)") local second = affstring:match("[<>=]+(%w+)") second = tonumber(second) or second affobject.first = first affobject.comp = comp affobject.second = second elseif string.match(affstring, "%(") then affobject.type = ssc.affcheck.const.affliction local an = affstring:match("^%a+") local as = tonumber(affstring:match("%((%d+)%)")) affobject.affliction = an affobject.stacks = as else if table.contains(ssc.affcheck.const.balances, affstring) then affobject.type = ssc.affcheck.const.balance affobject.balance = affstring else affobject.type = ssc.affcheck.const.affliction affobject.affliction = affstring end end return affobject end ssc.affcheck.handle = function(checkobject) if checkobject.classes and not table.contains(checkobject.classes, ssc.me.class) then return false end if checkobject.fighting then local matched = false for _, classobj in ipairs(checkobject.fighting) do -- for each class, check if we're fighting it (stop iteration when we find a class we're fighting) matched = ssc.isfighting(classobj.class) if classobj.negate then matched = not matched end if matched then break end end -- if we're not fighting any of these classes, return false if not matched then return false end end if checkobject.affs then for _, aff in ipairs(checkobject.affs) do if aff.type then -- if we don't have this aff, return false if not ssc.affcheck.check(aff) then return false end else local atleastone = false for _, a in ipairs(aff) do atleastone = ssc.affcheck.check(a) if atleastone then break end end -- if we don't have at least one of these afflictions, return false if not atleastone then return false end end end end return true end ssc.affcheck.check = function(affobject) local retval = false if affobject.type == ssc.affcheck.const.balance then retval = svo.bals[affobject.balance] elseif affobject.type == ssc.affcheck.const.affliction then if affobject.stacks then retval = ssc.affs.level(affobject.affliction) >= affobject.stacks else retval = ssc.affs.has(affobject.affliction) end elseif affobject.type == ssc.affcheck.const.comparison then local fn = 0 local sn = 0 if ssc.affs.curedby[affobject.first] then fn = ssc.affs.count(affobject.first) else fn = ssc.affs.curr(affobject.first) end if type(affobject.second) == "number" then sn = affobject.second else if ssc.affs.curedby[affobject.second] then sn = ssc.affs.count(affobject.second) else sn = ssc.affs.curr(affobject.second) end end retval = loadstring("return " .. fn .. " " .. affobject.comp .. " " .. sn)() end if affobject.negate then return not retval else return retval end end ssc.affcheck.afftostring = function(affobject) if affobject.type == ssc.affcheck.const.balance then return (affobject.negate and "not " or "") .. affobject.balance elseif affobject.type == ssc.affcheck.const.affliction then if affobject.stacks then return (affobject.negate and "not " or "") .. string.format("%s(%d)", affobject.affliction, affobject.stacks) else return (affobject.negate and "not " or "") .. affobject.affliction end elseif affobject.type == ssc.affcheck.const.comparison then return (affobject.negate and "not " or "") .. string.format("%s%s%s", affobject.first, affobject.comp, affobject.second) end end ssc.affcheck.tostring = function(checkobject) local s = {""} local affcolour = "" if checkobject.affs then table.insert(s, "IF ") for i, aff in ipairs(checkobject.affs) do table.insert(s, affcolour) if aff.type then table.insert(s, ssc.affcheck.afftostring(aff)) else table.insert(s, "(") for j, a in ipairs(aff) do table.insert(s, ssc.affcheck.afftostring(a)) if j < #aff then table.insert(s, " or" .. affcolour .. " ") end end table.insert(s, ")") end table.insert(s, " ") if i < #checkobject.affs then table.insert(s, "and ") end end end if checkobject.classes then table.insert(s, "CLASS ") for i, class in ipairs(checkobject.classes) do table.insert(s, affcolour) table.insert(s, class) table.insert(s, " ") if i < #checkobject.classes then table.insert(s, "or ") end end end if checkobject.fighting then table.insert(s, "FIGHTING ") for i, classobj in ipairs(checkobject.fighting) do table.insert(s, affcolour) if classobj.negate then table.insert(s, "not ") end table.insert(s, classobj.class) table.insert(s, " ") if i < #checkobject.fighting then table.insert(s, "or ") end end end return table.concat(s, "") end
The most important part is the ssc.affcheck.create function and its three parameters. The classes parameter is just an optional space separated list of classes you want to be considered, so if classes was "paladin runewarden infernal" then it would only consider the check to pass if your current class is one of those three; since you can only be one class at a time it's an OR list (i.e. A or B or C). The fighting parameter works in much the same way as the classes parameter (it's still an OR listing), though you can negate an option using "!"; for example, "paladin" means fighting a Paladin, whereas "!paladin" would mean not fighting a Paladin.
The ifaffs parameter is where most things happen, since this is checking various combinations of afflictions, curing balances, current priorities and number of afflictions cured by a type. A space is AND and a pipe ("|") is OR. "!" is, once again, negation. It supports comparisons (<, >, <=, >=, ==) of some things, checking some/most balances (the ones listed in the declaration for ssc.checkaffs.const.balances), and affliction "level" (number of stacks for stackable afflictions like torntendons, time you've had the affliction for any others) using "affname(n)" (where n is a number) - "torntendons(4)" would be 4 (or more!) stacks of the torntendons affliction, "darkshade(6)" would be you've had the darkshade affliction for six seconds or more. While you can do reasonably complicated checks, not every potentially conceivable combination is possible (though I've not come across anything that I've wanted to do that the current code won't let me).
Some examples:
"prone" - you're prone:
"paralysis asthma vernalius" - you've got all three of the paralysis, asthma and vernalius afflictions
"torntendons(4)" - you've got four or more stacks of torntendons
"prone(3)" - you've been prone for three or more seconds
"kelp>=2" - you've got two or more afflictions cured by kelp
"kelp<goldenseal impatience" - you have fewer kelp afflictions than goldenseal afflictions, and you have impatience
"paralysis|!tree" - you have paralysis or you don't have tree balance (i.e. you can't touch tree right now)
"asthma>paralysis" - asthma's current priority is higher than paralysis' current priority (i.e. you'll cure paralysis before asthma)
Okay, so you can check what afflictions you have (among other things), but that's pretty useless unless you do something with it. For priority swaps, I have three potential switches: swap affliction A to the current priority of affliction B and shifting affliction B's priority up one (so A is now cured before - I use this for switching broken (mending cured) legs before arms when I'm prone, but otherwise curing arms first so I can still attack, swap affliction A's priority to a static priority (e.g. 26 so it's ignored), and switching affliction A's priority to "top" (which is a specific priority dependent on type - all of my herb afflictions are in the 10+ range, so "top" for herb cured is 9 so I can say "always cure this first"). Only the last valid "top" switch for a type will work at any given time; if I have one switch that says move asthma to "top" if I have paralysis and asthma, and then a later switch that says move impatience to "top" if I'm fighting a serpent, if I'm fighting a serpent and currently have asthma and paralysis, it's going to move impatience and not asthma. Basically, order matters, so you need to pay attention to it. Here's the code for the priority switching. I call "ssc.prioswaps.handleswaps" when I gain/lose an affliction or balance.
ssc.prioswaps.create = function(name, position, affliction1, affliction2, ifaffs, classes, fighting) local prioswap = { name = name, enabled = true, affliction1 = affliction1 } if type(affliction2) == "number" then prioswap.affliction2 = affliction2 else local affs = {} for aff in string.gmatch(affliction2, "%a+") do table.insert(affs, aff) end if #affs == 1 then prioswap.affliction2 = affs[1] else prioswap.affliction2 = affs end end prioswap.checkobject = ssc.affcheck.create(ifaffs, classes, fighting) if position > -1 then table.insert(ssc.prioswaps.swaps, position, prioswap) else table.insert(ssc.prioswaps.swaps, prioswap) end local s = ssc.prioswaps.toshortstring(prioswap) ssc.echo(s) ssc.save() end ssc.prioswaps.handleswaps = function(event, arg) --if not (event == "svo got balance" or event == "got bal" or event == "svo lost balance" or event == "lost bal") then local handledpos = -1 local swapped = {} local prios = {} for _, swap in ipairs(ssc.prioswaps.swaps) do ssc.prioswaps.handle(swap, swapped, prios) end local i = 0 local cs = "" for aff, prio in pairs(prios) do if ssc.affs.curr(aff) ~= prio then i = i + 1 if i > 1 then cs = cs .. "||" end cs = cs .. "curing priority " .. aff .. " " .. prio ssc.conf.affs[aff] = prio if i == 10 then send(cs, false) i = 0 cs = "" end end --ssc.affs.set(aff, prio) end if #cs > 0 then send(cs, false) end --end end ssc.prioswaps.handle = function(swap, swapped, prios) -- if this isn't enabled, do nothing if not swap.enabled then return end local criteriamet = ssc.affcheck.handle(swap.checkobject) if criteriamet then ssc.prioswaps.swap(swap.affliction1, swap.affliction2, swapped, prios) else -- reset afflictions if necessary ssc.prioswaps.reset(swap.affliction1, swapped, prios) if type(swap.affliction2) == "string" and swap.affliction2 ~= "top" then ssc.prioswaps.reset(swap.affliction2, swapped, prios) end end end ssc.prioswaps.swap = function(fa, sa, swapped, prios) swapped[fa] = true local prioone = ssc.affs.curr(fa) local priotwo = 27 -- if second affliction is a number, it's a priority to swap this to if type(sa) == "number" then priotwo = sa elseif type(sa) == "string" then if sa == "top" then priotwo = ssc.affs.priotop(fa) else priotwo = ssc.affs.curr(sa) end end -- if this affliction is being set to the top priority for its type OR it's being set to a specific priority and isn't already that priority OR its a lower priority (prioone is higher than priotwo) and it isn't being swapped to a low enough priority by a previous swap if (type(sa) == "string" and sa == "top") or (type(sa) == "number" and prioone ~= priotwo) or (prioone > priotwo and (not prios[fa] or prios[fa] > priotwo)) then prios[fa] = priotwo else prios[fa] = nil end if type(sa) == "string" then if sa == "top" then for aff, prio in pairs(prios) do if prio == priotwo and aff ~= fa then prios[aff] = ssc.affs.prio(aff) end end else ssc.prioswaps.swapsecond(sa, prioone, priotwo, swapped, prios) end end end ssc.prioswaps.swapsecond = function(aff, prioone, priotwo, swapped, prios) swapped[aff] = true if ssc.affs.curr(aff) <= prioone then prios[aff] = priotwo + 1 else prios[aff] = nil end end ssc.prioswaps.reset = function(aff, swapped, prios) if not swapped[aff] then local prio = ssc.affs.prio(aff) local curr = ssc.affs.curr(aff) if curr ~= prio then prios[aff] = prio end end end
Hopefully the code is clear enough that people can work out what it's doing. Some parts are commented just so I could wrap my head around what the logic needed to be, so that should help. I don't just use the affcheck part for priority switches, either. So far I've used it for creating scenarios for using fitness, using tree, disabling focusing, and switching up health and mana sip/moss percentages. If anybody wants to try getting it to work with their own curing system and has questions on how it works, just post here.
Comments
@Aegoth No, it's not compatible with anything right now other than my own system, which isn't in a state where I'd be able to release it (far too many cross dependencies with other scripts I have and things that are coded specifically for me).
There'd definitely be some (maybe even a lot of) work in getting it to work with svof, but that would require somebody like @Ahmet or @Keneanung who knows how the curing priorities and such are structured. My system keeps track of what the priority should be by default (separately from/in addition to what the priority currently is) so when it's time to reset priorities (because a switch scenario no longer applies and nothing else is trying to switch that affliction) it knows what to set it back to. I have no idea if svof has something similar that would allow that. Then things like getting a count of how many afflictions cured by a given cure, how long you've had an affliction (I know svof tracks this, I just don't know how easy it is to get at that info), etc. that may need to be added. On the plus side, the balance tracking and what classes you're fighting that I use for it in my system is actually svof's (I've been too lazy to implement it myself) so that shouldn't take much work at all.
This is, unfortunately, just a code dump of something I've written that I think is pretty cool and could be useful for other people. It's only intended as a jumping off point if somebody did want to adapt it to be compatible with something like svof (or any other system). There's definitely a fair amount of work involved in that though.
Results of disembowel testing | Knight limb counter | GMCP AB files
Are you saving the string(s) or the built abstract syntax tree (the checkobjects)?
Can you show us an example how your alias to create an check looks like?
GMCP documentation: https://github.com/keneanung/GMCPAdditions
svof github site: https://github.com/svof/svof and documentation at https://svof.github.io/svof
@Keneanung Current version saves the checkobjects. The original version (basically the proof of concept) was saving the strings, and parsing them every time it needed to make decisions (which is pretty often during a regular fight), which seemed inefficient (not in terms of actual noticeable performance issues, just felt like a lot of unnecessary string parsing); seemed easier to do it once upfront on creation, then just iterate over the objects instead. That, however, comes with the trade off that when I rework the affcheck logic (such as only recently allowing the option to negate classes I'm fighting, which meant a change to the way they're stored) I also have to script one off updates to all of the already created ones to match the new structure (or code it in such a way that both old and new structures are accommodated).
Here's the alias. Potentially the regex could be a lot better, it's basically not been touched since I got it working:
Then a very basic example of usage would be something like 'ssc swap brokenrightleg above brokenrightarm if prone'.
Also have a UI to go with it (inspired very much by the ones in svof) so I can keep track of all the switches I have: https://ada-young.appspot.com/pastebin/051f8d18
For something a little more basic that still uses the affcheck stuff, here's what I've got for fitness scenarios. Since there's no switching of aff check priorities, it's just a yes/no check for whether fitness is required, it's just iterating through scenarios (just a name, enabled state and a checkobject) until it finds one that matches (if any do).
Alias is pretty similar:
Results of disembowel testing | Knight limb counter | GMCP AB files