Compare commits

..

4 commits

Author SHA1 Message Date
Jordan Irwin
e3b9c22ff7 Shorten achievement title to "Sowing Seeds"
Some checks failed
Java CI / build (push) Has been cancelled
2023-10-05 10:24:53 -07:00
Hendrik Brummermann
654452d1a6 use two digit year for fametype (#388) 2023-10-03 22:13:55 +02:00
Hendrik Brummermann
abd1095523 fixed webclient build 2023-10-02 20:15:14 +02:00
Hendrik Brummermann
ae89130c9d updated version number 2023-10-01 09:14:12 +02:00
4884 changed files with 126877 additions and 150200 deletions

View file

@ -25,5 +25,5 @@
<classpathentry kind="lib" path="libs/jakarta.annotation-api-2.1.1.jar"/>
<classpathentry exported="true" kind="lib" path="libs/tomcat-embed-core-10.1.13.jar"/>
<classpathentry kind="lib" path="libs/tomcat-embed-websocket-10.1.13.jar"/>
<classpathentry kind="output" path="build/eclipse"/>
<classpathentry kind="output" path="bin"/>
</classpath>

View file

@ -1,62 +0,0 @@
name: Android Build Test
on:
push:
paths:
- 'app/android/**'
pull_request:
paths:
- 'app/android/**'
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- id: job_info
uses: daleyjem/gh-job-id@v1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
job_name: 'build'
- name: Checkout Repository
uses: actions/checkout@v3
- name: Set Up JDK
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'adopt'
- name: Validate Gradle Wrapper .jar
run: |
cd app/android/gradle/wrapper
curl --location --output gradle-wrapper.jar.sha256 \
https://services.gradle.org/distributions/gradle-7.5.1-wrapper.jar.sha256
echo " gradle-wrapper.jar" >> gradle-wrapper.jar.sha256
sha256sum --check gradle-wrapper.jar.sha256
- name: Build APKs with Ant
uses: cedx/setup-ant@v6
with:
version: '1.10'
- run: ant -noinput -buildfile build.xml android -Dgradle.params="assembleDebug assembleRelease"
- name: Failure Report
uses: sarisia/actions-status-discord@v1
if: failure()
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK_DEVELOPMENT }}
with:
title: 'Android Build Failed'
description: '${{ steps.job_info.outputs.html_url }}'
nodetail: true
- uses: sarisia/actions-status-discord@v1
if: failure()
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK_DEVLOG }}
with:
title: 'Android Build Failed'
description: '${{ steps.job_info.outputs.html_url }}'
nodetail: true

View file

@ -29,15 +29,15 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
uses: github/codeql-action/init@v2
# Override language selection by uncommenting this and choosing your languages
# with:
# languages: go, javascript, csharp, python, cpp, java
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
#- name: Autobuild
# uses: github/codeql-action/autobuild@v3
- name: Autobuild
uses: github/codeql-action/autobuild@v2
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@ -46,7 +46,9 @@ jobs:
# and modify them (or add more) to build your code if your project
# uses a compiled language
- run: ant -noinput -f build.xml clean compile
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
uses: github/codeql-action/analyze@v2

View file

@ -39,7 +39,7 @@ jobs:
- name: Build APKs
run: >
ant -noinput -buildfile build.xml dist_android_client
-Dgradle.params="-Pandroid.ver=${{ steps.ver.outputs.value }} assembleDebug assembleRelease"
-Dgradle.params="--no-daemon -Pandroid.ver=${{ steps.ver.outputs.value }} assembleDebug assemblePrerelease"
- name: Publish
uses: softprops/action-gh-release@v1

3
.gitignore vendored
View file

@ -40,6 +40,7 @@ javadocs
statserver_stats.xml
/*.ks
build_client_data
/bin
.byecycle
build_server_xmlconf
*.umlproject
@ -48,5 +49,3 @@ build_server_xmlconf
/map.map
/hs_err_*.log
/node_modules/
/log/
/build-archive/

View file

@ -20,15 +20,4 @@
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.wst.common.project.facet.core.nature</nature>
</natures>
<filteredResources>
<filter>
<id>1771748271774</id>
<name></name>
<type>30</type>
<matcher>
<id>org.eclipse.core.resources.regexFilterMatcher</id>
<arguments>node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__</arguments>
</matcher>
</filter>
</filteredResources>
</projectDescription>

View file

@ -11,7 +11,7 @@ Stendhal is a fully fledged multiplayer online adventures game (MORPG). It is co
Stendhal features a new, rich and expanding world in which you can explore towns, buildings, plains, caves and dungeons.
You will meet NPCs and acquire tasks and quests for valuable experience.
Your character will develop and grow and with each new level up become stronger and better. With the money you acquire you can buy new items and improve your armor and weapons. And for the blood thirsty of you; satisfy your killing desires by roaming the world in search of evil monsters!
Your character will develop and grow and with each new level up become stronger and better. With the money you acquire you can buy new items and improve your armour and weapons. And for the blood thirsty of you; satisfy your killing desires by roaming the world in search of evil monsters!
Stendhal is platform independent, and completely open source.
@ -20,38 +20,19 @@ So what are you waiting for?! A whole new world awaits...
## How to play
### Web Browser
[![Play now](https://arianne-project.org/images/playbutton.png)](https://stendhalgame.org/account/mycharacters.html)
Play in any web browser for desktop or mobile with JavaScript support. Click on the "play now"
button to create or log in to your account.
### Mobile
Play using the [progressive web app](https://en.wikipedia.org/wiki/Progressive_web_app). Just visit
[stendhalgame.org](https://stendhalgame.org/) in your mobile browser and click "install" when prompted.
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" height="75">](https://f-droid.org/packages/org.stendhalgame.client/)
Play on your Android device using the dedicated app. You can get it from the
[F-Droid App Store](https://f-droid.org/) or download it from [stendhalgame.org](https://stendhalgame.org/download.html).
### Classic Desktop
[![Play now](https://arianne-project.org/images/playbutton.png)](https://stendhalgame.org/account/mycharacters.html) &nbsp; &nbsp; &nbsp; &nbsp;
[![Download](https://stendhalgame.org/images/downloadbutton.png)](https://arianne-project.org/download/stendhal.zip)
Play using the classic desktop client. You will need Java which you can download from
[java.com](https://www.java.com/en/download) on Microsoft Windows. Java is included in
most Linux distributions. Both Oracle Java and OpenJDK are supported.
You need Java which you can download from [https://www.java.com](https://www.java.com/en/download) on Microsoft Windows. Java is included in most Linux distributions. Both Oracle Java and OpenJDK are supported.
Please download Stendhal from [stendhalgame.org](https://stendhalgame.org/download.html).<br>
You can double click on `stendhal-starter.jar` and the client will run.<br>
If you prefer to run from command line, then just execute this command in the stendhal folder:
Please download Stendhal from [https://stendhalgame.org](https://stendhalgame.org)<br>
You can double click on stendhal-starter.jar and the client will run.<br>
If you prefer to run from command line, then just execute this command
in the stendhal folder:
`java -jar stendhal-starter.jar`
There is a [manual](https://stendhalgame.org/wiki/Stendhal_Manual), and there is Help from the client menu icons in the upper right hand side of the game screen.
There is a [manual](https://stendhalgame.org/wiki/Stendhal_Manual), and there is Help from the client menu icons in the upper right hand side of the game screen.
## Development environment
@ -63,18 +44,13 @@ There is a [manual](https://stendhalgame.org/wiki/Stendhal_Manual), and there is
## Legal
The server and java client are free software; you can redistribute it and/or modify it under the terms of the **GNU General Public License** 2 or later as published by the Free Software Foundation.
Stendhal is free software. You can redistribute it and/or modify under the following terms:
The web client is free software; you can redistribute it and/or modify it under the terms of the **GNU Affero General Public License** 3 or later as published by the Free Software Foundation.
- The **server**, **java client**, and **Android client**: *GNU General Public License* 2 or later as published by the Free Software Foundation
- The **web client** and **native client**: *GNU Affero General Public License* 3 or later as published by the Free Software Foundation with the definition of "System libraries" being extended to include authentication providers and software distribution platforms.
- The **nativehelper** extensions: *GNU Lesser General Public License* 3 or later as published by the Free Software Foundation
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
[LICENSE.txt](LICENSE.txt) and [src/js/LICENSE.html](src/js/LICENSE.html) file for more details.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the [LICENSE.txt](LICENSE.txt) file for more details.
Stendhal(c) is copyright of Miguel Angel Blanch Lardin, 2005-2008, arianne_rpg at users dot sourceforge dot net and others<br>
Stendhal(c) is copyright of the Arianne Project, 2006-2026, arianne-general at lists dot sourceforge dot net
Stendhal(c) is copyright of the Arianne Project, 2006-2023, arianne-general at lists dot sourceforge dot net
Please have a look at the list of [contributors](doc/contributors.md#contributors).
Please have a look at the list of [contributors](https://github.com/arianne/stendhal/blob/master/doc/contributors.md#contributors).

View file

@ -8,4 +8,3 @@
.externalNativeBuild
.cxx
local.properties
/deps/

13
android/TODO.txt Normal file
View file

@ -0,0 +1,13 @@
TODO:
- show input preview when typing
- scale page to fit inside view
- create activity for about information
- add credits to about information
- switch to Log4j for logging (maybe)
- use center of d-pad for setting position
- d-pads need to be different sizes for different resolutions
- either use unique PNGs or convert to scalable SVGs
BUILD TODO:
- script to download Android SDK

79
android/app/build.gradle Normal file
View file

@ -0,0 +1,79 @@
plugins {
id 'com.android.application'
}
android {
compileSdk 32
def buildDate = new Date().format("yyyyMMdd")
def appVer
if (project.hasProperty("android.ver") && project["android.ver"]) {
appVer = project["android.ver"]
} else {
appVer = "${buildDate}"
}
if (project.hasProperty("android.rev") && project["android.rev"]) {
appVer += "-" + project["android.rev"]
}
def outputName = "stendhal-webview-${appVer}"
defaultConfig {
applicationId "org.arianne.stendhal.client"
minSdk 21
targetSdk 32
versionCode 1
versionName "${appVer}"
setProperty("archivesBaseName", "${outputName}")
setProperty("buildDir", "${projectDir}/../../build/build_android_client")
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
targetSdkVersion 32
minSdkVersion 21
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
prerelease {
versionNameSuffix "-pre"
signingConfig signingConfigs.debug
}
debug {
applicationIdSuffix ".debug"
versionNameSuffix "-debug"
}
}
buildTypes.all { type ->
if (!type.name.equals("debug")) {
sourceSets[type.name].java { srcDirs "src/shared/java/" }
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
compileSdkVersion 32
buildToolsVersion '32.0.0'
lintOptions {
abortOnError false
}
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'com.google.android.material:material:1.4.0'
//testImplementation 'junit:junit:4.+'
//androidTestImplementation 'androidx.test.ext:junit:1.1.2'
//androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
implementation files('../../libs/marauroa-client-only.jar')
}

View file

@ -0,0 +1,157 @@
/***************************************************************************
* Copyright © 2022 - Arianne *
***************************************************************************
***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
package org.arianne.stendhal.client;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.util.Log;
import androidx.appcompat.app.AppCompatActivity;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* A class to output debugging information to logcat file.
*/
public class DebugLog {
private static File logsDir;
private static AppCompatActivity mainActivity;
public static enum DebugLevel {
INFO("INFO"),
WARN("WARN"),
ERROR("ERROR"),
DEBUG("DEBUG");
private final String label;
private DebugLevel(final String label) {
this.label = label;
}
public String getLabel() {
return label;
}
}
private static DebugLog instance;
public static DebugLog get() {
if (instance == null) {
instance = new DebugLog();
}
return instance;
}
public static void init(final File dir, final AppCompatActivity activity) {
logsDir = new File(dir.getPath() + "/logs");
mainActivity = activity;
writeLine("\n // -- debugging initialized -- //", null);
writeLine("logs directory: " + logsDir.getPath());
}
public static void writeLine(final String text) {
writeLine(text, DebugLevel.DEBUG);
}
public static void writeLine(String text, final DebugLevel level) {
if (logsDir == null) {
System.err.println("ERROR: DebugLog not initialized. Call DebugLog.init.");
return;
}
if (!logsDir.exists()) {
logsDir.mkdirs();
}
if (level != null) {
text = new SimpleDateFormat("yyyy.MM.dd.HH.mm.ss").format(new Date())
+ ": " + level.label + ": " + text;
}
text = text + "\n";
final String logFile = logsDir.getPath() + "/debug-"
+ new SimpleDateFormat("yyyy.MM.dd").format(new Date()) + ".txt";
BufferedWriter buffer;
try {
buffer = new BufferedWriter(new FileWriter(logFile, true));
buffer.write(text);
if (buffer != null) {
buffer.close();
}
} catch (final IOException e) {
System.err.println("ERROR: " + e.getMessage());
}
}
public static void info(final String text) {
Log.i("DebugLog", text);
writeLine(text, DebugLevel.INFO);
}
public static void warn(final String text) {
Log.w("DebugLog", text);
writeLine(text, DebugLevel.WARN);
}
public static void error(final String text) {
Log.e("DebugLog", text);
writeLine(text, DebugLevel.ERROR);
}
public static void debug(final String text) {
Log.d("DebugLog", text);
writeLine(text, DebugLevel.DEBUG);
}
public static void notify(final String message) {
notify(message, DebugLevel.DEBUG);
}
public static void notify(final String message, DebugLevel level) {
if (mainActivity == null) {
System.err.println("ERROR: DebugLog not initialized. Call DebugLog.init.");
return;
}
if (level == null) {
level = DebugLevel.DEBUG;
}
final AlertDialog.Builder builder = new AlertDialog.Builder(mainActivity);
builder.setTitle(level.label);
builder.setMessage(message);
builder.setPositiveButton("Ok", new DialogInterface.OnClickListener() {
public void onClick(final DialogInterface dialog, final int id) {
dialog.cancel();
}
});
builder.create().show();
}
public static String getLogsDir() {
return logsDir.getPath();
}
}

View file

@ -3,7 +3,5 @@
<resources>
<string name="app_name">Stendhal (debug)</string>
<string name="intent_url_scheme">stendhaldebug8gps5y99pu</string>
<string name="build_type">debug</string>
</resources>

View file

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.arianne.stendhal.client">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Stendhal.FullScreen">
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:exported="true"
android:configChanges="orientation|screenSize|keyboardHidden">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".PreferencesActivity"
android:label="Preferences"
android:exported="true"
android:configChanges="orientation|screenSize|keyboardHidden">
<intent-filter>
<action android:name="preferences" />
</intent-filter>
</activity>
</application>
</manifest>

View file

Before

Width:  |  Height:  |  Size: 505 KiB

After

Width:  |  Height:  |  Size: 505 KiB

Before After
Before After

View file

