Skip to content

Instantly share code, notes, and snippets.

@Elmuti
Created July 15, 2022 07:28
Show Gist options
  • Save Elmuti/d229f7ddae9065f65be0d5a901a8781f to your computer and use it in GitHub Desktop.
Save Elmuti/d229f7ddae9065f65be0d5a901a8781f to your computer and use it in GitHub Desktop.
--[[
Typical macro structure:
/cmd [cond, cond:2, cond=abc] arg
^ can be viewed as:
/cmd [cond?, cond:2?, cond=abc?]? arg?
where '?' are optional. More below:
Rules:
1. A macro always has a cmd.
2. A macro can an optionally have any number of conditionals with either no value, or values set with : or =
3. An argument is optional.
Examples:
/cast Fireball
/stopattacking
/cast [mod:alt, target=focus] Flash of Light
/yell hello motherfuckers!
Reasons why we cannot allow macros to run Lua:
1. Loadstring is disabled on client
2. We do not want to implement a lua compiler in lua
3. We do not want to sandbox said lua environment
4. All user input is evil
HandleLine will parse a line in a macro and return a command in the following format as an example:
/cast [nostealth, mod:shift, target=focus] tricks of the trade
^ would turn into:
{
Command = "cast";
Conditionals = {
{Conditional = "mod"; Value = "shift";};
{Conditional = "target"; Value = "focus";};
{Conditional = "nostealth"; Value = nil;};
};
Argument = "tricks of the trade";
}
Usage of this module:
local macro = MacroParser.ParseMacro("cast [mod:alt, target=focus] Flash of Light")
MacroParser.RunMacro(localUnit, macro)
]]
local StarterPlayer = game:GetService("StarterPlayer")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local UserInputService = game:GetService("UserInputService")
local ContextActionService = game:GetService("ContextActionService")
local MacroParser = {}
local String = require(ReplicatedStorage.Common.Util.String)
local Commands = require(StarterPlayer.StarterPlayerScripts.Client.Net.ClientCommands)
local SpellTarget = require(ReplicatedStorage.Common.Spell.SpellTarget)
local SpellDatabase = require(ReplicatedStorage.Common.Spell.SpellDatabase)
MacroParser.Commands = {
cast = {
Handler = function(macroTgt, arg)
Commands.CastSpell(arg) --TODO: take in target type(target, focus, arena1...)
end
};
stopcasting = {
Handler = function(macroTgt, arg)
Commands.StopCast()
end
}
}
--TODO make following conditionals: casting, channeling, combat
--Macro handlers:
--Handler(caster: C_Unit, val: any): boolean
MacroParser.Conditionals = {
stance = {
Handler = function(caster, val)
if caster:HasAura("Battle Stance") and val == 1 then
return true
elseif caster:HasAura("Defensive Stance") and val == 2 then
return true
elseif caster:HasAura("Berserker Stance") and val == 3 then
return true
else
return false
end
end;
ValidArgs = {1, 2, 3};
};
stealth = {
Handler = function(caster)
if caster:HasAura("Stealth") then
return true
else
return false
end
end;
};
nostealth = {
Handler = function(caster)
if not caster:HasAura("Stealth") then
return true
else
return false
end
end;
};
nomod = {
Handler = function()
if not UserInputService.IsKeyDown(Enum.KeyCode.LeftShift) and not UserInputService.IsKeyDown(Enum.KeyCode.LeftAlt) and not UserInputService.IsKeyDown(Enum.KeyCode.LeftControl) then
return true
else
return false
end
end;
};
mod = {
Handler = function(caster, val) --TODO: if you have a mod:shift macro on V, you should disable any other bind on shift-V to prevent double actions
if (val == "shift" and UserInputService.IsKeyDown(Enum.KeyCode.LeftShift)) or
(val == "alt" and UserInputService.IsKeyDown(Enum.KeyCode.LeftAlt)) or
(val == "ctrl" and UserInputService.IsKeyDown(Enum.KeyCode.LeftControl)) then
return true
else
return false
end
end;
ValidArgs = {"shift", "ctrl", "alt"};
};
help = {
Handler = function(caster, macroTgt)
if not caster:IsHostile(macroTgt) then
return true
else
return false
end
end;
TakesTarget = true;
};
harm = {
Handler = function(caster, macroTgt)
if caster:IsHostile(macroTgt) then
return true
else
return false
end
end;
TakesTarget = true;
};
exists = {
Handler = function(caster, macroTgt)
if macroTgt then
return true
else
return false
end
end;
TakesTarget = true;
};
notexists = {
Handler = function(caster, macroTgt)
if not macroTgt then
return true
else
return false
end
end;
TakesTarget = true;
};
dead = {
Handler = function(caster, macroTgt)
if not macroTgt:IsAlive() then
return true
else
return false
end
end;
TakesTarget = true;
};
notdead = {
Handler = function(caster, macroTgt)
if macroTgt:IsAlive() then
return true
else
return false
end
end;
TakesTarget = true;
};
}
function GetSpellEntry(entryId)
return SpellDatabase.GetSpellTemplate(entryId)
end
local function IsValidCmd(str: string)
if str ~= nil and str ~= "" then
return true
end
return false
end
local function IsValidCond(cond: string, val)
if cond ~= nil and cond ~= "" then
return true
end
return false
end
local function ValidCondArg(cond, arg)
local vargs = MacroParser.Conditionals[cond].ValidArgs
if vargs == nil and arg == nil then
return true
else
return false
end
for _, farg in ipairs(vargs) do
if farg == arg then
return true
end
end
return false
end
---Convert a line in a macro into data
---@param input string
local function HandleLine(input: string)
local parsedCmd = {}
local cmdCapture = "(%w+)%s*" --capture a command
local condsCapture = "%[?([%a%s:=,]*)%]?" --capture all conditionals within square brackets
local condCapture = "(%a+)%s*[:=]?%s*(%w*)" --capture a single conditional
local argCapture = "%s*(%w%s+)" --capture an argument
local pat = cmdCapture..condsCapture..argCapture
local cmd, condsLine, arg = input:gmatch(pat)()
local validCmd = IsValidCmd(cmd)
if validCmd then
parsedCmd.Command = cmd
--split conditionals if there are multiple and gmatch them individually
local conds = String.Split(condsLine, ",")
parsedCmd.Conditionals = {}
for _, cond in ipairs(conds) do
local fCond, fCondVal = cond:gmatch(condCapture)()
if IsValidCond(fCond) then
table.insert(parsedCmd.Conditionals, {Conditional = fCond, Value = fCondVal})
end
end
parsedCmd.Argument = arg
return parsedCmd
end
return nil
end
--getUnitFromName("target")
--getUnitFromName("elmu")
--getUnitFromName("arena2")
local function getUnitFromName(unit) --TODO
error("NIY")
return {}
end
---Run a macro from parsed macro data
---@param casterUnit Unit
---@param macro Macro
function MacroParser.RunMacro(casterUnit, macro)
--TODO: can I get the caster(local unit) from somewhere else?
for _, cmd in ipairs(macro) do
local tgtArgNum
local macroTgt
--find target cond first if any. if multiple target conditionals, last one is used because of ipairs
for i, cond in ipairs(cmd.Conditionals) do
if cond.Conditional == "target" then
macroTgt = cond.Value
tgtArgNum = i
end
end
--target cond handled, remove it
if tgtArgNum then
table.remove(cmd.Conditionals, tgtArgNum)
end
--get actual unit from macroTgt
local macroTgtUnit = getUnitFromName(macroTgt)
--check other conditions
local conditionsAreMet = true
for _, cond in ipairs(cmd.Conditionals) do
local condData = MacroParser.Conditionals[cond.Conditional]
--if conditional has an arg, check if its valid
if ValidCondArg(cond.Conditional, cond.Value) then
local condIsMet = condData.Handler(casterUnit, macroTgtUnit)
if not condIsMet then
conditionsAreMet = false --a condition is not met, skip this cmd and go to next cmd after semicolon
break
end
else
return
end
end
if conditionsAreMet then --all conditionals in this cmd are met, run the command
MacroParser.Commands[cmd.Command](cmd.Argument)
end
end
end
---Convert a macro string into data
---@param input string
---@return Macro | nil
function MacroParser.ParseMacro(input: string)
if input == "" then
return nil
end
local lines = String.Split(input, ";")
local cmds = {}
for _, line in ipairs(lines) do
local cmd = HandleLine(line)
if cmd then
table.insert(cmds, cmd)
end
end
if #cmds == 0 then
return nil
end
return cmds
end
return MacroParser
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment