Quantcast

v7.2.5 - Custom Rotations

Pawel [06-24-17 - 01:47]
v7.2.5 - Custom Rotations
Filename
Libs/ForAllIndentsAndPurposes.lua
MaxDps.toc
core.lua
custom.lua
diff --git a/Libs/ForAllIndentsAndPurposes.lua b/Libs/ForAllIndentsAndPurposes.lua
new file mode 100644
index 0000000..e9d17ba
--- /dev/null
+++ b/Libs/ForAllIndentsAndPurposes.lua
@@ -0,0 +1,1315 @@
+-- For All Indents And Purposes
+local revision = 19
+-- Maintainer: kristofer.karlsson@gmail.com
+
+-- For All Indents And Purposes -
+-- a indentation + syntax highlighting library
+-- All valid lua code should be processed correctly.
+
+-- Usage (for developers)
+--------
+-- Variant 1: - non embedded
+-- 1) Add ForAllIndentsAndPurposes to your dependencies (or optional dependencies)
+
+-- Variant 2: - embedded
+-- 1.a) Copy indent.lua to your addon directory
+-- 1.b) Put indent.lua first in your list of files in the TOC
+
+-- For both variants:
+-- 2) hook the editboxes that you want to have indentation like this:
+-- IndentationLib.enable(editbox [, colorTable [, tabWidth] ])
+-- if you don't select a color table, it will use the default.
+-- Read through this code for further usage help.
+-- (The documentation IS the code)
+
+-- luacheck: globals IndentationLib
+
+if not IndentationLib then
+    IndentationLib = {}
+end
+
+if not IndentationLib.revision or revision > IndentationLib.revision then
+    local lib = IndentationLib
+    lib.revision = revision
+
+    local stringlen = string.len
+    local stringformat = string.format
+    local stringfind = string.find
+    local stringsub = string.sub
+    local stringbyte = string.byte
+    local stringchar = string.char
+    local stringrep = string.rep
+    local stringgsub = string.gsub
+
+    local defaultTabWidth = 2
+    local defaultColorTable
+
+    local workingTable = {}
+    local workingTable2 = {}
+    local function tableclear(t)
+        for k in next,t do
+            t[k] = nil
+        end
+    end
+
+    local function stringinsert(s, pos, insertStr)
+        return stringsub(s, 1, pos) .. insertStr .. stringsub(s, pos + 1)
+    end
+    lib.stringinsert = stringinsert
+
+    local function stringdelete(s, pos1, pos2)
+        return stringsub(s, 1, pos1 - 1) .. stringsub(s, pos2 + 1)
+    end
+    lib.stringdelete = stringdelete
+
+    -- token types
+    local tokens = {}
+    lib.tokens = tokens
+    tokens.TOKEN_UNKNOWN = 0
+    tokens.TOKEN_NUMBER = 1
+    tokens.TOKEN_LINEBREAK = 2
+    tokens.TOKEN_WHITESPACE = 3
+    tokens.TOKEN_IDENTIFIER = 4
+    tokens.TOKEN_ASSIGNMENT = 5
+    tokens.TOKEN_EQUALITY = 6
+    tokens.TOKEN_MINUS = 7
+    tokens.TOKEN_COMMENT_SHORT = 8
+    tokens.TOKEN_COMMENT_LONG = 9
+    tokens.TOKEN_STRING = 10
+    tokens.TOKEN_LEFTBRACKET = 11
+    tokens.TOKEN_PERIOD = 12
+    tokens.TOKEN_DOUBLEPERIOD = 13
+    tokens.TOKEN_TRIPLEPERIOD = 14
+    tokens.TOKEN_LTE = 15
+    tokens.TOKEN_LT = 16
+    tokens.TOKEN_GTE = 17
+    tokens.TOKEN_GT = 18
+    tokens.TOKEN_NOTEQUAL = 19
+    tokens.TOKEN_COMMA = 20
+    tokens.TOKEN_SEMICOLON = 21
+    tokens.TOKEN_COLON = 22
+    tokens.TOKEN_LEFTPAREN = 23
+    tokens.TOKEN_RIGHTPAREN = 24
+    tokens.TOKEN_PLUS = 25
+    tokens.TOKEN_SLASH = 27
+    tokens.TOKEN_LEFTWING = 28
+    tokens.TOKEN_RIGHTWING = 29
+    tokens.TOKEN_CIRCUMFLEX = 30
+    tokens.TOKEN_ASTERISK = 31
+    tokens.TOKEN_RIGHTBRACKET = 32
+    tokens.TOKEN_KEYWORD = 33
+    tokens.TOKEN_SPECIAL = 34
+    tokens.TOKEN_VERTICAL = 35
+    tokens.TOKEN_TILDE = 36
+    -- WoW specific tokens
+    tokens.TOKEN_COLORCODE_START = 37
+    tokens.TOKEN_COLORCODE_STOP = 38
+    -- new as of lua 5.1
+    tokens.TOKEN_HASH = 39
+    tokens.TOKEN_PERCENT = 40
+
+
+    -- ascii codes
+    local bytes = {}
+    lib.bytes = bytes
+    bytes.BYTE_LINEBREAK_UNIX = stringbyte("\n")
+    bytes.BYTE_LINEBREAK_MAC = stringbyte("\r")
+    bytes.BYTE_SINGLE_QUOTE = stringbyte("'")
+    bytes.BYTE_DOUBLE_QUOTE = stringbyte('"')
+    bytes.BYTE_0 = stringbyte("0")
+    bytes.BYTE_9 = stringbyte("9")
+    bytes.BYTE_PERIOD = stringbyte(".")
+    bytes.BYTE_SPACE = stringbyte(" ")
+    bytes.BYTE_TAB = stringbyte("\t")
+    bytes.BYTE_E = stringbyte("E")
+    bytes.BYTE_e = stringbyte("e")
+    bytes.BYTE_MINUS = stringbyte("-")
+    bytes.BYTE_EQUALS = stringbyte("=")
+    bytes.BYTE_LEFTBRACKET = stringbyte("[")
+    bytes.BYTE_RIGHTBRACKET = stringbyte("]")
+    bytes.BYTE_BACKSLASH = stringbyte("\\")
+    bytes.BYTE_COMMA = stringbyte(",")
+    bytes.BYTE_SEMICOLON = stringbyte(";")
+    bytes.BYTE_COLON = stringbyte(":")
+    bytes.BYTE_LEFTPAREN = stringbyte("(")
+    bytes.BYTE_RIGHTPAREN = stringbyte(")")
+    bytes.BYTE_TILDE = stringbyte("~")
+    bytes.BYTE_PLUS = stringbyte("+")
+    bytes.BYTE_SLASH = stringbyte("/")
+    bytes.BYTE_LEFTWING = stringbyte("{")
+    bytes.BYTE_RIGHTWING = stringbyte("}")
+    bytes.BYTE_CIRCUMFLEX = stringbyte("^")
+    bytes.BYTE_ASTERISK = stringbyte("*")
+    bytes.BYTE_LESSTHAN = stringbyte("<")
+    bytes.BYTE_GREATERTHAN = stringbyte(">")
+    -- WoW specific chars
+    bytes.BYTE_VERTICAL = stringbyte("|")
+    bytes.BYTE_r = stringbyte("r")
+    bytes.BYTE_c = stringbyte("c")
+    -- new as of lua 5.1
+    bytes.BYTE_HASH = stringbyte("#")
+    bytes.BYTE_PERCENT = stringbyte("%")
+
+
+    local linebreakCharacters = {}
+    lib.linebreakCharacters = linebreakCharacters
+    linebreakCharacters[bytes.BYTE_LINEBREAK_UNIX] = 1
+    linebreakCharacters[bytes.BYTE_LINEBREAK_MAC] = 1
+
+    local whitespaceCharacters = {}
+    lib.whitespaceCharacters = whitespaceCharacters
+    whitespaceCharacters[bytes.BYTE_SPACE] = 1
+    whitespaceCharacters[bytes.BYTE_TAB] = 1
+
+    local specialCharacters = {}
+    lib.specialCharacters = specialCharacters
+    specialCharacters[bytes.BYTE_PERIOD] = -1
+    specialCharacters[bytes.BYTE_LESSTHAN] = -1
+    specialCharacters[bytes.BYTE_GREATERTHAN] = -1
+    specialCharacters[bytes.BYTE_LEFTBRACKET] = -1
+    specialCharacters[bytes.BYTE_EQUALS] = -1
+    specialCharacters[bytes.BYTE_MINUS] = -1
+    specialCharacters[bytes.BYTE_SINGLE_QUOTE] = -1
+    specialCharacters[bytes.BYTE_DOUBLE_QUOTE] = -1
+    specialCharacters[bytes.BYTE_TILDE] = -1
+    specialCharacters[bytes.BYTE_RIGHTBRACKET] = tokens.TOKEN_RIGHTBRACKET
+    specialCharacters[bytes.BYTE_COMMA] = tokens.TOKEN_COMMA
+    specialCharacters[bytes.BYTE_COLON] = tokens.TOKEN_COLON
+    specialCharacters[bytes.BYTE_SEMICOLON] = tokens.TOKEN_SEMICOLON
+    specialCharacters[bytes.BYTE_LEFTPAREN] = tokens.TOKEN_LEFTPAREN
+    specialCharacters[bytes.BYTE_RIGHTPAREN] = tokens.TOKEN_RIGHTPAREN
+    specialCharacters[bytes.BYTE_PLUS] = tokens.TOKEN_PLUS
+    specialCharacters[bytes.BYTE_SLASH] = tokens.TOKEN_SLASH
+    specialCharacters[bytes.BYTE_LEFTWING] = tokens.TOKEN_LEFTWING
+    specialCharacters[bytes.BYTE_RIGHTWING] = tokens.TOKEN_RIGHTWING
+    specialCharacters[bytes.BYTE_CIRCUMFLEX] = tokens.TOKEN_CIRCUMFLEX
+    specialCharacters[bytes.BYTE_ASTERISK] = tokens.TOKEN_ASTERISK
+    -- WoW specific
+    specialCharacters[bytes.BYTE_VERTICAL] = -1
+    -- new as of lua 5.1
+    specialCharacters[bytes.BYTE_HASH] = tokens.TOKEN_HASH
+    specialCharacters[bytes.BYTE_PERCENT] = tokens.TOKEN_PERCENT
+
+    local function nextNumberExponentPartInt(text, pos)
+        while true do
+            local byte = stringbyte(text, pos)
+            if not byte then
+                return tokens.TOKEN_NUMBER, pos
+            end
+
+            if byte >= bytes.BYTE_0 and byte <= bytes.BYTE_9 then
+                pos = pos + 1
+            else
+                return tokens.TOKEN_NUMBER, pos
+            end
+        end
+    end
+
+    local function nextNumberExponentPart(text, pos)
+        local byte = stringbyte(text, pos)
+        if not byte then
+            return tokens.TOKEN_NUMBER, pos
+        end
+
+        if byte == bytes.BYTE_MINUS then
+            -- handle this case: a = 1.2e-- some comment
+            -- i decide to let 1.2e be parsed as a a number
+            byte = stringbyte(text, pos + 1)
+            if byte == bytes.BYTE_MINUS then
+                return tokens.TOKEN_NUMBER, pos
+            end
+            return nextNumberExponentPartInt(text, pos + 1)
+        end
+
+        return nextNumberExponentPartInt(text, pos)
+    end
+
+    local function nextNumberFractionPart(text, pos)
+        while true do
+            local byte = stringbyte(text, pos)
+            if not byte then
+                return tokens.TOKEN_NUMBER, pos
+            end
+
+            if byte >= bytes.BYTE_0 and byte <= bytes.BYTE_9 then
+                pos = pos + 1
+            elseif byte == bytes.BYTE_E or byte == bytes.BYTE_e then
+                return nextNumberExponentPart(text, pos + 1)
+            else
+                return tokens.TOKEN_NUMBER, pos
+            end
+        end
+    end
+
+    local function nextNumberIntPart(text, pos)
+        while true do
+            local byte = stringbyte(text, pos)
+            if not byte then
+                return tokens.TOKEN_NUMBER, pos
+            end
+
+            if byte >= bytes.BYTE_0 and byte <= bytes.BYTE_9 then
+                pos = pos + 1
+            elseif byte == bytes.BYTE_PERIOD then
+                return nextNumberFractionPart(text, pos + 1)
+            elseif byte == bytes.BYTE_E or byte == bytes.BYTE_e then
+                return nextNumberExponentPart(text, pos + 1)
+            else
+                return tokens.TOKEN_NUMBER, pos
+            end
+        end
+    end
+
+    local function nextIdentifier(text, pos)
+        while true do
+            local byte = stringbyte(text, pos)
+
+            if not byte or
+            linebreakCharacters[byte] or
+            whitespaceCharacters[byte] or
+            specialCharacters[byte] then
+                return tokens.TOKEN_IDENTIFIER, pos
+            end
+            pos = pos + 1
+        end
+    end
+
+    -- returns false or: true, nextPos, equalsCount
+    local function isBracketStringNext(text, pos)
+        local byte = stringbyte(text, pos)
+        if byte == bytes.BYTE_LEFTBRACKET then
+            local pos2 = pos + 1
+            byte = stringbyte(text, pos2)
+            while byte == bytes.BYTE_EQUALS do
+                pos2 = pos2 + 1
+                byte = stringbyte(text, pos2)
+            end
+            if byte == bytes.BYTE_LEFTBRACKET then
+                return true, pos2 + 1, (pos2 - 1) - pos
+            else
+                return false
+            end
+        else
+            return false
+        end
+    end
+
+    -- Already parsed the [==[ part when get here
+    local function nextBracketString(text, pos, equalsCount)
+        local state = 0
+        while true do
+            local byte = stringbyte(text, pos)
+            if not byte then
+                return tokens.TOKEN_STRING, pos
+            end
+
+            if byte == bytes.BYTE_RIGHTBRACKET then
+                if state == 0 then
+                    state = 1
+                elseif state == equalsCount + 1 then
+                    return tokens.TOKEN_STRING, pos + 1
+                else
+                    state = 0
+                end
+            elseif byte == bytes.BYTE_EQUALS then
+                if state > 0 then
+                    state = state + 1
+                end
+            else
+                state = 0
+            end
+            pos = pos + 1
+        end
+    end
+
+    local function nextComment(text, pos)
+        -- When we get here we have already parsed the "--"
+        -- Check for long comment
+        local isBracketString, nextPos, equalsCount = isBracketStringNext(text, pos)
+        if isBracketString then
+            local tokenType, nextPos2 = nextBracketString(text, nextPos, equalsCount)
+            return tokens.TOKEN_COMMENT_LONG, nextPos2
+        end
+
+        local byte = stringbyte(text, pos)
+
+        -- Short comment, find the first linebreak
+        while true do
+            byte = stringbyte(text, pos)
+            if not byte then
+                return tokens.TOKEN_COMMENT_SHORT, pos
+            end
+            if linebreakCharacters[byte] then
+                return tokens.TOKEN_COMMENT_SHORT, pos
+            end
+            pos = pos + 1
+        end
+    end
+
+    local function nextString(text, pos, character)
+        local even = true
+        while true do
+            local byte = stringbyte(text, pos)
+            if not byte then
+                return tokens.TOKEN_STRING, pos
+            end
+
+            if byte == character then
+                if even then
+                    return tokens.TOKEN_STRING, pos + 1
+                end
+            end
+            if byte == bytes.BYTE_BACKSLASH then
+                even = not even
+            else
+                even = true
+            end
+
+            pos = pos + 1
+        end
+    end
+
+    -- INPUT
+    -- 1: text: text to search in
+    -- 2: tokenPos:  where to start searching
+    -- OUTPUT
+    -- 1: token type
+    -- 2: position after the last character of the token
+    local function nextToken(text, pos)
+        local byte = stringbyte(text, pos)
+        if not byte then
+            return nil
+        end
+
+        if linebreakCharacters[byte] then
+            return tokens.TOKEN_LINEBREAK, pos + 1
+        end
+
+        if whitespaceCharacters[byte] then
+            while true do
+                pos = pos + 1
+                byte = stringbyte(text, pos)
+                if not byte or not whitespaceCharacters[byte] then
+                    return tokens.TOKEN_WHITESPACE, pos
+                end
+            end
+        end
+
+        local token = specialCharacters[byte]
+        if token then
+            if token ~= -1 then
+                return token, pos + 1
+            end
+
+            -- WoW specific (for color codes)
+            if byte == bytes.BYTE_VERTICAL then
+                byte = stringbyte(text, pos + 1)
+                if byte == bytes.BYTE_VERTICAL then
+                    return tokens.TOKEN_VERTICAL, pos + 2
+                end
+                if byte == bytes.BYTE_c then
+                    return tokens.TOKEN_COLORCODE_START, pos + 10
+                end
+                if byte == bytes.BYTE_r then
+                    return tokens.TOKEN_COLORCODE_STOP, pos + 2
+                end
+                return tokens.TOKEN_UNKNOWN, pos + 1
+            end
+
+            if byte == bytes.BYTE_MINUS then
+                byte = stringbyte(text, pos + 1)
+                if byte == bytes.BYTE_MINUS then
+                    return nextComment(text, pos + 2)
+                end
+                return tokens.TOKEN_MINUS, pos + 1
+            end
+
+            if byte == bytes.BYTE_SINGLE_QUOTE then
+                return nextString(text, pos + 1, bytes.BYTE_SINGLE_QUOTE)
+            end
+
+            if byte == bytes.BYTE_DOUBLE_QUOTE then
+                return nextString(text, pos + 1, bytes.BYTE_DOUBLE_QUOTE)
+            end
+
+            if byte == bytes.BYTE_LEFTBRACKET then
+                local isBracketString, nextPos, equalsCount = isBracketStringNext(text, pos)
+                if isBracketString then
+                    return nextBracketString(text, nextPos, equalsCount)
+                else
+                    return tokens.TOKEN_LEFTBRACKET, pos + 1
+                end
+            end
+
+            if byte == bytes.BYTE_EQUALS then
+                byte = stringbyte(text, pos + 1)
+                if not byte then
+                    return tokens.TOKEN_ASSIGNMENT, pos + 1
+                end
+                if byte == bytes.BYTE_EQUALS then
+                    return tokens.TOKEN_EQUALITY, pos + 2
+                end
+                return tokens.TOKEN_ASSIGNMENT, pos + 1
+            end
+
+            if byte == bytes.BYTE_PERIOD then
+                byte = stringbyte(text, pos + 1)
+                if not byte then
+                    return tokens.TOKEN_PERIOD, pos + 1
+                end
+                if byte == bytes.BYTE_PERIOD then
+                    byte = stringbyte(text, pos + 2)
+                    if byte == bytes.BYTE_PERIOD then
+                        return tokens.TOKEN_TRIPLEPERIOD, pos + 3
+                    end
+                    return tokens.TOKEN_DOUBLEPERIOD, pos + 2
+                elseif byte >= bytes.BYTE_0 and byte <= bytes.BYTE_9 then
+                    return nextNumberFractionPart(text, pos + 2)
+                end
+                return tokens.TOKEN_PERIOD, pos + 1
+            end
+
+            if byte == bytes.BYTE_LESSTHAN then
+                byte = stringbyte(text, pos + 1)
+                if byte == bytes.BYTE_EQUALS then
+                    return tokens.TOKEN_LTE, pos + 2
+                end
+                return tokens.TOKEN_LT, pos + 1
+            end
+
+            if byte == bytes.BYTE_GREATERTHAN then
+                byte = stringbyte(text, pos + 1)
+                if byte == bytes.BYTE_EQUALS then
+                    return tokens.TOKEN_GTE, pos + 2
+                end
+                return tokens.TOKEN_GT, pos + 1
+            end
+
+            if byte == bytes.BYTE_TILDE then
+                byte = stringbyte(text, pos + 1)
+                if byte == bytes.BYTE_EQUALS then
+                    return tokens.TOKEN_NOTEQUAL, pos + 2
+                end
+                return tokens.TOKEN_TILDE, pos + 1
+            end
+
+            return tokens.TOKEN_UNKNOWN, pos + 1
+        elseif byte >= bytes.BYTE_0 and byte <= bytes.BYTE_9 then
+            return nextNumberIntPart(text, pos + 1)
+        else
+            return nextIdentifier(text, pos + 1)
+        end
+    end
+
+    -- Cool stuff begins here! (indentation and highlighting)
+
+    local noIndentEffect = {0, 0}
+    local indentLeft = {-1, 0}
+    local indentRight = {0, 1}
+    local indentBoth = {-1, 1}
+
+    local keywords = {}
+    lib.keywords = keywords
+    keywords["and"] = noIndentEffect
+    keywords["break"] = noIndentEffect
+    keywords["false"] = noIndentEffect
+    keywords["for"] = noIndentEffect
+    keywords["if"] = noIndentEffect
+    keywords["in"] = noIndentEffect
+    keywords["local"] = noIndentEffect
+    keywords["nil"] = noIndentEffect
+    keywords["not"] = noIndentEffect
+    keywords["or"] = noIndentEffect
+    keywords["return"] = noIndentEffect
+    keywords["true"] = noIndentEffect
+    keywords["while"] = noIndentEffect
+
+    keywords["until"] = indentLeft
+    keywords["elseif"] = indentLeft
+    keywords["end"] = indentLeft
+
+    keywords["do"] = indentRight
+    keywords["then"] = indentRight
+    keywords["repeat"] = indentRight
+    keywords["function"] = indentRight
+
+    keywords["else"] = indentBoth
+
+    local tokenIndentation = {}
+    lib.tokenIndentation = tokenIndentation
+    tokenIndentation[tokens.TOKEN_LEFTPAREN] = indentRight
+    tokenIndentation[tokens.TOKEN_LEFTBRACKET] = indentRight
+    tokenIndentation[tokens.TOKEN_LEFTWING] = indentRight
+
+    tokenIndentation[tokens.TOKEN_RIGHTPAREN] = indentLeft
+    tokenIndentation[tokens.TOKEN_RIGHTBRACKET] = indentLeft
+    tokenIndentation[tokens.TOKEN_RIGHTWING] = indentLeft
+
+    local function fillWithTabs(n)
+        return stringrep("\t", n)
+    end
+
+    local function fillWithSpaces(a, b)
+        return stringrep(" ", a*b)
+    end
+
+    function lib.colorCodeCode(code, colorTable, caretPosition)
+        local stopColor = colorTable and colorTable[0]
+        if not stopColor then
+            return code, caretPosition
+        end
+
+        local stopColorLen = stringlen(stopColor)
+
+        tableclear(workingTable)
+        local tsize = 0
+        local totalLen = 0
+
+        local numLines = 0
+        local newCaretPosition
+        local prevTokenWasColored = false
+        local prevTokenWidth = 0
+
+        local pos = 1
+        local level = 0
+
+        while true do
+            if caretPosition and not newCaretPosition and pos >= caretPosition then
+                if pos == caretPosition then
+                    newCaretPosition = totalLen
+                else
+                    newCaretPosition = totalLen
+                    local diff = pos - caretPosition
+                    if diff > prevTokenWidth then
+                        diff = prevTokenWidth
+                    end
+                    if prevTokenWasColored then
+                        diff = diff + stopColorLen
+                    end
+                    newCaretPosition = newCaretPosition - diff
+                end
+            end
+
+            prevTokenWasColored = false
+            prevTokenWidth = 0
+
+            local tokenType, nextPos = nextToken(code, pos)
+
+            if not tokenType then
+                break
+            end
+
+            if tokenType == tokens.TOKEN_COLORCODE_START or tokenType == tokens.TOKEN_COLORCODE_STOP or tokenType == tokens.TOKEN_UNKNOWN then
+                -- ignore color codes
+            elseif tokenType == tokens.TOKEN_LINEBREAK or tokenType == tokens.TOKEN_WHITESPACE then
+                if tokenType == tokens.TOKEN_LINEBREAK then
+                    numLines = numLines + 1
+                end
+                local str = stringsub(code, pos, nextPos - 1)
+                prevTokenWidth = nextPos - pos
+
+                tsize = tsize + 1
+                workingTable[tsize] = str
+                totalLen = totalLen + stringlen(str)
+            else
+                local str = stringsub(code, pos, nextPos - 1)
+
+                prevTokenWidth = nextPos - pos
+
+                -- Add coloring
+                if keywords[str] then
+                    tokenType = tokens.TOKEN_KEYWORD
+                end
+
+                local color
+                if stopColor then
+                    color = colorTable[str]
+                    if not color then
+                        color = colorTable[tokenType]
+                        if not color then
+                            if tokenType == tokens.TOKEN_IDENTIFIER then
+                                color = colorTable[tokens.TOKEN_IDENTIFIER]
+                            else
+                                color = colorTable[tokens.TOKEN_SPECIAL]
+                            end
+                        end
+                    end
+                end
+
+                if color then
+                    tsize = tsize + 1
+                    workingTable[tsize] = color
+                    tsize = tsize + 1
+                    workingTable[tsize] = str
+                    tsize = tsize + 1
+                    workingTable[tsize] = stopColor
+
+                    totalLen = totalLen + stringlen(color) + (nextPos - pos) + stopColorLen
+                    prevTokenWasColored = true
+                else
+                    tsize = tsize + 1
+                    workingTable[tsize] = str
+
+                    totalLen = totalLen + stringlen(str)
+                end
+            end
+
+            pos = nextPos
+        end
+        return table.concat(workingTable), newCaretPosition, numLines
+    end
+
+    function lib.indentCode(code, tabWidth, colorTable, caretPosition)
+        local fillFunction
+        if tabWidth == nil then
+            tabWidth = defaultTabWidth
+        end
+        if tabWidth then
+            fillFunction = fillWithSpaces
+        else
+            fillFunction = fillWithTabs
+        end
+
+        tableclear(workingTable)
+        local tsize = 0
+        local totalLen = 0
+
+        tableclear(workingTable2)
+        local tsize2 = 0
+        local totalLen2 = 0
+
+        local stopColor = colorTable and colorTable[0]
+        local stopColorLen = not stopColor or stringlen(stopColor)
+
+        local newCaretPosition
+        local newCaretPositionFinalized = false
+        local prevTokenWasColored = false
+        local prevTokenWidth = 0
+
+        local pos = 1
+        local level = 0
+
+        local hitNonWhitespace = false
+        local hitIndentRight = false
+        local preIndent = 0
+        local postIndent = 0
+        while true do
+            if caretPosition and not newCaretPosition and pos >= caretPosition then
+                if pos == caretPosition then
+                    newCaretPosition = totalLen + totalLen2
+                else
+                    newCaretPosition = totalLen + totalLen2
+                    local diff = pos - caretPosition
+                    if diff > prevTokenWidth then
+                        diff = prevTokenWidth
+                    end
+                    if prevTokenWasColored then
+                        diff = diff + stopColorLen
+                    end
+                    newCaretPosition = newCaretPosition - diff
+                end
+            end
+
+            prevTokenWasColored = false
+            prevTokenWidth = 0
+
+            local tokenType, nextPos = nextToken(code, pos)
+
+            if not tokenType or tokenType == tokens.TOKEN_LINEBREAK then
+                level = level + preIndent
+                if level < 0 then level = 0 end
+
+                local s = fillFunction(level, tabWidth)
+
+                tsize = tsize + 1
+                workingTable[tsize] = s
+                totalLen = totalLen + stringlen(s)
+
+                if newCaretPosition and not newCaretPositionFinalized then
+                    newCaretPosition = newCaretPosition + stringlen(s)
+                    newCaretPositionFinalized = true
+                end
+
+                for k, v in next,workingTable2 do
+                    tsize = tsize + 1
+                    workingTable[tsize] = v
+                    totalLen = totalLen + stringlen(v)
+                end
+
+                if not tokenType then
+                    break
+                end
+
+                tsize = tsize + 1
+                workingTable[tsize] = stringsub(code, pos, nextPos - 1)
+                totalLen = totalLen + nextPos - pos
+
+                level = level + postIndent
+                if level < 0 then level = 0 end
+
+                tableclear(workingTable2)
+                tsize2 = 0
+                totalLen2 = 0
+
+                hitNonWhitespace = false
+                hitIndentRight = false
+                preIndent = 0
+                postIndent = 0
+            elseif tokenType == tokens.TOKEN_WHITESPACE then
+                if hitNonWhitespace then
+                    prevTokenWidth = nextPos - pos
+                    tsize2 = tsize2 + 1
+                    local s = stringsub(code, pos, nextPos - 1)
+                    workingTable2[tsize2] = s
+                    totalLen2 = totalLen2 + stringlen(s)
+                end
+            elseif tokenType == tokens.TOKEN_COLORCODE_START or tokenType == tokens.TOKEN_COLORCODE_STOP or tokenType == tokens.TOKEN_UNKNOWN then
+                -- skip these, though they shouldn't be encountered here anyway
+            else
+                hitNonWhitespace = true
+                local str = stringsub(code, pos, nextPos - 1)
+                prevTokenWidth = nextPos - pos
+
+                -- See if this is an indent-modifier
+                local indentTable
+                if tokenType == tokens.TOKEN_IDENTIFIER then
+                    indentTable = keywords[str]
+                else
+                    indentTable = lib.tokenIndentation[tokenType]
+                end
+
+                if indentTable then
+                    if hitIndentRight then
+                        postIndent = postIndent + indentTable[1] + indentTable[2]
+                    else
+                        local pre = indentTable[1]
+                        local post = indentTable[2]
+                        if post > 0 then
+                            hitIndentRight = true
+                        end
+                        preIndent = preIndent + pre
+                        postIndent = postIndent + post
+                    end
+                end
+
+                -- Add coloring
+                if keywords[str] then
+                    tokenType = tokens.TOKEN_KEYWORD
+                end
+
+                local color
+                if stopColor then
+                    color = colorTable[str]
+                    if not color then
+                        color = colorTable[tokenType]
+                        if not color then
+                            if tokenType == tokens.TOKEN_IDENTIFIER then
+                                color = colorTable[tokens.TOKEN_IDENTIFIER]
+                            else
+                                color = colorTable[tokens.TOKEN_SPECIAL]
+                            end
+                        end
+                    end
+                end
+
+                if color then
+                    tsize2 = tsize2 + 1
+                    workingTable2[tsize2] = color
+                    totalLen2 = totalLen2 + stringlen(color)
+
+                    tsize2 = tsize2 + 1
+                    workingTable2[tsize2] = str
+                    totalLen2 = totalLen2 + nextPos - pos
+
+                    tsize2 = tsize2 + 1
+                    workingTable2[tsize2] = stopColor
+                    totalLen2 = totalLen2 + stopColorLen
+
+                    prevTokenWasColored = true
+                else
+                    tsize2 = tsize2 + 1
+                    workingTable2[tsize2] = str
+                    totalLen2 = totalLen2 + nextPos - pos
+
+                end
+            end
+            pos = nextPos
+        end
+        return table.concat(workingTable), newCaretPosition
+    end
+
+    -- WoW specific code:
+    local GetTime = GetTime
+
+    local editboxSetText
+    local editboxGetText
+
+    -- Caret code (thanks Tem!)
+    local function critical_enter(editbox)
+        local script = editbox:GetScript("OnTextSet")
+        if script then
+            editbox:SetScript("OnTextSet", nil)
+        end
+        return script
+    end
+
+    local function critical_leave(editbox, script)
+        if script then
+            editbox:SetScript("OnTextSet", script)
+        end
+    end
+
+    local function setCaretPos_main(editbox, pos)
+        local text = editboxGetText(editbox)
+
+        if stringlen(text) > 0 then
+            editboxSetText(editbox, stringinsert(text, pos, "a"))
+            editbox:HighlightText(pos, pos + 1)
+            editbox:Insert("\0")
+        end
+    end
+
+    local function getCaretPos(editbox)
+        local script = critical_enter(editbox)
+
+        local text = editboxGetText(editbox)
+        editbox:Insert("\1")
+        local pos = stringfind(editboxGetText(editbox), "\1", 1, 1)
+        editboxSetText(editbox, text)
+
+        if pos then
+            setCaretPos_main(editbox, pos - 1)
+        end
+        critical_leave(editbox, script)
+
+        return (pos or 0) - 1
+    end
+
+    local function setCaretPos(editbox, pos)
+        local script, script2 = critical_enter(editbox)
+        setCaretPos_main(editbox, pos)
+        critical_leave(editbox, script, script2)
+    end
+    -- end of caret code
+
+    function lib.stripWowColors(code)
+
+        -- HACK!
+        -- This is a fix for a bug, where an unfinished string causes a lot of newlines to be created.
+        -- The reason for the bug, is that a |r\n\n gets converted to \n\n|r after the next indent-run
+        -- The fix is to remove those last two linebreaks when stripping
+        code = stringgsub(code, "|r\n\n$", "|r")
+
+        tableclear(workingTable)
+        local tsize = 0
+
+        local pos = 1
+
+        local prevVertical = false
+        local even = true
+        local selectionStart = 1
+
+        while true do
+            local byte = stringbyte(code, pos)
+            if not byte then
+                break
+            end
+            if byte == bytes.BYTE_VERTICAL then
+                even = not even
+                prevVertical = true
+            else
+                if prevVertical and not even then
+                    if byte == bytes.BYTE_c then
+
+                        if pos - 2 >= selectionStart then
+                            tsize = tsize + 1
+                            workingTable[tsize] = stringsub(code, selectionStart, pos - 2)
+                        end
+
+                        pos = pos + 8
+                        selectionStart = pos + 1
+                    elseif byte == bytes.BYTE_r then
+
+                        if pos - 2 >= selectionStart then
+                            tsize = tsize + 1
+                            workingTable[tsize] = stringsub(code, selectionStart, pos - 2)
+                        end
+                        selectionStart = pos + 1
+                    end
+                end
+                prevVertical = false
+                even = true
+            end
+            pos = pos + 1
+        end
+        if pos >= selectionStart then
+            tsize = tsize + 1
+            workingTable[tsize] = stringsub(code, selectionStart, pos - 1)
+        end
+        return table.concat(workingTable)
+    end
+
+    function lib.decode(code)
+        if code then
+            code = lib.stripWowColors(code)
+            code = stringgsub(code, "||", "|")
+        end
+        return code or ""
+    end
+
+    function lib.encode(code)
+        if code then
+            code = stringgsub(code, "|", "||")
+        end
+        return code or ""
+    end
+
+    function lib.stripWowColorsWithPos(code, pos)
+        code = stringinsert(code, pos, "\2")
+        code = lib.stripWowColors(code)
+        pos = stringfind(code, "\2", 1, 1)
+        code = stringdelete(code, pos, pos)
+        return code, pos
+    end
+
+    -- returns the padded code, and true if modified, false if unmodified
+    local linebreak = stringbyte("\n")
+    function lib.padWithLinebreaks(code)
+        local len = stringlen(code)
+        if stringbyte(code, len) == linebreak then
+            if stringbyte(code, len - 1) == linebreak then
+                return code, false
+            end
+            return code .. "\n", true
+        end
+        return code .. "\n\n", true
+
+    end
+
+    -- Data tables
+    -- No weak table magic, since editboxes can never be removed in WoW
+    local enabled = {}
+    local dirty = {}
+
+    local editboxIndentCache = {}
+    local decodeCache = {}
+    local editboxStringCache = {}
+    local editboxNumLinesCache = {}
+
+    function lib.colorCodeEditbox(editbox)
+        dirty[editbox] = nil
+
+        local colorTable = editbox.faiap_colorTable or defaultColorTable
+        local tabWidth = editbox.faiap_tabWidth
+
+        local orgCode = editboxGetText(editbox)
+        local prevCode = editboxStringCache[editbox]
+        if prevCode == orgCode then
+            return
+        end
+
+        local pos = getCaretPos(editbox)
+
+        local code
+        code, pos = lib.stripWowColorsWithPos(orgCode, pos)
+
+        colorTable[0] = "|r"
+
+        local newCode, newPos, numLines = lib.colorCodeCode(code, colorTable, pos)
+        newCode = lib.padWithLinebreaks(newCode)
+
+        editboxStringCache[editbox] = newCode
+        if orgCode ~= newCode then
+            local script, script2 = critical_enter(editbox)
+            decodeCache[editbox] = nil
+            local stringlenNewCode = stringlen(newCode)
+
+            editboxSetText(editbox, newCode)
+            if newPos then
+                if newPos < 0 then newPos = 0 end
+                if newPos > stringlenNewCode then newPos = stringlenNewCode end
+
+                setCaretPos(editbox, newPos)
+            end
+            critical_leave(editbox, script, script2)
+        end
+
+        if editboxNumLinesCache[editbox] ~= numLines then
+            lib.indentEditbox(editbox)
+        end
+        editboxNumLinesCache[editbox] = numLines
+    end
+
+    function lib.indentEditbox(editbox)
+        dirty[editbox] = nil
+
+        local colorTable = editbox.faiap_colorTable or defaultColorTable
+        local tabWidth = editbox.faiap_tabWidth
+
+        local orgCode = editboxGetText(editbox)
+        local prevCode = editboxIndentCache[editbox]
+        if prevCode == orgCode then
+            return
+        end
+
+        local pos = getCaretPos(editbox)
+
+        local code
+        code, pos = lib.stripWowColorsWithPos(orgCode, pos)
+
+        colorTable[0] = "|r"
+        local newCode, newPos = lib.indentCode(code, tabWidth, colorTable, pos)
+        newCode = lib.padWithLinebreaks(newCode)
+        editboxIndentCache[editbox] = newCode
+        if code ~= newCode then
+            local script, script2 = critical_enter(editbox)
+            decodeCache[editbox] = nil
+
+            local stringlenNewCode = stringlen(newCode)
+
+            editboxSetText(editbox, newCode)
+
+            if newPos then
+                if newPos < 0 then newPos = 0 end
+                if newPos > stringlenNewCode then newPos = stringlenNewCode end
+
+                setCaretPos(editbox, newPos)
+            end
+            critical_leave(editbox, script, script2)
+        end
+    end
+
+    local function hookHandler(editbox, handler, newFun)
+        local oldFun = editbox:GetScript(handler)
+        if oldFun == newFun then
+            -- already hooked, ignore it
+            return
+        end
+        editbox["faiap_old_" .. handler] = oldFun
+        editbox:SetScript(handler, newFun)
+    end
+
+    local function textChangedHook(editbox, ...)
+        local oldFun = editbox["faiap_old_OnTextChanged"]
+        if oldFun then
+            oldFun(editbox, ...)
+        end
+        if enabled[editbox] then
+            dirty[editbox] = GetTime()
+        end
+    end
+
+    local function tabPressedHook(editbox, ...)
+        local oldFun = editbox["faiap_old_OnTabPressed"]
+        if oldFun then
+            oldFun(editbox, ...)
+        end
+        if enabled[editbox] then
+            return lib.indentEditbox(editbox)
+        end
+    end
+
+    local function onUpdateHook(editbox, ...)
+        local oldFun = editbox["faiap_old_OnUpdate"]
+        if oldFun then
+            oldFun(editbox, ...)
+        end
+        if enabled[editbox] then
+            local now = GetTime()
+            local lastUpdate = dirty[editbox] or now
+            if now - lastUpdate > 0.2 then
+                decodeCache[editbox] = nil
+                return lib.colorCodeEditbox(editbox)
+            end
+        end
+    end
+
+    local function newGetText(editbox)
+        local decoded = decodeCache[editbox]
+        if not decoded then
+            decoded = lib.decode(editboxGetText(editbox))
+            decodeCache[editbox] = decoded
+        end
+        return decoded or ""
+    end
+
+    local function newSetText(editbox, text)
+        decodeCache[editbox] = nil
+        if text then
+            local encoded = lib.encode(text)
+
+            return editboxSetText(editbox, encoded)
+        end
+    end
+
+    function lib.enable(editbox, colorTable, tabWidth)
+        if not editboxSetText then
+            editboxSetText = editbox.SetText
+            editboxGetText = editbox.GetText
+        end
+
+        local modified
+        if editbox.faiap_colorTable ~= colorTable then
+            editbox.faiap_colorTable = colorTable
+            modified = true
+        end
+        if editbox.faiap_tabWidth ~= tabWidth then
+            editbox.faiap_tabWidth = tabWidth
+            modified = true
+        end
+
+        if enabled[editbox] then
+            if modified then
+                lib.indentEditbox(editbox)
+            end
+            return
+        end
+
+        -- Editbox is possibly hooked, but disabled
+        enabled[editbox] = true
+
+        editbox.oldMaxBytes = editbox:GetMaxBytes()
+        editbox.oldMaxLetters = editbox:GetMaxLetters()
+        editbox:SetMaxBytes(0)
+        editbox:SetMaxLetters(0)
+
+        editbox.GetText = newGetText
+        editbox.SetText = newSetText
+
+        hookHandler(editbox, "OnTextChanged", textChangedHook)
+        hookHandler(editbox, "OnTabPressed", tabPressedHook)
+        hookHandler(editbox, "OnUpdate", onUpdateHook)
+
+        lib.indentEditbox(editbox)
+    end
+
+    -- Deprecated function
+    lib.addSmartCode = lib.enable
+
+    function lib.disable(editbox)
+        if not enabled[editbox] then
+            return
+        end
+        enabled[editbox] = nil
+
+        -- revert settings for max bytes / letters
+        editbox:SetMaxBytes(editbox.oldMaxBytes)
+        editbox:SetMaxLetters(editbox.oldMaxLetters)
+
+        -- try a real unhooking, if possible
+        if editbox:GetScript("OnTextChanged") == textChangedHook then
+            editbox:SetScript("OnTextChanged", editbox.faiap_old_OnTextChanged)
+            editbox.faiap_old_OnTextChanged = nil
+        end
+
+        if editbox:GetScript("OnTabPressed") == tabPressedHook then
+            editbox:SetScript("OnTabPressed", editbox.faiap_old_OnTabPressed)
+            editbox.faiap_old_OnTabPressed = nil
+        end
+
+        if editbox:GetScript("OnUpdate") == onUpdateHook then
+            editbox:SetScript("OnUpdate", editbox.faiap_old_OnUpdate)
+            editbox.faiap_old_OnUpdate = nil
+        end
+
+        editbox.GetText = nil
+        editbox.SetText = nil
+
+        -- change the text back to unformatted
+        editbox:SetText(newGetText(editbox))
+
+        -- clear caches
+        editboxIndentCache[editbox] = nil
+        decodeCache[editbox] = nil
+        editboxStringCache[editbox] = nil
+        editboxNumLinesCache[editbox] = nil
+    end
+
+    defaultColorTable = {}
+    lib.defaultColorTable = defaultColorTable
+    defaultColorTable[tokens.TOKEN_SPECIAL] = "|c00ff99ff"
+    defaultColorTable[tokens.TOKEN_KEYWORD] = "|c006666ff"
+    defaultColorTable[tokens.TOKEN_COMMENT_SHORT] = "|c00999999"
+    defaultColorTable[tokens.TOKEN_COMMENT_LONG] = "|c00999999"
+
+    local stringColor = "|c00ffff77"
+    defaultColorTable[tokens.TOKEN_STRING] = stringColor
+    defaultColorTable[".."] = stringColor
+
+    local tableColor = "|c00ff9900"
+    defaultColorTable["..."] = tableColor
+    defaultColorTable["{"] = tableColor
+    defaultColorTable["}"] = tableColor
+    defaultColorTable["["] = tableColor
+    defaultColorTable["]"] = tableColor
+
+    local arithmeticColor = "|c0033ff55"
+    defaultColorTable[tokens.TOKEN_NUMBER] = arithmeticColor
+    defaultColorTable["+"] = arithmeticColor
+    defaultColorTable["-"] = arithmeticColor
+    defaultColorTable["/"] = arithmeticColor
+    defaultColorTable["*"] = arithmeticColor
+
+    local logicColor1 = "|c0055ff88"
+    defaultColorTable["=="] = logicColor1
+    defaultColorTable["<"] = logicColor1
+    defaultColorTable["<="] = logicColor1
+    defaultColorTable[">"] = logicColor1
+    defaultColorTable[">="] = logicColor1
+    defaultColorTable["~="] = logicColor1
+
+    local logicColor2 = "|c0088ffbb"
+    defaultColorTable["and"] = logicColor2
+    defaultColorTable["or"] = logicColor2
+    defaultColorTable["not"] = logicColor2
+
+    defaultColorTable[0] = "|r"
+
+end
+
+-- just for testing
+--[[
+function testTokenizer()
+  local str = ""
+  for line in io.lines("indent.lua") do
+   str = str .. line .. "\n"
+  end
+
+  local pos = 1
+
+  while true do
+   local tokenType, nextPos = nextToken(str, pos)
+
+   if not tokenType then
+  break
+   end
+
+   if true or tokenType ~= tokens.TOKEN_WHITESPACE and tokenType ~= tokens.TOKEN_LINEBREAK then
+  print(stringformat("Found token %d (%d-%d): (%s)", tokenType, pos, nextPos - 1, stringsub(str, pos, nextPos - 1)))
+   end
+
+   if tokenType == tokens.TOKEN_UNKNOWN then
+  print("unknown token!")
+  break
+   end
+
+   pos = nextPos
+  end
+end
+
+
+function testIndenter(i)
+  local lib = IndentationLib
+  local str = ""
+  for line in io.lines("test.lua") do
+   str = str .. line .. "\n"
+  end
+
+  local colorTable = lib.defaultColorTable
+  print(lib.indentCode(str, 4, colorTable, i))
+end
+
+
+testIndenter()
+
+--]]
diff --git a/MaxDps.toc b/MaxDps.toc
index 0985270..fe88d0f 100644
--- a/MaxDps.toc
+++ b/MaxDps.toc
@@ -1,8 +1,8 @@
 ## Title: MaxDps
 ## Notes: Rotation helper framework.
-## Version: 7.1.4.3
+## Version: 7.2.5
 ## Author: Kaminaris
-## Interface: 70100
+## Interface: 70200
 ## SavedVariables: MaxDpsOptions
 ## OptionalDependencies: Bartender4, ElvUI, ButtonForge, SVUI_ActionBars

@@ -17,8 +17,10 @@ Libs\AceDB-3.0\AceDB-3.0.xml
 Libs\AceGUI-3.0\AceGUI-3.0.xml
 Libs\AceGUI-3.0-SharedMediaWidgets\widget.xml
 Libs\AceConfig-3.0\AceConfig-3.0.xml
+Libs\ForAllIndentsAndPurposes.lua

 core.lua
 buttons.lua
 helper.lua
-timetodie.lua
\ No newline at end of file
+timetodie.lua
+custom.lua
\ No newline at end of file
diff --git a/core.lua b/core.lua
index 85639f2..992649d 100644
--- a/core.lua
+++ b/core.lua
@@ -171,6 +171,11 @@ function MaxDps:OnInitialize()
 	LibStub('AceConfig-3.0'):RegisterOptionsTable('MaxDps', options, {'/maxdps'});
 	self.db = LibStub('AceDB-3.0'):New('MaxDpsOptions', defaultOptions);
 	self.optionsFrame = LibStub('AceConfigDialog-3.0'):AddToBlizOptions('MaxDps', 'MaxDps');
+	self:RegisterChatCommand('maxdps', 'ShowCustomWindow');
+
+	if not self.db.global.customRotations then
+		self.db.global.customRotations = {};
+	end
 end

 MaxDps.DefaultPrint = MaxDps.Print;
@@ -191,6 +196,7 @@ function MaxDps:EnableRotation()
 	self:Print(self.Colors.Info .. 'Fetching');
 	self.Fetch();

+	MaxDps:CheckTalents();
 	if self.ModuleOnEnable then
 		self.ModuleOnEnable();
 	end
@@ -259,7 +265,8 @@ function MaxDps:UNIT_ENTERED_VEHICLE(event, unit)
 end

 function MaxDps:UNIT_EXITED_VEHICLE(event, unit)
-	if unit == 'player' and self.ModuleLoaded then
+	if unit == 'player' then
+		self:InitRotations();
 		self:EnableRotation();
 	end
 end
@@ -281,8 +288,7 @@ end
 function MaxDps:PLAYER_REGEN_DISABLED()
 	if self.db.global.onCombatEnter and not self.rotationEnabled then
 		self:Print(self.Colors.Success .. 'Auto enable on combat!');
-		self:LoadModule();
-		self:CheckSpecialization();
+		self:InitRotations();
 		self:EnableRotation();
 	end
 end
@@ -310,7 +316,8 @@ function MaxDps:InvokeNextSpell()
 	-- invoke spell check
 	local oldSkill = self.Spell;

-	self.Spell = self:NextSpell();
+	local timeShift, currentSpell, gcd = MaxDps:EndCast();
+	self.Spell = self:NextSpell(timeShift, currentSpell, gcd, self.PlayerTalents);

 	if (oldSkill ~= self.Spell or oldSkill == nil) and self.Spell ~= nil then
 		self:GlowNextSpellId(self.Spell);
@@ -320,40 +327,48 @@ function MaxDps:InvokeNextSpell()
 	end
 end

-function MaxDps:LoadModule()
-	if self.ModuleLoaded then
-		return;
-	end
+function MaxDps:InitRotations()
+	self:Print(self.Colors.Info .. 'Initializing rotations');

-	self:Print(self.Colors.Info .. 'Loading class module');
 	local _, _, classId = UnitClass('player');
-	if self.Classes[classId] == nil then
-		self:Print(_tdError, 'Invalid player class, please contact author of addon.');
+	local spec = GetSpecialization();
+	self.ClassId = classId;
+	self.Spec = spec;
+
+	self:LoadCustomRotations();
+	if self.CustomRotations[classId] and self.CustomRotations[classId][spec] then
+		self.CurrentRotation = self.CustomRotations[classId][spec];
+		self.NextSpell = self.CurrentRotation.fn;
+		self:Print(self.Colors.Success .. 'Loaded Custom Rotation: ' .. self.CurrentRotation.name);
+	else
+		self:LoadModule();
+	end
+end
+
+function MaxDps:LoadModule()
+	if self.Classes[self.ClassId] == nil then
+		self:Print(self.Colors.Error .. 'Invalid player class, please contact author of addon.');
 		return;
 	end

-	local module = 'MaxDps_' .. self.Classes[classId];
+	local module = 'MaxDps_' .. self.Classes[self.ClassId];
+	local _, _, _, loadable, reason = GetAddOnInfo(module);

-	if not IsAddOnLoaded(module) then
-		LoadAddOn(module);
+	if IsAddOnLoaded(module) then
+		self:Print(self.Colors.Info .. self.Description);
+		self:EnableRotationModule(self.Spec);
+		self:Print(self.Colors.Info .. 'Finished Loading class module');
+		return;
 	end

-	if not IsAddOnLoaded(module) then
-		self:Print(self.Colors.Error .. 'Could not find class module.');
+	if reason == 'MISSING' or reason == 'DISABLED' then
+		self:Print(self.Colors.Error .. 'Could not find class module ' .. module .. ' or it was disabled.');
 		return;
 	end

-	local mode = GetSpecialization();
+	LoadAddOn(module);

-	self:EnableRotationModule(mode);
+	self:EnableRotationModule(self.Spec);
 	self:Print(self.Colors.Info .. self.Description);
-
 	self:Print(self.Colors.Info .. 'Finished Loading class module');
-	self.ModuleLoaded = true;
-end
-
-function MaxDps:CheckSpecialization()
-	local mode = GetSpecialization();
-
-	self:EnableRotationModule(mode);
 end
\ No newline at end of file
diff --git a/custom.lua b/custom.lua
new file mode 100644
index 0000000..0408224
--- /dev/null
+++ b/custom.lua
@@ -0,0 +1,412 @@
+local SharedMedia = LibStub('LibSharedMedia-3.0');
+
+MaxDps.Specs = {
+	[1] = {
+		[1] = 'Arms',
+		[2] = 'Fury',
+		[3] = 'Protection',
+	},
+	[2] = {
+		[1] = 'Holy',
+		[2] = 'Protection',
+		[3] = 'Retribution',
+	},
+	[3] = {
+		[1] = 'BeastMastery',
+		[2] = 'Marksmanship',
+		[3] = 'Survival',
+	},
+	[4] = {
+		[1] = 'Assassination',
+		[2] = 'Outlaw',
+		[3] = 'Subtlety',
+	},
+	[5] = {
+		[1] = 'Discipline',
+		[2] = 'Holy',
+		[3] = 'Shadow',
+	},
+	[6] = {
+		[1] = 'Blood',
+		[2] = 'Frost',
+		[3] = 'Unholy',
+	},
+	[7] = {
+		[1] = 'Elemental',
+		[2] = 'Enhancement',
+		[3] = 'Restoration',
+	},
+	[8] = {
+		[1] = 'Arcane',
+		[2] = 'Fire',
+		[3] = 'Frost',
+	},
+	[9] = {
+		[1] = 'Affliction',
+		[2] = 'Demonology',
+		[3] = 'Destruction',
+	},
+	[10] = {
+		[1] = 'Brewmaster',
+		[2] = 'Mistweaver',
+		[3] = 'Windwalker',
+	},
+	[11] = {
+		[1] = 'Balance',
+		[2] = 'Feral',
+		[3] = 'Guardian',
+		[4] = 'Restoration',
+	},
+	[12] = {
+		[1] = 'Havoc',
+		[2] = 'Vengeance',
+	},
+}
+
+MaxDps.CustomRotations = {};
+
+StaticPopupDialogs['REMOVE_MAXDPS_ROTATION'] = {
+	text = 'Are you sure?',
+	button1 = 'Yes',
+	button2 = 'No',
+	OnAccept = function()
+		MaxDps:RemoveCustomRotation();
+	end,
+	OnCancel = function (_,reason)
+	end,
+	whileDead = true,
+	hideOnEscape = true,
+}
+
+AceGUI:RegisterLayout('2Columns3', function(content, children)
+	if children[1] then
+		children[1]:SetWidth(200)
+		children[1].frame:SetPoint('TOPLEFT', content, 'TOPLEFT', 0, 0)
+		children[1].frame:SetPoint('BOTTOMLEFT', content, 'BOTTOMLEFT', 0, 0)
+		children[1].frame:Show();
+
+		if children[1].DoLayout then
+			children[1]:DoLayout()
+		end
+	end
+
+	if children[2] then
+		children[2].frame:SetPoint('TOPLEFT', children[1].frame, 'TOPRIGHT', 0, 0)
+		children[2].frame:SetPoint('RIGHT', content, 'RIGHT', 0, 0)
+		children[2]:SetHeight(100)
+		children[2].frame:Show();
+
+		if children[2].DoLayout then
+			children[2]:DoLayout()
+		end
+	end
+
+	if children[3] then
+		children[3].frame:SetPoint('TOPLEFT', children[2].frame, 'BOTTOMLEFT', 0, 0)
+		children[3].frame:SetPoint('BOTTOMRIGHT', content, 'BOTTOMRIGHT', 0, 0)
+		children[3].frame:Show();
+
+		if children[3].DoLayout then
+			children[3]:DoLayout()
+		end
+	end
+
+	if(content.obj.LayoutFinished) then
+		content.obj:LayoutFinished(content.obj, nil, nil);
+	end
+end)
+
+function MaxDps:ShowCustomWindow()
+	if not self.CustomWindow then
+		self.CustomWindow = AceGUI:Create('Window');
+		self.CustomWindow:SetTitle('MaxDps Custom Rotations');
+		self.CustomWindow.frame:SetFrameStrata('DIALOG');
+		self.CustomWindow:SetLayout('2Columns3');
+		self.CustomWindow:SetWidth(700);
+		self.CustomWindow:SetHeight(550);
+		self.CustomWindow:EnableResize(true);
+		self.CustomWindow:SetCallback('OnClose', function(widget)
+			MaxDps:LoadCustomRotations();
+		end)
+
+		local scrollLeft = AceGUI:Create('ScrollFrame');
+		scrollLeft:SetLayout('Flow');
+		self.CustomWindow.scrollLeft = scrollLeft;
+		self.CustomWindow:AddChild(scrollLeft);
+
+		local scrollRight = AceGUI:Create('ScrollFrame');
+		scrollRight:SetLayout('Flow');
+		self.CustomWindow:AddChild(scrollRight);
+
+--		Rotation Name
+		local rotationName = AceGUI:Create('EditBox');
+		rotationName:SetLabel('Rotation Name');
+		rotationName:SetCallback('OnTextChanged', function(self, event, text)
+			if not MaxDps.CurrentEditRotation then return end;
+			MaxDps.CurrentEditRotation.name = text;
+			MaxDps:UpdateCustomRotationButtons();
+		end);
+		scrollRight:AddChild(rotationName);
+		self.CustomWindow.rotationName = rotationName;
+
+--		Rotation Class
+		local rotationClass = AceGUI:Create('Dropdown');
+		rotationClass:SetLabel('Class');
+		rotationClass:SetList(MaxDps.Classes);
+		rotationClass:SetCallback('OnValueChanged', function(self, event, key)
+			if not MaxDps.CurrentEditRotation then return end;
+			MaxDps.CurrentEditRotation.class = key;
+			local specs = MaxDps.Specs[key];
+			if specs then
+				MaxDps.CustomWindow.rotationSpec:SetList(specs);
+			end
+		end);
+		scrollRight:AddChild(rotationClass);
+		self.CustomWindow.rotationClass = rotationClass;
+
+--		Rotation Spec
+		local rotationSpec = AceGUI:Create('Dropdown');
+		rotationSpec:SetLabel('Spec');
+		rotationSpec:SetCallback('OnValueChanged', function(self, event, key)
+			if not MaxDps.CurrentEditRotation then return end;
+			MaxDps.CurrentEditRotation.spec = key;
+		end);
+		scrollRight:AddChild(rotationSpec);
+		self.CustomWindow.rotationSpec = rotationSpec;
+
+--		Rotation Enabled
+		local rotationEnabled = AceGUI:Create('CheckBox');
+		rotationEnabled:SetLabel('Enabled');
+		rotationEnabled:SetCallback('OnValueChanged', function(self, event, val)
+			if not MaxDps.CurrentEditRotation then return end;
+			MaxDps.CurrentEditRotation.enabled = val;
+		end);
+		scrollRight:AddChild(rotationEnabled);
+		self.CustomWindow.rotationEnabled = rotationEnabled;
+
+--		Rotation Delete
+		local rotationDelete = AceGUI:Create('Button');
+		rotationDelete:SetText('Remove');
+		rotationDelete:SetCallback('OnClick', function()
+			if not MaxDps.CurrentEditRotation then return end;
+			StaticPopup_Show('REMOVE_MAXDPS_ROTATION');
+		end);
+		scrollRight:AddChild(rotationDelete);
+
+--		Editor
+		local editor = AceGUI:Create('MultiLineEditBox');
+		editor:SetLabel('Custom Rotation');
+		editor.button:Hide();
+		local fontPath = SharedMedia:Fetch('font', 'Fira Mono Medium');
+		if(fontPath) then
+			editor.editBox:SetFont(fontPath, 12);
+		end
+		editor:SetCallback('OnTextChanged', function(self, event, value)
+			if not MaxDps.CurrentEditRotation then return end;
+			value = IndentationLib.decode(value);
+			if MaxDps.CurrentEditRotation then
+				MaxDps.CurrentEditRotation.fn = value;
+			end
+		end);
+		self.CustomWindow:AddChild(editor);
+		self.CustomWindow.editor = editor;
+
+		IndentationLib.enable(editor.editBox, nil, 4);
+
+		self:UpdateCustomRotationButtons();
+		self:EnableDisableCustomFields(true, true);
+	end
+	self:DisableRotation();
+	self.CustomWindow:Show();
+end
+
+function MaxDps:UpdateCustomRotationButtons()
+	self.CustomWindow.scrollLeft:ReleaseChildren();
+
+	local btn = AceGUI:Create('Button');
+
+	btn:SetFullWidth(true);
+	btn:SetText('Add Rotation');
+	btn:SetHeight(40);
+	btn.text:SetTextColor(1, 0, 0);
+	btn:SetCallback('OnClick', function()
+		MaxDps:AddCustomRotation();
+	end);
+
+	self.CustomWindow.scrollLeft:AddChild(btn);
+
+	for k, rotation in pairs(self.db.global.customRotations) do
+		local btn = AceGUI:Create('Button');
+
+		btn:SetFullWidth(true);
+		btn:SetText(rotation.name);
+		btn:SetHeight(40);
+		btn:SetCallback('OnClick', function(self, event)
+			for k, btn in pairs(MaxDps.CustomWindow.scrollLeft.children) do
+				if k > 1 then
+					btn.text:SetTextColor(1, 1, 1);
+				end
+			end
+			self.text:SetTextColor(0, 1, 0);
+			MaxDps:EditRotation(rotation);
+		end);
+		if self.CurrentEditRotation == rotation then
+			btn.text:SetTextColor(0, 1, 0);
+		else
+			btn.text:SetTextColor(1, 1, 1);
+		end
+		self.CustomWindow.scrollLeft:AddChild(btn);
+	end
+	self.CustomWindow.scrollLeft:DoLayout();
+end
+
+function MaxDps:AddCustomRotation()
+	local customRotation = {
+		name = 'New Rotation',
+		enabled = false,
+		class = nil,
+		spec = nil,
+		fn = "function(_, timeShift, currentSpell, gcd, talents)\n    \nend",
+	};
+
+	tinsert(self.db.global.customRotations, customRotation);
+	self:UpdateCustomRotationButtons();
+	MaxDps:EditRotation(customRotation);
+end
+
+function MaxDps:RemoveCustomRotation()
+	for k, rotation in pairs(self.db.global.customRotations) do
+		if rotation == MaxDps.CurrentEditRotation then
+			self.db.global.customRotations[k] = nil;
+		end
+	end
+
+	self.CurrentEditRotation = nil;
+	self:UpdateCustomRotationButtons();
+	self:EnableDisableCustomFields(true, true);
+end
+
+function MaxDps:EditRotation(rotation)
+	self.CurrentEditRotation = rotation;
+
+	self.CustomWindow.rotationName:SetText(rotation.name);
+	self.CustomWindow.rotationEnabled:SetValue(rotation.enabled);
+	self.CustomWindow.rotationClass:SetValue(rotation.class);
+	local specs = MaxDps.Specs[rotation.class];
+	if specs then
+		self.CustomWindow.rotationSpec:SetList(specs);
+	else
+		self.CustomWindow.rotationSpec:SetList({});
+	end
+	self.CustomWindow.rotationSpec:SetValue(rotation.spec);
+	self.CustomWindow.editor:SetText(IndentationLib.encode(rotation.fn));
+	self:EnableDisableCustomFields(false);
+end
+
+function MaxDps:EnableDisableCustomFields(flag, clear)
+	clear = clear or false;
+	self.CustomWindow.rotationName:SetDisabled(flag);
+	self.CustomWindow.rotationEnabled:SetDisabled(flag);
+	self.CustomWindow.rotationClass:SetDisabled(flag);
+	self.CustomWindow.rotationSpec:SetDisabled(flag);
+	self.CustomWindow.editor:SetDisabled(flag);
+	if clear then
+		self.CustomWindow.rotationName:SetText('');
+		self.CustomWindow.rotationEnabled:SetValue(false);
+		self.CustomWindow.rotationClass:SetValue(nil);
+		self.CustomWindow.rotationSpec:SetValue(nil);
+		self.CustomWindow.editor:SetText('');
+	end
+end
+
+function MaxDps:LoadCustomRotations()
+	for k,v in pairs(self.CustomRotations) do
+		self.CustomRotations[k] = nil;
+	end
+
+	for k, rotation in pairs(self.db.global.customRotations) do
+		if rotation.enabled then
+			local fn = MaxDps.LoadFunction(rotation.fn);
+			if not self.CustomRotations[rotation.class] then
+				self.CustomRotations[rotation.class] = {}
+			end
+
+			self.CustomRotations[rotation.class][rotation.spec] = {
+				name = rotation.name,
+				fn = fn
+			}
+		end
+	end
+	self:Print(self.Colors.Info .. 'Custom Rotations Loaded!');
+end
+
+--[[
+	Borrowed from WeakAuras
+
+	This is free software: you can redistribute it and/or modify it under the terms of
+	the GNU General Public License version 2 as published by the Free Software
+	Foundation.
+
+	For more information see WeakAuras License
+]]
+local blockedFunctions = {
+	getfenv = true,
+	setfenv = true,
+	loadstring = true,
+	pcall = true,
+	SendMail = true,
+	SetTradeMoney = true,
+	AddTradeMoney = true,
+	PickupTradeMoney = true,
+	PickupPlayerMoney = true,
+	TradeFrame = true,
+	MailFrame = true,
+	EnumerateFrames = true,
+	RunScript = true,
+	AcceptTrade = true,
+	SetSendMailMoney = true,
+	EditMacro = true,
+	SlashCmdList = true,
+	DevTools_DumpCommand = true,
+	hash_SlashCmdList = true,
+	CreateMacro = true,
+	SetBindingMacro = true,
+}
+
+local function forbidden()
+	print('|cffffff00A MaxDps just tried to use a forbidden function but has been blocked from doing so.|r');
+end
+
+local env_getglobal;
+local exec_env = setmetatable({}, { __index =
+function(t, k)
+	if k == '_G' then
+		return t;
+	elseif k == 'getglobal' then
+		return env_getglobal;
+	elseif blockedFunctions[k] then
+		return forbidden;
+	else
+		return _G[k];
+	end
+end
+});
+
+local function_cache = {};
+function MaxDps.LoadFunction(string)
+	if function_cache[string] then
+		return function_cache[string];
+	else
+		local loadedFunction, errorString = loadstring('return ' .. string);
+		if errorString then
+			print(errorString);
+		else
+			setfenv(loadedFunction, exec_env);
+			local success, func = pcall(assert(loadedFunction));
+			if success then
+				function_cache[string] = func;
+				return func;
+			end
+		end
+	end
+end
\ No newline at end of file