@ -0,0 +1,503 @@
/***************************************************************************
* Copyright © 2022 - Arianne *
***************************************************************************
***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
package org.arianne.stendhal.client;
import android.app.AlertDialog;
import android.content.pm.ApplicationInfo;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Build;
import android.util.AttributeSet;
import android.view.inputmethod.BaseInputConnection;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.view.inputmethod.InputConnection;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.webkit.DownloadListener;
import android.webkit.WebChromeClient;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.ImageView;
import marauroa.common.Pair;
import org.arianne.stendhal.client.input.DPad;
import org.arianne.stendhal.client.js.JSInterface;
import org.arianne.stendhal.client.sound.MusicPlayer;
public class ClientView extends WebView {
private static ClientView instance;
private ImageView splash;
private final String defaultServer = "https://stendhalgame.org/";
private String clientUrlSuffix = "client";
private boolean testing = false;
private Boolean debugging;
private static PageId currentPage;
// denotes previous touch was remapped to mouse event
private boolean touchOverridden = false;
/**
* Retrieves the static instance.
*/
public static ClientView get() {
return instance;
}
public ClientView(final Context ctx) {
super(ctx);
onInit();
}
public ClientView(final Context ctx, final AttributeSet attrs) {
super(ctx, attrs);
onInit();
}
public ClientView(final Context ctx, final AttributeSet attrs, final int style) {
super(ctx, attrs, style);
onInit();
}
public ClientView(final Context ctx, final AttributeSet attrs, final int style,
final boolean prvtBrowse) {
super(ctx, attrs, style, prvtBrowse);
onInit();
}
private void onInit() {
instance = this;
setBackgroundColor(android.graphics.Color.TRANSPARENT);
final WebSettings viewSettings = getSettings();
viewSettings.setJavaScriptEnabled(true);
viewSettings.setDomStorageEnabled(true);
// keep elements in position in portrait mode
viewSettings.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.SINGLE_COLUMN);// <-- SINGLE_COLUMN deprecated
viewSettings.setLoadWithOverviewMode(true);
viewSettings.setUseWideViewPort(true);
// disable zoom
viewSettings.setSupportZoom(false);
viewSettings.setBuiltInZoomControls(false);
viewSettings.setDisplayZoomControls(false);
if (debugEnabled()) {
// make WebView debuggable for debug builds
setWebContentsDebuggingEnabled(true);
}
initWebViewClient();
initTouchHandler();
initJSInterface();
initDownloadHandler();
}
private void initWebViewClient() {
// XXX: not sure setting WebChromClient is doing anything, was recommended to
// fix touchmove events not registering
setWebChromeClient(new WebChromeClient());
setWebViewClient(new WebViewClient() {
/* handle changing URLs */
@Override
public boolean shouldOverrideUrlLoading(final WebView view, final String url) {
if (!isInternalUrl(url)) {
// FIXME: should we ask for confirmation?
MainActivity.get().startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url)));
return true;
}
view.loadUrl(checkClientUrl(url));
return false;
}
@Override
public void onPageStarted(final WebView view, final String url, final Bitmap favicon) {
DebugLog.debug("loading URL: " + url);
MusicPlayer.stopMusic();
super.onPageStarted(view, url, favicon);
}
@Override
public void onPageFinished(final WebView view, final String url) {
super.onPageFinished(view, url);
if (isClientUrl(url)) {
currentPage = PageId.WEBCLIENT;
} else if (url.equals("") || url.equals("about:blank")) {
currentPage = PageId.TITLE;
if (PreferencesActivity.getBoolean("title_music", true)) {
playTitleMusic();
}
} else {
currentPage = PageId.OTHER;
}
Menu.get().updateButtons();
// only show d-pad on client screen
final DPad pad = DPad.getCurrentPad();
if (pad != null && PreferencesActivity.getBoolean("show_dpad", false)) {
if (isGameActive()) {
pad.show();
} else {
pad.hide();
}
}
DebugLog.debug("page id: " + currentPage);
}
});
}
private void initTouchHandler() {
setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(final View view, final MotionEvent event) {
if (touchOverridden) {
// reset touch event state
touchOverridden = false;
return false;
}
final MotionEvent.PointerProperties[] props = {new MotionEvent.PointerProperties()};
event.getPointerProperties(0, props[0]);
if (isGameActive() && PreferencesActivity.getBoolean("map_touches", false)
&& (props[0].toolType == MotionEvent.TOOL_TYPE_FINGER
|| props[0].toolType == MotionEvent.TOOL_TYPE_STYLUS)) {
Integer action = event.getAction();
/*
switch (action) {
case MotionEvent.ACTION_DOWN:
action = MotionEvent.ACTION_BUTTON_PRESS;
break;
case MotionEvent.ACTION_UP:
action = MotionEvent.ACTION_BUTTON_RELEASE;
break;
}
*/
DebugLog.debug("mapping touch to mouse event");
props[0].toolType = MotionEvent.TOOL_TYPE_MOUSE;
final MotionEvent.PointerCoords[] coords = {new MotionEvent.PointerCoords()};
event.getPointerCoords(0, coords[0]);
touchOverridden = true;
view.dispatchTouchEvent(MotionEvent.obtain(event.getDownTime(),
event.getEventTime(), event.getAction(), event.getPointerCount(), props,
coords, event.getMetaState(), MotionEvent.BUTTON_PRIMARY,
event.getXPrecision(), event.getYPrecision(), event.getDeviceId(),
event.getEdgeFlags(), event.getSource(), event.getFlags()));
return true; // consume old event
}
return false;
}
});
}
@Override
public InputConnection onCreateInputConnection(final EditorInfo outAttrs) {
// allows capturing soft keyboard events
return new BaseInputConnection(this, false);
}
@Override
public boolean dispatchKeyEvent(final KeyEvent event) {
final boolean ret = super.dispatchKeyEvent(event);
// hide keyboard when "enter" pressed
if (isGameActive() && event.getAction() == KeyEvent.ACTION_UP
&& event.getKeyCode() == KeyEvent.KEYCODE_ENTER) {
((InputMethodManager) MainActivity.get()
.getSystemService(Context.INPUT_METHOD_SERVICE))
.hideSoftInputFromWindow(this.getWindowToken(), 0);
}
return ret;
}
private void initJSInterface() {
addJavascriptInterface(JSInterface.get(), "jsi");
}
/**
* Handles downloading screenshot created by web client.
*/
private void initDownloadHandler() {
setDownloadListener(new DownloadListener() {
@Override
public void onDownloadStart(final String url, final String userAgent,
final String contentDisposition, final String mimetype,
final long contentLength) {
final Pair<Boolean, String> res = new DownloadHandler().download(url, mimetype);
if (res.first()) {
final String msg = "Downloaded file: " + res.second();
DebugLog.debug(msg);
Notifier.toast(msg);
} else {
final String msg = res.second();
DebugLog.error(msg);
Notifier.toast("ERROR: " + msg);
}
}
});
}
/**
* Shows initial title screen.
*/
public void loadTitleScreen() {
setSplashResource(R.drawable.splash);
loadUrl("about:blank");
Menu.get().show();
}
/**
* Attempts to connect to client host.
*/
public void loadLogin() {
if (debugEnabled() && PreferencesActivity.getString("client_url").trim().equals("")) {
// debug builds support choosing between main & test server
selectServer();
} else {
onSelectServer();
}
}
/**
* Sets the background image.
*
* @param resId
* Resource ID.
*/
private void setSplashResource(final int resId) {
if (splash == null) {
splash = (ImageView) MainActivity.get().findViewById(R.id.splash);
splash.setBackgroundColor(android.graphics.Color.TRANSPARENT);
}
splash.setImageResource(resId);
}
/**
* Opens a message dialog for user to choose between main & test servers.
*/
private void selectServer() {
final AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.get());
builder.setMessage("Select a server");
builder.setPositiveButton("Main", new DialogInterface.OnClickListener() {
public void onClick(final DialogInterface dialog, final int id) {
testing = false;
dialog.cancel();
onSelectServer();
}
});
builder.setNegativeButton("Testing", new DialogInterface.OnClickListener() {
public void onClick(final DialogInterface dialog, final int id) {
testing = true;
clientUrlSuffix = "testclient";
dialog.cancel();
onSelectServer();
}
});
final AlertDialog selectServer = builder.create();
selectServer.show();
}
private void onSelectServer() {
// remove splash image
setSplashResource(android.R.color.transparent);
final String custom_page = checkCustomServer();
if (custom_page != null) {
DebugLog.debug("Connecing to custom page: " + custom_page);
loadUrl(custom_page);
} else {
// initial page
loadUrl(defaultServer + "account/mycharacters.html");
if (testing) {
DebugLog.debug("Connecting to test server");
} else {
DebugLog.debug("Connecting to main server");
Notifier.get().showMessage("CAUTION: This software is in early development and not recommended"
+ " for use on the main server. Proceed with caution.", false);
}
}
currentPage = PageId.OTHER;
// hide menu after exiting title screen
Menu.get().hide();
}
private String checkCustomServer() {
final String cs = PreferencesActivity.getString("client_url", "").trim();
if (cs.equals("")) {
return null;
}
return cs;
}
/**
* Checks if a URL is a link to one of the web clients.
*
* @param url
* URL to be checked.
* @return
* <code>true</code> if url links to "client" or "testclient".
*/
private boolean isClientUrl(final String url) {
final String custom_client = PreferencesActivity.getString("client_url").trim();
if (!custom_client.equals("")) {
return url.contains(custom_client);
}
return url.contains("stendhalgame.org/client/")
|| url.contains("stendhalgame.org/testclient/");
}
/**
* Formats client URL for currently selected server.
*
* @param url
* URL to be checked.
* @return
* URL to be loaded.
*/
private String checkClientUrl(String url) {
if (isClientUrl(url)) {
String replaceSuffix = "/testclient/";
if (testing) {
replaceSuffix = "/client/";
}
url = url.replace(replaceSuffix, "/" + clientUrlSuffix + "/");
}
return url;
}
/**
* Checks if requested URL is whitelisted to be opened within WebView client.
*
* @param url
* URL to be checked.
* @return
* <code>true</code> if URL is under domain stendhalgame.org or localhost.
*/
private boolean isInternalUrl(final String url) {
final String domain = getDomain(url);
final String cs = checkCustomServer();
if (cs != null) {
return domain.startsWith(getDomain(cs));
} else {
return domain.startsWith("stendhalgame.org") || domain.startsWith("localhost");
}
}
private String getDomain(final String url) {
return url.replaceAll("^https://", "").replaceAll("^http://", "")
.replaceAll("^www\\.", "");
}
public static boolean onTitleScreen() {
return currentPage == PageId.TITLE;
}
public static boolean isGameActive() {
return currentPage == PageId.WEBCLIENT;
}
public static PageId getCurrentPageId() {
return currentPage;
}
public static void playTitleMusic(String musicId) {
if (musicId == null) {
musicId = PreferencesActivity.getString("song_list");
}
int id = R.raw.title_01;
switch (musicId) {
case "title_02":
id = R.raw.title_02;
break;
case "title_03":
id = R.raw.title_03;
break;
case "title_04":
id = R.raw.title_04;
break;
case "title_05":
id = R.raw.title_05;
break;
}
DebugLog.debug("playing music: " + musicId);
MusicPlayer.playMusic(id, true);
}
public static void playTitleMusic() {
playTitleMusic(null);
}
/**
* Checks if this is a debug build.
*
* @return
* <code>true</code> if debug flag set.
*/
private boolean debugEnabled() {
if (debugging != null) {
return debugging;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
debugging = (MainActivity.get().getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0;
} else {
debugging = false;
}
return debugging;
}
}

View file

@ -0,0 +1,105 @@
/***************************************************************************
* Copyright © 2022 - Arianne *
***************************************************************************
***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
package org.arianne.stendhal.client;
import android.net.Uri;
import android.os.Environment;
import android.util.Base64;
import java.io.File;
import java.io.FileOutputStream;
import marauroa.common.Pair;
/**
* The main purpose of this class is to handle downloading screenshots
* created by the web client page.
*/
public class DownloadHandler {
public Pair<Boolean, String> download(final String url, final String mimetype) {
final Pair<Boolean, String> res = new Pair<Boolean, String>(false, null);
final Uri uri = Uri.parse(url);
final String scheme = uri.getScheme();
final String storageState = Environment.getExternalStorageState();
if (!ClientView.get().isGameActive()) {
res.setSecond("downloading from this page not supported");
} else if (!"data".equals(scheme)) {
res.setSecond("download type \"" + scheme + "\" not supported");
} else if (!"image/png".equals(mimetype) || !url.startsWith("data:image/png;base64,")) {
res.setSecond("mimetype not supported: " + mimetype);
} else if (!Environment.MEDIA_MOUNTED.equals(storageState)) {
res.setSecond("storage not available for writing (state: " + storageState + ")");
} else {
final File targetDir = new File(Environment.getExternalStorageDirectory()
+ "/Pictures/Screenshots");
// TODO: format output name using YYYYMMDD_HH.MM.SS.MS
final String targetName = "stendhal_" + System.currentTimeMillis() + ".png";
DebugLog.debug("Saving screenshot: " + targetDir.getPath()
+ "/" + targetName + " (" + mimetype + ")");
String msg;
String stacktrace = null;
try {
if (!targetDir.exists()) {
targetDir.mkdirs();
}
final byte[] data = Base64.decode(url.split("base64,")[1], Base64.DEFAULT);
final FileOutputStream fos = new FileOutputStream(new File(targetDir, targetName));
fos.write(data);
fos.close();
res.setFirst(true);
msg = "saved screenshot to " + targetDir.getPath() + "/" + targetName;
} catch (final java.lang.NoClassDefFoundError e) {
msg = "an error occurred while decoding data (see debug log for more info)";
stacktrace = stackTraceToString(e);
} catch (java.io.IOException e) {
msg = "an error occurred while attempting to write file (see debug log for more info)";
stacktrace = stackTraceToString(e);
}
res.setSecond(msg);
if (stacktrace != null) {
DebugLog.error(stacktrace.toString());
}
}
return res;
}
/**
* Converts an exception stacktrace to string.
*
* @param e
* The error with stacktrace information.
* @return
* String formatted stacktrace.
*/
private String stackTraceToString(final Throwable e) {
final StringBuilder sb = new StringBuilder();
sb.append(e.toString());
for (final StackTraceElement ste: e.getStackTrace()) {
if (sb.length() > 0) {
sb.append("\n");
}
sb.append(ste.toString());
}
return sb.toString();
}
}

View file

@ -0,0 +1,158 @@
/***************************************************************************
* Copyright © 2022 - Arianne *
***************************************************************************
***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
package org.arianne.stendhal.client;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.res.Configuration;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import androidx.constraintlayout.widget.ConstraintLayout;
import org.arianne.stendhal.client.input.DPad;
import org.arianne.stendhal.client.input.DPadArrows;
import org.arianne.stendhal.client.input.DPadJoy;
public class MainActivity extends AppCompatActivity {
private static MainActivity instance;
private ConstraintLayout layout;
private ClientView client;
private Menu menu;
public static MainActivity get() {
return instance;
}
@Override
protected void onCreate(final Bundle savedInstanceState) {
try {
super.onCreate(savedInstanceState);
instance = this;
// initialize debug logging mechanism
DebugLog.init(getExternalFilesDir(null), this);
setContentView(R.layout.activity_main);
layout = (ConstraintLayout) findViewById(R.id.content);
menu = new Menu(this);
client = (ClientView) findViewById(R.id.clientWebView);
// initialize d-pads
final DPad arrowPad = DPadArrows.get();
final DPad joyPad = DPadJoy.get();
if (PreferencesActivity.getBoolean("dpad_joy", true)) {
DPad.setCurrentPad(joyPad);
} else {
DPad.setCurrentPad(arrowPad);
}
layout.addView(arrowPad.getLayout());
layout.addView(joyPad.getLayout());
client.loadTitleScreen();
} catch (final Exception e) {
e.printStackTrace();
DebugLog.error(e.toString());
DebugLog.error("// -- //");
for (final StackTraceElement ste: e.getStackTrace()) {
DebugLog.error(ste.toString());
}
DebugLog.error("// -- //");
Notifier.get().showPrompt(
"An unhandled exception has occurred: " + e.getMessage()
+ "\n\nYou can report this error at: https://stendhalgame.org/development/bug.html",
new Notifier.Action() {
protected void onCall() {
finish();
}
});
}
}
@Override
public void onBackPressed() {
menu.toggleVisibility();
}
/**
* Attempts to connect to client host.
*/
public void loadLogin() {
client.loadLogin();
}
/**
* Opens a dialog confirm exiting activity.
*/
public void onRequestQuit() {
final AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setMessage("Quit Stendhal?");
builder.setPositiveButton("Yes", new DialogInterface.OnClickListener() {
public void onClick(final DialogInterface dialog, final int id) {
finish();
}
});
builder.setNegativeButton("No", new DialogInterface.OnClickListener() {
public void onClick(final DialogInterface dialog, final int id) {
dialog.cancel();
}
});
final AlertDialog confirmQuit = builder.create();
confirmQuit.show();
}
@Override
protected void onResume() {
super.onResume();
DPadArrows.get().onRefreshView();
DPadJoy.get().onRefreshView();
}
/**
* Updates direction pad position when screen orientation changes.
*/
@Override
public void onConfigurationChanged(final Configuration config) {
super.onConfigurationChanged(config);
DPadArrows.get().onRefreshView();
DPadJoy.get().onRefreshView();
}
@Override
public void finish() {
DebugLog.debug(MainActivity.class.getName() + ".finish() called");
super.finish();
}
@Override
protected void onDestroy() {
DebugLog.debug(MainActivity.class.getName() + ".onDestroy() called");
super.onDestroy();
}
public void showSettings() {
startActivity(new Intent(MainActivity.this, PreferencesActivity.class));
}
}

