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 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 tokens; private final int tokensCount; private int position; private final Map labels; /** Оптимизация, чтобы каждый раз не искать endmenu/endif, * если их попросту нет в необработанном сценарии */ private boolean hasEndMenu, hasEndIf; public Parser(List tokens) { this.tokens = tokens; tokensCount = tokens.size(); position = 0; labels = new HashMap(); hasEndMenu = false; hasEndIf = false; preScan(); Variables.init(); } public List 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) { 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(); } matchWithEffect(); ViewActivity.getInstance().sprite(whoid, params, position); 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); } }