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).
ssc.fitness = ssc.fitness or {} ssc.fitness.scenarios = ssc.fitness.scenarios or {} ssc.fitness.createscenario = function(name, ifaffs, classes, fighting) local scenario = {} scenario.name = name scenario.enabled = true scenario.checkobject = ssc.affcheck.create(ifaffs, classes, fighting) table.insert(ssc.fitness.scenarios, scenario) ssc.fitness.save() end ssc.fitness.checkscenario = function(scenario) if not scenario.enabled then return false end return ssc.affcheck.handle(scenario.checkobject) end ssc.fitness.needtofitness = function() -- don't try to/queue fitness if it's turned off, we aren't a class with fitness or we have weariness if not ssc.conf.usefitness or not table.contains({"infernal", "paladin", "runewarden"}, ssc.me.class) or ssc.affs.has("weariness") then return false end for _, scenario in ipairs(ssc.fitness.scenarios) do if ssc.fitness.checkscenario(scenario) then return true end end return false end ssc.fitness.save = function() table.save(getMudletHomeDir() .. "/ssc/fitness-scenarios.lua", ssc.fitness.scenarios) end ssc.fitness.load = function() table.load(getMudletHomeDir() .. "/ssc/fitness-scenarios.lua", ssc.fitness.scenarios) endAlias is pretty similar:
Results of disembowel testing | Knight limb counter | GMCP AB files