View file

@ -1,5 +1,5 @@
/***************************************************************************
* Copyright © 2022-2024 - Faiumoni e. V. *
* Copyright © 2022 - Arianne *
***************************************************************************
***************************************************************************
* *
@ -9,45 +9,40 @@
* (at your option) any later version. *
* *
***************************************************************************/
package org.stendhalgame.client;
package org.arianne.stendhal.client;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.view.View;
import android.view.ViewTreeObserver;
import android.widget.Button;
import android.widget.Toolbar;
/**
* Main menu for navigation & configuration.
*/
public class Menu {
/** Toolbar representing menu. */
private static Menu instance;
private final Context ctx;
private final Toolbar nav;
private Button btn_connect;
private Button btn_title;
private Button btn_reload;
/** Singleton instance. */
private static Menu instance;
/**
* Retrieves menu singleton instance.
*/
public static Menu get() {
if (Menu.instance == null) {
Menu.instance = new Menu();
}
return instance;
}
/**
* Hidden singleton constructor.
*/
private Menu() {
nav = (Toolbar) MainActivity.get().findViewById(R.id.menu_main);
public Menu(final Context ctx) {
instance = this;
this.ctx = ctx;
nav = (Toolbar) ((Activity) ctx).findViewById(R.id.menu_main);
nav.setTag(nav.getVisibility());
nav.getViewTreeObserver().addOnGlobalLayoutListener(
new ViewTreeObserver.OnGlobalLayoutListener() {
@ -58,30 +53,20 @@ public class Menu {
}
}
});
initButtonHandlers();
Logger.debug("menu initialized");
DebugLog.debug("menu initialized");
}
/**
* Retrieves the menu toolbar.
*/
public Toolbar getInternal() {
return nav;
}
/**
* Shows or hides menu.
*
* @param vType
* Visibility type. One of `View.VISIBLE`, `View.INVISIBLE`, or `View.GONE`.
*/
public void setVisibility(final int vType) {
nav.setVisibility(vType);
}
/**
* Toggles visible state of menu.
*/
public void toggleVisibility() {
if (nav.getVisibility() == View.GONE) {
nav.setVisibility(View.VISIBLE);
@ -90,25 +75,16 @@ public class Menu {
}
}
/**
* Sets menu visibility to visible.
*/
public void show() {
nav.setVisibility(View.VISIBLE);
}
/**
* Sets menu visibility to hidden.
*/
public void hide() {
nav.setVisibility(View.GONE);
}
/**
* Refreshes displayed menu buttons.
*/
public void updateButtons() {
final PageId page = MainActivity.get().getActiveClientView().getCurrentPageId();
final PageId page = ClientView.getCurrentPageId();
btn_connect.setVisibility(View.GONE);
btn_title.setVisibility(View.GONE);
@ -126,11 +102,10 @@ public class Menu {
* Sets actions when buttons are pressed.
*/
private void initButtonHandlers() {
final MainActivity activity = MainActivity.get();
final MainActivity activity = (MainActivity) ctx;
btn_connect = (Button) activity.findViewById(R.id.btn_connect);
btn_connect.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(final View v) {
activity.loadLogin();
}
@ -138,20 +113,17 @@ public class Menu {
btn_title = (Button) activity.findViewById(R.id.btn_title);
btn_title.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(final View v) {
nav.setVisibility(View.GONE);
Notifier.showPrompt("Return to title screen?",
Notifier.get().showPrompt("Return to title screen?",
new Notifier.Action() {
@Override
protected void onCall() {
MainActivity.get().getActiveClientView().loadTitleScreen();
ClientView.get().loadTitleScreen();
updateButtons();
}
},
new Notifier.Action() {
@Override
protected void onCall() {/* do nothing */}
}
);
@ -160,15 +132,14 @@ public class Menu {
btn_reload = (Button) activity.findViewById(R.id.btn_reload);
btn_reload.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(final View v) {
nav.setVisibility(View.GONE);
Notifier.showPrompt("Reload page?",
Notifier.get().showPrompt("Reload page?",
new Notifier.Action() {
@Override
protected void onCall() {
MainActivity.get().getActiveClientView().reload();
ClientView.get().reload();
}
},
new Notifier.Action() {
@ -182,57 +153,38 @@ public class Menu {
final Button btn_settings = (Button) activity.findViewById(R.id.btn_settings);
btn_settings.setOnClickListener(new ClickListener() {
@Override
public void onClick(final View v) {
super.onClick(v);
activity.showSettings();
((MainActivity) ctx).showSettings();
}
});
/*
final Button btn_about = (Button) activity.findViewById(R.id.btn_about);
btn_about.setOnClickListener(new ClickListener() {
@Override
public void onClick(final View v) {
super.onClick(v);
// TODO: create proper about activity
final ClientView client = MainActivity.get().getCurrentView();
final AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.get());
final String buildType = AppInfo.getBuildType();
String msg = "Android client version: " + AppInfo.getBuildVersion();
if (!"prod".equals(buildType)) {
msg += "\nAndroid client build: " + AppInfo.getBuildType();
String server_ver = "unavailable";
if (!(ClientView.getCurrentPageId() == PageId.WEBCLIENT)) {
server_ver = "not connected";
}
if (client.debugEnabled()) {
msg += "\nClient type: " + client.getSelectedClient()
+ "\nServer type: " + client.getSelectedServer()
+ "\nLogs directory: " + Logger.getLogsDir();
}
msg += "\n\nTitle Music:"
+ "\n\n- Treasure Hunter"
+ "\n by Tad Miller (TAD)"
+ "\n - opengameart.org/node/127124"
+ "\n\n- Woodland Fantasy"
+ "\n by Matthew Pablo"
+ "\n - matthewpablo.com"
+ "\n - opengameart.org/node/31929"
+ "\n\n- Land of Fearless"
+ "\n by Alexandr Zhelanov"
+ "\n - soundcloud.com/alexandr-zhelanov"
+ "\n - opengameart.org/node/79931"
+ "\n\n- Rejoicing"
+ "\n by RandomMind"
+ "\n - opengameart.org/node/100213"
+ "\n\n- The Old Tower Inn"
+ "\n by RandomMind"
+ "\n - opengameart.org/node/87510";
builder.setMessage(msg);
// FIXME: how to find server version if connected?
final AlertDialog.Builder builder = new AlertDialog.Builder((Activity) ctx);
builder.setMessage("WebView client version: "
+ BuildConfig.VERSION_NAME + "\nServer version: " + server_ver
+ "\n\nTitle Music:"
+ "\n- \"Treasure Hunter\": Tad Miller (TAD)"
+ "\n- \"Woodland Fantasy\": Matthew Pablo http://www.matthewpablo.com/"
+ "\n- \"Land of Fearless\": Alexandr Zhelanov https://soundcloud.com/alexandr-zhelanov"
+ "\n- \"Medieval: Rejoicing\": RandomMind"
+ "\n- \"Medieval: The Old Tower Inn\": RandomMind");
builder.setPositiveButton("Ok", new DialogInterface.OnClickListener() {
@Override
public void onClick(final DialogInterface dialog, final int id) {
dialog.cancel();
}
@ -241,11 +193,9 @@ public class Menu {
builder.create().show();
}
});
*/
final Button btn_quit = (Button) activity.findViewById(R.id.btn_quit);
btn_quit.setOnClickListener(new ClickListener() {
@Override
public void onClick(final View v) {
super.onClick(v);
activity.onRequestQuit();
@ -255,13 +205,9 @@ public class Menu {
updateButtons();
}
/**
* Listener to handle menu button presses.
*/
private class ClickListener implements View.OnClickListener {
@Override
public void onClick(final View v) {
if (!MainActivity.get().getActiveClientView().onTitleScreen()) {
if (!ClientView.onTitleScreen()) {
nav.setVisibility(View.GONE);
}
}

View file

@ -1,5 +1,5 @@
/***************************************************************************
* Copyright © 2022-2024 - Faiumoni e. V. *
* Copyright © 2022 - Arianne *
***************************************************************************
***************************************************************************
* *
@ -9,7 +9,7 @@
* (at your option) any later version. *
* *
***************************************************************************/
package org.stendhalgame.client;
package org.arianne.stendhal.client;
import android.app.AlertDialog;
import android.content.Context;
@ -19,32 +19,38 @@ import android.widget.Toast;
public class Notifier {
private static AlertDialog.Builder builder;
private AlertDialog.Builder builder;
private static Notifier instance;
/**
* Static methods only.
*/
private Notifier() {
// do nothing
public static Notifier get() {
if (instance == null) {
instance = new Notifier();
}
return instance;
}
private static void showDialog() {
private Notifier() {}
private void showDialog() {
if (builder != null) {
builder.create().show();
}
builder = null;
}
private static void createDialog(final Context ctx) {
private void createDialog(final Context ctx) {
builder = new AlertDialog.Builder(ctx);
}
private static void createDialog() {
private void createDialog() {
createDialog(MainActivity.get());
}
public static void showMessage(final String msg, final boolean cancelable, final String title) {
public void showMessage(final String msg, final boolean cancelable, final String title) {
createDialog();
builder.setCancelable(cancelable);
@ -53,7 +59,6 @@ public class Notifier {
builder.setTitle(title);
}
builder.setPositiveButton("Ok", new DialogInterface.OnClickListener() {
@Override
public void onClick(final DialogInterface dialog, final int id) {
dialog.cancel();
}
@ -62,19 +67,19 @@ public class Notifier {
showDialog();
}
public static void showMessage(final String msg, final boolean cancelable) {
public void showMessage(final String msg, final boolean cancelable) {
showMessage(msg, cancelable, null);
}
public static void showMessage(final String msg) {
public void showMessage(final String msg) {
showMessage(msg, true, null);
}
public static void showError(final String msg) {
public void showError(final String msg) {
showMessage(msg, false, "Error");
}
public static void showPrompt(final Context ctx, final String msg, final Action... actions) {
public void showPrompt(final Context ctx, final String msg, final Action... actions) {
createDialog(ctx);
builder.setCancelable(false);
@ -93,7 +98,6 @@ public class Notifier {
}
builder.setPositiveButton(labelY, new DialogInterface.OnClickListener() {
@Override
public void onClick(final DialogInterface dialog, final int id) {
dialog.cancel();
@ -113,7 +117,6 @@ public class Notifier {
}
builder.setNegativeButton(labelN, new DialogInterface.OnClickListener() {
@Override
public void onClick(final DialogInterface dialog, final int id) {
dialog.cancel();
@ -134,7 +137,6 @@ public class Notifier {
}
builder.setNeutralButton(labelO, new DialogInterface.OnClickListener() {
@Override
public void onClick(final DialogInterface dialog, final int id) {
dialog.cancel();
@ -148,7 +150,7 @@ public class Notifier {
showDialog();
}
public static void showPrompt(final String msg, final Action... actions) {
public void showPrompt(final String msg, final Action... actions) {
showPrompt(MainActivity.get(), msg, actions);
}

View file

@ -1,5 +1,5 @@
/***************************************************************************
* Copyright © 2022-2024 - Faiumoni e. V. *
* Copyright © 2022 - Arianne *
***************************************************************************
***************************************************************************
* *
@ -9,7 +9,7 @@
* (at your option) any later version. *
* *
***************************************************************************/
package org.stendhalgame.client;
package org.arianne.stendhal.client;
public enum PageId {

View file

@ -1,5 +1,5 @@
/***************************************************************************
* Copyright © 2022-2024 - Faiumoni e. V. *
* Copyright © 2022 - Arianne *
***************************************************************************
***************************************************************************
* *
@ -9,47 +9,38 @@
* (at your option) any later version. *
* *
***************************************************************************/
package org.stendhalgame.client;
package org.arianne.stendhal.client;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.os.Bundle;
import android.preference.CheckBoxPreference;
import android.preference.ListPreference;
import android.preference.Preference;
import android.preference.PreferenceFragment;
import android.preference.PreferenceManager;
import androidx.appcompat.app.AppCompatActivity;
import androidx.preference.CheckBoxPreference;
import androidx.preference.ListPreference;
import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceManager;
import org.arianne.stendhal.client.input.DPad;
import org.arianne.stendhal.client.input.DPadArrows;
import org.arianne.stendhal.client.input.DPadJoy;
import org.arianne.stendhal.client.sound.MusicPlayer;
/**
* Interface for configuring app preferences.
*/
public class PreferencesActivity extends AppCompatActivity {
/** Preferences activity instance. */
private static PreferencesActivity instance;
/**
* Retrieves preferences activity instance.
*/
public static PreferencesActivity get() {
return instance;
}
/**
* Called when the activity is created.
*/
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// FIXME: may be considered unsafe as this is not technically a singleton
instance = this;
// use same orientation as main activity
setRequestedOrientation(MainActivity.get().getRequestedOrientation());
setContentView(R.layout.activity_preferences);
if (findViewById(R.id.preferencesFrame) != null) {
@ -57,8 +48,8 @@ public class PreferencesActivity extends AppCompatActivity {
return;
}
getSupportFragmentManager().beginTransaction().add(R.id.preferencesFrame,
new PFragment()).commit();
getFragmentManager().beginTransaction().add(R.id.preferencesFrame,
new PFragment()).commit();
}
}
@ -128,62 +119,63 @@ public class PreferencesActivity extends AppCompatActivity {
// SharedPreferences.getInt is returning string
final Object obj = getSharedPreferences().getString(key, String.valueOf(defVal));
if (obj instanceof String) {
Logger.debug("PreferencesActivity.getInt: casting string return value to integer");
DebugLog.debug("PreferencesActivity.getInt: casting string return value to integer");
return Integer.parseInt((String) obj);
} else {
Logger.debug("PreferencesActivity.getInt return value is integer");
DebugLog.debug("PreferencesActivity.getInt return value is integer");
return (Integer) obj;
}
} catch (final NumberFormatException e) {
final String errMsg = "An error occurred: " + e.getMessage();
Logger.error(errMsg + "\n" + e.getStackTrace());
Notifier.showError(errMsg + "\n\nSee " + Logger.getLogsDir()
DebugLog.error(errMsg + "\n" + e.getStackTrace());
Notifier.get().showError(errMsg + "\n\nSee " + DebugLog.getLogsDir()
+ " for more information.");
}
return null;
}
@Override
public void finish() {
Logger.debug(PreferencesActivity.class.getName() + ".finish() called");
DebugLog.debug(PreferencesActivity.class.getName() + ".finish() called");
super.finish();
}
@Override
protected void onDestroy() {
Logger.debug(PreferencesActivity.class.getName() + ".onDestroy() called");
DebugLog.debug(PreferencesActivity.class.getName() + ".onDestroy() called");
super.onDestroy();
}
public static class PFragment extends PreferenceFragmentCompat
public static class PFragment extends PreferenceFragment
implements OnSharedPreferenceChangeListener {
@Override
public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) {
public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.preferences);
initListeners();
}
private void initListeners() {
final PreferenceManager pm = getPreferenceManager();
// FIXME: should update playing music
final Preference reset = pm.findPreference("reset");
if (reset != null) {
reset.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(final Preference pref) {
Notifier.showPrompt(getActivity(), "Restore prefrences defaults?",
Notifier.get().showPrompt(getActivity(), "Restore prefrences defaults?",
new Notifier.Action() {
@Override
protected void onCall() {
Logger.debug("restoring preferences default values");
DebugLog.debug("restoring preferences default values");
restoreDefaults();
}
},
new Notifier.Action() {
@Override
protected void onCall() {
// do nothing
}
@ -193,20 +185,19 @@ public class PreferencesActivity extends AppCompatActivity {
}
});
} else {
Logger.warn("preference \"reset\" not found");
DebugLog.warn("preference \"reset\" not found");
}
final CheckBoxPreference title_music = (CheckBoxPreference) pm.findPreference("title_music");
if (title_music != null) {
title_music.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(final Preference pref, final Object obj) {
// set music state as soon as preference is changed
if (MainActivity.get().getActiveClientView().onTitleScreen()) {
if (ClientView.onTitleScreen()) {
if (!((Boolean) obj)) {
MusicPlayer.stopMusic();
} else if (!MusicPlayer.isPlaying()) {
MusicPlayer.playTitleMusic();
ClientView.playTitleMusic();
}
}
@ -226,14 +217,13 @@ public class PreferencesActivity extends AppCompatActivity {
}
song_list.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(final Preference pref, final Object obj) {
// set music state as soon as preference is changed
if (MainActivity.get().getActiveClientView().onTitleScreen()) {
Logger.debug("changing title music preference: " + (String) obj);
if (ClientView.onTitleScreen()) {
DebugLog.debug("changing title music preference: " + (String) obj);
if (title_music.isChecked()) {
MusicPlayer.playTitleMusic((String) obj);
ClientView.playTitleMusic((String) obj);
}
}
@ -241,35 +231,6 @@ public class PreferencesActivity extends AppCompatActivity {
}
});
}
final Preference clear_cache = pm.findPreference("clear_cache");
if (clear_cache != null) {
clear_cache.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(final Preference pref) {
Notifier.showPrompt(getActivity(), "Delete cached data?",
new Notifier.Action() {
@Override
protected void onCall() {
Logger.debug("clearing cache");
for (final ClientView clientView: MainActivity.get().getClientViewList()) {
clientView.clearCache(true);
}
}
},
new Notifier.Action() {
@Override
protected void onCall() {
// do nothing
}
});
return true;
}
});
} else {
Logger.warn("preference \"clear_cache\" not found");
}
}
private void restoreDefaults() {
@ -285,9 +246,25 @@ public class PreferencesActivity extends AppCompatActivity {
@Override
public void onSharedPreferenceChanged(final SharedPreferences sp, final String key) {
if ("orientation".equals(key)) {
Logger.debug("orientation set to \"" + PreferencesActivity.getString(key) + "\"");
MainActivity.get().updateOrientation();
if (key.equals("show_dpad") || key.equals("dpad_joy")) {
// reset all d-pads states to hidden
DPadArrows.get().hide();
DPadJoy.get().hide();
if (getBoolean("dpad_joy", true)) {
DPad.setCurrentPad(DPadJoy.get());
} else {
DPad.setCurrentPad(DPadArrows.get());
}
// restore visible state
final DPad currentPad = DPad.getCurrentPad();
if (currentPad != null && ClientView.isGameActive() && getBoolean("show_dpad", false)) {
currentPad.show();
currentPad.onRefreshView();
}
} else if (key.equals("joypad_sensitivity")) {
DPadJoy.get().setSensitivity(getInt("joypad_sensitivity", 25));
}
}

View file

@ -0,0 +1,62 @@
/***************************************************************************
* Copyright © 2022 - Arianne *
***************************************************************************
***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
package org.arianne.stendhal.client.input;
import android.view.View;
import androidx.constraintlayout.widget.ConstraintLayout;
import marauroa.common.Pair;
public abstract class DPad {
public static enum Dir {
NONE,
LEFT,
RIGHT,
UP,
DOWN;
}
private static DPad currentPad = null;
public abstract ConstraintLayout getLayout();
public abstract void setVisibility(final int vis);
public abstract boolean isVisible();
public void show() {
setVisibility(View.VISIBLE);
}
public void hide() {
setVisibility(View.GONE);
}
public abstract void setPosition(final int x, final int y);
public abstract Pair<Integer, Integer> getPosition();
public abstract Pair<Integer, Integer> getSize();
public abstract void onRefreshView();
public static void setCurrentPad(final DPad pad) {
currentPad = pad;
}
public static DPad getCurrentPad() {
return currentPad;
}
}

View file

@ -0,0 +1,253 @@
/***************************************************************************
* Copyright © 2022 - Arianne *
***************************************************************************
***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
package org.arianne.stendhal.client.input;
import android.content.Context;
import android.graphics.Point;
import android.view.Display;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.widget.ImageView;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.constraintlayout.widget.ConstraintLayout.LayoutParams;
import java.util.LinkedList;
import java.util.List;
import marauroa.common.Pair;
import org.arianne.stendhal.client.DebugLog;
import org.arianne.stendhal.client.MainActivity;
import org.arianne.stendhal.client.PreferencesActivity;
import org.arianne.stendhal.client.R;
/**
* A directional pad on the screen.
*
* TODO: Option to use joystick style d-pad. Currently setting "dpad_joy"
* is disabled in preferences manager.
*/
public class DPadArrows extends DPad {
private static DPadArrows instance;
private static Context ctx;
private final List<ArrowView> arrows;
private final ConstraintLayout layout;
public static DPadArrows get() {
if (instance == null) {
instance = new DPadArrows();
}
return instance;
}
private DPadArrows() {
ctx = (Context) MainActivity.get();
arrows = new LinkedList<>();
layout = new ConstraintLayout(ctx);
layout.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
final ArrowView arrow_l = new ArrowView(ctx, Dir.LEFT);
final ArrowView arrow_r = new ArrowView(ctx, Dir.RIGHT);
final ArrowView arrow_u = new ArrowView(ctx, Dir.UP);
final ArrowView arrow_d = new ArrowView(ctx, Dir.DOWN);
arrows.add(arrow_l);
arrows.add(arrow_r);
arrows.add(arrow_u);
arrows.add(arrow_d);
for (final ArrowView av: arrows) {
layout.addView(av);
}
// hidden by default
hide();
}
public ConstraintLayout getLayout() {
return layout;
}
public void setVisibility(final int vis) {
for (final ArrowView av: arrows) {
av.setVisibility(vis);
}
}
public boolean isVisible() {
for (final ArrowView av: arrows) {
if (av.getVisibility() != View.VISIBLE) {
return false;
}
}
return true;
}
public void setPosition(final int x, final int y) {
ArrowView av = arrows.get(0);
av.setX(x);
av.setY(y + 60);
av = arrows.get(1);
av.setX(x + 150);
av.setY(y + 60);
av = arrows.get(2);
av.setX(x + 60);
av.setY(y);
av = arrows.get(3);
av.setX(x + 60);
av.setY(y + 150);
}
private int getX() {
return (int) arrows.get(0).getX();
}
private int getY() {
return (int) arrows.get(2).getY();
}
public Pair<Integer, Integer> getPosition() {
return new Pair<Integer, Integer>(getX(), getY());
}
private int getWidth() {
final ArrowView aRight = arrows.get(1);
final int absX = ((int) aRight.getX()) + aRight.getWidth();
return absX - getX();
}
private int getHeight() {
final ArrowView aDown = arrows.get(3);
final int absY = ((int) aDown.getY()) + aDown.getHeight();
return absY - getY();
}
public Pair<Integer, Integer> getSize() {
return new Pair<Integer, Integer>(getWidth(), getHeight());
}
public void onRefreshView() {
final Point size = new Point();
final Display disp = MainActivity.get().getWindowManager().getDefaultDisplay();
disp.getSize(size);
final PreferencesActivity prefs = PreferencesActivity.get();
final int x = prefs.getInt("dpad_offset_x", 100);
final int y = size.y - prefs.getInt("dpad_offset_y", 300);
setPosition(x, y);
if (isVisible()) {
DebugLog.debug("DPadArrows: pos (" + getX() + "," + getY() + "), size ("
+ getWidth() + "," + getHeight() + ")");
DebugLog.debug("DPadArrows: left arrow: x (" + arrows.get(0).getX() + "), width ("
+ arrows.get(0).getWidth() + ")");
DebugLog.debug("DPadArrows: down arrow: y (" + arrows.get(3).getY() + "), height ("
+ arrows.get(3).getHeight() + ")");
}
}
private static class ArrowView extends ImageView {
private final Dir dir;
private final int id;
private final int id_active;
private boolean keyDown = false;
public ArrowView(final Context ctx, final Dir dir) {
super(ctx);
this.dir = dir;
switch (dir) {
case LEFT:
id = R.drawable.dpad_arrow_left;
id_active = R.drawable.dpad_arrow_left_active;
break;
case RIGHT:
id = R.drawable.dpad_arrow_right;
id_active = R.drawable.dpad_arrow_right_active;
break;
case UP:
id = R.drawable.dpad_arrow_up;
id_active = R.drawable.dpad_arrow_up_active;
break;
default:
id = R.drawable.dpad_arrow_down;
id_active = R.drawable.dpad_arrow_down_active;
break;
}
setBackgroundColor(android.graphics.Color.TRANSPARENT);
setImageResource(id);
setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(final View view, final MotionEvent event) {
final int action = event.getAction();
Integer newAction = null;
Integer keyCode = null;
if (action == MotionEvent.ACTION_DOWN && !keyDown) {
setImageResource(id_active);
newAction = KeyEvent.ACTION_DOWN;
keyDown = true;
} else if (action == MotionEvent.ACTION_UP && keyDown) {
setImageResource(id);
newAction = KeyEvent.ACTION_UP;
keyDown = false;
}
if (newAction != null) {
switch (dir) {
case LEFT:
keyCode = KeyEvent.KEYCODE_DPAD_LEFT;
break;
case RIGHT:
keyCode = KeyEvent.KEYCODE_DPAD_RIGHT;
break;
case UP:
keyCode = KeyEvent.KEYCODE_DPAD_UP;
break;
case DOWN:
keyCode = KeyEvent.KEYCODE_DPAD_DOWN;
break;
}
MainActivity.get().dispatchKeyEvent(new KeyEvent(newAction, keyCode));
return true; // consume event
}
return false;
}
});
}
}
}

View file

@ -0,0 +1,332 @@
/***************************************************************************
* Copyright © 2022 - Arianne *
***************************************************************************
***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
package org.arianne.stendhal.client.input;
import android.content.Context;
import android.graphics.Point;
import android.view.Display;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.constraintlayout.widget.ConstraintLayout.LayoutParams;
import java.util.ArrayList;
import java.util.List;
import marauroa.common.Pair;
import org.arianne.stendhal.client.DebugLog;
import org.arianne.stendhal.client.MainActivity;
import org.arianne.stendhal.client.PreferencesActivity;
import org.arianne.stendhal.client.R;
public class DPadJoy extends DPad {
private static DPadJoy instance;
private static Context ctx;
private static ConstraintLayout layout;
private static OuterButton outerButton;
private static int sensitivity;
public static DPadJoy get() {
if (instance == null) {
instance = new DPadJoy();
}
return instance;
}
private DPadJoy() {
ctx = (Context) MainActivity.get();
layout = new ConstraintLayout(ctx);
layout.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
outerButton = new OuterButton(ctx);
outerButton.addToLayout();
setSensitivity(PreferencesActivity.getInt("joypad_sensitivity", 25));
// hidden by default
hide();
}
public ConstraintLayout getLayout() {
return layout;
}
public void setVisibility(final int vis) {
outerButton.setVisibility(vis);
}
public boolean isVisible() {
return outerButton.isVisible();
}
public void setPosition(final int x, final int y) {
outerButton.setPosition(x, y);
}
private int getX() {
return (int) outerButton.getX();
}
private int getY() {
return (int) outerButton.getY();
}
public Pair<Integer, Integer> getPosition() {
return new Pair<Integer, Integer>(getX(), getY());
}
private int getWidth() {
return outerButton.getWidth();
}
private int getHeight() {
return outerButton.getHeight();
}
public Pair<Integer, Integer> getSize() {
return new Pair<Integer, Integer>(getWidth(), getHeight());
}
public void setSensitivity(final int value) {
sensitivity = value;
}
public void onRefreshView() {
final Point size = new Point();
final Display disp = MainActivity.get().getWindowManager().getDefaultDisplay();
disp.getSize(size);
final PreferencesActivity prefs = PreferencesActivity.get();
final int x = prefs.getInt("dpad_offset_x", 100);
final int y = size.y - prefs.getInt("dpad_offset_y", 300);
setPosition(x, y);
if (isVisible()) {
DebugLog.debug("DPadJoy: pos (" + getX() + "," + getY() + "), size ("
+ getWidth() + "," + getHeight() + ")");
}
}
private static class OuterButton extends JoyPadButton {
private final InnerButton innerButton;
private DPad.Dir currentDir = DPad.Dir.NONE;
private boolean keyDown = false;
private final List<Integer> pressedKeys;
public OuterButton(final Context ctx) {
super(ctx);
pressedKeys = new ArrayList<>();
setBackgroundColor(android.graphics.Color.TRANSPARENT);
setImageResource(R.drawable.dpad_circle_outer);
innerButton = new InnerButton(ctx);
innerButton.setBackgroundColor(android.graphics.Color.TRANSPARENT);
innerButton.setImageResource(R.drawable.dpad_circle_inner);
setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(final View view, final MotionEvent event) {
final int action = event.getAction();
DebugLog.debug("joypad action: " + action);
if (action == MotionEvent.ACTION_DOWN && !keyDown) {
keyDown = true;
innerButton.setImageResource(R.drawable.dpad_circle_inner_active);
innerButton.setRelativeX(event.getX());
innerButton.setRelativeY(event.getY());
final DPad.Dir newDir = getTouchDirection();
if (newDir != currentDir) {
currentDir = newDir;
onDirectionChange(KeyEvent.ACTION_DOWN);
}
} else if (action == MotionEvent.ACTION_UP && keyDown) {
keyDown = false;
innerButton.setImageResource(R.drawable.dpad_circle_inner);
innerButton.centerOn((JoyPadButton) view);
clearPresses();
currentDir = DPad.Dir.NONE;
} else if (action == MotionEvent.ACTION_MOVE && keyDown) {
innerButton.setRelativeX(event.getX());
innerButton.setRelativeY(event.getY());
final DPad.Dir newDir = getTouchDirection();
if (newDir != currentDir) {
currentDir = newDir;
onDirectionChange(KeyEvent.ACTION_DOWN);
}
}
return true;
}
});
}
public void addToLayout() {
layout.addView(this);
layout.addView(innerButton);
// FIXME: inner button not centering at startup
innerButton.centerOn(this);
}
@Override
public void setVisibility(final int vis) {
innerButton.setVisibility(vis);
super.setVisibility(vis);
}
public boolean isVisible() {
return getVisibility() == View.VISIBLE
&& innerButton.getVisibility() == View.VISIBLE;
}
public void setPosition(final int x, final int y) {
setX(x);
setY(y);
innerButton.centerOn(this);
}
/**
* Determines pressed direction based on inner button position
* relative to outer button position.
*/
private DPad.Dir getTouchDirection() {
final float outerX = getCenterX();
final float outerY = getCenterY();
final float innerX = innerButton.getCenterX();
final float innerY = innerButton.getCenterY();
final float offsetX = outerX - innerX;
final float offsetY = outerY - innerY;
final int absX = (int) Math.abs(offsetX);
final int absY = (int) Math.abs(offsetY);
if (absX < sensitivity && absY < sensitivity) {
return DPad.Dir.NONE;
}
// horizontal directions take precedence if they are the same as vertical
if (absX >= absY) {
if (innerX < outerX) {
return DPad.Dir.LEFT;
} else if (innerX > outerX) {
return DPad.Dir.RIGHT;
}
} else {
if (innerY < outerY) {
return DPad.Dir.UP;
} else if (innerY > outerY) {
return DPad.Dir.DOWN;
}
}
return DPad.Dir.NONE;
}
private void clearPresses() {
for (final int key: pressedKeys) {
MainActivity.get().dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, key));
}
}
/**
* Sends a direction key press event to client.
*/
private void onDirectionChange(final int action) {
Integer keyCode = null;
switch (currentDir) {
case LEFT:
keyCode = KeyEvent.KEYCODE_DPAD_LEFT;
break;
case RIGHT:
keyCode = KeyEvent.KEYCODE_DPAD_RIGHT;
break;
case UP:
keyCode = KeyEvent.KEYCODE_DPAD_UP;
break;
case DOWN:
keyCode = KeyEvent.KEYCODE_DPAD_DOWN;
break;
}
if (keyCode != null) {
// clear pressed keys before dispatching new key press
clearPresses();
if (action == KeyEvent.ACTION_DOWN) {
MainActivity.get().dispatchKeyEvent(new KeyEvent(action, keyCode));
pressedKeys.add(keyCode);
}
}
}
}
private static class InnerButton extends JoyPadButton {
public InnerButton(final Context ctx) {
super(ctx);
}
/**
* Sets X coordinate relative to OuterButton.
*/
public void setRelativeX(float x) {
x = x + outerButton.getX();
final float maxL = outerButton.getX();
final float maxR = maxL + outerButton.getWidth();
// don't move outside bounds of outer button
if (x >= maxL && x <= maxR) {
// use circle center
setX(x - (getWidth() / 2));
}
}
/**
* Sets Y coordinate relative to OuterButton.
*/
public void setRelativeY(float y) {
y = y + outerButton.getY();
final float maxT = outerButton.getY();
final float maxB = maxT + outerButton.getHeight();
// don't move outside bounds of outer button
if (y >= maxT && y <= maxB) {
// use circle center
setY(y - (getHeight() / 2));
}
}
}
}

View file

@ -1,5 +1,5 @@
/***************************************************************************
* Copyright © 2024 - Faiumoni e. V. *
* Copyright © 2022 - Arianne *
***************************************************************************
***************************************************************************
* *
@ -9,33 +9,29 @@
* (at your option) any later version. *
* *
***************************************************************************/
package games.stendhal.server.entity.npc.behaviour.impl.idle;
package org.arianne.stendhal.client.input;
import games.stendhal.server.entity.npc.NPC;
import android.content.Context;
//import android.view.View;
import android.widget.ImageView;
public abstract class AbstractIdleBehaviour implements IdleBehaviour {
abstract class JoyPadButton extends ImageView {
@Override
public abstract void perform(NPC npc);
@Override
public void onMoved(NPC npc) {
// does nothing in this implementation
public JoyPadButton(final Context ctx) {
super(ctx);
}
@Override
public void reset() {
// does nothing in this implementation
protected float getCenterX() {
return getX() + (getWidth() / 2);
}
@Override
public boolean handleSimpleCollision(NPC npc, int nx, int ny) {
return false;
protected float getCenterY() {
return getY() + (getHeight() / 2);
}
@Override
public boolean handleObjectCollision(NPC npc) {
return false;
protected void centerOn(final JoyPadButton parent) {
super.setX(parent.getCenterX() - (getWidth() / 2));
super.setY(parent.getCenterY() - (getHeight() / 2));
}
}

View file

@ -1,5 +1,5 @@
/***************************************************************************
* Copyright © 2023 - Stendhal *
* Copyright © 2022 - Arianne *
***************************************************************************
***************************************************************************
* *
@ -9,20 +9,29 @@
* (at your option) any later version. *
* *
***************************************************************************/
package games.stendhal.server.entity.npc;
package org.arianne.stendhal.client.js;
public class CrystalNPCBase extends SpeakerNPC {
import android.webkit.JavascriptInterface;
public CrystalNPCBase(final String name) {
super(name);
// hide location from website
hideLocation();
public class JSInterface {
/** Singleton instance. */
private static JSInterface instance;
/**
* Retrieves the singleton instance.
*/
public static JSInterface get() {
if (instance == null) {
instance = new JSInterface();
}
return instance;
}
@Override
protected void createPath() {
// doesn't move
setPath(null);
private JSInterface() {
// singleton
}
}

View file

@ -1,5 +1,5 @@
/***************************************************************************
* Copyright © 2022-2024 - Faiumoni e. V. *
* Copyright © 2022 - Arianne *
***************************************************************************
***************************************************************************
* *
@ -9,14 +9,17 @@
* (at your option) any later version. *
* *
***************************************************************************/
package org.stendhalgame.client;
import java.io.IOException;
package org.arianne.stendhal.client.sound;
import android.content.Context;
import android.media.MediaPlayer;
import android.net.Uri;
import java.io.IOException;
import org.arianne.stendhal.client.DebugLog;
import org.arianne.stendhal.client.MainActivity;
public class MusicPlayer {
@ -25,24 +28,22 @@ public class MusicPlayer {
public static void playMusic(final Context ctx, final int id, final boolean loop) {
if (isPlaying()) {
Logger.debug("freeing up MusicPlayer instance to play new song");
DebugLog.debug("freeing up MusicPlayer instance to play new song");
stopMusic();
}
Logger.debug("starting music (loop: " + loop + ")");
DebugLog.debug("starting music (loop: " + loop + ")");
mplayer = new MediaPlayer();
mplayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(final MediaPlayer mp) {
Logger.debug("starting music");
DebugLog.debug("starting music");
// FIXME: not working
//mplayer.setLooping(loop);
mplayer.start();
}
});
mplayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(final MediaPlayer mp) {
// free up resources & reset player to null
stopMusic();
@ -58,7 +59,7 @@ public class MusicPlayer {
mplayer.setDataSource(ctx, parseResourceUri(id));
mplayer.prepareAsync();
} catch (final IOException e) {
Logger.error("failed to load resource " + id + ":\n" + e.getStackTrace());
DebugLog.error("failed to load resource " + id + ":\n" + e.getStackTrace());
}
}
@ -66,46 +67,10 @@ public class MusicPlayer {
playMusic(MainActivity.get(), id, loop);
}
/**
* Plays selected music.
*
* @param musicId
* String identifier of music to play or `null` to play music configured in preferences.
*/
public static void playTitleMusic(String musicId) {
if (musicId == null) {
musicId = PreferencesActivity.getString("song_list");
}
int id = R.raw.title_01;
switch (musicId) {
case "title_02":
id = R.raw.title_02;
break;
case "title_03":
id = R.raw.title_03;
break;
case "title_04":
id = R.raw.title_04;
break;
case "title_05":
id = R.raw.title_05;
break;
}
Logger.debug("playing music: " + musicId);
MusicPlayer.playMusic(id, true);
}
/**
* Plays music configured in preferences.
*/
public static void playTitleMusic() {
playTitleMusic(null);
}
public static void stopMusic() {
if (mplayer != null) {
if (mplayer.isPlaying()) {
Logger.debug("stopping music");
DebugLog.debug("stopping music");
mplayer.stop();
}
mplayer.release();

Binary file not shown.

After

Width:  |  Height:  |  Size: 342 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 354 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 389 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 400 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 375 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 386 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 303 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 441 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 443 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Before After
Before After

View file

@ -0,0 +1,98 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/content"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="org.arianne.stendhal.client.StendhalWebView">
<ImageView
android:id="@+id/splash"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
/>
<org.arianne.stendhal.client.ClientView
android:id="@+id/clientWebView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
/>
<Toolbar
android:id="@+id/menu_main"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#80ffffff"
android:logo="@mipmap/ic_launcher_round"
android:title="Menu"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintBottom_toBottomOf="parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="right">
<Button
android:id="@+id/btn_connect"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Connect"
android:background="#00000000"
/>
<Button
android:id="@+id/btn_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Title"
android:background="#00000000"
/>
<Button
android:id="@+id/btn_reload"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Reload"
android:background="#00000000"
/>
<Button
android:id="@+id/btn_settings"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Settings"
android:background="#00000000"
/>
<Button
android:id="@+id/btn_about"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="About"
android:background="#00000000"
/>
<Button
android:id="@+id/btn_quit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Quit"
android:background="#00000000"
/>
</LinearLayout>
</Toolbar>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/preferences"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="org.arianne.stendhal.client.PreferencesActivity">
<FrameLayout
android:id="@+id/preferencesFrame"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 9.9 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 75 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 139 KiB

After

Width:  |  Height:  |  Size: 139 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Before After
Before After

View file

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="titleMusic">
<item>Treasure Hunter</item>
<item>Woodland Fantasy</item>
<item>Land of Fearless</item>
<item>Medieval: Rejoicing</item>
<item>Medieval: The Old Tower Inn</item>
</string-array>
<string-array name="titleMusicId">
<item>title_01</item>
<item>title_02</item>
<item>title_03</item>
<item>title_04</item>
<item>title_05</item>
</string-array>
</resources>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Stendhal</string>
</resources>

View file

@ -0,0 +1,107 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceCategory android:title="General">
<EditTextPreference
android:key="client_url"
android:inputType="text"
android:maxLines="1"
android:title="Client URL"
android:summary="Custom URL pointing to web client."
android:selectAllOnFocus="true"
/>
</PreferenceCategory>
<PreferenceCategory android:title="Input">
<CheckBoxPreference
android:key="show_dpad"
android:defaultValue="false"
android:title="Show D-Pad"
android:summary="Show on-screen directional pad."
/>
<CheckBoxPreference
android:key="dpad_joy"
android:defaultValue="true"
android:title="Joystick Style D-Pad (experimental)"
android:summary="Use a joystick style directional pad."
android:dependency="show_dpad"
/>
<EditTextPreference
android:key="joypad_sensitivity"
android:inputType="numberDecimal"
android:digits="0123456789"
android:defaultValue="25"
android:title="Joypad Sensitivity"
android:summary="Threshold, in pixels, at which joypad will dispatch move event (lower = more sensitive)."
android:dependency="dpad_joy"
android:selectAllOnFocus="true"
/>
<EditTextPreference
android:key="dpad_offset_x"
android:inputType="numberDecimal"
android:digits="0123456789"
android:defaultValue="100"
android:title="D-Pad Offset X"
android:summary="Horizontal offset of directional pad (from left)."
android:dependency="show_dpad"
android:selectAllOnFocus="true"
/>
<EditTextPreference
android:key="dpad_offset_y"
android:inputType="numberDecimal"
android:digits="0123456789"
android:defaultValue="300"
android:title="D-Pad Offset Y"
android:summary="Vertical offset of directional pad (from bottom)."
android:dependency="show_dpad"
android:selectAllOnFocus="true"
/>
<CheckBoxPreference
android:key="map_touches"
android:defaultValue="false"
android:title="Remap Touches (experimental)"
android:summary="Map touches to mouse click events."
/>
</PreferenceCategory>
<PreferenceCategory android:title="Audio">
<CheckBoxPreference
android:key="title_music"
android:defaultValue="true"
android:title="Title Music"
android:summary="Play title screen music."
/>
<ListPreference
android:key="song_list"
android:dependency="title_music"
android:title="Songs"
android:entries="@array/titleMusic"
android:entryValues="@array/titleMusicId"
/>
</PreferenceCategory>
<PreferenceCategory android:title="Meta">
<Preference
android:key="reset"
android:title="Restore Defaults"
android:summary="Restore all preferences default values."
/>
</PreferenceCategory>
</PreferenceScreen>

View file

@ -0,0 +1,83 @@
/***************************************************************************
* Copyright © 2022 - Arianne *
***************************************************************************
***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
package org.arianne.stendhal.client;
import java.io.File;
import androidx.appcompat.app.AppCompatActivity;
/**
* Dummy class for release builds.
*/
public class DebugLog {
private static File logsDir;
public static enum DebugLevel {
INFO,
WARN,
ERROR,
DEBUG
}
private static DebugLog instance;
public static DebugLog get() {
if (instance == null) {
instance = new DebugLog();
}
return instance;
}
public static void init(final File dir, final AppCompatActivity activity) {
logsDir = new File(dir.getPath() + "/logs");
}
public static void writeLine(final String text) {
// do nothing
}
public static void writeLine(final String text, final DebugLevel level) {
// do nothing
}
public static void info(final String text) {
// do nothing
}
public static void warn(final String text) {
// do nothing
}
public static void error(final String text) {
// do nothing
}
public static void debug(final String text) {
// do nothing
}
public static void notify(final String message) {
// do nothing
}
public static void notify(final String message, final DebugLevel level) {
// do nothing
}
public static String getLogsDir() {
return logsDir.getPath();
}
}

View file

@ -5,7 +5,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.4.2'
classpath "com.android.tools.build:gradle:7.1.1"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files

View file

@ -6,7 +6,7 @@
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx512m -Dfile.encoding=UTF-8
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
@ -16,6 +16,4 @@ org.gradle.jvmargs=-Xmx512m -Dfile.encoding=UTF-8
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Automatically convert third-party libraries to use AndroidX
android.enableJetifier=true
# Stop daemon after build
org.gradle.daemon=false
android.enableJetifier=true

View file

@ -1,6 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=f6b8596b10cce501591e92f229816aa4046424f3b24d771751b06779d58c8ec4
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

185
android/gradlew vendored Executable file
View file

@ -0,0 +1,185 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"

View file

@ -14,7 +14,7 @@
@rem limitations under the License.
@rem
@if "%DEBUG%"=="" @echo off
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@ -25,7 +25,7 @@
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@ -40,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
@ -75,15 +75,13 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal

View file

@ -6,4 +6,4 @@ dependencyResolutionManagement {
}
}
rootProject.name = "Stendhal"
include ':client'
include ':app'

1
app/.gitignore vendored
View file

@ -1 +0,0 @@
/godot/

View file

@ -1,7 +0,0 @@
TODO:
- create activity for about information
- add credits to about information
- switch to Log4j for logging (maybe)
- write debugging logs to sub-directory in user's home instead of ~/Android/data/org.stendhalgame.client
- replace title_03 (Land of Fearless), title_04 (Rejoicing), & title_05 (The Old Tower Inn) music with non-looping versions or fix loops

View file

@ -1,149 +0,0 @@
plugins {
id 'com.android.application'
}
android {
namespace "org.stendhalgame.client"
compileSdk 32
defaultConfig {
applicationId namespace
minSdk 21
targetSdk 32
versionCode 1049005
versionName "1.49.5"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
targetSdkVersion 32
minSdkVersion 21
}
// F-Droid doesn't allow encrypted DependencyInfoBlock in APK
dependenciesInfo {
// disables dependency metadata when building APKs
includeInApk = false
// disables dependency metadata when building Android App Bundles
includeInBundle = false
}
// build output
project.setProperty("archivesBaseName", "${namespace}-${defaultConfig.versionName}")
project.setProperty("buildDir", "${projectDir}/../../../build/build_android_client")
/* A keystore.properties file can be placed in the root Android project directory and must
* include the following values:
* storeFile=<path-to-keystore-file>
* storePassword=<store-password>
* keyPassword=<key-password>
* keyAlias=<key-alias>
* The `storeFile` path value must use double backslashes (\\) on Windows.
*/
def keystorePropertiesFile = rootProject.file("keystore.properties")
def keystoreProperties = new Properties()
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}
// denotes whether a signing key is found/configured
def unsigned = true;
// property to force use of ~/.android/debug.keystore signing key & generate if not found
def generateDebugKey = project.properties["generate-debug-key"] != null;
signingConfigs {
main {
if (keystorePropertiesFile.exists()) {
// use key configured in keystore.properties file
storeFile file(keystoreProperties["storeFile"])
storePassword keystoreProperties["storePassword"]
keyPassword keystoreProperties["keyPassword"]
keyAlias keystoreProperties["keyAlias"]
unsigned = false;
} else if (signingConfigs.debug.storeFile.exists()) {
// use debug key
initWith signingConfigs.debug
unsigned = false;
}
}
if (generateDebugKey && signingConfigs.main != signingConfigs.debug) {
// force using debug key
main {
initWith signingConfigs.debug
}
unsigned = false;
}
}
if (!unsigned && !generateDebugKey && !signingConfigs.main.storeFile.exists()) {
throw new Error("Signing key not found: " + signingConfigs.main.storeFile);
}
buildTypes {
debug {
applicationIdSuffix ".debug"
versionNameSuffix "-debug"
// prevent automatic generation of ~/.android/debug.keystore
signingConfig null
}
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
if (generateDebugKey) {
// force using debug key & generate if not found
debug.signingConfig signingConfigs.debug
release.signingConfig signingConfigs.debug
} else if (!unsigned) {
// apply configured signing key to builds
debug.signingConfig signingConfigs.main
release.signingConfig signingConfigs.main
}
}
buildTypes.all { type ->
if (!type.name.equals("debug")) {
sourceSets[type.name].java { srcDirs "src/shared/java/" }
}
}
applicationVariants.all { variant ->
variant.outputs.all {
def buildRoot = "../../../../"
if (variant.name.equals("release")) {
// exclude "-release" suffix
outputFileName = outputFileName.replaceAll("(-release)(?!.*-release)", "")
}
// output to <source_root>/build
outputFileName = new File("${buildRoot}", "${outputFileName}")
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
compileSdkVersion 32
buildToolsVersion '32.0.0'
lintOptions {
abortOnError false
}
// print some build info for debugging
println("Package: " + namespace)
println("Application ID: " + defaultConfig.applicationId)
println("Version: " + defaultConfig.versionCode)
println("Output basename: " + archivesBaseName)
println("Signing key: " + (unsigned ? "unsigned" : signingConfigs.main.storeFile))
println("Build directory: " + project.buildDir)
}
dependencies {
implementation "androidx.appcompat:appcompat:1.4.1"
implementation "com.google.android.material:material:1.4.0"
implementation "androidx.preference:preference-ktx:1.2.0"
//testImplementation 'junit:junit:4.+'
//androidTestImplementation 'androidx.test.ext:junit:1.1.2'
//androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
}

View file

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- allow client url http://localhost:xxxx via Chrome forwarding to a local server -->
<application android:usesCleartextTraffic="true">
</application>
</manifest>

View file

@ -1,277 +0,0 @@
/***************************************************************************
* Copyright © 2022-2024 - Faiumoni e. V. *
***************************************************************************
***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
package org.stendhalgame.client;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.util.Log;
/**
* A class to output debugging information to logcat file.
*/
public class Logger {
/** Attribute denoting state of initialization. */
private static boolean initialized = false;
/** Logs directory. */
private static File logsDir;
/** Singleton instance. */
private static Logger instance;
/**
* Retrieves singleton instance.
*/
public static Logger get() {
if (Logger.instance == null) {
Logger.instance = new Logger();
}
return Logger.instance;
}
/**
* Hidden singleton constructor.
*/
private Logger() {
// do nothing
}
/**
* Initializes logs directory.
*/
public static void init(final File dir) {
if (Logger.initialized) {
Logger.warn(true, "tried to re-initialize logger");
return;
}
Logger.initialized = true;
logsDir = new File(dir.getPath() + "/logs");
writeLine("\n // -- debugging initialized -- //", null);
writeLine("logs directory: " + logsDir.getPath());
}
/**
* Writes a line of text to log file.
*
* @param text
* Text to be written.
*/
public static void writeLine(final String text) {
writeLine(text, LogLevel.DEBUG);
}
/**
* Writes a line of text to log file.
*
* @param text
* Text to be written.
* @param level
* Logging verbosity level.
*/
public static void writeLine(String text, final LogLevel level) {
if (logsDir == null) {
System.err.println("ERROR: Logger not initialized. Call Logger.init.");
return;
}
if (!logsDir.exists()) {
logsDir.mkdirs();
}
if (level != null) {
text = new SimpleDateFormat("yyyy.MM.dd.HH.mm.ss").format(new Date())
+ ": " + level.label + ": " + text;
}
text = text + "\n";
final String logFile = logsDir.getPath() + "/debug-"
+ new SimpleDateFormat("yyyy.MM.dd").format(new Date()) + ".txt";
BufferedWriter buffer;
try {
buffer = new BufferedWriter(new FileWriter(logFile, true));
buffer.write(text);
if (buffer != null) {
buffer.close();
}
} catch (final IOException e) {
System.err.println("ERROR: " + e.getMessage());
}
}
/**
* Writes a line of text to log file at `LogLevel.INFO` verbosity.
*
* @param text
* Text to be written.
*/
public static void info(final String text) {
Log.i("Logger", text);
writeLine(text, LogLevel.INFO);
}
/**
* Writes a line of text to log file at `LogLevel.INFO` verbosity.
*
* @param notify
* If `true` a toast notification is displayed to user.
* @param text
* Text to be written.
*/
public static void info(final boolean notify, final String text) {
Logger.info(text);
if (notify) {
Logger.notify(text, LogLevel.INFO);
}
}
/**
* Writes a line of text to log file at `LogLevel.WARN` verbosity.
*
* @param text
* Text to be written.
*/
public static void warn(final String text) {
Log.w("Logger", text);
writeLine(text, LogLevel.WARN);
}
/**
* Writes a line of text to log file at `LogLevel.WARN` verbosity.
*
* @param notify
* If `true` a toast notification is displayed to user.
* @param text
* Text to be written.
*/
public static void warn(final boolean notify, final String text) {
Logger.warn(text);
if (notify) {
Logger.notify(text, LogLevel.WARN);
}
}
/**
* Writes a line of text to log file at `LogLevel.ERROR` verbosity.
*
* @param text
* Text to be written.
*/
public static void error(final String text) {
Log.e("Logger", text);
writeLine(text, LogLevel.ERROR);
}
/**
* Writes a line of text to log file at `LogLevel.ERROR` verbosity.
*
* @param notify
* If `true` a toast notification is displayed to user.
* @param text
* Text to be written.
*/
public static void error(final boolean notify, final String text) {
Logger.error(text);
if (notify) {
Logger.notify(text, LogLevel.ERROR);
}
}
/**
* Writes a line of text to log file at `LogLevel.DEBUG` verbosity.
*
* @param text
* Text to be written.
*/
public static void debug(final String text) {
Log.d("Logger", text);
writeLine(text, LogLevel.DEBUG);
}
/**
* Writes a line of text to log file at `LogLevel.DEBUG` verbosity.
*
* @param notify
* If `true` a toast notification is displayed to user.
* @param text
* Text to be written.
*/
public static void debug(final boolean notify, final String text) {
Logger.debug(text);
if (notify) {
Logger.notify(text, LogLevel.DEBUG);
}
}
/**
* Displays a toast notification to user.
*
* @param text
* Text to display in notification.
*/
public static void notify(final String text) {
notify(text, LogLevel.INFO);
}
/**
* Displays a toast notification to user.
*
* Does not output to log file.
*
* @param text
* Text to display in notification.
* @param level
* Logging verbosity level.
*/
public static void notify(final String text, LogLevel level) {
if (!Logger.initialized) {
System.err.println("ERROR: Logger not initialized. Call Logger.init.");
return;
}
if (level == null) {
level = LogLevel.INFO;
}
final AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.get());
builder.setTitle(level.label);
builder.setMessage(text);
builder.setPositiveButton("Ok", new DialogInterface.OnClickListener() {
@Override
public void onClick(final DialogInterface dialog, final int id) {
dialog.cancel();
}
});
builder.create().show();
}
/**
* Retrieves logs directory.
*
* @return
* Directory path to outpu logs.
*/
public static String getLogsDir() {
return logsDir.getPath();
}
}

View file

@ -1,52 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Stendhal.FullScreen">
<activity
android:name=".MainActivity"
android:exported="true"
android:configChanges="orientation|screenSize|keyboardHidden">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".PreferencesActivity"
android:label="Preferences"
android:exported="true"
android:configChanges="orientation|screenSize|keyboardHidden">
<intent-filter>
<action android:name="preferences" />
</intent-filter>
</activity>
<activity
android:name=".OpenAppByUrl"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="@string/intent_url_scheme"
android:host="callback" />
</intent-filter>
</activity>
</application>
</manifest>

View file

@ -1,41 +0,0 @@
/***************************************************************************
* Copyright © 2024 - Faiumoni e. V. *
***************************************************************************
***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
package org.stendhalgame.client;
/**
* App configuration information.
*/
class AppInfo {
/**
* Retrieves configured build type string.
*/
public static String getBuildType() {
return MainActivity.get().getResources().getString(R.string.build_type);
}
/**
* Retrieves configured build version string.
*/
public static String getBuildVersion() {
return MainActivity.get().getResources().getString(R.string.build_version);
}
/**
* Retrieves configured intent URL scheme string.
*/
public static String getIntentUrlScheme() {
return MainActivity.get().getResources().getString(R.string.intent_url_scheme);
}
}

View file

@ -1,603 +0,0 @@
/***************************************************************************
* Copyright © 2022-2024 - Faiumoni e. V. *
***************************************************************************
***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
package org.stendhalgame.client;
import java.util.Random;
import android.app.AlertDialog;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.net.Uri;
import android.os.Build;
import android.util.AttributeSet;
import android.view.KeyEvent;
import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
import android.webkit.DownloadListener;
import android.webkit.WebChromeClient;
import android.webkit.WebResourceRequest;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
/**
* Interface to handle web content.
*/
public class ClientView extends WebView {
/** Client URL path. */
private String clientUrlSuffix = "client";
/** Determines type of client to connect to. */
private boolean testClient = false;
/** Determines type of server to connect to. */
private boolean testServer = false;
private Boolean debugging;
/** ID of page currently loaded. */
private PageId currentPage;
/** ID of page previously loaded. */
private PageId previousPage;
private String stateId = "";
private String seed = "";
/**
* Creates a new view.
*
* @param ctx
* Activity Context to access application assets.
*/
public ClientView(final Context ctx) {
super(ctx);
onInit();
}
/**
* Creates a new view.
*
* @param ctx
* Activity Context to access application assets.
* @param attrs
* AttributeSet passed to parent (may be `null`).
*/
public ClientView(final Context ctx, final AttributeSet attrs) {
super(ctx, attrs);
onInit();
}
/**
* Creates a new view.
*
* @param ctx
* Activity Context to access application assets.
* @param attrs
* AttributeSet passed to parent (may be `null`).
* @param style
* Attribute in the current theme that contains a reference to a style resource that supplies
* default values for the view (can be 0 to not look for defaults).
*/
public ClientView(final Context ctx, final AttributeSet attrs, final int style) {
super(ctx, attrs, style);
onInit();
}
/**
* Initializes client WebView interface.
*/
private void onInit() {
setActive(false); // main activity manages initial visibility
setBackgroundColor(Color.TRANSPARENT);
// expand to size of parent constraints
setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT));
final WebSettings viewSettings = getSettings();
viewSettings.setJavaScriptEnabled(true);
viewSettings.setDomStorageEnabled(true);
// keep elements in position in portrait mode
viewSettings.setLoadWithOverviewMode(true);
viewSettings.setUseWideViewPort(true);
// zoom controls
viewSettings.setSupportZoom(true);
viewSettings.setBuiltInZoomControls(true);
viewSettings.setDisplayZoomControls(false);
// allow autoplay of music
viewSettings.setMediaPlaybackRequiresUserGesture(false);
if (debugEnabled()) {
// make WebView debuggable for debug builds
setWebContentsDebuggingEnabled(true);
}
initWebViewClient();
initDownloadHandler();
}
/**
* Sets active state of this client view.
*
* @param active
* If `true` shows this client view, otherwise hides it.
*/
public void setActive(final boolean active) {
if (active) {
setVisibility(ClientView.VISIBLE);
return;
}
setVisibility(ClientView.GONE);
}
/**
* Checks active state of client view.
*
* @return
* `true` is this view is visible.
*/
public boolean isActive() {
return getVisibility() == ClientView.VISIBLE;
}
/**
* Initializes method overrides to handle page loading.
*/
private void initWebViewClient() {
// XXX: not sure setting WebChromClient is doing anything, was recommended to
// fix touchmove events not registering
setWebChromeClient(new WebChromeClient());
setWebViewClient(new WebViewClient() {
/**
* Handles pages loaded indirectly.
*
* @param view
* The default WebView instance handling the request.
* @param request
* Web request including the URL to be loaded.
* @return
* `true` to abort default loading.
*/
@Override
public boolean shouldOverrideUrlLoading(final WebView view, final WebResourceRequest request) {
Uri uri = request.getUrl();
final Uri.Builder builder = uri.buildUpon();
if (UrlHelper.isLoginUri(uri)) {
builder.appendQueryParameter("build", AppInfo.getBuildType());
builder.appendQueryParameter("version", AppInfo.getBuildVersion());
builder.appendQueryParameter("state", stateId);
builder.appendQueryParameter("seed", seed);
}
uri = builder.build();
if (!UrlHelper.isInternalUri(uri)) {
// open external links in default browser/app
// FIXME: should we ask for confirmation?
try {
MainActivity.get().startActivity(new Intent(Intent.ACTION_VIEW, uri));
} catch (final ActivityNotFoundException e) {
Notifier.toast("No app found to open this link");
}
return true;
}
view.loadUrl(UrlHelper.checkClientUrl(uri.toString()));
return true;
}
/**
* Called before a URL is loaded.
*
* @param view
* WebView instance handling page contents.
* @param url
* HTTP string of target page.
* @param favicon
* Favicon bitmap image.
*/
@Override
public void onPageStarted(final WebView view, final String url, final Bitmap favicon) {
MusicPlayer.stopMusic();
super.onPageStarted(view, url, favicon);
}
/**
* Called after a URL is loaded.
*
* @param view
* WebView instance handling page contents.
* @param url
* HTTP string of target page.
*/
@Override
public void onPageFinished(final WebView view, final String url) {
super.onPageFinished(view, url);
if (UrlHelper.isClientUrl(url)) {
setPage(PageId.WEBCLIENT);
} else if (url.equals("") || url.equals("about:blank")) {
setPage(PageId.TITLE);
if (PreferencesActivity.getBoolean("title_music", true)) {
MusicPlayer.playTitleMusic();
}
} else {
setPage(PageId.OTHER);
}
Menu.get().updateButtons();
Logger.debug("page id: " + currentPage);
}
});
}
/**
* deprecated?
*/
@Override
public boolean dispatchKeyEvent(final KeyEvent event) {
final boolean ret = super.dispatchKeyEvent(event);
// hide keyboard when "enter" pressed
if (isGameActive() && event.getAction() == KeyEvent.ACTION_UP
&& event.getKeyCode() == KeyEvent.KEYCODE_ENTER) {
((InputMethodManager) MainActivity.get()
.getSystemService(Context.INPUT_METHOD_SERVICE))
.hideSoftInputFromWindow(this.getWindowToken(), 0);
}
return ret;
}
/**
* Initializes interface for handling screenshots.
*/
private void initDownloadHandler() {
setDownloadListener(new DownloadListener() {
/**
* Handles downloading screenshot created by web client.
*
* TODO: get filename to pass to `DownloadHandler.download` from "download" property of
* anchor
*
* @param url
* Content URL to the screenshot image.
* @param userAgent
* User agent used for download.
* @param contentDisposition
* Content-disposition HTTP header, if present.
* @param mimetype
* Content type reported by server (web client creates PNG image).
* @param contentLength
* File size reported by server.
*/
@Override
public void onDownloadStart(final String url, final String userAgent,
final String contentDisposition, final String mimetype, final long contentLength) {
final DownloadHandler handler = new DownloadHandler();
handler.download(url, mimetype);
if (handler.getResult()) {
final String msg = "Downloaded file: " + handler.getMessage();
Logger.debug(msg);
Notifier.toast(msg);
} else {
final String msg = handler.getMessage();
Logger.error(msg);
Notifier.toast("ERROR: " + msg);
}
}
});
}
/**
* Verifies intent state & parses & loads target URL.
*
* @param intent
* Called login intent with state verification.
*/
public void checkLoginIntent(final Intent intent) {
final Uri intentUri = intent.getData();
final String url = intentUri.getQueryParameter("url");
final String loginseed = intentUri.getQueryParameter("loginseed");
final String intentStateId = intentUri.getQueryParameter("state");
if (stateId == null || intentStateId == null || url == null || "".equals(stateId) || !stateId.equals(intentStateId)) {
final String err = "There was an error verifying login";
Logger.error(err + " (\"" + stateId + "\" == \"" + intentStateId + "\")");
Notifier.showMessage(err);
if (currentPage == null || PageId.TITLE.equals(previousPage)) {
// reload title
loadTitleScreen();
}
return;
}
String completeUrl = url + "&loginseed=" + loginseed + seed;
loadUrl(UrlHelper.checkClientUrl(completeUrl));
}
/**
* Determines if URL to load is test web client.
*/
public boolean isTestClient() {
return testClient;
}
/**
* Determines if connection is to test server.
*/
public boolean isTestServer() {
return testServer;
}
/**
* Retrieves a string representing the selected client.
*
* @return
* One of "main", "test", or "none".
*/
public String getSelectedClient() {
if (onTitleScreen()) {
return "none";
}
return testClient ? "test" : "main";
}
/**
* Retrieves a string representing the selected server.
*
* @return
* One of "main", "test", or "none".
*/
public String getSelectedServer() {
if (onTitleScreen()) {
return "none";
}
return testServer ? "test" : "main";
}
/**
* Resets selected client & server values to default.
*/
private void reset() {
testClient = false;
testServer = false;
clientUrlSuffix = "client";
stateId = "";
seed = "";
}
/**
* Retrieves client type string.
*
* @return
* Either "client" or "testclient".
*/
public String getClientUrlSuffix() {
return clientUrlSuffix;
}
/**
* Generates a random string for identifying state.
*/
private String generateRandomString() {
// ensure state is reset before generating
String result = "";
// useable characters
String charList = "0123456789";
for (char c = 'A'; c <= 'Z'; c++) {
charList += c;
}
for (char c = 'a'; c <= 'z'; c++) {
charList += c;
}
// generate random state
final Random rand = new Random();
final int ccount = charList.length();
for (int idx = 0; idx <= 20 ; idx++) {
result += charList.charAt(rand.nextInt(ccount));
}
return result;
}
/**
* Opens location in client WebView.
*
* @param url
* URL to load.
*/
@Override
public void loadUrl(final String url) {
Logger.debug("Loading URL: " + url);
super.loadUrl(url);
}
/**
* Shows initial title screen.
*/
public void loadTitleScreen() {
reset();
setPage(PageId.TITLE);
SplashUtil.get().setVisible(true);
loadUrl("about:blank");
Menu.get().show();
}
/**
* Attempts to connect to client host.
*
* FIXME: rename as it may cause confusion in regards to loading page "account/login.html"
*/
public void loadLogin() {
if (debugEnabled() && PreferencesActivity.getString("client_url").trim().equals("")) {
// debug builds support choosing between main & test client/server
selectClient();
} else {
onSelectServer();
}
}
/**
* Opens a message dialog for user to choose between main & test clients.
*/
private void selectClient() {
final AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
builder.setMessage("Select client");
builder.setPositiveButton("Main", new DialogInterface.OnClickListener() {
@Override
public void onClick(final DialogInterface dialog, final int id) {
testClient = false;
clientUrlSuffix = "client";
// skip server confirmation as only test client has this option
onSelectServer();
dialog.cancel();
}
});
builder.setNegativeButton("Test", new DialogInterface.OnClickListener() {
@Override
public void onClick(final DialogInterface dialog, final int id) {
testClient = true;
clientUrlSuffix = "testclient";
selectServer();
dialog.cancel();
}
});
builder.create().show();
}
/**
* Opens a message dialog for user to choose between main & test servers.
*/
private void selectServer() {
final AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
builder.setMessage("Select server");
builder.setPositiveButton("Main", new DialogInterface.OnClickListener() {
@Override
public void onClick(final DialogInterface dialog, final int id) {
testServer = false;
dialog.cancel();
onSelectServer();
}
});
builder.setNegativeButton("Test", new DialogInterface.OnClickListener() {
@Override
public void onClick(final DialogInterface dialog, final int id) {
testServer = true;
dialog.cancel();
onSelectServer();
}
});
builder.create().show();
}
/**
* Connects to server & loads initial page.
*/
private void onSelectServer() {
// create a unique state
stateId = generateRandomString();
seed = generateRandomString();
// hide splash image
SplashUtil.get().setVisible(false);
final String initialPage = UrlHelper.getInitialPageUrl();
Logger.debug("Loading initial page: " + initialPage);
loadUrl(initialPage);
setPage(PageId.OTHER);
// hide menu after exiting title screen
Menu.get().hide();
}
/**
* Checks if user has specified a URL pointing to a custom server/client.
*
* @return
* Server address specified in preferences or `null`.
*/
public String checkCustomServer() {
final String cs = PreferencesActivity.getString("client_url", "").trim();
if (cs.equals("")) {
return null;
}
return cs;
}
/**
* Checks if title page is currently loaded.
*
* @return
* `true` if current page matches `PageId.TITLE`.
*/
public boolean onTitleScreen() {
return currentPage == PageId.TITLE;
}
/**
* Checks if web client page is currently loaded.
*
* @return
* `true` if current page matches `PageId.WEBCLIENT`.
*/
public boolean isGameActive() {
return currentPage == PageId.WEBCLIENT;
}
/**
* Retrieves ID of current page.
*
* @return
* Current `PageId`.
*/
public PageId getCurrentPageId() {
return currentPage;
}
/**
* Sets current page ID.
*
* @param newPage
* Page ID to be used.
*/
private void setPage(final PageId newPage) {
previousPage = currentPage;
currentPage = newPage;
if (previousPage == null) {
previousPage = currentPage;
}
}
/**
* Checks if this is a debug build.
*
* @return
* `true` if debug flag set.
*/
public boolean debugEnabled() {
if (debugging != null) {
return debugging;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
debugging = (MainActivity.get().getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0;
} else {
debugging = false;
}
return debugging;
}
}

View file

@ -1,228 +0,0 @@
/***************************************************************************
* Copyright © 2022-2024 - Faiumoni e. V. *
***************************************************************************
***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
package org.stendhalgame.client;
import java.io.File;
import java.io.FileOutputStream;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.Date;
import android.net.Uri;
import android.os.Environment;
import android.util.Base64;
/**
* The main purpose of this class is to handle downloading screenshots
* created by the web client page.
*/
public class DownloadHandler {
private static enum DownloadType {
IMAGE_PNG("image/png"),
TEXT_PLAIN("text/plain"),
UNSUPPORTED(null);
private String value;
private DownloadType(final String value) {
this.value = value;
}
public static DownloadType fromString(final String value) {
for (final DownloadType v: DownloadType.values()) {
if (v.value != null && v.value.equals(value)) {
return v;
}
}
return DownloadType.UNSUPPORTED;
}
}
private boolean result = false;
private String message = null;
/**
* Checks for a supported MIME type.
*
* @param url
* File or data URL.
* @param mimetype
* Detected MIME type.
* @return
* Download type or unsupported.
*/
private DownloadType checkMimeType(final String url, final String mimetype) {
final DownloadType dtype = DownloadType.fromString(mimetype);
if (DownloadType.UNSUPPORTED.equals(dtype) && url.startsWith("data:image/png;base64,")) {
return DownloadType.IMAGE_PNG;
}
return dtype;
}
/**
* Writes file data to device storage.
*
* @param dir
* Directory where new file is to be created.
* @param basename
* Filename of new file.
* @param data
* File contents to be written to storage.
*/
private void downloadInternal(final File dir, final String basename, final byte[] data) {
String stacktrace = null;
try {
if (!dir.exists()) {
dir.mkdirs();
}
final FileOutputStream fos = new FileOutputStream(new File(dir, basename));
fos.write(data);
fos.close();
this.result = true;
} catch (final java.lang.NoClassDefFoundError e) {
this.message = "an error occurred while decoding data (see debug log for more info)";
stacktrace = stackTraceToString(e);
} catch (java.io.IOException e) {
this.message = "an error occurred while attempting to write file (see debug log for more info)";
stacktrace = stackTraceToString(e);
}
if (stacktrace != null) {
Logger.error(stacktrace.toString());
}
}
/**
* Writes file data to device storage.
*
* @param url
* A data URL.
* @param mimetype
* Detected MIME type.
* @param targetName
* Filename for new file.
*/
public void download(final String url, final String mimetype, String targetName) {
final Uri uri = Uri.parse(url);
final String scheme = uri.getScheme();
final String storageState = Environment.getExternalStorageState();
final DownloadType dtype = checkMimeType(url, mimetype);
if (!MainActivity.get().getActiveClientView().isGameActive()) {
this.message = "downloading from this page not supported";
} else if (!"data".equals(scheme)) {
// only data URL supported
this.message = "download type \"" + scheme + "\" not supported";
} else if (DownloadType.UNSUPPORTED.equals(dtype)) {
this.message = "mimetype not supported: " + mimetype;
} else if (!Environment.MEDIA_MOUNTED.equals(storageState)) {
this.message = "storage not available for writing (state: " + storageState + ")";
} else if (DownloadType.IMAGE_PNG.equals(dtype)) {
final File targetDir = new File(Environment.getExternalStorageDirectory()
+ "/Pictures/Screenshots");
if (targetName == null) {
// default filename
targetName = "stendhal_" + new SimpleDateFormat("yyyy-MM-dd_HH.mm.ss").format(new Date())
+ ".png";
}
Logger.debug("Saving screenshot: " + targetDir.getPath()
+ "/" + targetName + " (" + mimetype + ")");
final byte[] data = Base64.decode(url.split("base64,")[1], Base64.DEFAULT);
downloadInternal(targetDir, targetName, data);
if (this.result) {
this.message = "saved screenshot to " + targetDir.getPath() + "/" + targetName;
}
} else if (DownloadType.TEXT_PLAIN.equals(dtype)) {
// `Environment.DIRECTORY_DOWNLOADS` fails on newer Android versions
//final File targetDir = new File(Environment.DIRECTORY_DOWNLOADS);
final File targetDir = new File(Environment.getExternalStorageDirectory() + "/Download");
if (targetName == null) {
// default filename
targetName = "stendhal_" + new SimpleDateFormat("yyyy-MM-dd_HH.mm.ss").format(new Date())
+ ".txt";
}
Logger.debug("Saving file: " + targetDir.getPath()
+ "/" + targetName + " (" + mimetype + ")");
final String[] parts = url.split(",");
byte[] data;
if (parts[0].endsWith(";base64")) {
data = Base64.decode(parts[1], Base64.DEFAULT);
} else {
try {
data = URLDecoder.decode(parts[1], StandardCharsets.UTF_8.name())
.getBytes(StandardCharsets.UTF_8);
} catch (final java.io.UnsupportedEncodingException e) {
this.message = "an error occurred while attempting to decode data URL (see debug log for more info)";
Logger.error(stackTraceToString(e));
return;
}
}
downloadInternal(targetDir, targetName, data);
if (this.result) {
this.message = "file saved to " + targetDir.getPath() + "/" + targetName;
}
} else {
this.message = "an unknown error occurred";
}
}
/**
* Writes file data to device storage.
*
* @param url
* A data URL.
* @param mimetype
* Detected MIME type.
*/
public void download(final String url, final String mimetype) {
download(url, mimetype, null);
}
public boolean getResult() {
return result;
}
public String getMessage() {
return message;
}
/**
* Converts an exception stacktrace to string.
*
* @param e
* The error with stacktrace information.
* @return
* String formatted stacktrace.
*/
private String stackTraceToString(final Throwable e) {
final StringBuilder sb = new StringBuilder();
sb.append(e.toString());
for (final StackTraceElement ste: e.getStackTrace()) {
if (sb.length() > 0) {
sb.append("\n");
}
sb.append(ste.toString());
}
return sb.toString();
}
}

View file

@ -1,39 +0,0 @@
/***************************************************************************
* Copyright © 2024 - Faiumoni e. V. *
***************************************************************************
***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
package org.stendhalgame.client;
/**
* Logger verbosity levels.
*/
public enum LogLevel {
// available verbosity levels
INFO("INFO"),
WARN("WARN"),
ERROR("ERROR"),
DEBUG("DEBUG");
/** String identifier. */
public final String label;
/**
* Creates a new logging verbosity level.
*
* @param label
* String identifier.
*/
private LogLevel(final String label) {
this.label = label;
}
}

View file

@ -1,282 +0,0 @@
/***************************************************************************
* Copyright © 2022-2024 - Faiumoni e. V. *
***************************************************************************
***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
package org.stendhalgame.client;
import java.util.LinkedList;
import java.util.List;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
import android.os.Bundle;
import android.view.ViewGroup;
import androidx.appcompat.app.AppCompatActivity;
/**
* Main app activity.
*/
public class MainActivity extends AppCompatActivity {
/** Menu instance. */
private Menu menu;
/** Active clients. **/
private ViewGroup clientList;
/** Static activity instance. */
private static MainActivity instance;
/**
* Retrieves activity instance.
*
* NOTE: This may not be necessary if Android has built-in methods to retrieve current activity.
*/
public static MainActivity get() {
return MainActivity.instance;
}
/**
* Called when main activity is created.
*/
@Override
protected void onCreate(final Bundle savedInstanceState) {
try {
super.onCreate(savedInstanceState);
// FIXME: may be considered unsafe as this is not technically a singleton
MainActivity.instance = this;
// initialize debug logging mechanism
Logger.init(getExternalFilesDir(null));
setContentView(R.layout.activity_main);
clientList = findViewById(R.id.clientList);
createClientView();
// NOTE: client view instance must be created before initializing menu
menu = Menu.get();
updateOrientation();
} catch (final Exception e) {
// TODO: add option to save to file or copy to clipboard the error
e.printStackTrace();
Logger.error(e.toString());
Logger.error("// -- //");
final StringBuilder sb = new StringBuilder();
for (final StackTraceElement ste: e.getStackTrace()) {
final String traceLine = ste.toString();
if (sb.length() > 0) {
sb.append("\n");
}
sb.append(traceLine);
Logger.error(traceLine);
}
Logger.error("// -- //");
Notifier.showPrompt(
"An unhandled exception has occurred: \"" + e.getMessage() + "\""
+ "\n\nYou can report this error at: https://stendhalgame.org/development/bug.html"
+ "\n\nStack trace:\n" + sb.toString(),
new Notifier.Action() {
@Override
protected void onCall() {
finish();
}
});
}
}
/**
* Handles toggling menu when the system "back" button is pressed.
*/
@Override
public void onBackPressed() {
menu.toggleVisibility();
}
/**
* Creates view to host a client instance.
*/
private void createClientView() {
final ClientView clientView = new ClientView(this);
setActiveClientView(clientView);
clientList.addView(clientView);
clientView.loadTitleScreen();
// show splash when a new view is created
SplashUtil.get().setVisible(true);
}
/**
* Sets client view to be shown and hides all others.
*
* @param clientView
* The `ClientView` instance to be visible.
*/
private void setActiveClientView(final ClientView clientView) {
for (int idx = 0; idx < clientList.getChildCount(); idx++) {
((ClientView) clientList.getChildAt(idx)).setActive(false);
}
clientView.setActive(true);
}
/**
* Sets client view to be shown and hides all others.
*
* @param clientIndex
* The index of `ClientView` instance to be visible.
*/
private void setActiveClientView(final int clientIndex) {
if (clientIndex < 0 || clientIndex >= clientList.getChildCount()) {
Logger.error(true, "Tried to access invalid client index: " + clientIndex);
return;
}
setActiveClientView((ClientView) clientList.getChildAt(clientIndex));
}
/**
* Retrieves active client view.
*
* @return
* `ClientView` instance that is visible.
*/
public ClientView getActiveClientView() {
for (int idx = 0; idx < clientList.getChildCount(); idx++) {
final ClientView clientView = (ClientView) clientList.getChildAt(idx);
if (clientView.isActive()) {
return clientView;
}
}
// default to first client view
return (ClientView) clientList.getChildAt(0);
}
/**
* Retrieves list of available client views.
*
* @return
* List containing all created `ClientView` instances.
*/
public List<ClientView> getClientViewList() {
final List<ClientView> available = new LinkedList<>();
for (int idx = 0; idx < clientList.getChildCount(); idx++) {
available.add((ClientView) clientList.getChildAt(idx));
}
return available;
}
/**
* Attempts to connect to client host.
*/
public void loadLogin() {
getActiveClientView().loadLogin();
}
/**
* Retrieves app orientation.
*
* @return
* One of `Configuration.ORIENTATION_PORTRAIT` (1), `Configuration.ORIENTATION_LANDSCAPE` (2),
* or `Configuration.ORIENTATION_UNDEFINED` (0).
*/
public int getOrientation() {
return getResources().getConfiguration().orientation;
}
/**
* Sets screen orientation to user setting or locks in landscape or portrait.
*/
public void updateOrientation() {
final String value = PreferencesActivity.getString("orientation");
int orient = ActivityInfo.SCREEN_ORIENTATION_USER;
switch (value) {
case "landscape":
orient = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE;
break;
case "portrait":
orient = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
break;
}
// set orientation for main & sub activities
setRequestedOrientation(orient);
final PreferencesActivity preferencesActivity = PreferencesActivity.get();
if (preferencesActivity != null) {
preferencesActivity.setRequestedOrientation(orient);
}
}
/**
* Opens a dialog to confirm exiting activity.
*/
public void onRequestQuit() {
final AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setMessage("Quit Stendhal?");
builder.setPositiveButton("Yes", new DialogInterface.OnClickListener() {
@Override
public void onClick(final DialogInterface dialog, final int id) {
finish();
android.os.Process.killProcess(android.os.Process.myPid());
}
});
builder.setNegativeButton("No", new DialogInterface.OnClickListener() {
@Override
public void onClick(final DialogInterface dialog, final int id) {
dialog.cancel();
}
});
final AlertDialog confirmQuit = builder.create();
confirmQuit.show();
}
//~ @Override
//~ protected void onResume() {
//~ super.onResume();
//~ }
/**
* Listens for changes to orientation.
*/
@Override
public void onConfigurationChanged(final Configuration config) {
super.onConfigurationChanged(config);
final ClientView clientView = getActiveClientView();
if (clientView != null && PageId.TITLE.equals(clientView.getCurrentPageId())) {
SplashUtil.get().update();
}
}
/**
* Called when main activity ends.
*/
@Override
public void finish() {
Logger.debug(MainActivity.class.getName() + ".finish() called");
super.finish();
}
/**
* Called when main activity is destroyed.
*/
@Override
protected void onDestroy() {
Logger.debug(MainActivity.class.getName() + ".onDestroy() called");
super.onDestroy();
}
/**
* Creates preferences activity.
*/
public void showSettings() {
startActivity(new Intent(this, PreferencesActivity.class));
}
}

View file

@ -1,32 +0,0 @@
/***************************************************************************
* Copyright © 2024 - Faiumoni e. V. *
***************************************************************************
***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
package org.stendhalgame.client;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
public class OpenAppByUrl extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent intent = getIntent();
// String action = intent.getAction();
Uri data = intent.getData();
Logger.debug("URL: " + data);
MainActivity.get().getActiveClientView().checkLoginIntent(intent);
finish();
}
}

View file

@ -1,90 +0,0 @@
/***************************************************************************
* Copyright © 2024 - Faiumoni e. V. *
***************************************************************************
***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
package org.stendhalgame.client;
import android.content.res.Configuration;
import android.widget.ImageView;
public class SplashUtil {
/** Image used as title page background. */
private final ImageView splash;
/** Singleton instance. */
private static SplashUtil instance;
/**
* Retrieves singleton instance.
*/
public static SplashUtil get() {
if (SplashUtil.instance == null) {
SplashUtil.instance = new SplashUtil();
}
return SplashUtil.instance;
}
/**
* Hidden singleton constructor.
*/
private SplashUtil() {
splash = (ImageView) MainActivity.get().findViewById(R.id.splash);
setVisible(false);
}
/**
* Sets the background splash image.
*
* @param resId
* Resource ID.
* @param bgColor
* Coloring to fill behind splash image.
*/
private void setImage(final int resId, final int bgColor) {
splash.setBackgroundColor(bgColor);
splash.setImageResource(resId);
}
/**
* Sets splash image dependent on device orientation.
*/
public void update() {
if (!isVisible()) {
return;
}
int resId = R.drawable.splash;
if (MainActivity.get().getOrientation() == Configuration.ORIENTATION_PORTRAIT) {
resId = R.drawable.splash_portrait;
}
// light blue background color
setImage(resId, 0xff6c9ed1);
}
/**
* Shows or hides background splash image.
*
* @param visible
* `true` if image should visible, `false` if not.
*/
public void setVisible(final boolean visible) {
splash.setVisibility(visible ? ImageView.VISIBLE : ImageView.GONE);
update();
}
/**
* Checks if splash is visible.
*/
public boolean isVisible() {
return ImageView.VISIBLE == splash.getVisibility();
}
}

View file

@ -1,280 +0,0 @@
/***************************************************************************
* Copyright © 2024 - Faiumoni e. V. *
***************************************************************************
***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
package org.stendhalgame.client;
import android.net.Uri;
class UrlHelper {
private static final String defaultServer = "https://stendhalgame.org/";
private UrlHelper() {
// static methods only
}
/**
* Creates a URI from a string HTTP URL.
*
* @return
* `android.net.Uri`.
*/
public static Uri toUri(String url) {
if (!url.startsWith("https://") && !url.startsWith("http://") && !url.startsWith("about:")) {
// Uri.getHost returns `null` if localhost not prefixed with protocol
url = "http://" + url;
}
return Uri.parse(url);
}
/**
* Retrieves the HTTP URL of the default server (stendhalgame.org).
*
* @return
* String URL.
*/
public static String getDefaultServer() {
return UrlHelper.defaultServer;
}
/**
* Retrieves the URI of the default server (stendhalgame.org).
*
* @return
* `android.net.Uri` of default server.
*/
public static Uri getDefaultServerUri() {
return UrlHelper.toUri(UrlHelper.defaultServer);
}
/**
* Retrieves the default host.
*
* @return
* Host portion of the default URI.
*/
public static String getDefaultHost() {
return UrlHelper.getDefaultServerUri().getHost();
}
/**
* Removes protocol & "www" prefixes from a URL.
*
* FIXME: rename to "trimProtocol"
*
* @return
* Trimmed URL string.
*/
public static String stripHost(final String url) {
if (url == null) {
return "";
}
return url.replaceAll("^https://", "").replaceAll("^http://", "")
.replaceAll("^www\\.", "");
}
/**
* Extracts character name from URL fragment identifier & converts to query string.
*
* Deprecated. Should use `UrlHelper.formatCharName(Uri, Uri.Builder).
*
* @param url
* HTTP string to be formatted.
* @return
* Formatted URL.
*/
@Deprecated
public static String formatCharName(String url) {
final int idx = url.indexOf("#");
if (idx > -1) {
url = url.substring(0, idx) + "?char=" + url.substring(idx+1);
}
return url;
}
/**
* Converts URL fragment identifier to character name query string parameter.
*
* @param uri
* URI being checked for fragment identifier.
* @param builder
* URI builder to update.
*/
public static void formatCharName(final Uri uri, final Uri.Builder builder) {
final String name = uri.getFragment();
if (name != null) {
builder.appendQueryParameter("char", name);
}
}
/**
* Formats client URL for currently selected server.
*
* @param url
* URL to be checked.
* @return
* URL to be loaded.
*/
public static String checkClientUrl(String url) {
final Uri uri = UrlHelper.toUri(url);
final Uri.Builder builder = uri.buildUpon();
if (UrlHelper.isClientUrl(url)) {
final ClientView client = MainActivity.get().getActiveClientView();
final boolean testClient = client.isTestClient();
String replaceSuffix = "/testclient/";
if (testClient) {
replaceSuffix = "/client/";
}
// ensure website directs to configured client
builder.path(uri.getPath().replace(replaceSuffix, "/" + client.getClientUrlSuffix() + "/"));
UrlHelper.formatCharName(uri, builder);
if (testClient && !client.isTestServer() && uri.getQueryParameter("server") == null) {
// connect test client to main server
builder.appendQueryParameter("server", "main");
}
}
return builder.toString();
}
/**
* Checks if a URL is a link to one of the web clients.
*
* @param url
* HTTP URL to be checked.
* @return
* `true` if `url` links to "client" or "testclient".
*/
public static boolean isClientUrl(final String url) {
final String custom_client = PreferencesActivity.getString("client_url").trim();
if (!custom_client.equals("")) {
return url.contains(custom_client);
}
final String defaultHost = UrlHelper.stripHost(UrlHelper.getDefaultServerUri().getHost());
return url.contains(defaultHost + "/client/") || url.contains(defaultHost + "/testclient/");
}
/**
* Retrieves client URL.
*
* @return
* Client URL string.
*/
public static String getClientUrl() {
final String custom_client = PreferencesActivity.getString("client_url").trim();
if (!custom_client.equals("")) {
return custom_client;
}
return UrlHelper.defaultServer + "client/stendhal.html";
}
/**
* Retrieves URL string for initial page to be loaded from server.
*
* If custom client URL is used then client URL is returned.
*
* @return
* HTTP URL string.
*/
public static String getInitialPageUrl() {
final String custom_client = PreferencesActivity.getString("client_url").trim();
if (!custom_client.equals("")) {
return custom_client;
}
return UrlHelper.defaultServer + "account/mycharacters.html";
}
/**
* Checks if requested URL is whitelisted to be opened within WebView client.
*
* @param uri
* `android.net.Uri` to be checked.
* @return
* `true` if URI is under default domain (stendhalgame.org) or localhost.
*/
public static boolean isInternalUri(final Uri uri) {
if (UrlHelper.isLoginUri(uri)) {
// open login page in external browser
return false;
}
final String defaultHost = UrlHelper.stripHost(UrlHelper.getDefaultHost());
final String host = UrlHelper.stripHost(uri.getHost());
if (defaultHost.equals(host)) {
// allow pages from stendhalgame.org
return true;
}
final String cs = MainActivity.get().getActiveClientView().checkCustomServer();
if (cs != null) {
return UrlHelper.stripHost(cs).equals(host);
}
return "localhost".equals(host);
}
/**
* Checks if requested URL is whitelisted to be opened within WebView client.
*
* @param url
* HTTP URL string to be checked.
* @return
* `true` if URL is under default domain (stendhalgame.org) or localhost.
*/
public static boolean isInternal(final String url) {
return UrlHelper.isInternalUri(UrlHelper.toUri(url));
}
/**
* Checks if a URI matches the intent URL scheme.
*
* @param uri
* URI to be check.
* @return
* `true` if URI's host is the intent scheme.
*/
public static boolean isIntentUri(final Uri uri) {
return AppInfo.getIntentUrlScheme().equals(UrlHelper.stripHost(uri.getHost()));
}
/**
* Checks if a URL matches the intent URL scheme.
*
* @param url
* HTTP URL string to be check.
* @return
* `true` if URI's host is the intent scheme.
*/
public static boolean isIntentUrl(final String url) {
return UrlHelper.isIntentUri(UrlHelper.toUri(url));
}
/**
* Checks if a URI represents a login page.
*
* @param uri
* Page URI to check.
* @return
* `true` if URI path equals "/account/login.html" or `id` parameter of query string equals
* "content/account/login".
*/
public static boolean isLoginUri(final Uri uri) {
if ("/account/login.html".equals(uri.getPath())) {
return true;
}
if (!uri.isHierarchical()) {
return false;
}
final String id = uri.getQueryParameter("id");
if (id == null) {
return false;
}
return "content/account/login".equals(id);
}
}

View file

@ -1 +0,0 @@
https://sourceforge.net/projects/arianne/support

View file

@ -1 +0,0 @@
https://stendhalgame.org/

View file

@ -1,9 +0,0 @@
Stendhal has a huge and rich world. You can explore cities, forest, mountains, plains and dungeons.
You can fight monsters and become a hero.
You will meet a wide variety of characters. Many will give you tasks and quests for valuable
experience. You may be asked to help protect land, feed the hungry, heal the sick, make someone
happy, solve a puzzle or simply lend a hand.
So what are you waiting for? A whole new world awaits... And if you like, get the source code, and
add your own ideas!

Some files were not shown because too many files have changed in this diff Show more