/*
* This file is part of the Illarion project.
*
* Copyright © 2015 - Illarion e.V.
*
* Illarion is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Illarion is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*/
package illarion.easynpc.parser;
import illarion.common.data.Skill;
import illarion.common.data.SkillLoader;
import illarion.easynpc.ParsedNpc;
import illarion.easynpc.data.Color;
import illarion.easynpc.data.EquipmentSlots;
import illarion.easynpc.data.Items;
import illarion.easynpc.grammar.EasyNpcBaseVisitor;
import illarion.easynpc.parsed.AbstractParsedTrade.TradeMode;
import illarion.easynpc.parsed.*;
import illarion.easynpc.parsed.ParsedColors.ColorTarget;
import illarion.easynpc.parsed.ParsedHair.HairType;
import illarion.easynpc.parsed.shared.ParsedItemData;
import illarion.easynpc.parsed.talk.conditions.*;
import illarion.easynpc.parsed.talk.consequences.*;
import org.antlr.v4.runtime.*;
import org.antlr.v4.runtime.atn.ATNConfigSet;
import org.antlr.v4.runtime.dfa.DFA;
import org.antlr.v4.runtime.tree.ErrorNode;
import org.antlr.v4.runtime.tree.TerminalNode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.*;
import static illarion.easynpc.grammar.EasyNpcParser.*;
import static illarion.easynpc.parsed.ParsedGuardText.TextType.*;
import static illarion.easynpc.parsed.ParsedTradeText.TradeTextType.*;
import static illarion.easynpc.parser.Utils.*;
/**
* @author Martin Karing <nitram@illarion.org>
*/
public class ParsedNpcVisitor extends EasyNpcBaseVisitor<ParsedNpcVisitor> implements ANTLRErrorListener {
@Nonnull
private static final Logger LOGGER = LoggerFactory.getLogger(ParsedNpcVisitor.class);
static {
SkillLoader.load();
}
@Nonnull
private final ParsedNpc npc = new ParsedNpc();
@Nullable
private ParsedTalk currentTalkingLine;
@Nullable
@Override
public ParsedNpcVisitor visitTalkCommand(TalkCommandContext ctx) {
currentTalkingLine = new ParsedTalk();
ParsedNpcVisitor result = super.visitTalkCommand(ctx);
npc.addNpcData(currentTalkingLine);
currentTalkingLine = null;
return result;
}
@Nullable
@Override
public ParsedNpcVisitor visitCondition(ConditionContext ctx) {
if (currentTalkingLine == null) {
LOGGER.error("Visiting condition while there is no active talking line.");
return super.visitCondition(ctx);
}
switch (ctx.getStart().getText()) {
case "isAdmin":
currentTalkingLine.addCondition(new ConditionAdmin());
break;
case "attrib":
currentTalkingLine.addCondition(
new ConditionAttrib(getAttribute(ctx.attribute()), getOperator(ctx.compare()),
getAdvancedNumber(ctx.advancedNumber()))
);
break;
case "chance":
TerminalNode intNode = ctx.INT();
if (intNode != null) {
currentTalkingLine.addCondition(new ConditionChance(getInteger(intNode)));
} else {
currentTalkingLine.addCondition(new ConditionChance(getFloat(ctx.FLOAT())));
}
break;
case "item":
Items item = getItem(ctx.itemId());
if (item != null) {
currentTalkingLine.addCondition(
new ConditionItem(item, getItemPosition(ctx.itemPos()), getOperator(ctx.compare()),
getAdvancedNumber(ctx.advancedNumber()),
new ParsedItemData(getItemDataOpt(ctx.itemDataList())))
);
}
break;
case "magictype":
currentTalkingLine.addCondition(new ConditionMagicType(getMagicType(ctx.magictype())));
break;
case "money":
currentTalkingLine.addCondition(
new ConditionMoney(getOperator(ctx.compare()), getAdvancedNumber(ctx.advancedNumber())));
break;
case "%NUMBER":
currentTalkingLine.addCondition(new ConditionNumber(getOperator(ctx.compare()), getInteger(ctx.INT())));
break;
case "queststatus":
currentTalkingLine.addCondition(
new ConditionQueststatus(getQuestId(ctx.questId()), getOperator(ctx.compare()),
getAdvancedNumber(ctx.advancedNumber()))
);
break;
case "race":
currentTalkingLine.addCondition(new ConditionRace(getRace(ctx.race())));
break;
case "rank":
currentTalkingLine.addCondition(
new ConditionRank(getOperator(ctx.compare()), getAdvancedNumber(ctx.advancedNumber())));
break;
case "sex":
currentTalkingLine.addCondition(new ConditionSex(getSex(ctx.gender())));
break;
case "skill":
Skill skill = getSkill(ctx.skill());
if (skill != null) {
currentTalkingLine.addCondition(new ConditionSkill(skill, getOperator(ctx.compare()),
getAdvancedNumber(ctx.advancedNumber())));
}
break;
case "state":
currentTalkingLine.addCondition(
new ConditionState(getOperator(ctx.compare()), getAdvancedNumber(ctx.advancedNumber())));
break;
case "talkMode":
currentTalkingLine.addCondition(new ConditionTalkMode(getTalkMode(ctx.talkMode())));
break;
case "town":
currentTalkingLine.addCondition(new ConditionTown(getTown(ctx.town())));
break;
default:
return super.visitCondition(ctx);
}
return super.visitCondition(ctx);
}
@Nullable
@Override
public ParsedNpcVisitor visitConsequence(ConsequenceContext ctx) {
if (currentTalkingLine == null) {
LOGGER.error("Visiting consequence while there is no active talking line.");
return defaultResult();
}
switch (ctx.getStart().getText()) {
case "arena":
currentTalkingLine.addConsequence(new ConsequenceArena(getArenaTask(ctx.arenaTask())));
break;
case "attrib":
currentTalkingLine.addConsequence(
new ConsequenceAttribute(getAttribute(ctx.attribute()), getOperator(ctx.set()),
getAdvancedNumber(ctx.advancedNumber()))
);
break;
case "deleteItem":
Items item = getItem(ctx.itemId());
if (item != null) {
currentTalkingLine.addConsequence(
new ConsequenceDeleteItem(item, getAdvancedNumber(ctx.advancedNumber()),
new ParsedItemData(getItemDataOpt(ctx.itemDataList())))
);
} else {
ctx.addErrorNode(ctx.itemId().getStart());
}
break;
case "gemcraft":
currentTalkingLine.addConsequence(new ConsequenceGemcraft());
break;
case "inform":
currentTalkingLine.addConsequence(new ConsequenceInform(getString(ctx.STRING())));
break;
case "introduce":
currentTalkingLine.addConsequence(new ConsequenceIntroduce());
break;
case "item":
item = getItem(ctx.itemId());
if (item != null) {
currentTalkingLine.addConsequence(new ConsequenceItem(item, getAdvancedNumber(ctx.advancedNumber()),
getItemQualityOpt(ctx.itemQuality()),
new ParsedItemData(
getItemDataOpt(ctx.itemDataList()))
));
} else {
ctx.addErrorNode(ctx.itemId().getStart());
}
break;
case "money":
currentTalkingLine.addConsequence(
new ConsequenceMoney(getOperator(ctx.set()), getAdvancedNumber(ctx.advancedNumber())));
break;
case "queststatus":
currentTalkingLine.addConsequence(
new ConsequenceQueststatus(getQuestId(ctx.questId()), getOperator(ctx.set()),
getAdvancedNumber(ctx.advancedNumber()))
);
break;
case "rankpoints":
currentTalkingLine.addConsequence(
new ConsequenceRankpoints(getOperator(ctx.set()), getAdvancedNumber(ctx.advancedNumber())));
break;
case "repair":
currentTalkingLine.addConsequence(new ConsequenceRepair());
break;
case "rune":
currentTalkingLine.addConsequence(
new ConsequenceRune(getMagicType(ctx.magictypeWithRunes()), getInteger(ctx.INT())));
break;
case "skill":
currentTalkingLine.addConsequence(new ConsequenceSkill(getSkill(ctx.skill()), getOperator(ctx.set()),
getAdvancedNumber(ctx.advancedNumber())));
break;
case "spawn":
currentTalkingLine.addConsequence(
new ConsequenceSpawn(getMonsterId(ctx.monsterId()), getMonsterCount(ctx.monsterCount()),
getRadius(ctx.radius()), getLocation(ctx.location())));
break;
case "state":
currentTalkingLine.addConsequence(
new ConsequenceState(getOperator(ctx.set()), getAdvancedNumber(ctx.advancedNumber())));
break;
case "town":
currentTalkingLine.addConsequence(new ConsequenceTown(getTown(ctx.town())));
break;
case "trade":
currentTalkingLine.addConsequence(new ConsequenceTrade());
break;
case "treasure":
currentTalkingLine.addConsequence(new ConsequenceTreasure(getAdvancedNumber(ctx.advancedNumber())));
break;
case "warp":
currentTalkingLine.addConsequence(new ConsequenceWarp(getLocation(ctx.location())));
break;
default:
return super.visitConsequence(ctx);
}
return super.visitConsequence(ctx);
}
@Nullable
@Override
public ParsedNpcVisitor visitBasicConfiguration(BasicConfigurationContext ctx) {
Token startToken = ctx.getStart();
switch (startToken.getText()) {
case "affiliation":
npc.setAffiliation(getTown(ctx.getRuleContext(TownContext.class, 0)));
break;
case "author":
npc.addAuthor(getString(ctx.STRING()));
break;
case "autointroduce":
npc.setAutoIntroduce(getBoolean(ctx.BOOLEAN()));
break;
case "defaultLanguage":
npc.setDefaultLanguage(getCharacterLanguage(ctx.charLanguage()));
break;
case "direction":
npc.setNpcDir(getDirection(ctx.direction()));
break;
case "job":
npc.setJob(getString(ctx.STRING()));
break;
case "language":
npc.addLanguage(getCharacterLanguage(ctx.charLanguage()));
break;
case "lookatDE":
npc.setGermanLookAt(getString(ctx.STRING()));
break;
case "lookatUS":
npc.setEnglishLookAt(getString(ctx.STRING()));
break;
case "name":
npc.setNpcName(getString(ctx.STRING()));
break;
case "position":
npc.setNpcPos(getLocation(ctx.location()));
break;
case "race":
npc.setNpcRace(getRace(ctx.race()));
break;
case "sex":
npc.setNpcSex(getSex(ctx.gender()));
break;
case "useMsgDE":
npc.setGermanUse(getString(ctx.STRING()));
break;
case "useMsgUS":
npc.setEnglishUse(getString(ctx.STRING()));
break;
case "wrongLangDE":
npc.setGermanWrongLang(getString(ctx.STRING()));
break;
case "wrongLangUS":
npc.setEnglishWrongLang(getString(ctx.STRING()));
break;
default:
ctx.addErrorNode(startToken);
LOGGER.warn("Unknown basic configuration key: {}", startToken.getText());
}
return super.visitBasicConfiguration(ctx);
}
@Nullable
@Override
public ParsedNpcVisitor visitColorConfiguration(ColorConfigurationContext ctx) {
Token startToken = ctx.getStart();
Color color = getColor(ctx.color());
switch (startToken.getText()) {
case "colorHair":
npc.addNpcData(new ParsedColors(ColorTarget.Hair, color));
break;
case "colorSkin":
npc.addNpcData(new ParsedColors(ColorTarget.Skin, color));
break;
default:
ctx.addErrorNode(startToken);
LOGGER.warn("Unknown color configuration key: {}", startToken.getText());
}
return super.visitColorConfiguration(ctx);
}
@Nullable
@Override
public ParsedNpcVisitor visitEquipmentConfiguration(EquipmentConfigurationContext ctx) {
Token startToken = ctx.getStart();
Items item = getItem(ctx.itemId());
if (item == null) {
LOGGER.warn("Failed to match item id for equipment slot: {}", startToken.getText());
return defaultResult();
}
switch (startToken.getText()) {
case "itemChest":
npc.addNpcData(new ParsedEquipment(EquipmentSlots.chest, item));
break;
case "itemCoat":
npc.addNpcData(new ParsedEquipment(EquipmentSlots.coat, item));
break;
case "itemHands":
npc.addNpcData(new ParsedEquipment(EquipmentSlots.hands, item));
break;
case "itemHead":
npc.addNpcData(new ParsedEquipment(EquipmentSlots.head, item));
break;
case "itemMainHand":
npc.addNpcData(new ParsedEquipment(EquipmentSlots.mainHand, item));
break;
case "itemSecondHand":
npc.addNpcData(new ParsedEquipment(EquipmentSlots.secondHand, item));
break;
case "itemShoes":
npc.addNpcData(new ParsedEquipment(EquipmentSlots.feet, item));
break;
case "itemTrousers":
npc.addNpcData(new ParsedEquipment(EquipmentSlots.trousers, item));
break;
default:
ctx.addErrorNode(startToken);
LOGGER.warn("Unknown equipment configuration key: {}", startToken.getText());
}
return super.visitEquipmentConfiguration(ctx);
}
@Nullable
@Override
public ParsedNpcVisitor visitGuardConfiguration(GuardConfigurationContext ctx) {
Token startToken = ctx.getStart();
switch (startToken.getText()) {
case "guardRange":
int north = getInteger(ctx.INT(0));
int south = getInteger(ctx.INT(1));
int west = getInteger(ctx.INT(2));
int east = getInteger(ctx.INT(3));
npc.addNpcData(new ParsedGuardRange(north, south, west, east));
break;
case "guardWarpTarget":
npc.addNpcData(new ParsedGuardWarpTarget(getLocation(ctx.location())));
break;
default:
ctx.addErrorNode(startToken);
LOGGER.warn("Unknown guard configuration key: {}", startToken.getText());
}
return super.visitGuardConfiguration(ctx);
}
@Nullable
@Override
public ParsedNpcVisitor visitHairConfiguration(HairConfigurationContext ctx) {
Token startToken = ctx.getStart();
int id = getInteger(ctx.INT());
switch (startToken.getText()) {
case "hairID":
npc.addNpcData(new ParsedHair(HairType.Hair, id));
break;
case "beardID":
npc.addNpcData(new ParsedHair(HairType.Beard, id));
break;
default:
ctx.addErrorNode(startToken);
LOGGER.warn("Unknown hair configuration key: {}", startToken.getText());
}
return super.visitHairConfiguration(ctx);
}
@Nullable
@Override
public ParsedNpcVisitor visitTraderSimpleConfiguration(
TraderSimpleConfigurationContext ctx) {
Token startToken = ctx.getStart();
TradeMode tradeMode;
switch (startToken.getText()) {
case "sellItems":
tradeMode = TradeMode.selling;
break;
case "buyPrimaryItems":
tradeMode = TradeMode.buyingPrimary;
break;
case "buySecondaryItems":
tradeMode = TradeMode.buyingSecondary;
break;
default:
ctx.addErrorNode(startToken);
LOGGER.warn("Unknown simple trade configuration key: {}", startToken.getText());
return super.visitTraderSimpleConfiguration(ctx);
}
List<ItemIdContext> itemIds = ctx.itemId();
List<Integer> ids = new ArrayList<>(itemIds.size());
for (ItemIdContext itemId : itemIds) {
Items item = getItem(itemId);
if (item == null) {
ctx.addErrorNode(itemId.getStart());
} else {
ids.add(item.getItemId());
}
}
npc.addNpcData(new ParsedTradeSimple(tradeMode, ids));
return super.visitTraderSimpleConfiguration(ctx);
}
@Nullable
@Override
public ParsedNpcVisitor visitTraderComplexConfiguration(TraderComplexConfigurationContext ctx) {
Token startToken = ctx.getStart();
TradeMode tradeMode;
switch (startToken.getText()) {
case "sellItem":
tradeMode = TradeMode.selling;
break;
case "buyPrimaryItem":
tradeMode = TradeMode.buyingPrimary;
break;
case "buySecondaryItem":
tradeMode = TradeMode.buyingSecondary;
break;
default:
ctx.addErrorNode(startToken);
LOGGER.warn("Unknown complex trade configuration key: {}", startToken.getText());
return super.visitTraderComplexConfiguration(ctx);
}
Map<String, String> data = new HashMap<>();
Items item = getItem(ctx.traderComplexItemId());
if (item == null) {
ctx.addErrorNode(ctx.getStart());
LOGGER.warn("Failed to match item id for complex trade entry.");
return super.visitTraderComplexConfiguration(ctx);
}
int itemId = item.getItemId();
String textDe = null;
String textEn = null;
int price = 0;
int stackSize = 0;
int quality = 0;
for (TraderComplexEntryContext entry : ctx.traderComplexEntry()) {
switch (entry.getStart().getText()) {
case "de":
textDe = getString(entry.STRING());
break;
case "en":
textEn = getString(entry.STRING());
break;
case "price":
price = getInteger(entry.INT());
break;
case "stack":
stackSize = getInteger(entry.INT());
break;
case "quality":
quality = getItemQuality(entry.itemQuality());
break;
case "data":
data.putAll(getItemData(entry.itemDataList()));
break;
default:
ctx.addErrorNode(entry.getStart());
LOGGER.warn("Unknown key for complex item entry: {}", entry.getStart().getText());
break;
}
}
npc.addNpcData(new ParsedTradeComplex(tradeMode, itemId, textDe, textEn, price, stackSize, quality,
new ParsedItemData(data)));
return super.visitTraderComplexConfiguration(ctx);
}
@Nullable
@Override
public ParsedNpcVisitor visitWalkConfiguration(WalkConfigurationContext ctx) {
Token startToken = ctx.getStart();
switch (startToken.getText()) {
case "radius":
npc.addNpcData(new ParsedWalkingRadius(getInteger(ctx.INT())));
break;
default:
ctx.addErrorNode(startToken);
LOGGER.warn("Unknown walking configuration key: {}", startToken.getText());
}
return super.visitWalkConfiguration(ctx);
}
@Nullable
@Override
public ParsedNpcVisitor visitTextConfiguration(TextConfigurationContext ctx) {
TextKeyContext textKeyContext = ctx.textKey();
if (textKeyContext == null) {
ctx.addErrorNode(ctx.getStart());
LOGGER.warn("Missing text key for text configuration.");
return defaultResult();
}
String german = getString(ctx.STRING(0));
String english = getString(ctx.STRING(1));
switch (textKeyContext.getStart().getText()) {
case "cycletext":
npc.addNpcData(new ParsedCycleText(german, english));
break;
case "hitPlayerMsg":
npc.addNpcData(new ParsedGuardText(HitPlayer, german, english));
break;
case "tradeFinishedMsg":
npc.addNpcData(new ParsedTradeText(TradingCanceled, german, english));
break;
case "tradeFinishedWithoutTradingMsg":
npc.addNpcData(new ParsedTradeText(TradingCanceledWithoutTrade, german, english));
break;
case "tradeNotEnoughMoneyMsg":
npc.addNpcData(new ParsedTradeText(NoMoney, german, english));
break;
case "tradeWrongItemMsg":
npc.addNpcData(new ParsedTradeText(WrongItem, german, english));
break;
case "warpedMonsterMsg":
npc.addNpcData(new ParsedGuardText(WarpedMonster, german, english));
break;
case "warpedPlayerMsg":
npc.addNpcData(new ParsedGuardText(WarpedPlayer, german, english));
break;
default:
ctx.addErrorNode(textKeyContext.getStart());
LOGGER.warn("Unknown basic text key: {}", textKeyContext.getText());
}
return super.visitTextConfiguration(ctx);
}
@Nullable
@Override
public ParsedNpcVisitor visitTrigger(TriggerContext ctx) {
if (currentTalkingLine == null) {
LOGGER.error("Visiting trigger while there is no active talking line.");
} else {
currentTalkingLine.addCondition(new ConditionTrigger(getString(ctx.STRING())));
}
return super.visitTrigger(ctx);
}
@Nullable
@Override
public ParsedNpcVisitor visitAnswer(AnswerContext ctx) {
if (currentTalkingLine == null) {
LOGGER.error("Visiting consequence while there is no active talking line.");
} else {
currentTalkingLine.addConsequence(new ConsequenceAnswer(getString(ctx.STRING())));
}
return super.visitAnswer(ctx);
}
@Nullable
@Override
public ParsedNpcVisitor visitLanguage(LanguageContext ctx) {
if (currentTalkingLine != null) {
currentTalkingLine.addCondition(new ConditionLanguage(getPlayerLanguage(ctx)));
}
return super.visitLanguage(ctx);
}
@Nullable
@Override
public ParsedNpcVisitor visitTalkstateGet(TalkstateGetContext ctx) {
if (currentTalkingLine == null) {
LOGGER.error("Visiting talk state get while there is no active talking line.");
} else {
currentTalkingLine.addCondition(new ConditionTalkstate(getTalkState(ctx)));
}
return super.visitTalkstateGet(ctx);
}
@Nullable
@Override
public ParsedNpcVisitor visitTalkstateSet(TalkstateSetContext ctx) {
if (currentTalkingLine == null) {
LOGGER.error("Visiting consequence while there is no active talking line.");
return defaultResult();
}
currentTalkingLine.addConsequence(new ConsequenceTalkstate(getTalkState(ctx)));
return defaultResult();
}
@Nullable
@Override
public ParsedNpcVisitor visitErrorNode(ErrorNode node) {
npc.addError(node.getSymbol().getLine(), node.getSymbol().getCharPositionInLine(), node.getText());
return defaultResult();
}
@Nonnull
public ParsedNpc getParsedNpc() {
return npc;
}
@Override
public void syntaxError(
Recognizer<?, ?> recognizer,
Object offendingSymbol,
int line,
int charPositionInLine,
String msg,
RecognitionException e) {
npc.addError(line, charPositionInLine, msg);
}
@Override
public void reportAmbiguity(
Parser recognizer,
DFA dfa,
int startIndex,
int stopIndex,
boolean exact,
BitSet ambigAlts,
ATNConfigSet configs) {
}
@Override
public void reportAttemptingFullContext(
Parser recognizer,
DFA dfa,
int startIndex,
int stopIndex,
BitSet conflictingAlts,
ATNConfigSet configs) {
}
@Override
public void reportContextSensitivity(
Parser recognizer,
DFA dfa,
int startIndex,
int stopIndex,
int prediction,
ATNConfigSet configs) {
}
}