Code-free priority switches

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 B) - 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

  • is this compatible with svof?

  • @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.

  • Svof already had a couple systems in it that allows you to easily switch curing priorities. You can instantly load whole curing priority lists, and can switch priorities with simple commands. @Aegoth

  • So basically you created a DSL (domain specific languange) and an interpreter for it.

    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?
  • @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:

    Pattern: ^ssc swap (?:(\w+) )?(?:(\d+) )?(\w+) (?:above|to) (\w+) if (.+?)(?: class (.+?))?(?: fighting (.+))?$
    
    Code: 
    local name = matches[2] ~= "" and not tonumber(matches[2]) and matches[2] or nil -- optional name
    local position = tonumber(matches[2]) or matches[3] and tonumber(matches[3]) or -1 -- optional position to insert this switch (defaults to the end)
    local affliction1 = matches[4] -- the affliction to switch
    local affliction2 = tonumber(matches[5]) or matches[5] -- position to switch it to: either a number, affliction name or "top"
    local ifaffs = matches[6]
    local classes = matches[7] ~= "" and matches[7] or nil
    local fighting = matches[8]
    
    ssc.prioswaps.create(name, position, affliction1, affliction2, ifaffs, classes, fighting)
    

    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)
    end
    

    Alias is pretty similar:

    
    Pattern: ^ssc fitness(?: (\w+))?(?: if (.+?))?(?: class (.+?))?(?: fighting (.+?))?$
    
    Code:
    local name = matches[2] ~= "" and matches[2] or nil
    local ifaffs = matches[3] ~= "" and matches[3] or nil
    local classes = matches[4] ~= "" and matches[4] or nil
    local fighting = matches[5] ~= "" and matches[5] or nil
    ssc.fitness.createscenario(name, ifaffs, classes, fighting)
    
Sign In or Register to comment.