mirror of
https://github.com/OpenDiablo2/OpenDiablo2
synced 2025-02-03 07:07:25 -05:00
2ceba68c73
* Add objgroup.txt loader * Add parser * Add parser * Add tests
313 lines
6.2 KiB
Go
313 lines
6.2 KiB
Go
package d2parser
|
|
|
|
import (
|
|
"log"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2calculation"
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2calculation/d2lexer"
|
|
)
|
|
|
|
// Parser is a parser for calculations used for skill and missiles.
|
|
type Parser struct {
|
|
lex *d2lexer.Lexer
|
|
|
|
binaryOperations map[string]binaryOperation
|
|
unaryOperations map[string]unaryOperation
|
|
ternaryOperations map[string]ternaryOperation
|
|
fixedFunctions map[string]func(v1, v2 int) int
|
|
|
|
currentType string
|
|
currentName string
|
|
}
|
|
|
|
// New creates a new parser.
|
|
func New() *Parser {
|
|
return &Parser{
|
|
binaryOperations: getBinaryOperations(),
|
|
unaryOperations: getUnaryOperations(),
|
|
ternaryOperations: getTernaryOperations(),
|
|
fixedFunctions: getFunctions(),
|
|
}
|
|
}
|
|
|
|
// SetCurrentReference sets the current reference type and name, such as "skill" and skill name.
|
|
func (parser *Parser) SetCurrentReference(propType, propName string) {
|
|
parser.currentType = propType
|
|
parser.currentName = propName
|
|
}
|
|
|
|
// Parse parses the calculation string and creates a Calculation tree.
|
|
func (parser *Parser) Parse(calc string) d2calculation.Calculation {
|
|
calc = strings.TrimSpace(calc)
|
|
if calc == "" {
|
|
return &d2calculation.ConstantCalculation{Value: 0}
|
|
}
|
|
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
log.Printf("Error parsing calculation: %v", calc)
|
|
}
|
|
}()
|
|
|
|
parser.lex = d2lexer.New([]byte(calc))
|
|
|
|
return parser.parseLevel(0)
|
|
}
|
|
|
|
func (parser *Parser) peek() d2lexer.Token {
|
|
return parser.lex.Peek()
|
|
}
|
|
|
|
func (parser *Parser) consume() d2lexer.Token {
|
|
return parser.lex.NextToken()
|
|
}
|
|
|
|
func (parser *Parser) parseLevel(level int) d2calculation.Calculation {
|
|
node := parser.parseProduction()
|
|
|
|
t := parser.peek()
|
|
if t.Type == d2lexer.EOF {
|
|
return node
|
|
}
|
|
|
|
for {
|
|
if t.Type != d2lexer.Symbol {
|
|
break
|
|
}
|
|
|
|
op, ok := parser.binaryOperations[t.Value]
|
|
if !ok || op.Precedence < level {
|
|
break
|
|
}
|
|
|
|
parser.consume()
|
|
|
|
var nextLevel int
|
|
|
|
if op.IsRightAssociated {
|
|
nextLevel = op.Precedence
|
|
} else {
|
|
nextLevel = op.Precedence + 1
|
|
}
|
|
|
|
otherCalculation := parser.parseLevel(nextLevel)
|
|
node = &d2calculation.BinaryCalculation{
|
|
Left: node,
|
|
Right: otherCalculation,
|
|
Op: op.Function,
|
|
}
|
|
t = parser.peek()
|
|
}
|
|
|
|
for {
|
|
if t.Type != d2lexer.Symbol {
|
|
break
|
|
}
|
|
|
|
op, ok := parser.ternaryOperations[t.Value]
|
|
if !ok || op.Precedence < level {
|
|
break
|
|
}
|
|
|
|
parser.consume()
|
|
|
|
var nextLevel int
|
|
|
|
if op.IsRightAssociated {
|
|
nextLevel = op.Precedence
|
|
} else {
|
|
nextLevel = op.Precedence + 1
|
|
}
|
|
|
|
middleCalculation := parser.parseLevel(nextLevel)
|
|
|
|
t = parser.peek()
|
|
if t.Type != d2lexer.Symbol || t.Value != op.Marker {
|
|
panic("Invalid ternary! " + t.Value + ", expected: " + op.Marker)
|
|
}
|
|
|
|
parser.consume()
|
|
rightCalculation := parser.parseLevel(nextLevel)
|
|
|
|
node = &d2calculation.TernaryCalculation{
|
|
Left: node,
|
|
Middle: middleCalculation,
|
|
Right: rightCalculation,
|
|
Op: op.Function,
|
|
}
|
|
t = parser.peek()
|
|
}
|
|
|
|
return node
|
|
}
|
|
|
|
func (parser *Parser) parseProduction() d2calculation.Calculation {
|
|
t := parser.peek()
|
|
|
|
switch {
|
|
case t.Type == d2lexer.Symbol:
|
|
if t.Value == "(" {
|
|
parser.consume()
|
|
node := parser.parseLevel(0)
|
|
|
|
t = parser.peek()
|
|
if t.Type != d2lexer.Symbol ||
|
|
t.Value != ")" {
|
|
if t.Type == d2lexer.EOF { // Ignore unclosed final parenthesis due to syntax error in original Fire Wall calculation.
|
|
return node
|
|
}
|
|
|
|
panic("Parenthesis not closed!")
|
|
}
|
|
|
|
parser.consume()
|
|
|
|
return node
|
|
}
|
|
|
|
op, ok := parser.unaryOperations[t.Value]
|
|
if !ok {
|
|
panic("Invalid unary symbol: " + t.Value)
|
|
}
|
|
|
|
parser.consume()
|
|
node := parser.parseLevel(op.Precedence)
|
|
|
|
return &d2calculation.UnaryCalculation{
|
|
Child: node,
|
|
Op: op.Function,
|
|
}
|
|
|
|
case t.Type == d2lexer.Name || t.Type == d2lexer.Number:
|
|
return parser.parseLeafCalculation()
|
|
default:
|
|
panic("Expected parenthesis, unary operator, function or value!")
|
|
}
|
|
}
|
|
|
|
func (parser *Parser) parseLeafCalculation() d2calculation.Calculation {
|
|
t := parser.peek()
|
|
|
|
if t.Type == d2lexer.Number {
|
|
val, err := strconv.Atoi(t.Value)
|
|
if err != nil {
|
|
panic("Invalid number: " + t.Value)
|
|
}
|
|
|
|
parser.consume()
|
|
|
|
return &d2calculation.ConstantCalculation{Value: val}
|
|
}
|
|
|
|
if t.Value == "skill" ||
|
|
t.Value == "miss" ||
|
|
t.Value == "stat" {
|
|
return parser.parseProperty()
|
|
}
|
|
|
|
if parser.fixedFunctions[t.Value] != nil {
|
|
return parser.parseFunction(t.Value)
|
|
}
|
|
|
|
if t.Type == d2lexer.Name {
|
|
parser.consume()
|
|
|
|
return &d2calculation.PropertyReferenceCalculation{
|
|
Type: parser.currentType,
|
|
Name: parser.currentName,
|
|
Qualifier: t.Value,
|
|
}
|
|
}
|
|
|
|
panic(t.Value + " is not a function, property, or number!")
|
|
}
|
|
|
|
func (parser *Parser) parseFunction(name string) d2calculation.Calculation {
|
|
function := parser.fixedFunctions[name]
|
|
parser.consume()
|
|
|
|
t := parser.peek()
|
|
if t.Value != "(" {
|
|
panic("Invalid function!")
|
|
}
|
|
|
|
parser.consume()
|
|
|
|
firstParam := parser.parseLevel(0)
|
|
|
|
t = parser.peek()
|
|
if t.Type != d2lexer.Symbol || t.Value != "," {
|
|
panic("Invalid function!")
|
|
}
|
|
|
|
parser.consume()
|
|
|
|
secondParam := parser.parseLevel(0)
|
|
|
|
t = parser.peek()
|
|
if t.Value != ")" {
|
|
panic("Invalid function!")
|
|
}
|
|
|
|
parser.consume()
|
|
|
|
return &d2calculation.BinaryCalculation{
|
|
Left: firstParam,
|
|
Right: secondParam,
|
|
Op: function,
|
|
}
|
|
}
|
|
|
|
func (parser *Parser) parseProperty() d2calculation.Calculation {
|
|
t := parser.peek()
|
|
propType := t.Value
|
|
t = parser.consume()
|
|
|
|
t = parser.peek()
|
|
if t.Value != "(" {
|
|
panic("Invalid property: " + propType + ", open parenthesis missing.")
|
|
}
|
|
|
|
parser.consume()
|
|
|
|
t = parser.peek()
|
|
if t.Type != d2lexer.String {
|
|
panic("Property name must be in quotes: " + propType)
|
|
}
|
|
|
|
propName := t.Value
|
|
|
|
parser.consume()
|
|
|
|
t = parser.peek()
|
|
if t.Type != d2lexer.Symbol || t.Value != "." {
|
|
panic("Property name must be followed by dot: " + propType)
|
|
}
|
|
|
|
parser.consume()
|
|
|
|
t = parser.peek()
|
|
if t.Type != d2lexer.Name {
|
|
panic("Invalid propery qualifier: " + propType)
|
|
}
|
|
|
|
propQual := t.Value
|
|
|
|
parser.consume()
|
|
|
|
t = parser.peek()
|
|
if t.Value != ")" {
|
|
panic("Invalid property: " + propType + ", closed parenthesis missing.")
|
|
}
|
|
|
|
parser.consume()
|
|
|
|
return &d2calculation.PropertyReferenceCalculation{
|
|
Type: propType,
|
|
Name: propName,
|
|
Qualifier: propQual,
|
|
}
|
|
}
|