2015-04-04 15:09:43 +03:00
|
|
|
|
package com.annimon.everlastingsummer;
|
|
|
|
|
|
|
|
|
|
import java.util.List;
|
|
|
|
|
import android.text.TextUtils;
|
|
|
|
|
import android.util.Log;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private final List<Token> tokens;
|
2015-04-10 22:33:28 +03:00
|
|
|
|
private final int tokensCount;
|
2015-04-04 15:09:43 +03:00
|
|
|
|
private int position;
|
|
|
|
|
|
|
|
|
|
public Parser(List<Token> tokens) {
|
|
|
|
|
this.tokens = tokens;
|
2015-04-10 22:33:28 +03:00
|
|
|
|
tokensCount = tokens.size();
|
2015-04-04 15:09:43 +03:00
|
|
|
|
position = 0;
|
|
|
|
|
}
|
|
|
|
|
|
2015-04-10 22:33:28 +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) {
|
|
|
|
|
Log.e("Parser", re.getMessage(), re);
|
|
|
|
|
}
|
|
|
|
|
// антизацикливание
|
|
|
|
|
counter++;
|
|
|
|
|
if (counter >= 1000) {
|
|
|
|
|
position++;
|
|
|
|
|
counter = 0;
|
|
|
|
|
}
|
|
|
|
|
} while (!terminal);
|
|
|
|
|
}
|
|
|
|
|
|
2015-04-10 23:39:51 +03:00
|
|
|
|
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();
|
|
|
|
|
|
2015-04-04 15:46:54 +03:00
|
|
|
|
if (match(token, TokenType.HIDE)) {
|
|
|
|
|
ViewActivity.getInstance().hideSprite(consume(TokenType.WORD).getText());
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2015-04-06 23:49:36 +03:00
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2015-04-12 00:20:02 +03:00
|
|
|
|
if (match(token, TokenType.MENU)) return menu();
|
|
|
|
|
|
2015-04-06 17:07:47 +03:00
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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();
|
2015-04-06 17:07:18 +03:00
|
|
|
|
String params = ""; // emotion? cloth? distance?
|
|
|
|
|
while (lookMatch(0, TokenType.WORD)) {
|
2015-04-06 23:22:50 +03:00
|
|
|
|
final String text = consume(TokenType.WORD).getText();
|
|
|
|
|
params += text;
|
2015-04-07 13:03:08 +03:00
|
|
|
|
if (text.equals("close") || text.equals("far")) break;
|
2015-04-06 17:07:18 +03:00
|
|
|
|
}
|
2015-04-04 15:09:43 +03:00
|
|
|
|
// Положение (left, cleft, ...)
|
|
|
|
|
String position = "";
|
|
|
|
|
if (match(TokenType.AT)) {
|
|
|
|
|
position = consume(TokenType.WORD).getText();
|
|
|
|
|
}
|
|
|
|
|
matchWithEffect();
|
2015-04-06 17:07:18 +03:00
|
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Ищем элементы выбора
|
|
|
|
|
final Menu menu = new Menu(title);
|
|
|
|
|
int pos = 0;
|
2015-04-12 00:39:05 +03:00
|
|
|
|
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++;
|
|
|
|
|
}
|
2015-04-12 00:39:05 +03:00
|
|
|
|
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:39:05 +03:00
|
|
|
|
}
|
2015-04-12 00:20:02 +03:00
|
|
|
|
|
|
|
|
|
ViewActivity.getInstance().menu(menu);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
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) {
|
2015-04-10 22:33:28 +03:00
|
|
|
|
if (position + offset >= tokensCount) return EOF;
|
2015-04-04 15:09:43 +03:00
|
|
|
|
return tokens.get(position + offset);
|
|
|
|
|
}
|
|
|
|
|
}
|