659 lines
22 KiB
Java
659 lines
22 KiB
Java
package com.annimon.everlastingsummer;
|
||
|
||
import java.util.HashMap;
|
||
import java.util.List;
|
||
import java.util.Map;
|
||
import com.annimon.everlastingsummer.ast.*;
|
||
import com.annimon.everlastingsummer.ast.BinaryExpression.Operator;
|
||
import android.text.TextUtils;
|
||
|
||
/**
|
||
* @author aNNiMON
|
||
*/
|
||
public final class Parser {
|
||
|
||
private static final Token EOF = new Token("", TokenType.EOF);
|
||
|
||
private static Parser instance;
|
||
|
||
public static Parser parse(List<Token> tokens) {
|
||
instance = new Parser(tokens);
|
||
return instance;
|
||
}
|
||
|
||
public static Parser getInstance() {
|
||
return instance;
|
||
}
|
||
|
||
public static void release() {
|
||
if (instance != null) {
|
||
instance.tokens.clear();
|
||
instance.labels.clear();
|
||
}
|
||
}
|
||
|
||
private final List<Token> tokens;
|
||
private final int tokensCount;
|
||
private int position;
|
||
|
||
private final Map<String, Integer> labels;
|
||
/** Оптимизация, чтобы каждый раз не искать endmenu/endif,
|
||
* если их попросту нет в необработанном сценарии */
|
||
private boolean hasEndMenu, hasEndIf;
|
||
|
||
public Parser(List<Token> tokens) {
|
||
this.tokens = tokens;
|
||
tokensCount = tokens.size();
|
||
position = 0;
|
||
labels = new HashMap<String, Integer>();
|
||
hasEndMenu = false;
|
||
hasEndIf = false;
|
||
preScan();
|
||
Variables.init();
|
||
}
|
||
|
||
public List<Token> getTokens() {
|
||
return tokens;
|
||
}
|
||
|
||
public int getTokensCount() {
|
||
return tokensCount;
|
||
}
|
||
|
||
public int getPosition() {
|
||
return position;
|
||
}
|
||
|
||
public void setPosition(int position) {
|
||
this.position = position;
|
||
next();
|
||
}
|
||
|
||
public void next() {
|
||
// Команды разделяются на терминальные и нетерминальные.
|
||
// Нетерминальные подготавливают сцену к выводу.
|
||
// Терминальные выводят всё на экран и ожидают следующего вызова.
|
||
boolean terminal = false;
|
||
int counter = 0;
|
||
do {
|
||
try {
|
||
terminal = statement();
|
||
} catch (RuntimeException re) {
|
||
if (Logger.DEBUG) Logger.log("Parser.next()", re);
|
||
if (tokens.isEmpty()) return;
|
||
}
|
||
// антизацикливание
|
||
counter++;
|
||
if (counter >= 1000) {
|
||
position++;
|
||
counter = 0;
|
||
}
|
||
} while (!terminal);
|
||
}
|
||
|
||
public void prevScene() {
|
||
// Для перехода к предыдущей сцене, следует два раза найти токен SCENE,
|
||
// т.к. при первом поиске мы найдём лишь текущую.
|
||
final int currentScene = find(TokenType.SCENE, -1);
|
||
final int pos = findFrom(currentScene, TokenType.SCENE, -1);
|
||
// Если не нашли - перематываем на начало.
|
||
if (pos == position) position = 0;
|
||
else position = pos;
|
||
next();
|
||
}
|
||
|
||
public void nextScene() {
|
||
position = find(TokenType.SCENE, 1);
|
||
next();
|
||
}
|
||
|
||
private int find(TokenType which, int step) {
|
||
return findFrom(position, which, step);
|
||
}
|
||
|
||
private int findFrom(int from, TokenType which, int step) {
|
||
int pos = from;
|
||
while (true) {
|
||
pos += step;
|
||
if (pos < 0 || pos >= tokensCount) break;
|
||
|
||
final Token token = tokens.get(pos);
|
||
if (which == token.getType()) return pos;
|
||
}
|
||
// Возвращаем текущее значение во избежание лишних проверок.
|
||
return from;
|
||
}
|
||
|
||
private boolean statement() {
|
||
// http://www.renpy.org/wiki/renpy/doc/reference/The_Ren%27Py_Language#Grammar_Rules
|
||
final Token token = get(0);
|
||
if (match(token, TokenType.COMMAND)) return command();
|
||
if (match(token, TokenType.SCENE)) return scene();
|
||
if (match(token, TokenType.PLAY)) return play();
|
||
if (match(token, TokenType.STOP)) return stop();
|
||
if (match(token, TokenType.SHOW)) return show();
|
||
|
||
if (match(token, TokenType.HIDE)) {
|
||
ViewActivity.getInstance().hideSprite(consume(TokenType.WORD).getText());
|
||
return false;
|
||
}
|
||
|
||
if (match(token, TokenType.JUMP)) return jump();
|
||
if (match(token, TokenType.IF)) return ifStatement();
|
||
|
||
if (lookMatch(1, TokenType.COLON)) {
|
||
// menu:
|
||
if (match(token, TokenType.MENU)) return menu();
|
||
|
||
// Остаток от меню выбора. Пропускаем до появления ENDMENU.
|
||
if (match(token, TokenType.TEXT)) {
|
||
if (hasEndMenu) position += skipMenu();
|
||
return false;
|
||
}
|
||
|
||
// Остаток от условного выражения. Пропускаем до появления ENDIF.
|
||
if (match(token, TokenType.ELSE)) {
|
||
if (hasEndIf) position += skipIf();
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// Текст с именем автора реплики.
|
||
if (lookMatch(1, TokenType.TEXT) && match(token, TokenType.WORD)) {
|
||
final String whoid = token.getText();
|
||
ViewActivity.getInstance().text(whoid, consume(TokenType.TEXT).getText());
|
||
return true;
|
||
}
|
||
|
||
// Обычный текст.
|
||
if (match(token, TokenType.TEXT)) {
|
||
ViewActivity.getInstance().text(token.getText());
|
||
return true;
|
||
}
|
||
|
||
if (match(token, TokenType.EOF)) {
|
||
ViewActivity.getInstance().finish();
|
||
return true;
|
||
}
|
||
|
||
if (match(token, TokenType.WINDOW)) {
|
||
if (match(TokenType.SHOW))
|
||
ViewActivity.getInstance().windowShow();
|
||
else if (match(TokenType.HIDE))
|
||
ViewActivity.getInstance().windowHide();
|
||
return false;
|
||
}
|
||
|
||
if (!TextUtils.isEmpty(matchWithEffect())) return false;
|
||
|
||
position++;
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* Парсинг команд на языке Python. Начинаются на $.
|
||
* Поддерживается только самое нужное, весь питон я вам не интерпретирую.
|
||
*/
|
||
private boolean command() {
|
||
final Token token = get(0);
|
||
|
||
if (match(token, TokenType.RENPY_PAUSE)) {
|
||
consume(TokenType.LPAREN);
|
||
final double pause = consumeDouble();
|
||
ViewActivity.getInstance().pause((int)(1000 * pause));
|
||
consume(TokenType.RPAREN);
|
||
return true;
|
||
}
|
||
|
||
if (match(token, TokenType.PERSISTENT_SPRITE_TIME)) {
|
||
consume(TokenType.EQ);
|
||
consume(TokenType.TEXT);
|
||
return false;
|
||
}
|
||
|
||
if (match(token, TokenType.PROLOG_TIME) ||
|
||
match(token, TokenType.DAY_TIME) ||
|
||
match(token, TokenType.SUNSET_TIME) ||
|
||
match(token, TokenType.NIGHT_TIME)) {
|
||
consume(TokenType.LPAREN);
|
||
consume(TokenType.RPAREN);
|
||
return false;
|
||
}
|
||
|
||
if (match(token, TokenType.WORD)) {
|
||
if (match(TokenType.EQ)) {
|
||
// variable = expression
|
||
Variables.setVariable(token.getText(), expression().eval());
|
||
return false;
|
||
}
|
||
if (lookMatch(1, TokenType.EQ) && match(TokenType.PLUS)) {
|
||
// variable += expression
|
||
consume(TokenType.EQ);
|
||
final double varValue = Variables.getVariable(token.getText());
|
||
Variables.setVariable(token.getText(), varValue + expression().eval());
|
||
return false;
|
||
}
|
||
if (lookMatch(1, TokenType.EQ) && match(TokenType.MINUS)) {
|
||
// variable -= expression
|
||
consume(TokenType.EQ);
|
||
final double varValue = Variables.getVariable(token.getText());
|
||
Variables.setVariable(token.getText(), varValue - expression().eval());
|
||
return false;
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
private boolean scene() {
|
||
String type;
|
||
if (match(TokenType.BG)) type = "bg";
|
||
else if (match(TokenType.CG)) type = "cg";
|
||
else if (match(TokenType.ANIM)) type = "anim";
|
||
else type = "";
|
||
|
||
final String name = consume(TokenType.WORD).getText();
|
||
final String effect = matchWithEffect();
|
||
ViewActivity.getInstance().background(type, name, effect);
|
||
return false;
|
||
}
|
||
|
||
private boolean show() {
|
||
final String whoid = consume(TokenType.WORD).getText();
|
||
String params = ""; // emotion? cloth? distance?
|
||
while (lookMatch(0, TokenType.WORD)) {
|
||
final String text = consume(TokenType.WORD).getText();
|
||
params += text;
|
||
if (text.equals("close") || text.equals("far")) break;
|
||
}
|
||
// Положение (left, cleft, ...)
|
||
String position = "";
|
||
if (match(TokenType.AT)) {
|
||
position = consume(TokenType.WORD).getText();
|
||
}
|
||
// Псевдоним (для показа одно и того же спрайта в разных местах)
|
||
String alias = "";
|
||
if (match(TokenType.AS)) {
|
||
alias = consume(TokenType.WORD).getText();
|
||
}
|
||
matchWithEffect();
|
||
ViewActivity.getInstance().sprite(whoid, params, position, alias);
|
||
return false;
|
||
}
|
||
|
||
private boolean play() {
|
||
if (match(TokenType.MUSIC)) return playMusic();
|
||
if (match(TokenType.AMBIENCE)) return playAmbience();
|
||
if (lookMatch(0, TokenType.SOUND) || lookMatch(0, TokenType.SOUNDLOOP)) {
|
||
return playSound();
|
||
}
|
||
return false;
|
||
}
|
||
|
||
private boolean playMusic() {
|
||
consume(TokenType.WORD);
|
||
consume(TokenType.LBRACKET);
|
||
final String name = consume(TokenType.TEXT).getText();
|
||
consume(TokenType.RBRACKET);
|
||
|
||
final FadeInfo fade = matchFade();
|
||
ViewActivity.getInstance().music(name, fade);
|
||
return false;
|
||
}
|
||
|
||
private boolean playSound() {
|
||
boolean loop = false;
|
||
if (match(TokenType.SOUND)) loop = false;
|
||
else if (match(TokenType.SOUNDLOOP)) loop = true;
|
||
|
||
final String name = consume(TokenType.WORD).getText();
|
||
final FadeInfo fade = matchFade();
|
||
ViewActivity.getInstance().sound(name, loop, fade);
|
||
return false;
|
||
}
|
||
|
||
private boolean playAmbience() {
|
||
// Ambient не реализован, но парсится.
|
||
final String name = consume(TokenType.WORD).getText();
|
||
final FadeInfo fade = matchFade();
|
||
// ViewActivity.getInstance().sound(name, loop, fade);
|
||
return false;
|
||
}
|
||
|
||
private boolean stop() {
|
||
if (match(TokenType.MUSIC)) {
|
||
final FadeInfo fade = matchFade();
|
||
ViewActivity.getInstance().stopMusic(fade);
|
||
}
|
||
if (match(TokenType.SOUND) || match(TokenType.SOUNDLOOP)) {
|
||
final FadeInfo fade = matchFade();
|
||
ViewActivity.getInstance().stopSound(fade);
|
||
}
|
||
if (match(TokenType.AMBIENCE)) {
|
||
final FadeInfo fade = matchFade();
|
||
// ViewActivity.getInstance().stopMusic(fade);
|
||
}
|
||
return false;
|
||
}
|
||
|
||
private boolean menu() {
|
||
// menu: title?
|
||
consume(TokenType.COLON);
|
||
|
||
String title = null;
|
||
if (lookMatch(0, TokenType.TEXT) && !lookMatch(1, TokenType.COLON)) {
|
||
title = consume(TokenType.TEXT).getText();
|
||
}
|
||
|
||
if (!hasEndMenu) return false;
|
||
// Ищем элементы выбора
|
||
final Menu menu = new Menu(title);
|
||
int pos = 0;
|
||
int level = 1; // уровень вложенности меню
|
||
while (true) {
|
||
// Расчёт уровня меню.
|
||
if (lookMatch(pos, TokenType.MENU) && lookMatch(pos + 1, TokenType.COLON)) {
|
||
level++;
|
||
pos++;
|
||
}
|
||
if (lookMatch(pos, TokenType.ENDMENU)) {
|
||
level--;
|
||
// Завершаем работу по достижению ENDMENU первого уровня.
|
||
if (level <= 0) break;
|
||
}
|
||
|
||
if (level == 1) {
|
||
// Добавляем только пункты из меню первого уровня.
|
||
if (lookMatch(pos, TokenType.TEXT) && lookMatch(pos + 1, TokenType.COLON)) {
|
||
menu.addItem(get(pos).getText(), position + pos + 2);
|
||
pos++;
|
||
}
|
||
}
|
||
|
||
if (lookMatch(pos, TokenType.EOF)) return false;
|
||
pos++;
|
||
}
|
||
|
||
ViewActivity.getInstance().menu(menu);
|
||
return true;
|
||
}
|
||
|
||
private boolean jump() {
|
||
final String labelName = consume(TokenType.WORD).getText();
|
||
if (labels.containsKey(labelName)) {
|
||
position = labels.get(labelName);
|
||
}
|
||
return false;
|
||
}
|
||
|
||
private boolean ifStatement() {
|
||
final Expression condition = expression();
|
||
consume(TokenType.COLON);
|
||
|
||
if (!hasEndIf) return false;
|
||
if (condition.eval() == 0) {
|
||
// Если условие не верно, пропускаем блок до следующего ENDIF
|
||
int pos = 0;
|
||
int level = 1; // уровень вложенности блока
|
||
while (true) {
|
||
// Расчёт уровня блока.
|
||
if (lookMatch(pos, TokenType.IF)) {
|
||
level++;
|
||
pos++;
|
||
}
|
||
if (lookMatch(pos, TokenType.ELSE)) {
|
||
level--;
|
||
pos += 2; // пропускаем ELSE и двоеточие
|
||
// Завершаем работу по достижению ELSE первого уровня.
|
||
if (level <= 0) break;
|
||
}
|
||
if (lookMatch(pos, TokenType.ENDIF)) {
|
||
level--;
|
||
// Завершаем работу по достижению ENDIF первого уровня.
|
||
if (level <= 0) break;
|
||
}
|
||
|
||
if (lookMatch(pos, TokenType.EOF)) return false;
|
||
pos++;
|
||
}
|
||
position += pos;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
private Expression expression() {
|
||
return orTest();
|
||
}
|
||
|
||
private Expression orTest() {
|
||
Expression expression = andTest();
|
||
|
||
while (true) {
|
||
if (match(TokenType.OR)) {
|
||
expression = new BinaryExpression(Operator.BOOLEAN_OR, expression, andTest());
|
||
continue;
|
||
}
|
||
break;
|
||
}
|
||
|
||
return expression;
|
||
}
|
||
|
||
private Expression andTest() {
|
||
Expression expression = notTest();
|
||
|
||
while (true) {
|
||
if (match(TokenType.AND)) {
|
||
expression = new BinaryExpression(Operator.BOOLEAN_AND, expression, notTest());
|
||
continue;
|
||
}
|
||
break;
|
||
}
|
||
|
||
return expression;
|
||
}
|
||
|
||
private Expression notTest() {
|
||
if (match(TokenType.NOT)) {
|
||
return new ValueExpression( notTest().eval() != 0 ? 0 : 1 );
|
||
}
|
||
return comparison();
|
||
}
|
||
|
||
private Expression comparison() {
|
||
Expression expression = additive();
|
||
|
||
while (true) {
|
||
if (lookMatch(1, TokenType.EQ)) {
|
||
if (match(TokenType.EQ)) {
|
||
// ==
|
||
consume(TokenType.EQ);
|
||
expression = new BinaryExpression(Operator.EQUALS, expression, additive());
|
||
continue;
|
||
}
|
||
if (match(TokenType.GT)) {
|
||
// >=
|
||
consume(TokenType.EQ);
|
||
expression = new BinaryExpression(Operator.GTEQ, expression, additive());
|
||
continue;
|
||
}
|
||
if (match(TokenType.LT)) {
|
||
// <=
|
||
consume(TokenType.EQ);
|
||
expression = new BinaryExpression(Operator.LTEQ, expression, additive());
|
||
continue;
|
||
}
|
||
if (match(TokenType.EXCL)) {
|
||
// !=
|
||
consume(TokenType.EQ);
|
||
expression = new BinaryExpression(Operator.NOTEQUALS, expression, additive());
|
||
continue;
|
||
}
|
||
}
|
||
|
||
if (match(TokenType.LT)) {
|
||
expression = new BinaryExpression(Operator.LT, expression, additive());
|
||
continue;
|
||
}
|
||
if (match(TokenType.GT)) {
|
||
expression = new BinaryExpression(Operator.GT, expression, additive());
|
||
continue;
|
||
}
|
||
break;
|
||
}
|
||
return expression;
|
||
}
|
||
|
||
private Expression additive() {
|
||
Expression expression = unary();
|
||
|
||
while (true) {
|
||
if (match(TokenType.PLUS)) {
|
||
expression = new BinaryExpression(Operator.ADD, expression, unary());
|
||
continue;
|
||
}
|
||
if (match(TokenType.MINUS)) {
|
||
expression = new BinaryExpression(Operator.SUBTRACT, expression, unary());
|
||
continue;
|
||
}
|
||
break;
|
||
}
|
||
|
||
return expression;
|
||
}
|
||
|
||
private Expression unary() {
|
||
if (match(TokenType.MINUS)) {
|
||
return new ValueExpression( -primary().eval() );
|
||
}
|
||
if (match(TokenType.PLUS)) {
|
||
return primary();
|
||
}
|
||
return primary();
|
||
}
|
||
|
||
private Expression primary() {
|
||
final Token current = get(0);
|
||
if (match(current, TokenType.NUMBER)) {
|
||
return new ValueExpression( Double.parseDouble(current.getText()) );
|
||
}
|
||
if (match(current, TokenType.WORD)) {
|
||
return new VariableExpression(current.getText());
|
||
}
|
||
if (match(current, TokenType.LPAREN)) {
|
||
Expression expr = expression();
|
||
match(TokenType.RPAREN);
|
||
return expr;
|
||
}
|
||
throw new RuntimeException();
|
||
}
|
||
|
||
private void preScan() {
|
||
// Сканируем все метки, для быстрого перехода командой jump.
|
||
// А также определяем параметры для оптимизации.
|
||
for (int i = 0; i < tokensCount - 2; i++) {
|
||
final TokenType current = tokens.get(i).getType();
|
||
if (current == TokenType.ENDMENU) {
|
||
hasEndMenu = true;
|
||
} else if (current == TokenType.ENDIF) {
|
||
hasEndIf = true;
|
||
} else if ( (current == TokenType.LABEL) &&
|
||
(tokens.get(i + 2).getType() == TokenType.COLON) ) {
|
||
// label word :
|
||
final Token token = tokens.get(i + 1);
|
||
if (token.getType() == TokenType.WORD) {
|
||
// Добавляем позицию команды, следующей после метки.
|
||
labels.put(token.getText(), i + 3);
|
||
}
|
||
i += 2;
|
||
}
|
||
}
|
||
}
|
||
|
||
private int skipMenu() {
|
||
int pos = 0;
|
||
int level = 1; // уровень вложенности меню
|
||
while (true) {
|
||
// Расчёт уровня меню.
|
||
if (lookMatch(pos, TokenType.MENU) && lookMatch(pos + 1, TokenType.COLON)) {
|
||
level++;
|
||
pos += 2;
|
||
}
|
||
if (lookMatch(pos, TokenType.ENDMENU)) {
|
||
pos++;
|
||
level--;
|
||
// Завершаем работу по достижению ENDMENU первого уровня.
|
||
if (level <= 0) break;
|
||
}
|
||
pos++;
|
||
}
|
||
return pos;
|
||
}
|
||
|
||
private int skipIf() {
|
||
int pos = 0;
|
||
int level = 1;
|
||
while (true) {
|
||
if (lookMatch(pos, TokenType.IF)) level++;
|
||
else if (lookMatch(pos, TokenType.ENDIF)) {
|
||
pos++;
|
||
level--;
|
||
if (level <= 0) break;
|
||
} else if (lookMatch(pos, TokenType.EOF)) break;
|
||
pos++;
|
||
}
|
||
return pos;
|
||
}
|
||
|
||
|
||
private double consumeDouble() {
|
||
return Double.parseDouble(consume(TokenType.NUMBER).getText());
|
||
}
|
||
|
||
private FadeInfo matchFade() {
|
||
final FadeInfo result = new FadeInfo();
|
||
if (match(TokenType.FADEIN)) {
|
||
result.setIn(true);
|
||
result.setDuration(consumeDouble());
|
||
} else if (match(TokenType.FADEOUT)) {
|
||
result.setOut(true);
|
||
result.setDuration(consumeDouble());
|
||
}
|
||
return result;
|
||
}
|
||
|
||
private String matchWithEffect() {
|
||
if (match(TokenType.WITH)) {
|
||
return consume(TokenType.WORD).getText();
|
||
}
|
||
return "";
|
||
}
|
||
|
||
|
||
private boolean match(TokenType type) {
|
||
if (get(0).getType() != type) return false;
|
||
position++;
|
||
return true;
|
||
}
|
||
|
||
private boolean match(Token token, TokenType type) {
|
||
if (type != token.getType()) return false;
|
||
position++;
|
||
return true;
|
||
}
|
||
|
||
private Token consume(TokenType type) {
|
||
if (get(0).getType() != type) throw new RuntimeException("Ожидался токен " + type + ".");
|
||
return tokens.get(position++);
|
||
}
|
||
|
||
private boolean lookMatch(int pos, TokenType type) {
|
||
return (type == get(pos).getType());
|
||
}
|
||
|
||
private Token get(int offset) {
|
||
if (position + offset >= tokensCount) return EOF;
|
||
return tokens.get(position + offset);
|
||
}
|
||
}
|