RpyPlayer/src/com/annimon/everlastingsummer/Parser.java

649 lines
22 KiB
Java
Raw Normal View History

2015-04-04 15:09:43 +03:00
package com.annimon.everlastingsummer;
import java.util.HashMap;
2015-04-04 15:09:43 +03:00
import java.util.List;
import java.util.Map;
import com.annimon.everlastingsummer.ast.*;
import com.annimon.everlastingsummer.ast.BinaryExpression.Operator;
2015-04-04 15:09:43 +03:00
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();
}
}
2015-04-04 15:09:43 +03:00
private final List<Token> tokens;
private final int tokensCount;
2015-04-04 15:09:43 +03:00
private int position;
private final Map<String, Integer> labels;
/** Оптимизация, чтобы каждый раз не искать endmenu/endif,
* если их попросту нет в необработанном сценарии */
private boolean hasEndMenu, hasEndIf;
2015-04-04 15:09:43 +03:00
public Parser(List<Token> tokens) {
this.tokens = tokens;
tokensCount = tokens.size();
2015-04-04 15:09:43 +03:00
position = 0;
labels = new HashMap<String, Integer>();
hasEndMenu = false;
hasEndIf = false;
preScan();
Variables.init();
2015-04-04 15:09:43 +03:00
}
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();
}
2015-04-04 15:09:43 +03:00
public void next() {
// Команды разделяются на терминальные и нетерминальные.
// Нетерминальные подготавливают сцену к выводу.
// Терминальные выводят всё на экран и ожидают следующего вызова.
boolean terminal = false;
int counter = 0;
do {
try {
terminal = statement();
} catch (RuntimeException re) {
Logger.log("Parser.next()", re);
2015-04-16 19:41:52 +03:00
if (tokens.isEmpty()) return;
2015-04-04 15:09:43 +03:00
}
// антизацикливание
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;
}
2015-04-04 15:09:43 +03:00
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;
}
2015-04-16 00:00:27 +03:00
// Остаток от условного выражения. Пропускаем до появления ENDIF.
if (match(token, TokenType.ELSE)) {
if (hasEndIf) position += skipIf();
2015-04-16 00:00:27 +03:00
return false;
}
}
2015-04-04 15:09:43 +03:00
// Текст с именем автора реплики.
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)) {
2015-04-04 15:09:43 +03:00
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;
2015-04-04 15:09:43 +03:00
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;
}
}
2015-04-04 15:09:43 +03:00
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;
}
2015-04-04 15:09:43 +03:00
// Положение (left, cleft, ...)
String position = "";
if (match(TokenType.AT)) {
position = consume(TokenType.WORD).getText();
}
matchWithEffect();
ViewActivity.getInstance().sprite(whoid, params, position);
2015-04-04 15:09:43 +03:00
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;
}
2015-04-12 00:20:02 +03:00
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;
2015-04-12 00:20:02 +03:00
// Ищем элементы выбора
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++;
2015-04-12 00:20:02 +03:00
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;
2015-04-12 00:20:02 +03:00
pos++;
}
2015-04-12 00:20:02 +03:00
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++;
}
2015-04-16 00:00:27 +03:00
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)) {
// ==
2015-04-16 19:41:52 +03:00
consume(TokenType.EQ);
expression = new BinaryExpression(Operator.EQUALS, expression, additive());
continue;
}
if (match(TokenType.GT)) {
// >=
2015-04-16 19:41:52 +03:00
consume(TokenType.EQ);
expression = new BinaryExpression(Operator.GTEQ, expression, additive());
continue;
}
if (match(TokenType.LT)) {
// <=
2015-04-16 19:41:52 +03:00
consume(TokenType.EQ);
expression = new BinaryExpression(Operator.LTEQ, expression, additive());
continue;
}
if (match(TokenType.EXCL)) {
// !=
2015-04-16 19:41:52 +03:00
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());
}
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++;
2015-04-16 19:41:52 +03:00
pos += 2;
}
if (lookMatch(pos, TokenType.ENDMENU)) {
2015-04-16 19:41:52 +03:00
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)) {
2015-04-16 19:41:52 +03:00
pos++;
level--;
if (level <= 0) break;
2015-04-16 19:41:52 +03:00
} else if (lookMatch(pos, TokenType.EOF)) break;
pos++;
}
return pos;
}
2015-04-04 15:09:43 +03:00
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;
2015-04-04 15:09:43 +03:00
return tokens.get(position + offset);
}
}