Created
July 15, 2022 07:28
-
-
Save Elmuti/d229f7ddae9065f65be0d5a901a8781f to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--[[ | |
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