commit e8f7ff45ebade9daee211108bc60c800feef64e4 Author: Victor Date: Sat Apr 4 15:09:43 2015 +0300 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7ab6ae7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +.classpath +.cproject +.project +.settings +bin/ +gen/ +obj/ +tmp/ +proguard/ \ No newline at end of file diff --git a/AndroidManifest.xml b/AndroidManifest.xml new file mode 100644 index 0000000..5e46a1a --- /dev/null +++ b/AndroidManifest.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/ic_launcher-web.png b/ic_launcher-web.png new file mode 100644 index 0000000..03c9537 Binary files /dev/null and b/ic_launcher-web.png differ diff --git a/proguard-project.txt b/proguard-project.txt new file mode 100644 index 0000000..f2fe155 --- /dev/null +++ b/proguard-project.txt @@ -0,0 +1,20 @@ +# To enable ProGuard in your project, edit project.properties +# to define the proguard.config property as described in that file. +# +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in ${sdk.dir}/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the ProGuard +# include property in project.properties. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/project.properties b/project.properties new file mode 100644 index 0000000..4ab1256 --- /dev/null +++ b/project.properties @@ -0,0 +1,14 @@ +# This file is automatically generated by Android Tools. +# Do not modify this file -- YOUR CHANGES WILL BE ERASED! +# +# This file must be checked in Version Control Systems. +# +# To customize properties used by the Ant build system edit +# "ant.properties", and override values to adapt the script to your +# project structure. +# +# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): +#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt + +# Project target. +target=android-19 diff --git a/res/drawable-hdpi/ic_launcher.png b/res/drawable-hdpi/ic_launcher.png new file mode 100644 index 0000000..2a81b09 Binary files /dev/null and b/res/drawable-hdpi/ic_launcher.png differ diff --git a/res/drawable-mdpi/ic_launcher.png b/res/drawable-mdpi/ic_launcher.png new file mode 100644 index 0000000..b3ba870 Binary files /dev/null and b/res/drawable-mdpi/ic_launcher.png differ diff --git a/res/drawable-xhdpi/ic_launcher.png b/res/drawable-xhdpi/ic_launcher.png new file mode 100644 index 0000000..1e0e82d Binary files /dev/null and b/res/drawable-xhdpi/ic_launcher.png differ diff --git a/res/drawable-xxhdpi/ic_launcher.png b/res/drawable-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..63340a8 Binary files /dev/null and b/res/drawable-xxhdpi/ic_launcher.png differ diff --git a/res/layout/main.xml b/res/layout/main.xml new file mode 100644 index 0000000..7e203c9 --- /dev/null +++ b/res/layout/main.xml @@ -0,0 +1,32 @@ + + + + + + + + + + diff --git a/res/values/strings.xml b/res/values/strings.xml new file mode 100644 index 0000000..cbd84ae --- /dev/null +++ b/res/values/strings.xml @@ -0,0 +1,5 @@ + + + Everlasting Summer RpyPlayer + + diff --git a/src/com/annimon/everlastingsummer/FadeInfo.java b/src/com/annimon/everlastingsummer/FadeInfo.java new file mode 100644 index 0000000..85d34c6 --- /dev/null +++ b/src/com/annimon/everlastingsummer/FadeInfo.java @@ -0,0 +1,45 @@ +package com.annimon.everlastingsummer; + +/** + * Информация о плавном переходе состояния воспроизведения. + * @author aNNiMON + */ +public final class FadeInfo { + + private boolean in, out; + private double duration; + + public FadeInfo() { + this(false, false, 0); + } + + public FadeInfo(boolean in, boolean out, double duration) { + this.in = in; + this.out = out; + this.duration = duration; + } + + public boolean isIn() { + return in; + } + + public void setIn(boolean in) { + this.in = in; + } + + public boolean isOut() { + return out; + } + + public void setOut(boolean out) { + this.out = out; + } + + public double getDuration() { + return duration; + } + + public void setDuration(double duration) { + this.duration = duration; + } +} diff --git a/src/com/annimon/everlastingsummer/IOUtil.java b/src/com/annimon/everlastingsummer/IOUtil.java new file mode 100644 index 0000000..52fd916 --- /dev/null +++ b/src/com/annimon/everlastingsummer/IOUtil.java @@ -0,0 +1,49 @@ +package com.annimon.everlastingsummer; + +import java.io.BufferedReader; +import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.os.Environment; + +/** + * Класс для работы с файловой системой. + * @author aNNiMON + */ +public final class IOUtil { + + private static String SDCARD = Environment.getExternalStorageDirectory().getPath(); + private static String ES = SDCARD + "/everlastingsummer/"; + + public static Bitmap readBitmap(String file) throws IOException { + final InputStream is = open(file); + final Bitmap result = BitmapFactory.decodeStream(is); + is.close(); + return result; + } + + public static FileDescriptor getFD(String file) throws IOException { + // TODO: is = ...; result = ..getFD; is.close; return result + return new FileInputStream(ES + file).getFD(); + } + + public static InputStream open(String file) throws IOException { + return new FileInputStream(ES + file); + } + + public static String readContents(InputStream is) throws IOException { + final StringBuilder sb = new StringBuilder(); + final BufferedReader reader = new BufferedReader(new InputStreamReader(is)); + String line; + while ( (line = reader.readLine()) != null ) { + sb.append(line); + sb.append("\n"); + } + reader.close(); + return sb.toString(); + } +} diff --git a/src/com/annimon/everlastingsummer/Lexer.java b/src/com/annimon/everlastingsummer/Lexer.java new file mode 100644 index 0000000..dee9bc0 --- /dev/null +++ b/src/com/annimon/everlastingsummer/Lexer.java @@ -0,0 +1,190 @@ +package com.annimon.everlastingsummer; + +import java.util.LinkedList; +import java.util.List; + +/** + * @author aNNiMON + */ +public final class Lexer { + + public static List tokenize(String input) { + return new Lexer().process(input).getTokens(); + } + + private final List tokens; + private final StringBuilder buffer; + + private static final String OPERATOR_CHARS = "=+-()[]$"; + private static final TokenType[] OPERATOR_TYPES = new TokenType[] { + TokenType.EQ, + TokenType.PLUS, TokenType.MINUS, + TokenType.LPAREN, TokenType.RPAREN, TokenType.LBRACKET, TokenType.RBRACKET, + TokenType.COMMAND + }; + + private static final String[] KEYWORDS = { + "play", "stop", + "music", "ambience", "sound", "sound_loop", + "fadein", "fadeout", + + "scene", "anim", "bg", "cg", + "at", + "window", "hide", "show", + "with", + "return", + + "renpy.pause", "persistent.sprite_time", + "prolog_time", "day_time", "sunset_time", "night_time" + }; + private static final TokenType[] KEYWORD_TYPES = new TokenType[] { + TokenType.PLAY, TokenType.STOP, + TokenType.MUSIC, TokenType.AMBIENCE, TokenType.SOUND, TokenType.SOUNDLOOP, + TokenType.FADEIN, TokenType.FADEOUT, + + TokenType.SCENE, TokenType.ANIM, TokenType.BG, TokenType.CG, + TokenType.AT, + TokenType.WINDOW, TokenType.HIDE, TokenType.SHOW, + TokenType.WITH, + TokenType.RETURN, + + TokenType.RENPY_PAUSE, TokenType.PERSISTENT_SPRITE_TIME, + TokenType.PROLOG_TIME, TokenType.DAY_TIME, TokenType.SUNSET_TIME, TokenType.NIGHT_TIME + }; + + private TokenizeState state; + private int pos; + + private enum TokenizeState { + DEFAULT, NUMBER, OPERATOR, WORD, TEXT, COMMENT + } + + private Lexer() { + tokens = new LinkedList(); + buffer = new StringBuilder(); + state = TokenizeState.DEFAULT; + } + + public List getTokens() { + return tokens; + } + + public Lexer process(String input) { + final int length = input.length(); + for (pos = 0; pos < length; pos++) { + tokenize(input.charAt(pos)); + } + tokenize('\0');// EOF + addToken(TokenType.EOF, false); + return this; + } + + private void tokenize(char ch) { + switch (state) { + case DEFAULT: tokenizeDefault(ch); break; + case WORD: tokenizeWord(ch); break; + case NUMBER: tokenizeNumber(ch); break; + case OPERATOR: tokenizeOperator(ch); break; + case TEXT: tokenizeText(ch); break; + case COMMENT: tokenizeComment(ch); break; + } + } + + private void tokenizeDefault(char ch) { + if (Character.isLetter(ch)) { + // Слово (ключевое слово или команда) + buffer.append(ch); + state = TokenizeState.WORD; + } else if (Character.isDigit(ch)) { + // Число + buffer.append(ch); + state = TokenizeState.NUMBER; + } else if (ch == '"') { + // Текст в "кавычках" + state = TokenizeState.TEXT; + } else if (ch == '#') { + clearBuffer(); + state = TokenizeState.COMMENT; + } else { + // Операторы и спецсимволы + tokenizeOperator(ch); + } + } + + private void tokenizeWord(char ch) { + if (ch == ':') { + addToken(TokenType.LABEL, false); + return; + } + if (Character.isLetterOrDigit(ch) || (ch == '_') || (ch == '.')) { + buffer.append(ch); + } else { + final String word = buffer.toString(); + for (int i = 0; i < KEYWORDS.length; i++) { + if (KEYWORDS[i].equalsIgnoreCase(word)) { + addToken(KEYWORD_TYPES[i]); + return; + } + } + addToken(TokenType.WORD); + } + } + + private void tokenizeNumber(char ch) { + // Целое или вещественное число. + if (ch == '.') { + // Пропускаем десятичные точки, если они уже были в числе. + if (buffer.indexOf(".") == -1) buffer.append(ch); + } else if (Character.isDigit(ch)) { + buffer.append(ch); + } else { + addToken(TokenType.NUMBER); + } + } + + private void tokenizeOperator(char ch) { + final int index = OPERATOR_CHARS.indexOf(ch); + if (index != -1) { + addToken(OPERATOR_TYPES[index], false); + } + } + + private void tokenizeText(char ch) { + if (ch == '"') { + final int len = buffer.length(); + // Добавляем токен, если не было экранирования символа кавычки. + if (len == 0 || + ( (len > 0) && (buffer.charAt(len - 1) != '\\') )) { + addToken(TokenType.TEXT, false); + return; + } + // Экранируем символ кавычки. + if (len > 0) { + buffer.setCharAt(len - 1, '\"'); + return; + } + } + buffer.append(ch); + } + + private void tokenizeComment(char ch) { + if (ch == '\n' || ch == '\r') { + state = TokenizeState.DEFAULT; + } + } + + private void addToken(TokenType type) { + addToken(type, true); + } + + private void addToken(TokenType type, boolean reprocessLastChar) { + tokens.add(new Token(buffer.toString(), type)); + clearBuffer(); + if (reprocessLastChar) pos--; + state = TokenizeState.DEFAULT; + } + + private void clearBuffer() { + buffer.setLength(0); + } +} diff --git a/src/com/annimon/everlastingsummer/MainActivity.java b/src/com/annimon/everlastingsummer/MainActivity.java new file mode 100644 index 0000000..424c7b4 --- /dev/null +++ b/src/com/annimon/everlastingsummer/MainActivity.java @@ -0,0 +1,43 @@ +package com.annimon.everlastingsummer; + +import java.io.IOException; +import android.app.ListActivity; +import android.content.Intent; +import android.os.Bundle; +import android.view.View; +import android.widget.ArrayAdapter; +import android.widget.ListView; +import android.widget.Toast; + +/** + * Экран выбора сценариев из папки assets. + * @author aNNiMON + */ +public final class MainActivity extends ListActivity { + + private String[] scripts; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + try { + scripts = getAssets().list(PathResolver.SCRIPT_ASSETS); + } catch (IOException ioe) { + scripts = null; + } + if (scripts == null || scripts.length == 0) { + Toast.makeText(this, "Нет скриптов в папке " + PathResolver.SCRIPT_ASSETS, + Toast.LENGTH_LONG).show(); + finish(); + } + setListAdapter(new ArrayAdapter(this, android.R.layout.simple_list_item_1, scripts)); + } + + @Override + protected void onListItemClick(ListView l, View v, int index, long id) { + final Intent intent = new Intent(this, ViewActivity.class); + intent.putExtra(ViewActivity.EXTRA_NAME, scripts[index]); + startActivity(intent); + } +} diff --git a/src/com/annimon/everlastingsummer/Parser.java b/src/com/annimon/everlastingsummer/Parser.java new file mode 100644 index 0000000..6c2b5c8 --- /dev/null +++ b/src/com/annimon/everlastingsummer/Parser.java @@ -0,0 +1,262 @@ +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 tokens) { + instance = new Parser(tokens); + return instance; + } + + public static Parser getInstance() { + return instance; + } + + + private final List tokens; + private int position; + + public Parser(List tokens) { + this.tokens = tokens; + position = 0; + } + + 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); + } + + 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 (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.RETURN) || 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; + } + + 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(); + final String emotion = lookMatch(0, TokenType.WORD) ? consume(TokenType.WORD).getText() : ""; + final String cloth = lookMatch(0, TokenType.WORD) ? consume(TokenType.WORD).getText() : ""; + final String distance = lookMatch(0, TokenType.WORD) ? consume(TokenType.WORD).getText() : ""; + // Положение (left, cleft, ...) + String position = ""; + if (match(TokenType.AT)) { + position = consume(TokenType.WORD).getText(); + } + matchWithEffect(); + ViewActivity.getInstance().sprite(whoid, emotion, cloth, distance, 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 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 >= tokens.size()) return EOF; + return tokens.get(position + offset); + } +} diff --git a/src/com/annimon/everlastingsummer/PathResolver.java b/src/com/annimon/everlastingsummer/PathResolver.java new file mode 100644 index 0000000..5db80ef --- /dev/null +++ b/src/com/annimon/everlastingsummer/PathResolver.java @@ -0,0 +1,61 @@ +package com.annimon.everlastingsummer; + +import java.util.Locale; +import android.text.TextUtils; + +/** + * Корректировщик путей к ресурсам. + * @author aNNiMON + */ +public final class PathResolver { + + public static final String SCRIPT_ASSETS = "scripts"; + private static final String SPRITE = "sprites/"; + private static final String MUSIC = "music/"; + private static final String SOUND = "sfx/"; + + private static final String PNG = ".png"; + private static final String JPG = ".jpg"; + private static final String OGG = ".ogg"; + + public static String script(String name) { + return SCRIPT_ASSETS + "/" + name; + } + + public static String sprite(String whoid, String emotion, String cloth, String distance) { + final StringBuilder sb = new StringBuilder(); + sb.append(SPRITE); + sb.append(TextUtils.isEmpty(distance) ? "normal" : distance).append('/'); + sb.append(whoid.toLowerCase(Locale.ENGLISH)).append('/'); + sb.append(emotion); + sb.append(cloth); + sb.append(PNG); + return sb.toString(); + } + + public static String background(String type, String name) { + final StringBuilder sb = new StringBuilder(); + sb.append(type.toLowerCase(Locale.ENGLISH)).append('/'); + sb.append(name); + sb.append(JPG); + return sb.toString(); + } + + public static String music(String name) { + final StringBuilder sb = new StringBuilder(); + sb.append(MUSIC); + sb.append(name); + sb.append(OGG); + return sb.toString(); + } + + public static String sound(String name) { + final StringBuilder sb = new StringBuilder(); + sb.append(SOUND); + if (name.startsWith("sfx_")) sb.append(name.substring(4)); + else sb.append(name); + sb.append(OGG); + return sb.toString(); + } + +} diff --git a/src/com/annimon/everlastingsummer/Token.java b/src/com/annimon/everlastingsummer/Token.java new file mode 100644 index 0000000..3eb217e --- /dev/null +++ b/src/com/annimon/everlastingsummer/Token.java @@ -0,0 +1,28 @@ +package com.annimon.everlastingsummer; + +/** + * @author aNNiMON + */ +public final class Token { + + private final String text; + private final TokenType type; + + public Token(String text, TokenType type) { + this.text = text; + this.type = type; + } + + public String getText() { + return text; + } + + public TokenType getType() { + return type; + } + + @Override + public String toString() { + return type.name() + " " + text; + } +} diff --git a/src/com/annimon/everlastingsummer/TokenType.java b/src/com/annimon/everlastingsummer/TokenType.java new file mode 100644 index 0000000..5e685f2 --- /dev/null +++ b/src/com/annimon/everlastingsummer/TokenType.java @@ -0,0 +1,56 @@ +package com.annimon.everlastingsummer; + +/** + * @author aNNiMON + */ +public enum TokenType { + + COMMAND, // начинается с $ + LABEL, // заканчивается на : + WORD, + TEXT, + NUMBER, + + // операторы и спецсимволы + EQ, + PLUS, + MINUS, + LPAREN, + RPAREN, + LBRACKET, + RBRACKET, + + // ключевые слова + PLAY, + STOP, + MUSIC, + AMBIENCE, + SOUND, + SOUNDLOOP, + FADEIN, + FADEOUT, + + SCENE, + ANIM, + BG, + CG, + + WINDOW, + HIDE, + SHOW, + + AT, + WITH, + + RETURN, + + // команды + RENPY_PAUSE, + PERSISTENT_SPRITE_TIME, + PROLOG_TIME, + DAY_TIME, + SUNSET_TIME, + NIGHT_TIME, + + EOF +} diff --git a/src/com/annimon/everlastingsummer/ViewActivity.java b/src/com/annimon/everlastingsummer/ViewActivity.java new file mode 100644 index 0000000..046cd89 --- /dev/null +++ b/src/com/annimon/everlastingsummer/ViewActivity.java @@ -0,0 +1,265 @@ +package com.annimon.everlastingsummer; + +import java.util.HashMap; +import java.util.Map; +import android.app.Activity; +import android.media.MediaPlayer; +import android.os.Bundle; +import android.text.*; +import android.text.style.ForegroundColorSpan; +import android.view.View; +import android.view.WindowManager; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.TextView; +import android.widget.Toast; + +/** + * Экран воспроизведения rpy-сценария. + * @author aNNiMON + */ +public final class ViewActivity extends Activity { + + public static final String EXTRA_NAME = "name"; + private static final FadeInfo NO_FADE = new FadeInfo(false, false, 0); + private static ViewActivity instance; + + public static ViewActivity getInstance() { + return instance; + } + + private ImageView background; + private FrameLayout container; + private TextView textview; + private MediaPlayer musicPlayer, soundPlayer; + + private Map names; + private Map spriteInContainer; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Постоянная подсветка. + getWindow().setFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON, + WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + + setContentView(R.layout.main); + instance = this; + + background = (ImageView) findViewById(R.id.background); + container = (FrameLayout) findViewById(R.id.container); + textview = (TextView) findViewById(R.id.text); + + background.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Parser.getInstance().next(); + } + }); + + spriteInContainer = new HashMap(); + + // Маппинг <короткое_имя, имя/цвет> + names = new HashMap(); + // https://github.com/yakui-lover/eroge-dopil/blob/master/media.rpy#L365 + names.put("me", new Person("Семён", 0xFFE1DD7D)); + names.put("un", new Person("Лена", 0xFFB956FF)); // night 0xFFAA64D9, sunset: 0xFFB956FF, day: 0xFFB956FF, prolog: 0xFFB956FF + names.put("dv", new Person("Алиса", 0xFFFFAA00)); // night 0xFFD28B10, sunset: 0xFFFFAA00, day: 0xFFFFAA00, prolog: 0xFFFFAA00 + names.put("sl", new Person("Славя", 0xFFFFD200)); // night 0xFFD6B000, sunset: 0xFFFFD200, day: 0xFFFFD200, prolog: 0xFFFFD200 + names.put("us", new Person("Ульяна", 0xFFFF3200)); // night 0xFFEA3700, sunset: 0xFFFF3200, day: 0xFFFF3200, prolog: 0xFFFF3200 + names.put("mt", new Person("Ольга Дмитриевна", 0xFF00EA32)); // night 0xFF00B627, sunset: 0xFF00EA32, day: 0xFF00EA32, prolog: 0xFF00EA32 + names.put("cs", new Person("Виола", 0xFFA5A5FF)); // night 0xFF8686E6, sunset: 0xFFA5A5FF, day: 0xFFA5A5FF, prolog: 0xFFA5A5FF + names.put("mz", new Person("Женя", 0xFF4A86FF)); // night 0xFF5481DB, sunset: 0xFF72A0FF, day: 0xFF4A86FF, prolog: 0xFF4A86FF + names.put("mi", new Person("Мику", 0xFF00DEFF)); // night 0xFF00B4CF, sunset: 0xFF00FBFF, day: 0xFF00DEFF, prolog: 0xFF00DEFF + names.put("uv", new Person("Юля", 0xFF4EFF00)); // night 0xFF40D000, sunset: 0xFF4EFF00, day: 0xFF4EFF00, prolog: 0xFF4EFF00 + names.put("lk", new Person("Луркмор-кун", 0xFFFF8080)); + names.put("sh", new Person("Шурик", 0xFFFFF226)); // night 0xFFCDC212, sunset: 0xFFFFF226, day: 0xFFFFF226, prolog: 0xFFFFF226 + names.put("el", new Person("Электроник", 0xFFFFFF00)); // night 0xFFCDCD00, sunset: 0xFFFFFF00, day: 0xFFFFFF00, prolog: 0xFFFFFF00 + names.put("pi", new Person("Пионер", 0xFFE60101)); // night 0xFFE60000, sunset: 0xFFE60000, day: 0xFFE60101, prolog: 0xFFE60000 + + names.put("dy", new Person("Голос из динамика", 0xFFC0C0C0)); + names.put("voice", new Person("Голос", 0xFFE1DD7D)); + names.put("voices", new Person("Голоса", 0xFFC0C0C0)); + names.put("message", new Person("Сообщение", 0xFFC0C0C0)); + names.put("all", new Person("Пионеры", 0xFFED4444)); // night 0xFFE33A3A, sunset: 0xFFE33A3A, day: 0xFFED4444, prolog: 0xFFE33A3A + names.put("kids", new Person("Малышня", 0xFFEB7883)); + names.put("dreamgirl", new Person("...", 0xFFC0C0C0)); + names.put("bush", new Person("Голос", 0xFFC0C0C0)); + names.put("FIXME_voice", new Person("Голос", 0xFFC0C0C0)); + names.put("odn", new Person("Одногруппник", 0xFFC0C0C0)); + names.put("mt_voice", new Person("Голос", 0xFF00EA32)); // night 0xFF00B627, sunset: 0xFF00EA32, day: 0xFF00EA32, prolog: 0xFF00EA32 + + final String name = getIntent().getStringExtra(EXTRA_NAME); + final String scriptpath = PathResolver.script(name); + try { + Parser.parse(Lexer.tokenize( IOUtil.readContents(getAssets().open(scriptpath)) )); + Parser.getInstance().next(); + } catch (Exception ex) { + Toast.makeText(this, "Ошибка при открытии файла " + scriptpath, Toast.LENGTH_LONG).show(); + finish(); + } + } + + @Override + protected void onPause() { + stopMusic(NO_FADE); + stopSound(NO_FADE); + super.onPause(); + } + + + public void windowShow() { + if (textview.getVisibility() != View.VISIBLE) + textview.setVisibility(View.VISIBLE); + } + + public void windowHide() { + if (textview.getVisibility() != View.INVISIBLE) + textview.setVisibility(View.INVISIBLE); + } + + public void background(String type, String name, String effect) { + spritesClear(); + if (name.equalsIgnoreCase("black")) background.setImageResource(android.R.color.black); + else if (name.equalsIgnoreCase("white")) background.setImageResource(android.R.color.white); + else { + try { + background.setImageBitmap(IOUtil.readBitmap(PathResolver.background(type, name))); + } catch (Exception ioe) { + background.setImageResource(android.R.color.black); + } + } + } + + public void spritesClear() { + container.removeAllViews(); + spriteInContainer.clear(); + } + + public void sprite(String whoid, String emotion, String cloth, String distance, String position) { + ImageView img; + if (spriteInContainer.containsKey(whoid)) { + img = spriteInContainer.get(whoid); + } else { + img = new ImageView(this); + spriteInContainer.put(whoid, img); + } + final String path = PathResolver.sprite(whoid, emotion, cloth, distance);; + try { + img.setImageBitmap(IOUtil.readBitmap(path)); + container.addView(img); + } catch (Exception ioe) {} + } + + public void pause(final long duration) { + text(""); + new Thread(new Runnable() { + @Override + public void run() { + try { + Thread.sleep(duration); + } catch (InterruptedException e) { } + runOnUiThread(nextCommandRunnable); + } + }).start(); + } + + public void text(String text) { + if (TextUtils.isEmpty(text)) windowHide(); + else { + windowShow(); + textview.setText(formatString(text)); + } + } + + public void text(String whoid, String text) { + if (whoid.equalsIgnoreCase("th")) text("~ " + text + " ~"); + else if (!names.containsKey(whoid)) text(text); + else { + windowShow(); + final Person person = names.get(whoid); + final String who = person.name; + Spannable spannable = formatString(who + "\n" + text); + spannable.setSpan(new ForegroundColorSpan(person.color), 0, who.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + textview.setText(spannable); + } + } + + private Spannable formatString(String text) { + String edited = text.replace("{w}", ""); + final char[] codes = {'b','i','s','u'}; + boolean html = false; + for (int i = 0; i < codes.length; i++) { + final char ch = codes[i]; + if (edited.contains("{"+ch+"}")) { + edited = edited.replace("{"+ch+"}", "<"+ch+">"); + edited = edited.replace("{/"+ch+"}", ""); + html = true; + } + } + return new SpannableString(html ? Html.fromHtml(edited) : edited); + } + + public void music(String name, FadeInfo fade) { + try { + stopMusic(fade); + musicPlayer = new MediaPlayer(); + musicPlayer.setDataSource( IOUtil.getFD(PathResolver.music(name)) ); + musicPlayer.prepare(); + musicPlayer.setVolume(1f, 1f); + musicPlayer.setLooping(true); + musicPlayer.start(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public void stopMusic(FadeInfo fade) { + if (musicPlayer == null) return; + if (musicPlayer.isPlaying()) { + musicPlayer.stop(); + musicPlayer.release(); + } + musicPlayer = null; + } + + public void sound(String name, boolean loop, FadeInfo fade) { + try { + stopSound(fade); + soundPlayer = new MediaPlayer(); + soundPlayer.setDataSource( IOUtil.getFD(PathResolver.sound(name)) ); + soundPlayer.prepare(); + soundPlayer.setVolume(1f, 1f); + soundPlayer.setLooping(loop); + soundPlayer.start(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public void stopSound(FadeInfo fade) { + if (soundPlayer == null) return; + if (soundPlayer.isPlaying()) { + soundPlayer.stop(); + soundPlayer.release(); + } + soundPlayer = null; + } + + private final Runnable nextCommandRunnable = new Runnable() { + @Override + public void run() { + Parser.getInstance().next(); + } + }; + + private class Person { + String name; + int color; + Person(String fullName, int color) { + this.name = fullName; + this.color = color; + } + } +}