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

794 lines
27 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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, SaveInfo save) {
if (save == null) instance = new Parser(tokens);
else instance = new Parser(tokens, save);
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, lastPosition;
private final Map<String, Integer> labels;
/** Оптимизация, чтобы каждый раз не искать endmenu/endif,
* если их попросту нет в необработанном сценарии */
private boolean hasEndMenu, hasEndIf;
public Parser(List<Token> tokens) {
this(tokens, 0);
}
public Parser(List<Token> tokens, SaveInfo info) {
this(tokens, info.getPosition());
Variables.setVariables(info.getVariables());
}
private Parser(List<Token> tokens, int position) {
this.tokens = tokens;
tokensCount = tokens.size();
this.position = position;
lastPosition = position;
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 getLastPosition() {
return lastPosition;
}
public int getPosition() {
return position;
}
public void setPosition(int position) {
this.position = position;
next();
}
public void jumpLabel(String label) {
if (labels.containsKey(label)) {
position = labels.get(label);
}
}
public void next() {
lastPosition = position;
// Команды разделяются на терминальные и нетерминальные.
// Нетерминальные подготавливают сцену к выводу.
// Терминальные выводят всё на экран и ожидают следующего вызова.
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.QUEUE)) return queue();
if (match(token, TokenType.STOP)) return stop();
if (match(token, TokenType.SHOW)) return show();
if (match(token, TokenType.HIDE)) return hide();
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))
return ViewActivity.getInstance().windowShow(matchWithEffect());
else if (match(TokenType.HIDE))
return ViewActivity.getInstance().windowHide(matchWithEffect());
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();
boolean hard = false;
if (!lookMatch(0, TokenType.RPAREN)) {
consume(TokenType.WORD); // hard
consume(TokenType.EQ);
hard = consumeBoolean();
}
consume(TokenType.RPAREN);
ViewActivity.getInstance().pause((int)(1000 * pause), hard);
return true;
}
if (match(token, TokenType.RENPY_SAY)) {
consume(TokenType.LPAREN);
final String whoid = consume(TokenType.WORD).getText();
// TODO: consume(TokenType.COMMA)
final String text = consume(TokenType.TEXT).getText();
// TODO: consume(TokenType.COMMA)
consume(TokenType.WORD); // interact
consume(TokenType.EQ);
final boolean interact = consumeBoolean();
consume(TokenType.RPAREN);
ViewActivity.getInstance().text(whoid, text);
return interact;
}
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.MAKE_NAMES_KNOWN) ||
match(token, TokenType.MAKE_NAMES_UNKNOWN)) {
consume(TokenType.LPAREN);
consume(TokenType.RPAREN);
if (token.getType() == TokenType.MAKE_NAMES_KNOWN) {
ViewActivity.getInstance().makeNamesKnown();
} else ViewActivity.getInstance().makeNamesUnknown();
return false;
}
if (match(token, TokenType.SET_NAME)) {
consume(TokenType.LPAREN);
final String whoid = consume(TokenType.TEXT).getText();
// TODO: consume(TokenType.COMMA)
final String name = consume(TokenType.TEXT).getText();
consume(TokenType.RPAREN);
ViewActivity.getInstance().meet(whoid, name);
return false;
}
// Карта
if (match(token, TokenType.DISABLE_ALL_ZONES)) {
consume(TokenType.LPAREN);
consume(TokenType.RPAREN);
ViewActivity.getInstance().disableAllZones();
return false;
}
if (match(token, TokenType.DISABLE_CURRENT_ZONE)) {
consume(TokenType.LPAREN);
consume(TokenType.RPAREN);
ViewActivity.getInstance().disableCurrentZone();
return false;
}
if (match(token, TokenType.RESET_ZONE)) {
consume(TokenType.LPAREN);
final String zone = consume(TokenType.TEXT).getText();
consume(TokenType.RPAREN);
ViewActivity.getInstance().resetZone(zone);
return false;
}
if (match(token, TokenType.SET_ZONE)) {
consume(TokenType.LPAREN);
final String zone = consume(TokenType.TEXT).getText();
// TODO: consume(TokenType.COMMA)
final String label = consume(TokenType.TEXT).getText();
consume(TokenType.RPAREN);
ViewActivity.getInstance().setZone(zone, label);
return false;
}
if (match(token, TokenType.SHOW_MAP)) {
consume(TokenType.LPAREN);
consume(TokenType.RPAREN);
ViewActivity.getInstance().showMap();
return true;
}
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 true;
}
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();
}
final String effect = matchWithEffect();
ViewActivity.getInstance().sprite(whoid, params, position, alias, effect);
return false;
}
private boolean hide() {
final String whoid = consume(TokenType.WORD).getText();
final String effect = matchWithEffect();
ViewActivity.getInstance().hideSprite(whoid, effect);
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() {
final String name = consumeMusicName();
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 = consumeMusicName();
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 queue() {
if (match(TokenType.MUSIC)) return queueMusic();
if (match(TokenType.SOUND)) return queueSound();
return false;
}
private boolean queueMusic() {
final String name = consumeMusicName();
ViewActivity.getInstance().addMusicToQueue(name);
return false;
}
private boolean queueSound() {
final String name = consumeMusicName();
ViewActivity.getInstance().addSoundToQueue(name);
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();
jumpLabel(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;
}
if (lookMatch(pos, TokenType.EOF)) return 0;
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)) return 0;
pos++;
}
return pos;
}
private double consumeDouble() {
return Double.parseDouble(consume(TokenType.NUMBER).getText());
}
private boolean consumeBoolean() {
return "true".equalsIgnoreCase(consume(TokenType.WORD).getText());
}
private String consumeMusicName() {
final String name;
if (lookMatch(1, TokenType.LBRACKET)) {
// music_list["music"]
consume(TokenType.WORD);
consume(TokenType.LBRACKET);
name = consume(TokenType.TEXT).getText();
consume(TokenType.RBRACKET);
} else if (lookMatch(0, TokenType.TEXT)) {
name = consume(TokenType.TEXT).getText();
} else {
name = consume(TokenType.WORD).getText();
}
return name;
}
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);
}
}