Compare commits

...

48 Commits

Author SHA1 Message Date
OMGeeky
a4fbe20871 refactor window insets handling for fullscreen and touch area
Signed-off-by: OMGeeky <aalaalgmx@gmail.com>
2025-08-03 18:57:39 +02:00
OMGeeky
0d73ee10fa set usable padding for title logo in StartScreenActivity 2025-07-29 05:52:29 +02:00
OMGeeky
f20529df62 setSystemBarsBehavior so it disappears automatically when in fullscreen
also don't run code, that is deprecated when on higher version
2025-07-29 05:38:01 +02:00
OMGeeky
ffef76b28b continue implementing edge-to-edge behaviour and insets 2025-07-29 05:05:40 +02:00
OMGeeky
cd49473e86 continue implementing edge-to-edge behaviour and insets (Start-Screen) 2025-07-29 04:06:06 +02:00
OMGeeky
363a395169 start implementing edge-to-edge behaviour and insets 2025-07-29 03:33:10 +02:00
OMGeeky
a09176d344 Refactor activity view setup to use app.setView for consistency 2025-07-29 03:05:50 +02:00
OMGeeky
f884af20b8 Replace custom Pair implementation with android.util.Pair 2025-07-29 00:08:02 +02:00
OMGeeky
a2859fbc56 Remove unnecessary @TargetApi annotations and remove unnecessary SDK-Version check 2025-07-28 23:33:23 +02:00
OMGeeky
11d5966a51 Sort summary output of errors and warnings by file path 2025-07-28 22:00:24 +02:00
OMGeeky
fd169e9f40 Add total error and warning summary to format specifier check output 2025-07-28 21:59:19 +02:00
OMGeeky
149ad3ae1c Differentiate between missing and extra format specifiers
Missing format specifiers in translated strings are now reported as warnings,
while extra format specifiers are reported as errors.
This change allows the script to pass if only warnings (missing specifiers)
are present, but still fail if there are errors (extra specifiers).

The summary now also includes counts for both errors and warnings per file.
2025-07-28 21:57:29 +02:00
OMGeeky
6e74243ea8 Refactor: Improve format specifier regex
The regular expression for identifying format specifiers in XML string resources has been updated. The new regex now correctly identifies specifiers like `%.2f` which include precision for floating point numbers. This change ensures more accurate validation of format specifiers.
2025-07-28 21:51:59 +02:00
OMGeeky
9c4009034f Enhance format specifier check script output
The script now provides a summary of errors per file at the end of its execution.
This makes it easier to identify which translation files have issues with format specifiers or non-escaped percentage signs.
2025-07-28 21:51:31 +02:00
OMGeeky
0422beb855 Update string formatter check script
- Add scanning for used keys in Java files.
- Refactor argument parsing to separate project root and res root.
- If no specific key is provided, check only keys used in Java code.
2025-07-28 21:35:38 +02:00
OMGeeky
2cc12b3530 Improve check_format_specifiers.py to check all keys if none specified 2025-07-28 21:25:28 +02:00
OMGeeky
80997ddc36 Exit with error code on specifier mismatches in format checker 2025-07-28 21:20:17 +02:00
OMGeeky
c244ba1390 Added checks for unescaped percent signs 2025-07-28 21:12:23 +02:00
OMGeeky
8a74ffd2ef Add Python script to check format specifier consistency in Android strings
This script, `check_format_specifiers.py`, helps ensure that format
specifiers (like `%s`, `%d`, `%1$s`) are used consistently across
different language versions of a string resource in an Android project.
2025-07-28 21:01:11 +02:00
OMGeeky
57f7a0dc91 Refactor fullscreen mode setting
Move fullscreen mode setting logic into a static method in AndorsTrailApplication and use it in Preferences and CustomDialogFactory.
2025-07-28 18:06:43 +02:00
OMGeeky
78cf59c90c Updated full screen flag for API 30+ 2025-07-28 18:00:54 +02:00
Nut.andor
c4ac6b1046 whatsnew 2025-07-26 00:18:21 +02:00
Nut.andor
a5ed53a763 added graphic 2025-07-19 01:09:00 +02:00
Nut.andor
d6b11d8242 added graphic 2025-07-17 20:48:09 +02:00
Nut.andor
d9e7509daa whatsnew 2025-07-17 20:48:09 +02:00
Nut.andor
e15583e3c5 settings 2025-07-17 20:48:08 +02:00
Nut.andor
25b70ae818 typos 2025-06-24 00:00:27 +02:00
Nut.andor
1aa98de579 new png 2025-06-24 00:00:27 +02:00
Nut.andor
be027bb303 Item description could have $playername 2025-06-22 22:25:23 +02:00
Nut.andor
d2fe192466 settings 2025-06-22 22:25:23 +02:00
Nut.andor
469d2bfc46 new part of falsly extended map_cavewall_5 moved to new map_cavewall_6 2025-06-19 16:19:40 +02:00
Nut.andor
ce399ffe1f settings 2025-06-19 12:35:13 +02:00
Nut.andor
054c6e5d23 new map_outdoor_2.png 2025-06-17 22:15:54 +02:00
Nut.andor
e4633c0689 exploding star item icon 2025-06-10 21:51:38 +02:00
Nut.andor
9b9d8a7c3a dbg 2025-06-10 21:51:38 +02:00
Nut.andor
0cfa6a97c9 Merge branch 'pulls/1829009049/90' into AT_Source_Next_Release 2025-06-06 22:10:10 +02:00
Nut.andor
9729d5a34f On miss effects 2025-06-06 22:09:19 +02:00
Nut.andor
c024474128 Merge branch 'pulls/1829009049/86' into AT_Source_Next_Release 2025-06-06 22:07:13 +02:00
Nut.andor
3100ffd717 Anti cheat checksum 2025-06-06 21:56:42 +02:00
Nut.andor
e3c8c0ecfb useful links 2025-05-11 18:10:32 +02:00
Nut.andor
cd35ad673f next version v0.8.14dev 2025-05-11 11:12:25 +02:00
Nut.andor
5f000ef210 Merge branch 'master' into AT_Source_Next_Release 2025-05-11 11:10:27 +02:00
Nut.andor
2078eedb94 Pull Request #90: On miss effects 2025-05-02 22:26:37 +02:00
OMGeeky
6f2ba0fe7a naming & accidental change 2025-05-02 22:25:09 +02:00
OMGeeky
06f1a791e3 implement on-miss and on-miss-received item effects
this should allow for something like a shield, that applies an actor condition to the attacker or the player, when it successfully blocks an attack (like a generic taunt for items), but also, when the player misses with his weapon, he could get an effect (for example something like embarrassed if we stay in the direction taunt is going)
2025-05-02 22:22:36 +02:00
Nut.andor
b341d7e6f1 Pull Request #86: Anti cheat checksum 2025-02-25 15:32:58 +01:00
OMGeeky
31f25a963f fix whitespace formatting
Signed-off-by: OMGeeky <aalaalgmx@gmail.com>
2025-02-25 15:28:52 +01:00
OMGeeky
30030031b2 add checksum to savegame to track if the savegame has been modified
Signed-off-by: OMGeeky <aalaalgmx@gmail.com>
2025-02-24 21:04:22 +01:00
84 changed files with 1233 additions and 278 deletions

View File

@@ -2,12 +2,12 @@ apply plugin: 'com.android.application'
android {
compileSdkVersion 34
compileSdkVersion 35
defaultConfig {
applicationId "com.gpl.rpg.AndorsTrail"
minSdkVersion 14
targetSdkVersion 34
targetSdkVersion 35
}
buildTypes {

View File

@@ -3,8 +3,8 @@
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="com.gpl.rpg.AndorsTrail"
android:versionCode="80"
android:versionName="0.8.13"
android:versionCode="81"
android:versionName="0.8.14dev"
android:installLocation="auto"
>

View File

@@ -8,31 +8,39 @@ import com.gpl.rpg.AndorsTrail.context.ControllerContext;
import com.gpl.rpg.AndorsTrail.context.WorldContext;
import com.gpl.rpg.AndorsTrail.controller.Constants;
import com.gpl.rpg.AndorsTrail.util.AndroidStorage;
import com.gpl.rpg.AndorsTrail.util.Pair;
import android.app.Activity;
import android.app.Application;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Insets;
import android.os.Build;
import android.os.Environment;
import android.util.Pair;
import android.view.View;
import android.view.Window;
import android.view.WindowInsets;
import android.view.WindowInsetsController;
import android.view.WindowManager;
import androidx.annotation.RequiresApi;
public final class AndorsTrailApplication extends Application {
public static final boolean DEVELOPMENT_DEBUGRESOURCES = false;
public static final boolean DEVELOPMENT_FORCE_STARTNEWGAME = false;
public static final boolean DEVELOPMENT_FORCE_CONTINUEGAME = false;
public static final boolean DEVELOPMENT_DEBUGBUTTONS = false;
public static final boolean DEVELOPMENT_DEBUGBUTTONS = true;
public static final boolean DEVELOPMENT_FASTSPEED = false;
public static final boolean DEVELOPMENT_VALIDATEDATA = false;
public static final boolean DEVELOPMENT_DEBUGMESSAGES = false;
public static final String CURRENT_VERSION_DISPLAY = "0.8.13";
public static final boolean DEVELOPMENT_DEBUGMESSAGES = true;
public static final String CURRENT_VERSION_DISPLAY = "0.8.14dev";
public static final boolean IS_RELEASE_VERSION = !CURRENT_VERSION_DISPLAY.matches(".*[a-d].*");
public static final boolean DEVELOPMENT_INCOMPATIBLE_SAVEGAMES = DEVELOPMENT_DEBUGRESOURCES || DEVELOPMENT_DEBUGBUTTONS || DEVELOPMENT_FASTSPEED || !IS_RELEASE_VERSION;
public static final int DEVELOPMENT_INCOMPATIBLE_SAVEGAME_VERSION = 999;
public static final int CURRENT_VERSION = DEVELOPMENT_INCOMPATIBLE_SAVEGAMES ? DEVELOPMENT_INCOMPATIBLE_SAVEGAME_VERSION : 80;
public static final int CURRENT_VERSION = DEVELOPMENT_INCOMPATIBLE_SAVEGAMES ? DEVELOPMENT_INCOMPATIBLE_SAVEGAME_VERSION : 81;
private final AndorsTrailPreferences preferences = new AndorsTrailPreferences();
private WorldContext world = new WorldContext();
@@ -57,13 +65,45 @@ public final class AndorsTrailApplication extends Application {
public void setWindowParameters(Activity activity) {
activity.requestWindowFeature(Window.FEATURE_NO_TITLE);
if (preferences.fullscreen) {
activity.getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
}
public void setFullscreenMode(Activity activity) {
setFullscreenMode(preferences.fullscreen, activity.getWindow());
}
public static void setFullscreenMode(boolean fullscreen, Window window) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
final WindowInsetsController insetsController = window.getInsetsController();
if (insetsController != null) {
insetsController.setSystemBarsBehavior(WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
int insetType = WindowInsets.Type.statusBars();
if (fullscreen) {
insetsController.hide(insetType);
} else {
insetsController.show(insetType);
}
}
} else {
activity.getWindow().setFlags(0, WindowManager.LayoutParams.FLAG_FULLSCREEN);
if (fullscreen) {
window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
} else {
window.setFlags(0, WindowManager.LayoutParams.FLAG_FULLSCREEN);
}
}
}
@RequiresApi(Build.VERSION_CODES.R)
public int getUsableTouchAreaInsetMask(){
int i = 0;
i |= WindowInsets.Type.displayCutout();
i |= WindowInsets.Type.navigationBars();
if (!preferences.fullscreen) {
i |= WindowInsets.Type.statusBars();
}
return i;
}
//Get default locale at startup, as somehow it seems that changing the app's
//configured locale impacts the value returned by Locale.getDefault() nowadays.
private final Locale defaultLocale = Locale.getDefault();
@@ -166,4 +206,18 @@ public final class AndorsTrailApplication extends Application {
controllers = new ControllerContext(this, world);
setup = new WorldSetup(world, controllers, getApplicationContext());
}
public void setUsablePadding(View root) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
root.setOnApplyWindowInsetsListener((v, insets) -> {
Insets bars = insets.getInsets(getUsableTouchAreaInsetMask());
int left = Math.max(bars.left, v.getPaddingLeft());
int top = Math.max(bars.top, v.getPaddingTop());
int right = Math.max(bars.right, v.getPaddingRight());
int bottom = Math.max(bars.bottom, v.getPaddingBottom());
v.setPadding(left, top, right, bottom);
return WindowInsets.CONSUMED;
});
}
}
}

View File

@@ -346,7 +346,6 @@ public final class Dialogs {
CustomDialogFactory.show(d);
}
@TargetApi(23)
private static boolean hasPermissions(final Activity activity) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (activity.getApplicationContext().checkSelfPermission(android.Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED

View File

@@ -24,9 +24,7 @@ public final class AboutActivity extends AndorsTrailBaseActivity implements Imag
super.onCreate(savedInstanceState);
AndorsTrailApplication app = AndorsTrailApplication.getApplicationFromActivity(this);
app.setWindowParameters(this);
setContentView(R.layout.about);
initializeView(this, R.layout.about, R.id.about_root);
final Resources res = getResources();
final TextView tv = (TextView) findViewById(R.id.about_contents);

View File

@@ -30,9 +30,7 @@ public final class ActorConditionInfoActivity extends AndorsTrailBaseActivity {
String conditionTypeID = getIntent().getData().getLastPathSegment();
ActorConditionType conditionType = world.actorConditionsTypes.getActorConditionType(conditionTypeID);
setContentView(R.layout.actorconditioninfo);
initializeView(this, R.layout.actorconditioninfo, R.id.actorconditioninfo_root);
TextView tv = (TextView) findViewById(R.id.actorconditioninfo_title);
tv.setText(conditionType.name);

View File

@@ -2,6 +2,10 @@ package com.gpl.rpg.AndorsTrail.activity;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import androidx.annotation.IdRes;
import androidx.annotation.LayoutRes;
import com.gpl.rpg.AndorsTrail.AndorsTrailApplication;
@@ -19,5 +23,13 @@ public abstract class AndorsTrailBaseActivity extends Activity {
AndorsTrailApplication app = AndorsTrailApplication.getApplicationFromActivity(this);
app.setLocale(this);
}
protected void initializeView(Activity activity, @LayoutRes int layoutId, @IdRes int rootViewId) {
AndorsTrailApplication app = AndorsTrailApplication.getApplicationFromActivity(activity);
app.setWindowParameters(activity);
activity.setContentView(layoutId);
View root = activity.findViewById(rootViewId);
app.setUsablePadding(root);
app.setFullscreenMode(activity);
}
}

View File

@@ -1,6 +1,11 @@
package com.gpl.rpg.AndorsTrail.activity;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import androidx.annotation.IdRes;
import androidx.annotation.LayoutRes;
import androidx.fragment.app.FragmentActivity;
import com.gpl.rpg.AndorsTrail.AndorsTrailApplication;
@@ -19,4 +24,13 @@ public abstract class AndorsTrailBaseFragmentActivity extends FragmentActivity {
AndorsTrailApplication app = AndorsTrailApplication.getApplicationFromActivity(this);
app.setLocale(this);
}
protected void initializeView(Activity activity, @LayoutRes int layoutId, @IdRes int rootViewId) {
AndorsTrailApplication app = AndorsTrailApplication.getApplicationFromActivity(activity);
app.setWindowParameters(activity);
activity.setContentView(layoutId);
View root = activity.findViewById(rootViewId);
app.setUsablePadding(root);
app.setFullscreenMode(activity);
}
}

View File

@@ -89,8 +89,7 @@ public final class BulkSelectionInterface extends AndorsTrailBaseActivity implem
interfaceType = BulkInterfaceType.valueOf(params.getString("interfaceType"));
int intialSelection = 1;
setContentView(R.layout.bulkselection);
initializeView(this, R.layout.bulkselection, R.id.bulkselection_root);
// initialize UI variables
TextView bulkselection_action_type = (TextView)findViewById(R.id.bulkselection_action_type);

View File

@@ -75,13 +75,11 @@ public final class ConversationActivity
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.conversation);
initializeView(this, R.layout.conversation, R.id.conversation_root);
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
setFinishOnTouchOutside(false);
}
setFinishOnTouchOutside(false);
replyGroup = new RadioGroup(this);
replyGroup = new RadioGroup(this);
replyGroup.setLayoutParams(new ListView.LayoutParams(ListView.LayoutParams.MATCH_PARENT, ListView.LayoutParams.WRAP_CONTENT));
statementList = (ListView) findViewById(R.id.conversation_statements);
statementList.addFooterView(replyGroup);

View File

@@ -365,7 +365,7 @@ public final class DebugInterface {
,new DebugButton("#2", new OnClickListener() {
@Override
public void onClick(View arg0) {
controllerContext.movementController.placePlayerAsyncAt(MapObject.MapObjectType.newmap, "crackshot_hideout3", "South", 0, 0);
controllerContext.movementController.placePlayerAsyncAt(MapObject.MapObjectType.newmap, "waytolake12", "tower_door", 0, 0);
}
})
@@ -373,21 +373,21 @@ public final class DebugInterface {
,new DebugButton("#3", new OnClickListener() {
@Override
public void onClick(View arg0) {
controllerContext.movementController.placePlayerAsyncAt(MapObject.MapObjectType.newmap, "fallhaven_derelict2", "up", 0, 0);
controllerContext.movementController.placePlayerAsyncAt(MapObject.MapObjectType.newmap, "galmore_cavea", "up", 0, 0);
}
})
,new DebugButton("#4", new OnClickListener() {
@Override
public void onClick(View arg0) {
controllerContext.movementController.placePlayerAsyncAt(MapObject.MapObjectType.newmap, "loneford4", "south", 0, 0);
controllerContext.movementController.placePlayerAsyncAt(MapObject.MapObjectType.newmap, "galmore_35", "south", 11, 0);
}
})
,new DebugButton("#5", new OnClickListener() {
@Override
public void onClick(View arg0) {
controllerContext.movementController.placePlayerAsyncAt(MapObject.MapObjectType.newmap, "waytogalmore1", "entrance2", 0, 0);
controllerContext.movementController.placePlayerAsyncAt(MapObject.MapObjectType.newmap, "way_to_sullengard_west_5", "east", 0, 5);
}
})
@@ -408,7 +408,7 @@ public final class DebugInterface {
,new DebugButton("#8", new OnClickListener() {
@Override
public void onClick(View arg0) {
controllerContext.movementController.placePlayerAsyncAt(MapObject.MapObjectType.newmap, "galmore_86", "west", 0, 5);
controllerContext.movementController.placePlayerAsyncAt(MapObject.MapObjectType.newmap, "wayto_feygard_duleian_2", "south", 0, 5);
}
})
@@ -422,7 +422,28 @@ public final class DebugInterface {
,new DebugButton("#10", new OnClickListener() {
@Override
public void onClick(View arg0) {
controllerContext.movementController.placePlayerAsyncAt(MapObject.MapObjectType.newmap, "laerothprison3", "south", 2, 0);
controllerContext.movementController.placePlayerAsyncAt(MapObject.MapObjectType.newmap, "moesforest_03", "north1", 0, 0);
}
})
,new DebugButton("#11", new OnClickListener() {
@Override
public void onClick(View arg0) {
controllerContext.movementController.placePlayerAsyncAt(MapObject.MapObjectType.newmap, "galmore_17", "outside", 0, 0);
}
})
,new DebugButton("#12", new OnClickListener() {
@Override
public void onClick(View arg0) {
controllerContext.movementController.placePlayerAsyncAt(MapObject.MapObjectType.newmap, "galmore_48", "west", 0, 10);
}
})
,new DebugButton("#13", new OnClickListener() {
@Override
public void onClick(View arg0) {
controllerContext.movementController.placePlayerAsyncAt(MapObject.MapObjectType.newmap, "wild18", "south", 6, 0);
}
})

View File

@@ -38,10 +38,7 @@ public final class DisplayWorldMapActivity extends AndorsTrailBaseActivity {
AndorsTrailApplication app = AndorsTrailApplication.getApplicationFromActivity(this);
if (!app.isInitialized()) { finish(); return; }
this.world = app.getWorld();
app.setWindowParameters(this);
setContentView(R.layout.displayworldmap);
initializeView(this, R.layout.displayworldmap, R.id.worldmap_root);
displayworldmap_webview = (WebView) findViewById(R.id.displayworldmap_webview);
displayworldmap_webview.setBackgroundColor(ThemeHelper.getThemeColor(this, R.attr.ui_theme_displayworldmap_bg_color));

View File

@@ -29,10 +29,7 @@ public final class HeroinfoActivity extends AndorsTrailBaseFragmentActivity {
AndorsTrailApplication app = AndorsTrailApplication.getApplicationFromActivity(this);
if (!app.isInitialized()) { finish(); return; }
this.world = app.getWorld();
app.setWindowParameters(this);
setContentView(R.layout.tabbedlayout);
initializeView(this, R.layout.tabbedlayout, android.R.id.tabhost);
Resources res = getResources();

View File

@@ -14,6 +14,7 @@ import android.widget.TextView;
import com.gpl.rpg.AndorsTrail.AndorsTrailApplication;
import com.gpl.rpg.AndorsTrail.R;
import com.gpl.rpg.AndorsTrail.context.WorldContext;
import com.gpl.rpg.AndorsTrail.controller.Constants;
import com.gpl.rpg.AndorsTrail.model.item.ItemType;
import com.gpl.rpg.AndorsTrail.util.ThemeHelper;
import com.gpl.rpg.AndorsTrail.view.ItemEffectsView;
@@ -45,7 +46,7 @@ public final class ItemInfoActivity extends AndorsTrailBaseActivity {
boolean buttonEnabled = params.getBoolean("buttonEnabled");
boolean moreButtonEnabled = params.getBoolean("moreActions");
setContentView(R.layout.iteminfo);
initializeView(this, R.layout.iteminfo, R.id.iteminfo_root);
TextView tv = (TextView) findViewById(R.id.iteminfo_title);
tv.setText(itemType.getName(world.model.player));
@@ -54,7 +55,7 @@ public final class ItemInfoActivity extends AndorsTrailBaseActivity {
tv = (TextView) findViewById(R.id.iteminfo_description);
String description = itemType.getDescription();
if (description != null) {
tv.setText(description);
tv.setText(description.replace(Constants.PLACEHOLDER_PLAYERNAME, world.model.player.getName()));
tv.setVisibility(View.VISIBLE);
} else {
tv.setVisibility(View.GONE);
@@ -67,8 +68,10 @@ public final class ItemInfoActivity extends AndorsTrailBaseActivity {
itemType.effects_equip,
itemType.effects_use == null ? null : Collections.singletonList(itemType.effects_use),
itemType.effects_hit == null ? null : Collections.singletonList(itemType.effects_hit),
itemType.effects_miss == null ? null : Collections.singletonList(itemType.effects_miss),
itemType.effects_kill == null ? null : Collections.singletonList(itemType.effects_kill),
itemType.effects_hitReceived == null ? null : Collections.singletonList(itemType.effects_hitReceived),
itemType.effects_missReceived == null ? null : Collections.singletonList(itemType.effects_missReceived),
null,
itemType.isWeapon()
);

View File

@@ -37,7 +37,7 @@ public final class LevelUpActivity extends AndorsTrailBaseActivity {
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.levelup);
initializeView(this, R.layout.levelup, R.id.levelup_root);
levelup_title = (TextView) findViewById(R.id.levelup_title);
levelup_description = (TextView) findViewById(R.id.levelup_description);

View File

@@ -70,7 +70,7 @@ public final class LoadSaveActivity extends AndorsTrailBaseActivity implements O
String loadsave = getIntent().getData().getLastPathSegment();
isLoading = (loadsave.equalsIgnoreCase("load"));
setContentView(R.layout.loadsave);
initializeView(this, R.layout.loadsave, R.id.loadsave_root);
TextView tv = (TextView) findViewById(R.id.loadsave_title);
if (isLoading) {

View File

@@ -36,8 +36,7 @@ public final class LoadingActivity extends AndorsTrailBaseActivity implements On
setTheme(ThemeHelper.getBaseTheme());
super.onCreate(savedInstanceState);
AndorsTrailApplication app = AndorsTrailApplication.getApplicationFromActivity(this);
app.setWindowParameters(this);
setContentView(R.layout.startscreen);
initializeView(this, R.layout.startscreen, R.id.startscreen_fragment_container);
TextView tv = (TextView) findViewById(R.id.startscreen_version);
tv.setVisibility(View.GONE);

View File

@@ -91,9 +91,9 @@ public final class MainActivity
AndorsTrailPreferences preferences = app.getPreferences();
this.world = app.getWorld();
this.controllers = app.getControllerContext();
app.setWindowParameters(this);
setContentView(R.layout.main);
initializeView(this, R.layout.main, R.id.main_container);
mainview = (MainView) findViewById(R.id.main_mainview);
statusview = (StatusView) findViewById(R.id.main_statusview);
combatview = (CombatView) findViewById(R.id.main_combatview);

View File

@@ -34,7 +34,7 @@ public final class MonsterEncounterActivity extends AndorsTrailBaseActivity {
return;
}
setContentView(R.layout.monsterencounter);
initializeView(this, R.layout.monsterencounter, R.id.monsterencounter_root);
CharSequence difficulty = getText(MonsterInfoActivity.getMonsterDifficultyResource(controllers, monster));

View File

@@ -43,7 +43,7 @@ public final class MonsterInfoActivity extends AndorsTrailBaseActivity {
this.controllers = app.getControllerContext();
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.monsterinfo);
initializeView(this, R.layout.monsterinfo, R.id.monsterinfo_root);
monsterinfo_title = (TextView) findViewById(R.id.monsterinfo_title);
monsterinfo_difficulty = (TextView) findViewById(R.id.monsterinfo_difficulty);
@@ -90,8 +90,10 @@ public final class MonsterInfoActivity extends AndorsTrailBaseActivity {
null,
null,
monster.getOnHitEffectsAsList(),
monster.getOnMissEffectsAsList(),
null,
monster.getOnHitReceivedEffectsAsList(),
monster.getOnMissReceivedEffectsAsList(),
monster.getOnDeathEffects(),
false);
hp.update(monster.getMaxHP(), monster.getCurrentHP());

View File

@@ -13,17 +13,16 @@ public final class Preferences extends PreferenceActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
setTheme(ThemeHelper.getBaseTheme());
requestWindowFeature(Window.FEATURE_NO_TITLE);
super.onCreate(savedInstanceState);
AndorsTrailApplication app = AndorsTrailApplication.getApplicationFromActivity(this);
if (app.getPreferences().fullscreen) {
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
} else {
getWindow().setFlags(0, WindowManager.LayoutParams.FLAG_FULLSCREEN);
}
app.setWindowParameters(this);
super.onCreate(savedInstanceState);
app.setFullscreenMode(this);
app.setLocale(this);
addPreferencesFromResource(R.xml.preferences);
}
@Override

View File

@@ -23,9 +23,8 @@ public final class ShopActivity extends AndorsTrailBaseFragmentActivity {
AndorsTrailApplication app = AndorsTrailApplication.getApplicationFromActivity(this);
if (!app.isInitialized()) { finish(); return; }
app.setWindowParameters(this);
setContentView(R.layout.tabbedlayout);
initializeView(this, R.layout.tabbedlayout, android.R.id.tabhost);
final Resources res = getResources();

View File

@@ -31,9 +31,7 @@ public final class SkillInfoActivity extends AndorsTrailBaseActivity {
final WorldContext world = app.getWorld();
final Player player = world.model.player;
app.setWindowParameters(this);
setContentView(R.layout.skill_info_view);
initializeView(this, R.layout.skill_info_view, R.id.skillinfo_root);
final Resources res = getResources();
final Intent intent = getIntent();

View File

@@ -21,6 +21,8 @@ import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.graphics.drawable.AnimationDrawable;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager.OnBackStackChangedListener;
import android.view.KeyEvent;
@@ -49,9 +51,9 @@ public final class StartScreenActivity extends AndorsTrailBaseFragmentActivity i
final Resources res = getResources();
TileManager tileManager = app.getWorld().tileManager;
tileManager.setDensity(res);
app.setWindowParameters(this);
setContentView(R.layout.startscreen);
initializeView(this, R.layout.startscreen, R.id.startscreen_fragment_container);
app.setFullscreenMode(this);
if (findViewById(R.id.startscreen_fragment_container) != null) {
StartScreenActivity_MainMenu mainMenu = new StartScreenActivity_MainMenu();
@@ -67,9 +69,11 @@ public final class StartScreenActivity extends AndorsTrailBaseFragmentActivity i
tv = (TextView) findViewById(R.id.startscreen_version);
app.setUsablePadding(tv);
tv.setText('v' + AndorsTrailApplication.CURRENT_VERSION_DISPLAY);
development_version = (TextView) findViewById(R.id.startscreen_dev_version);
app.setUsablePadding((View) development_version.getParent());
if (AndorsTrailApplication.DEVELOPMENT_INCOMPATIBLE_SAVEGAMES) {
development_version.setText(R.string.startscreen_incompatible_savegames);
development_version.setVisibility(View.VISIBLE);
@@ -96,6 +100,10 @@ public final class StartScreenActivity extends AndorsTrailBaseFragmentActivity i
}
});
}
View titleLogo = findViewById(R.id.title_logo);
if (titleLogo != null) {
app.setUsablePadding(titleLogo);
}
if (development_version.getVisibility() == View.VISIBLE) {
development_version.setText(development_version.getText()
@@ -112,7 +120,7 @@ public final class StartScreenActivity extends AndorsTrailBaseFragmentActivity i
}
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, int[] grantResults) {
if (grantResults.length > 0 && grantResults[0] != PackageManager.PERMISSION_GRANTED) {
final CustomDialog d = CustomDialogFactory.createDialog(this,

View File

@@ -183,19 +183,25 @@ public final class HeroinfoActivity_Stats extends Fragment {
);
ArrayList<ItemTraits_OnUse> effects_hit = new ArrayList<ItemTraits_OnUse>();
ArrayList<ItemTraits_OnUse> effects_miss = new ArrayList<ItemTraits_OnUse>();
ArrayList<ItemTraits_OnUse> effects_kill = new ArrayList<ItemTraits_OnUse>();
ArrayList<ItemTraits_OnHitReceived> effects_hitReceived = new ArrayList<ItemTraits_OnHitReceived>();
ArrayList<ItemTraits_OnHitReceived> effects_missReceived = new ArrayList<ItemTraits_OnHitReceived>();
for (Inventory.WearSlot slot : Inventory.WearSlot.values()) {
ItemType type = player.inventory.getItemTypeInWearSlot(slot);
if (type == null) continue;
if (type.effects_hit != null) effects_hit.add(type.effects_hit);
if (type.effects_miss != null) effects_miss.add(type.effects_miss);
if (type.effects_kill != null) effects_kill.add(type.effects_kill);
if (type.effects_hitReceived != null) effects_hitReceived.add(type.effects_hitReceived);
if (type.effects_missReceived != null) effects_missReceived.add(type.effects_missReceived);
}
if (effects_hit.isEmpty()) effects_hit = null;
if (effects_miss.isEmpty()) effects_miss = null;
if (effects_kill.isEmpty()) effects_kill = null;
if (effects_hitReceived.isEmpty()) effects_hitReceived = null;
actorinfo_onhiteffects.update(null, null, effects_hit, effects_kill, effects_hitReceived, null, false);
if (effects_missReceived.isEmpty()) effects_missReceived = null;
actorinfo_onhiteffects.update(null, null, effects_hit, effects_miss, effects_kill, effects_hitReceived,effects_missReceived, null, false);
updateStatsTableRow(world.model.statistics.getNumberOfCompletedQuests(world), R.id.heroinfo_gamestats_quests, R.id.heroinfo_gamestats_quests_row);

View File

@@ -59,7 +59,6 @@ public class StartScreenActivity_MainMenu extends Fragment {
updatePreferences(false);
super.onCreateView(inflater, container, savedInstanceState);
if (container != null) {
container.removeAllViews();
}
@@ -196,7 +195,6 @@ public class StartScreenActivity_MainMenu extends Fragment {
}
@TargetApi(29)
public void migrateDataOnDemand(final Activity activity) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
if (activity.getApplicationContext().checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
@@ -232,7 +230,6 @@ public class StartScreenActivity_MainMenu extends Fragment {
private static final int READ_EXTERNAL_STORAGE_REQUEST=1;
private static final int WRITE_EXTERNAL_STORAGE_REQUEST=2;
@TargetApi(23)
public static void checkAndRequestPermissions(final Activity activity) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
if (activity.getApplicationContext().checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {

View File

@@ -3,6 +3,7 @@ package com.gpl.rpg.AndorsTrail.activity.fragment;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import androidx.fragment.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
@@ -39,8 +40,7 @@ public class StartScreenActivity_NewGame extends Fragment {
}
View root = inflater.inflate(R.layout.startscreen_newgame, container, false);
startscreen_enterheroname = (TextView) root.findViewById(R.id.startscreen_enterheroname);
new SpinnerEmulator(root, R.id.startscreen_mode_selector_button, R.array.startscreen_mode_selector, R.string.startscreen_game_mode) {

View File

@@ -1,5 +1,6 @@
package com.gpl.rpg.AndorsTrail.context;
import com.gpl.rpg.AndorsTrail.model.ChecksumBuilder;
import com.gpl.rpg.AndorsTrail.model.ModelContainer;
import com.gpl.rpg.AndorsTrail.model.ability.ActorConditionTypeCollection;
import com.gpl.rpg.AndorsTrail.model.ability.SkillCollection;
@@ -13,6 +14,8 @@ import com.gpl.rpg.AndorsTrail.resource.ConversationLoader;
import com.gpl.rpg.AndorsTrail.resource.VisualEffectCollection;
import com.gpl.rpg.AndorsTrail.resource.tiles.TileManager;
import java.security.DigestException;
public final class WorldContext {
//Objectcollections
public final ConversationLoader conversationLoader;
@@ -62,4 +65,11 @@ public final class WorldContext {
public void resetForNewGame() {
maps.resetForNewGame();
}
public byte[] getChecksum() throws DigestException {
ChecksumBuilder checksumBuilder = new ChecksumBuilder();
model.addToChecksum(checksumBuilder);
maps.addToChecksum(checksumBuilder, this);
return checksumBuilder.build();
}
}

View File

@@ -595,7 +595,10 @@ public final class CombatController implements VisualEffectCompletedCallback {
private AttackResult attack(final Actor attacker, final Actor target) {
int hitChance = getAttackHitChance(attacker, target);
if (!Constants.roll100(hitChance)) return AttackResult.MISS;
if (!Constants.roll100(hitChance)) {
applyAttackMissStatusEffects(attacker, target);
return AttackResult.MISS;
}
int damage = Constants.rollValue(attacker.getDamagePotential());
boolean isCriticalHit = false;
@@ -629,6 +632,21 @@ public final class CombatController implements VisualEffectCompletedCallback {
}
}
private void applyAttackMissStatusEffects(Actor attacker, Actor target) {
ItemTraits_OnUse[] onMissEffects = attacker.getOnMissEffects();
ItemTraits_OnHitReceived[] onMissReceivedEffects = target.getOnMissReceivedEffects();
if (onMissEffects != null) {
for (ItemTraits_OnUse e : onMissEffects) {
controllers.actorStatsController.applyUseEffect(attacker, target, e);
}
}
if (onMissReceivedEffects != null) {
for (ItemTraits_OnHitReceived e : onMissReceivedEffects) {
controllers.actorStatsController.applyHitReceivedEffect(target, attacker, e);
}
}
}
public void endOfCombatRound() {
world.model.worldData.tickWorldTime();
controllers.gameRoundController.resetRoundTimers();

View File

@@ -195,19 +195,27 @@ public final class ItemController {
public static void recalculateHitEffectsFromWornItems(Player player) {
ArrayList<ItemTraits_OnUse> effects_onHit = null;
ArrayList<ItemTraits_OnUse> effects_onMiss = null;
ArrayList<ItemTraits_OnHitReceived> effects_onHitReceived = null;
ArrayList<ItemTraits_OnHitReceived> effects_onMissReceived = null;
for (Inventory.WearSlot slot : Inventory.WearSlot.values()) {
ItemType type = player.inventory.getItemTypeInWearSlot(slot);
if (type == null) continue;
ItemTraits_OnUse eh = type.effects_hit;
ItemTraits_OnHitReceived ehr = type.effects_hitReceived;
if (eh == null && ehr == null) continue;
ItemTraits_OnUse em = type.effects_miss;
ItemTraits_OnHitReceived emr = type.effects_missReceived;
if (eh == null && ehr == null && em == null && emr == null) continue;
if (effects_onHit == null) effects_onHit = new ArrayList<ItemTraits_OnUse>();
if (eh != null) effects_onHit.add(eh);
if (effects_onMiss == null) effects_onMiss = new ArrayList<ItemTraits_OnUse>();
if (em != null) effects_onMiss.add(em);
if (effects_onHitReceived == null) effects_onHitReceived = new ArrayList<ItemTraits_OnHitReceived>();
if (ehr != null) effects_onHitReceived.add(ehr);
if (effects_onMissReceived == null) effects_onMissReceived = new ArrayList<ItemTraits_OnHitReceived>();
if (emr != null) effects_onMissReceived.add(emr);
}
if (effects_onHit != null) {
@@ -217,6 +225,13 @@ public final class ItemController {
} else {
player.onHitEffects = null;
}
if (effects_onMiss != null) {
ItemTraits_OnUse[] effects_ = new ItemTraits_OnUse[effects_onMiss.size()];
effects_ = effects_onMiss.toArray(effects_);
player.onMissEffects = effects_;
} else {
player.onMissEffects = null;
}
if (effects_onHitReceived != null) {
ItemTraits_OnHitReceived[] effects_ = new ItemTraits_OnHitReceived[effects_onHitReceived.size()];
@@ -225,6 +240,13 @@ public final class ItemController {
} else {
player.onHitReceivedEffects = null;
}
if (effects_onMissReceived != null) {
ItemTraits_OnHitReceived[] effects_ = new ItemTraits_OnHitReceived[effects_onMissReceived.size()];
effects_ = effects_onMissReceived.toArray(effects_);
player.onMissReceivedEffects = effects_;
} else {
player.onMissReceivedEffects = null;
}
}
public void consumeNonItemLoot(Loot loot) {

View File

@@ -0,0 +1,115 @@
package com.gpl.rpg.AndorsTrail.model;
import android.os.Build;
import com.gpl.rpg.AndorsTrail.util.L;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.security.DigestException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class ChecksumBuilder {
public static final int CHECKSUM_LENGTH = 32;// 256 bits (depends on the hash algorithm)
public static final String CHECKSUM_ALGORITHM = "SHA-256"; //Should be available in all Android versions
private ByteBuffer buffer;
private final MessageDigest digest;
private ChecksumBuilder(int initialCapacity, ByteOrder byteOrder) {
buffer = ByteBuffer.allocate(initialCapacity);
buffer.order(byteOrder);
try {
digest = MessageDigest.getInstance(CHECKSUM_ALGORITHM); // Or SHA-512 for even stronger hash
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("Hash algorithm not found", e);
}
}
private ChecksumBuilder(int initialCapacity) {
this(initialCapacity, ByteOrder.BIG_ENDIAN); // Default to big-endian
}
public ChecksumBuilder() {
this(1024*10); // A reasonable default initial capacity
}
// --- Methods for adding different data types ---
public ChecksumBuilder add(String value) {
if (value != null) {
byte[] bytes;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
bytes = value.getBytes(StandardCharsets.UTF_8);
} else {
bytes = value.getBytes();
}
add(bytes);
} else {
add(-1); // Use -1 to represent a null string
}
return this;
}
public ChecksumBuilder add(byte[] bytes) {
add(bytes.length); // Add length prefix
ensureCapacity(bytes.length + 4); // +4 for length prefix (int)
buffer.put(bytes);
return this;
}
public ChecksumBuilder add(boolean value) {
ensureCapacity(1);
buffer.put(value ? (byte) 1 : (byte) 0);
return this;
}
public ChecksumBuilder add(long value) {
ensureCapacity(8);
buffer.putLong(value);
return this;
}
public ChecksumBuilder add(int value) {
ensureCapacity(4);
buffer.putInt(value);
return this;
}
public ChecksumBuilder add(float value) {
ensureCapacity(8);
buffer.putFloat(value);
return this;
}
public ChecksumBuilder add(double value) {
ensureCapacity(4);
buffer.putDouble(value);
return this;
}
// --- Method to finalize and get the checksum ---
public byte[] build() throws DigestException {
buffer.flip(); // Prepare for reading
digest.update(buffer);// Only use the actually used part of the buffer
buffer.flip(); // Prepare for further writing
return digest.digest();
}
// --- Utility method to ensure sufficient capacity ---
private void ensureCapacity(int required) {
if (buffer.remaining() < required) {
int newCapacity = Math.max(buffer.capacity() * 2, buffer.capacity() + required);
ByteBuffer newBuffer = ByteBuffer.allocate(newCapacity);
newBuffer.order(buffer.order());
buffer.flip(); // Prepare for reading
newBuffer.put(buffer); // Copy existing data
buffer = newBuffer; // Assign the new buffer to the field
}
}
}

View File

@@ -3,6 +3,7 @@ package com.gpl.rpg.AndorsTrail.model;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
@@ -22,6 +23,8 @@ import com.gpl.rpg.AndorsTrail.model.quest.Quest;
import com.gpl.rpg.AndorsTrail.util.HashMapHelper;
public final class GameStatistics {
private boolean isAlteredSavegame = false;
private byte[] checksum = new byte[ChecksumBuilder.CHECKSUM_LENGTH];
private int deaths = 0;
private final HashMap<String, Integer> killedMonstersByTypeID = new HashMap<String, Integer>();
private final HashMap<String, Integer> killedMonstersByName = new HashMap<String, Integer>();
@@ -67,6 +70,7 @@ public final class GameStatistics {
public boolean hasUnlimitedLives() { return startLives == -1; }
public int getStartLives() { return startLives; }
public boolean getIsAlteredSavegame() { return isAlteredSavegame; }
public int getLivesLeft() { return hasUnlimitedLives() ? -1 : startLives - deaths; }
@@ -158,6 +162,18 @@ public final class GameStatistics {
}
};
public void setChecksum(byte[] checksum) {
if (checksum.length != ChecksumBuilder.CHECKSUM_LENGTH) throw new IllegalArgumentException("Invalid checksum length.");
this.checksum = checksum;
}
public boolean compareChecksum(byte[] checksum) {
return this.checksum.length == checksum.length && MessageDigest.isEqual(this.checksum, checksum);
}
public void markAsAlteredSavegame() {
isAlteredSavegame = true;
}
// ====== PARCELABLE ===================================================================
@@ -194,6 +210,11 @@ public final class GameStatistics {
this.startLives = src.readInt();
this.unlimitedSaves = src.readBoolean();
if (fileversion < 81) return;
this.isAlteredSavegame = src.readBoolean();
final int checksumLength = src.readInt();
this.checksum = new byte[checksumLength];
if( src.read(checksum) != checksumLength) throw new IOException("Failed to read full checksum.");
}
public void writeToParcel(DataOutputStream dest) throws IOException {
@@ -213,5 +234,29 @@ public final class GameStatistics {
dest.writeInt(spentGold);
dest.writeInt(startLives);
dest.writeBoolean(unlimitedSaves);
dest.writeBoolean(isAlteredSavegame);
dest.writeInt(checksum.length);
dest.write(checksum);
}
public void addToChecksum(ChecksumBuilder builder) {
builder.add(deaths);
Set<Entry<String, Integer> > set = killedMonstersByTypeID.entrySet();
builder.add(set.size());
for (Entry<String, Integer> e : set) {
builder.add(e.getKey());
builder.add(e.getValue());
}
set = usedItems.entrySet();
builder.add(set.size());
for (Entry<String, Integer> e : set) {
builder.add(e.getKey());
builder.add(e.getValue());
}
builder.add(spentGold);
builder.add(startLives);
builder.add(unlimitedSaves);
builder.add(isAlteredSavegame);
}
}

View File

@@ -51,4 +51,12 @@ public final class InterfaceData {
}
dest.writeUTF(selectedTabHeroInfo);
}
public void addToChecksum(ChecksumBuilder builder) {
builder.add(isMainActivityVisible);
builder.add(isInCombat);
builder.add(selectedPosition != null);
if (selectedPosition != null) selectedPosition.addToChecksum(builder);
builder.add(selectedTabHeroInfo);
}
}

View File

@@ -49,4 +49,11 @@ public final class ModelContainer {
statistics.writeToParcel(dest);
worldData.writeToParcel(dest);
}
public void addToChecksum(ChecksumBuilder builder){
player.addToChecksum(builder);
builder.add(currentMaps.map.name);
uiSelections.addToChecksum(builder);
statistics.addToChecksum(builder);
worldData.addToChecksum(builder);
}
}

View File

@@ -114,4 +114,13 @@ public final class WorldData {
dest.writeLong(e.getValue());
}
}
public void addToChecksum(ChecksumBuilder builder) {
builder.add(worldTime);
builder.add(timers.size());
for(Map.Entry<String, Long> e : timers.entrySet()) {
builder.add(e.getKey());
builder.add(e.getValue());
}
}
}

View File

@@ -5,6 +5,7 @@ import java.io.DataOutputStream;
import java.io.IOException;
import com.gpl.rpg.AndorsTrail.context.WorldContext;
import com.gpl.rpg.AndorsTrail.model.ChecksumBuilder;
public final class ActorCondition {
public static final int MAGNITUDE_REMOVE_ALL = -99;
@@ -51,4 +52,11 @@ public final class ActorCondition {
dest.writeInt(magnitude);
dest.writeInt(duration);
}
public void addToChecksum(ChecksumBuilder builder) {
builder.add(conditionType.conditionTypeID);
builder.add(magnitude);
builder.add(duration);
}
}

View File

@@ -35,7 +35,9 @@ public class Actor {
public int blockChance;
public int damageResistance;
public ItemTraits_OnUse[] onHitEffects;
public ItemTraits_OnUse[] onMissEffects;
public ItemTraits_OnHitReceived[] onHitReceivedEffects;
public ItemTraits_OnHitReceived[] onMissReceivedEffects;
public ItemTraits_OnUse onDeathEffects;
public boolean hasVFXRunning = false;
public long vfxStartTime = 0;
@@ -69,8 +71,12 @@ public class Actor {
public int getDamageResistance() { return damageResistance; }
public ItemTraits_OnUse[] getOnHitEffects() { return onHitEffects; }
public List<ItemTraits_OnUse> getOnHitEffectsAsList() { return onHitEffects == null ? null : Arrays.asList(onHitEffects); }
public ItemTraits_OnUse[] getOnMissEffects() { return onMissEffects; }
public List<ItemTraits_OnUse> getOnMissEffectsAsList() { return onMissEffects == null ? null : Arrays.asList(onMissEffects); }
public ItemTraits_OnHitReceived[] getOnHitReceivedEffects() { return onHitReceivedEffects; }
public List<ItemTraits_OnHitReceived> getOnHitReceivedEffectsAsList() { return onHitReceivedEffects == null ? null : Arrays.asList(onHitReceivedEffects); }
public ItemTraits_OnHitReceived[] getOnMissReceivedEffects() { return onMissReceivedEffects; }
public List<ItemTraits_OnHitReceived> getOnMissReceivedEffectsAsList() { return onMissReceivedEffects == null ? null : Arrays.asList(onMissReceivedEffects); }
public ItemTraits_OnUse getOnDeathEffects() { return onDeathEffects; }
public boolean hasCriticalSkillEffect() { return getCriticalSkill() != 0; }

View File

@@ -6,6 +6,7 @@ import java.io.IOException;
import com.gpl.rpg.AndorsTrail.context.WorldContext;
import com.gpl.rpg.AndorsTrail.controller.Constants;
import com.gpl.rpg.AndorsTrail.model.ChecksumBuilder;
import com.gpl.rpg.AndorsTrail.model.ability.ActorCondition;
import com.gpl.rpg.AndorsTrail.model.ability.SkillCollection;
import com.gpl.rpg.AndorsTrail.model.item.DropList;
@@ -193,4 +194,43 @@ public final class Monster extends Actor {
dest.writeBoolean(false);
}
}
public void addToChecksum(ChecksumBuilder builder) {
builder.add(getMonsterTypeID());
if (attackCost == monsterType.attackCost
&& attackChance == monsterType.attackChance
&& criticalSkill == monsterType.criticalSkill
&& criticalMultiplier == monsterType.criticalMultiplier
&& damagePotential.equals(monsterType.damagePotential)
&& blockChance == monsterType.blockChance
&& damageResistance == monsterType.damageResistance
) {
builder.add(false);
} else {
builder.add(true);
builder.add(attackCost);
builder.add(attackChance);
builder.add(criticalSkill);
builder.add(criticalMultiplier);
damagePotential.addToChecksum(builder);
builder.add(blockChance);
builder.add(damageResistance);
}
ap.addToChecksum(builder);
health.addToChecksum(builder);
position.addToChecksum(builder);
builder.add(conditions.size());
for (ActorCondition c : conditions) {
c.addToChecksum(builder);
}
builder.add(moveCost);
builder.add(forceAggressive);
if (shopItems != null) {
builder.add(true);
shopItems.addToChecksum(builder);
} else {
builder.add(false);
}
}
}

View File

@@ -19,6 +19,7 @@ import com.gpl.rpg.AndorsTrail.AndorsTrailApplication;
import com.gpl.rpg.AndorsTrail.context.ControllerContext;
import com.gpl.rpg.AndorsTrail.context.WorldContext;
import com.gpl.rpg.AndorsTrail.controller.Constants;
import com.gpl.rpg.AndorsTrail.model.ChecksumBuilder;
import com.gpl.rpg.AndorsTrail.model.ability.ActorCondition;
import com.gpl.rpg.AndorsTrail.model.ability.SkillCollection;
import com.gpl.rpg.AndorsTrail.model.item.DropListCollection;
@@ -475,5 +476,62 @@ public final class Player extends Actor {
dest.writeUTF(id);
dest.writeLong(savedVersion);
}
public void addToChecksum(ChecksumBuilder builder) {
//builder.add(baseTraits.iconID);// Do not add to checksum so that it can be changed without invalidating checksums
builder.add(baseTraits.maxAP);
builder.add(baseTraits.maxHP);
//builder.add(name);// Do not add to checksum so that it can be changed without invalidating checksums
builder.add(moveCost); // TODO: Should we really write this?
builder.add(baseTraits.attackCost);
builder.add(baseTraits.attackChance);
builder.add(baseTraits.criticalSkill);
builder.add(baseTraits.criticalMultiplier);
baseTraits.damagePotential.addToChecksum(builder);
builder.add(baseTraits.blockChance);
builder.add(baseTraits.damageResistance);
builder.add(baseTraits.moveCost);
ap.addToChecksum(builder);
health.addToChecksum(builder);
position.addToChecksum(builder);
builder.add(conditions.size());
for (ActorCondition c : conditions) {
c.addToChecksum(builder);
}
builder.add(immunities.size());
for (ActorCondition c : immunities) {
c.addToChecksum(builder);
}
lastPosition.addToChecksum(builder);
nextPosition.addToChecksum(builder);
builder.add(level);
builder.add(totalExperience);
inventory.addToChecksum(builder);
builder.add(baseTraits.useItemCost);
builder.add(baseTraits.reequipCost);
builder.add(skillLevels.size());
for (int i = 0; i < skillLevels.size(); ++i) {
builder.add(skillLevels.keyAt(i));
builder.add(skillLevels.valueAt(i));
}
builder.add(spawnMap);
builder.add(spawnPlace);
builder.add(questProgress.size());
for(Entry<String, LinkedHashSet<Integer> > e : questProgress.entrySet()) {
builder.add(e.getKey());
builder.add(e.getValue().size());
for(int progress : e.getValue()) {
builder.add(progress);
}
}
builder.add(availableSkillIncreases);
builder.add(alignments.size());
for(Entry<String, Integer> e : alignments.entrySet()) {
builder.add(e.getKey());
builder.add(e.getValue());
}
builder.add(id);
builder.add(savedVersion);
}
}

View File

@@ -5,6 +5,7 @@ import java.io.DataOutputStream;
import java.io.IOException;
import com.gpl.rpg.AndorsTrail.context.WorldContext;
import com.gpl.rpg.AndorsTrail.model.ChecksumBuilder;
import com.gpl.rpg.AndorsTrail.savegames.LegacySavegameFormatReaderForItemContainer;
public final class Inventory extends ItemContainer {
@@ -208,4 +209,21 @@ public final class Inventory extends ItemContainer {
}
}
}
public void addToChecksum(ChecksumBuilder builder) {
super.addToChecksum(builder);
builder.add(gold);
builder.add(NUM_WORN_SLOTS);
for(int i = 0; i < NUM_WORN_SLOTS; ++i) {
if (wear[i] != null) {
builder.add(wear[i].id);
}
}
builder.add(NUM_QUICK_SLOTS);
for(int i = 0; i < NUM_QUICK_SLOTS; ++i) {
if (quickitem[i] != null) {
builder.add(quickitem[i].id);
}
}
}
}

View File

@@ -8,6 +8,7 @@ import java.util.Collections;
import java.util.Comparator;
import com.gpl.rpg.AndorsTrail.context.WorldContext;
import com.gpl.rpg.AndorsTrail.model.ChecksumBuilder;
import com.gpl.rpg.AndorsTrail.model.actor.Player;
public class ItemContainer {
@@ -42,6 +43,10 @@ public class ItemContainer {
dest.writeUTF(itemType.id);
dest.writeInt(quantity);
}
public void addToChecksum(ChecksumBuilder builder) {
builder.add(itemType.id);
builder.add(quantity);
}
}
public void addItem(ItemType itemType, int quantity) {
@@ -280,4 +285,10 @@ public class ItemContainer {
e.writeToParcel(dest);
}
}
public void addToChecksum(ChecksumBuilder builder) {
builder.add(items.size());
for (ItemEntry e : items) {
e.addToChecksum(builder);
}
}
}

View File

@@ -33,8 +33,10 @@ public final class ItemType {
public final ItemTraits_OnEquip effects_equip;
public final ItemTraits_OnUse effects_use;
public final ItemTraits_OnUse effects_hit;
public final ItemTraits_OnUse effects_miss;
public final ItemTraits_OnUse effects_kill;
public final ItemTraits_OnHitReceived effects_hitReceived;
public final ItemTraits_OnHitReceived effects_missReceived;
public ItemType(
String id
@@ -48,8 +50,10 @@ public final class ItemType {
, ItemTraits_OnEquip effects_equip
, ItemTraits_OnUse effects_use
, ItemTraits_OnUse effects_hit
, ItemTraits_OnUse effects_miss
, ItemTraits_OnUse effects_kill
, ItemTraits_OnHitReceived effects_hitReceived
, ItemTraits_OnHitReceived effects_missReceived
) {
this.id = id;
this.iconID = iconID;
@@ -63,8 +67,10 @@ public final class ItemType {
this.effects_equip = effects_equip;
this.effects_use = effects_use;
this.effects_hit = effects_hit;
this.effects_miss = effects_miss;
this.effects_kill = effects_kill;
this.effects_hitReceived = effects_hitReceived;
this.effects_missReceived = effects_missReceived;
this.hasPersonalizedName = name.contains(Constants.PLACEHOLDER_PLAYERNAME);
}

View File

@@ -5,6 +5,7 @@ import java.io.DataOutputStream;
import java.io.IOException;
import com.gpl.rpg.AndorsTrail.context.WorldContext;
import com.gpl.rpg.AndorsTrail.model.ChecksumBuilder;
import com.gpl.rpg.AndorsTrail.savegames.LegacySavegameFormatReaderForItemContainer;
import com.gpl.rpg.AndorsTrail.util.Coord;
@@ -88,4 +89,12 @@ public final class Loot {
position.writeToParcel(dest);
dest.writeBoolean(isVisible);
}
public void addToChecksum(ChecksumBuilder builder) {
builder.add(exp);
builder.add(gold);
items.addToChecksum(builder);
position.addToChecksum(builder);
builder.add(isVisible);
}
}

View File

@@ -11,6 +11,7 @@ import java.util.List;
import com.gpl.rpg.AndorsTrail.AndorsTrailApplication;
import com.gpl.rpg.AndorsTrail.context.ControllerContext;
import com.gpl.rpg.AndorsTrail.context.WorldContext;
import com.gpl.rpg.AndorsTrail.model.ChecksumBuilder;
import com.gpl.rpg.AndorsTrail.savegames.LegacySavegameFormatReaderForMap;
import com.gpl.rpg.AndorsTrail.util.L;
@@ -99,4 +100,16 @@ public final class MapCollection {
map.writeToParcel(dest, world);
}
}
public void addToChecksum(ChecksumBuilder checksumBuilder, WorldContext world) {
List<PredefinedMap> mapsToExport = new ArrayList<PredefinedMap>();
for(PredefinedMap map : getAllMaps()) {
if (shouldSaveMap(world, map)) mapsToExport.add(map);
}
checksumBuilder.add(mapsToExport.size());
for(PredefinedMap map : mapsToExport) {
checksumBuilder.add(map.name);
map.addToChecksum(checksumBuilder, world);
}
}
}

View File

@@ -8,6 +8,7 @@ import java.util.concurrent.CopyOnWriteArrayList;
import com.gpl.rpg.AndorsTrail.context.WorldContext;
import com.gpl.rpg.AndorsTrail.controller.Constants;
import com.gpl.rpg.AndorsTrail.model.ChecksumBuilder;
import com.gpl.rpg.AndorsTrail.model.actor.Monster;
import com.gpl.rpg.AndorsTrail.model.actor.MonsterType;
import com.gpl.rpg.AndorsTrail.util.Coord;
@@ -140,4 +141,12 @@ public final class MonsterSpawnArea {
m.writeToParcel(dest);
}
}
public void addToChecksum(ChecksumBuilder builder) {
builder.add(isSpawning);
builder.add(monsters.size());
for (Monster m : monsters) {
m.addToChecksum(builder);
}
}
}

View File

@@ -12,6 +12,7 @@ import com.gpl.rpg.AndorsTrail.context.ControllerContext;
import com.gpl.rpg.AndorsTrail.context.WorldContext;
import com.gpl.rpg.AndorsTrail.controller.Constants;
import com.gpl.rpg.AndorsTrail.controller.VisualEffectController.BloodSplatter;
import com.gpl.rpg.AndorsTrail.model.ChecksumBuilder;
import com.gpl.rpg.AndorsTrail.model.actor.Monster;
import com.gpl.rpg.AndorsTrail.model.item.ItemType;
import com.gpl.rpg.AndorsTrail.model.item.Loot;
@@ -398,4 +399,30 @@ public final class PredefinedMap {
dest.writeBoolean(visited);
dest.writeUTF(lastSeenLayoutHash);
}
public void addToChecksum(ChecksumBuilder builder, WorldContext world) {
if (shouldSaveMapData(world)) {
builder.add(true);
builder.add(spawnAreas.length);
for(MonsterSpawnArea a : spawnAreas) {
builder.add(a.areaID);
a.addToChecksum(builder);
}
builder.add(activeMapObjectGroups.size());
for(String s : activeMapObjectGroups) {
builder.add(s);
}
builder.add(groundBags.size());
for(Loot l : groundBags) {
l.addToChecksum(builder);
}
builder.add(currentColorFilter != null);
if (currentColorFilter != null) builder.add(currentColorFilter);
builder.add(lastVisitTime);
} else {
builder.add(false);
}
builder.add(visited);
builder.add(lastSeenLayoutHash);
}
}

View File

@@ -385,6 +385,7 @@ public final class ResourceLoader {
loader.prepareTileset(R.drawable.monsters_omi2, "monsters_omi2", sz8x3, sz1x1, mTileSize);
loader.prepareTileset(R.drawable.monsters_phoenix01, "monsters_phoenix01", sz16x8, sz1x1, mTileSize);
loader.prepareTileset(R.drawable.monsters_cats, "monsters_cats", new Size(10, 2), sz1x1, mTileSize);
loader.prepareTileset(R.drawable.monsters_1x2, "monsters_1x2", new Size(10, 5), sz1x2, mTileSize);
/*INSERT_NPCS_TILESETS_HERE*/
loader.prepareTileset(R.drawable.map_bed_1, "map_bed_1", sz16x10, sz1x1, mTileSize);
@@ -399,6 +400,7 @@ public final class ResourceLoader {
loader.prepareTileset(R.drawable.map_cavewall_3, "map_cavewall_3", new Size(18, 6), sz1x1, mTileSize);
loader.prepareTileset(R.drawable.map_cavewall_4, "map_cavewall_4", new Size(18, 6), sz1x1, mTileSize);
loader.prepareTileset(R.drawable.map_cavewall_5, "map_cavewall_5", new Size(6, 6), sz1x1, mTileSize);
loader.prepareTileset(R.drawable.map_cavewall_6, "map_cavewall_6", new Size(18, 6), sz1x1, mTileSize);
loader.prepareTileset(R.drawable.map_chair_table_1, "map_chair_table_1", mapTileSize, sz1x1, mTileSize);
loader.prepareTileset(R.drawable.map_chair_table_2, "map_chair_table_2", mapTileSize, sz1x1, mTileSize);
loader.prepareTileset(R.drawable.map_crate_1, "map_crate_1", mapTileSize, sz1x1, mTileSize);
@@ -427,6 +429,7 @@ public final class ResourceLoader {
loader.prepareTileset(R.drawable.map_indoor_2, "map_indoor_2", mapTileSize, sz1x1, mTileSize);
loader.prepareTileset(R.drawable.map_kitchen_1, "map_kitchen_1", mapTileSize, sz1x1, mTileSize);
loader.prepareTileset(R.drawable.map_outdoor_1, "map_outdoor_1", mapTileSize, sz1x1, mTileSize);
loader.prepareTileset(R.drawable.map_outdoor_2, "map_outdoor_2", mapTileSize, sz1x1, mTileSize);
loader.prepareTileset(R.drawable.map_pillar_1, "map_pillar_1", mapTileSize, sz1x1, mTileSize);
loader.prepareTileset(R.drawable.map_pillar_2, "map_pillar_2", mapTileSize, sz1x1, mTileSize);
loader.prepareTileset(R.drawable.map_pillar_3, "map_pillar_3", new Size(16, 10), sz1x1, mTileSize);

View File

@@ -8,7 +8,7 @@ import com.gpl.rpg.AndorsTrail.resource.DynamicTileLoader;
import com.gpl.rpg.AndorsTrail.resource.TranslationLoader;
import com.gpl.rpg.AndorsTrail.resource.parsers.json.JsonCollectionParserFor;
import com.gpl.rpg.AndorsTrail.resource.parsers.json.JsonFieldNames;
import com.gpl.rpg.AndorsTrail.util.Pair;
import android.util.Pair;
public final class ActorConditionsTypeParser extends JsonCollectionParserFor<ActorConditionType> {

View File

@@ -13,7 +13,7 @@ import com.gpl.rpg.AndorsTrail.resource.parsers.json.JsonArrayParserFor;
import com.gpl.rpg.AndorsTrail.resource.parsers.json.JsonCollectionParserFor;
import com.gpl.rpg.AndorsTrail.resource.parsers.json.JsonFieldNames;
import com.gpl.rpg.AndorsTrail.util.L;
import com.gpl.rpg.AndorsTrail.util.Pair;
import android.util.Pair;
public final class ConversationListParser extends JsonCollectionParserFor<Phrase> {

View File

@@ -11,7 +11,7 @@ import com.gpl.rpg.AndorsTrail.resource.parsers.json.JsonArrayParserFor;
import com.gpl.rpg.AndorsTrail.resource.parsers.json.JsonCollectionParserFor;
import com.gpl.rpg.AndorsTrail.resource.parsers.json.JsonFieldNames;
import com.gpl.rpg.AndorsTrail.util.L;
import com.gpl.rpg.AndorsTrail.util.Pair;
import android.util.Pair;
public final class DropListParser extends JsonCollectionParserFor<DropList> {

View File

@@ -8,7 +8,7 @@ import com.gpl.rpg.AndorsTrail.model.item.ItemCategory;
import com.gpl.rpg.AndorsTrail.resource.TranslationLoader;
import com.gpl.rpg.AndorsTrail.resource.parsers.json.JsonCollectionParserFor;
import com.gpl.rpg.AndorsTrail.resource.parsers.json.JsonFieldNames;
import com.gpl.rpg.AndorsTrail.util.Pair;
import android.util.Pair;
public final class ItemCategoryParser extends JsonCollectionParserFor<ItemCategory> {

View File

@@ -13,7 +13,7 @@ import com.gpl.rpg.AndorsTrail.resource.DynamicTileLoader;
import com.gpl.rpg.AndorsTrail.resource.TranslationLoader;
import com.gpl.rpg.AndorsTrail.resource.parsers.json.JsonCollectionParserFor;
import com.gpl.rpg.AndorsTrail.resource.parsers.json.JsonFieldNames;
import com.gpl.rpg.AndorsTrail.util.Pair;
import android.util.Pair;
public final class ItemTypeParser extends JsonCollectionParserFor<ItemType> {
@@ -41,9 +41,11 @@ public final class ItemTypeParser extends JsonCollectionParserFor<ItemType> {
final ItemTraits_OnEquip equipEffect = itemTraitsParser.parseItemTraits_OnEquip(o.optJSONObject(JsonFieldNames.ItemType.equipEffect));
final ItemTraits_OnUse useEffect = itemTraitsParser.parseItemTraits_OnUse(o.optJSONObject(JsonFieldNames.ItemType.useEffect));
final ItemTraits_OnUse hitEffect = itemTraitsParser.parseItemTraits_OnUse(o.optJSONObject(JsonFieldNames.ItemType.hitEffect));
final ItemTraits_OnUse missEffect = itemTraitsParser.parseItemTraits_OnUse(o.optJSONObject(JsonFieldNames.ItemType.missEffect));
final ItemTraits_OnUse killEffect = itemTraitsParser.parseItemTraits_OnUse(o.optJSONObject(JsonFieldNames.ItemType.killEffect));
final ItemTraits_OnHitReceived hitReceivedEffect = itemTraitsParser.parseItemTraits_OnHitReceived(o.optJSONObject(JsonFieldNames.ItemType.hitReceivedEffect));
final ItemTraits_OnHitReceived missReceivedEffect = itemTraitsParser.parseItemTraits_OnHitReceived(o.optJSONObject(JsonFieldNames.ItemType.missReceivedEffect));
final int baseMarketCost = o.optInt(JsonFieldNames.ItemType.baseMarketCost);
final boolean hasManualPrice = o.optInt(JsonFieldNames.ItemType.hasManualPrice, 0) > 0;
final ItemType itemType = new ItemType(
@@ -58,8 +60,10 @@ public final class ItemTypeParser extends JsonCollectionParserFor<ItemType> {
, equipEffect
, useEffect
, hitEffect
, missEffect
, killEffect
, hitReceivedEffect
, missReceivedEffect
);
return new Pair<String, ItemType>(id, itemType);
}

View File

@@ -14,7 +14,7 @@ import com.gpl.rpg.AndorsTrail.resource.TranslationLoader;
import com.gpl.rpg.AndorsTrail.resource.parsers.json.JsonCollectionParserFor;
import com.gpl.rpg.AndorsTrail.resource.parsers.json.JsonFieldNames;
import com.gpl.rpg.AndorsTrail.util.ConstRange;
import com.gpl.rpg.AndorsTrail.util.Pair;
import android.util.Pair;
import com.gpl.rpg.AndorsTrail.util.Size;
public final class MonsterTypeParser extends JsonCollectionParserFor<MonsterType> {

View File

@@ -12,7 +12,7 @@ import com.gpl.rpg.AndorsTrail.resource.TranslationLoader;
import com.gpl.rpg.AndorsTrail.resource.parsers.json.JsonArrayParserFor;
import com.gpl.rpg.AndorsTrail.resource.parsers.json.JsonCollectionParserFor;
import com.gpl.rpg.AndorsTrail.resource.parsers.json.JsonFieldNames;
import com.gpl.rpg.AndorsTrail.util.Pair;
import android.util.Pair;
public final class QuestParser extends JsonCollectionParserFor<Quest> {
private final TranslationLoader translationLoader;

View File

@@ -16,7 +16,7 @@ import com.gpl.rpg.AndorsTrail.model.map.WorldMapSegment.WorldMapSegmentMap;
import com.gpl.rpg.AndorsTrail.resource.TranslationLoader;
import com.gpl.rpg.AndorsTrail.util.Coord;
import com.gpl.rpg.AndorsTrail.util.L;
import com.gpl.rpg.AndorsTrail.util.Pair;
import android.util.Pair;
import com.gpl.rpg.AndorsTrail.util.XmlResourceParserUtils;
public final class WorldMapParser {

View File

@@ -11,7 +11,7 @@ import org.json.JSONException;
import com.gpl.rpg.AndorsTrail.AndorsTrailApplication;
import com.gpl.rpg.AndorsTrail.util.L;
import com.gpl.rpg.AndorsTrail.util.Pair;
import android.util.Pair;
public abstract class JsonCollectionParserFor<T> extends JsonParserFor<Pair<String, T>> {
public HashSet<String> parseRows(String input, HashMap<String, T> dest) {

View File

@@ -167,8 +167,10 @@ public final class JsonFieldNames {
public static final String equipEffect = "equipEffect";
public static final String useEffect = "useEffect";
public static final String hitEffect = "hitEffect";
public static final String missEffect = "missEffect";
public static final String killEffect = "killEffect";
public static final String hitReceivedEffect = "hitReceivedEffect";
public static final String missReceivedEffect = "missReceivedEffect";
}

View File

@@ -12,6 +12,7 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.security.DigestException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -79,7 +80,7 @@ public final class Savegames {
}
return true;
} catch (IOException e) {
} catch (IOException | DigestException e) {
L.log("Error saving world: " + e.toString());
return false;
}
@@ -87,7 +88,7 @@ public final class Savegames {
private static void writeBackup(Context androidContext, byte[] savegame, String playerId) throws IOException {
File cheatDetectionFolder = AndroidStorage.getStorageDirectory(androidContext, Constants.CHEAT_DETECTION_FOLDER);
ensureDirExists(cheatDetectionFolder);
ensureDirExists(cheatDetectionFolder);
File backupFile = new File(cheatDetectionFolder, playerId + "X");
FileOutputStream fileOutputStream = new FileOutputStream(backupFile);
fileOutputStream.write(savegame);
@@ -108,15 +109,15 @@ public final class Savegames {
LoadSavegameResult result = loadWorld(androidContext.getResources(), world, controllers, androidContext, fos, fh);
fos.close();
if (result == LoadSavegameResult.success && slot != SLOT_QUICKSAVE && !world.model.statistics.hasUnlimitedSaves()) {
// save to the quicksave slot before deleting the file
if (!saveWorld(world, androidContext, SLOT_QUICKSAVE)) {
// save to the quicksave slot before deleting the file
if (!saveWorld(world, androidContext, SLOT_QUICKSAVE)) {
return LoadSavegameResult.unknownError;
}
getSlotFile(slot, androidContext).delete();
writeCheatCheck(androidContext, DENY_LOADING_BECAUSE_GAME_IS_CURRENTLY_PLAYED, fh.playerId);
}
return result;
} catch (IOException e) {
} catch (IOException | DigestException e) {
if (AndorsTrailApplication.DEVELOPMENT_DEBUGMESSAGES) {
L.log("Error loading world: " + e.toString());
StringWriter sw = new StringWriter();
@@ -128,33 +129,33 @@ public final class Savegames {
}
}
private static boolean triedToCheat(Context androidContext, FileHeader fh) throws IOException {
long savedVersionToCheck = 0;
File cheatDetectionFolder = AndroidStorage.getStorageDirectory(androidContext, Constants.CHEAT_DETECTION_FOLDER);
ensureDirExists(cheatDetectionFolder);
File cheatDetectionFile = new File(cheatDetectionFolder, fh.playerId);
if (cheatDetectionFile.exists()) {
FileInputStream fileInputStream = new FileInputStream(cheatDetectionFile);
DataInputStream dataInputStream = new DataInputStream(fileInputStream);
final CheatDetection cheatDetection = new CheatDetection(dataInputStream);
savedVersionToCheck = cheatDetection.savedVersion;
dataInputStream.close();
fileInputStream.close();
}
private static boolean triedToCheat(Context androidContext, FileHeader fh) throws IOException {
long savedVersionToCheck = 0;
File cheatDetectionFolder = AndroidStorage.getStorageDirectory(androidContext, Constants.CHEAT_DETECTION_FOLDER);
ensureDirExists(cheatDetectionFolder);
File cheatDetectionFile = new File(cheatDetectionFolder, fh.playerId);
if (cheatDetectionFile.exists()) {
FileInputStream fileInputStream = new FileInputStream(cheatDetectionFile);
DataInputStream dataInputStream = new DataInputStream(fileInputStream);
final CheatDetection cheatDetection = new CheatDetection(dataInputStream);
savedVersionToCheck = cheatDetection.savedVersion;
dataInputStream.close();
fileInputStream.close();
}
if (savedVersionToCheck == DENY_LOADING_BECAUSE_GAME_IS_CURRENTLY_PLAYED) {
return true;
}
if (androidContext.getFileStreamPath(fh.playerId).exists()) {
FileInputStream fileInputStream = androidContext.openFileInput(fh.playerId);
DataInputStream dataInputStream = new DataInputStream(fileInputStream);
final CheatDetection cheatDetection = new CheatDetection(dataInputStream);
if (cheatDetection.savedVersion == DENY_LOADING_BECAUSE_GAME_IS_CURRENTLY_PLAYED) {
savedVersionToCheck = DENY_LOADING_BECAUSE_GAME_IS_CURRENTLY_PLAYED;
} else if (cheatDetection.savedVersion > savedVersionToCheck) {
savedVersionToCheck = cheatDetection.savedVersion;
}
if (androidContext.getFileStreamPath(fh.playerId).exists()) {
FileInputStream fileInputStream = androidContext.openFileInput(fh.playerId);
DataInputStream dataInputStream = new DataInputStream(fileInputStream);
final CheatDetection cheatDetection = new CheatDetection(dataInputStream);
if (cheatDetection.savedVersion == DENY_LOADING_BECAUSE_GAME_IS_CURRENTLY_PLAYED) {
savedVersionToCheck = DENY_LOADING_BECAUSE_GAME_IS_CURRENTLY_PLAYED;
} else if (cheatDetection.savedVersion > savedVersionToCheck) {
savedVersionToCheck = cheatDetection.savedVersion;
}
if (AndorsTrailApplication.DEVELOPMENT_DEBUGMESSAGES) {
L.log("Internal cheatcheck file savedVersion: " + cheatDetection.savedVersion);
@@ -167,83 +168,98 @@ public final class Savegames {
return (savedVersionToCheck == DENY_LOADING_BECAUSE_GAME_IS_CURRENTLY_PLAYED || fh.savedVersion < savedVersionToCheck);
}
private static FileOutputStream getOutputFile(Context androidContext, int slot) throws IOException {
if (slot == SLOT_QUICKSAVE) {
return androidContext.openFileOutput(Constants.FILENAME_SAVEGAME_QUICKSAVE, Context.MODE_PRIVATE);
} else {
ensureSavegameDirectoryExists(androidContext);
return new FileOutputStream(getSlotFile(slot, androidContext));
}
}
private static FileOutputStream getOutputFile(Context androidContext, int slot) throws IOException {
if (slot == SLOT_QUICKSAVE) {
return androidContext.openFileOutput(Constants.FILENAME_SAVEGAME_QUICKSAVE, Context.MODE_PRIVATE);
} else {
ensureSavegameDirectoryExists(androidContext);
return new FileOutputStream(getSlotFile(slot, androidContext));
}
}
private static void ensureSavegameDirectoryExists(Context context) {
File dir = AndroidStorage.getStorageDirectory(context, Constants.FILENAME_SAVEGAME_DIRECTORY);
ensureDirExists(dir);
}
private static void ensureSavegameDirectoryExists(Context context) {
File dir = AndroidStorage.getStorageDirectory(context, Constants.FILENAME_SAVEGAME_DIRECTORY);
ensureDirExists(dir);
}
public static boolean ensureDirExists(File dir) {
if (!dir.exists()) {
boolean worked = dir.mkdir();
return worked;
}
return true;
}
public static boolean ensureDirExists(File dir) {
if (!dir.exists()) {
boolean worked = dir.mkdir();
return worked;
}
return true;
}
private static FileInputStream getInputFile(Context androidContext, int slot) throws IOException {
if (slot == SLOT_QUICKSAVE) {
return androidContext.openFileInput(Constants.FILENAME_SAVEGAME_QUICKSAVE);
} else {
return new FileInputStream(getSlotFile(slot, androidContext));
}
}
private static FileInputStream getInputFile(Context androidContext, int slot) throws IOException {
if (slot == SLOT_QUICKSAVE) {
return androidContext.openFileInput(Constants.FILENAME_SAVEGAME_QUICKSAVE);
} else {
return new FileInputStream(getSlotFile(slot, androidContext));
}
}
public static File getSlotFile(int slot, Context context) {
File root = AndroidStorage.getStorageDirectory(context, Constants.FILENAME_SAVEGAME_DIRECTORY);
return getSlotFile(slot, root);
}
public static File getSlotFile(int slot, Context context) {
File root = AndroidStorage.getStorageDirectory(context, Constants.FILENAME_SAVEGAME_DIRECTORY);
return getSlotFile(slot, root);
}
public static File getSlotFile(int slot, File directory) {
return new File(directory, getSlotFileName(slot));
}
public static File getSlotFile(int slot, File directory) {
return new File(directory, getSlotFileName(slot));
}
public static String getSlotFileName(int slot) {
return Constants.FILENAME_SAVEGAME_FILENAME_PREFIX + slot;
}
public static String getSlotFileName(int slot) {
return Constants.FILENAME_SAVEGAME_FILENAME_PREFIX + slot;
}
public static void saveWorld(WorldContext world, OutputStream outStream, String displayInfo) throws IOException {
public static void saveWorld(WorldContext world, OutputStream outStream, String displayInfo) throws IOException, DigestException {
DataOutputStream dest = new DataOutputStream(outStream);
FileHeader.writeToParcel(dest, world.model.player.getName(),
displayInfo, world.model.player.iconID,
world.model.statistics.isDead(),
world.model.statistics.hasUnlimitedSaves(),
world.model.player.id,
world.model.player.savedVersion);
world.model.player.savedVersion,
world.model.statistics.getIsAlteredSavegame());
byte[] checksum = world.getChecksum();
world.model.statistics.setChecksum(checksum);
world.maps.writeToParcel(dest, world);
world.model.writeToParcel(dest);
dest.close();
}
public static LoadSavegameResult loadWorld(Resources res, WorldContext world, ControllerContext controllers, Context androidContext, InputStream inState, FileHeader fh) throws IOException {
DataInputStream src = new DataInputStream(inState);
final FileHeader header = new FileHeader(src, fh.skipIcon);
if (header.fileversion > AndorsTrailApplication.CURRENT_VERSION)
return LoadSavegameResult.savegameIsFromAFutureVersion;
public static LoadSavegameResult loadWorld(Resources res, WorldContext world, ControllerContext controllers, Context androidContext, InputStream inState, FileHeader fh) throws IOException, DigestException {
DataInputStream src = new DataInputStream(inState);
final FileHeader header = new FileHeader(src, fh.skipIcon);
if (header.fileversion > AndorsTrailApplication.CURRENT_VERSION)
return LoadSavegameResult.savegameIsFromAFutureVersion;
world.maps.readFromParcel(src, world, controllers, header.fileversion);
world.model = new ModelContainer(src, world, controllers, header.fileversion);
WorldMapController.populateWorldMap(androidContext, world, controllers.getResources());
src.close();
if (header.fileversion >= 81) {
checkChecksum(world);
}
WorldMapController.populateWorldMap(androidContext, world, controllers.getResources());
if (header.fileversion < 45) {
LegacySavegamesContentAdaptations.adaptToNewContentForVersion45(world, controllers, res);
}
onWorldLoaded(res, world, controllers);
return LoadSavegameResult.success;
}
private static void checkChecksum(WorldContext world) throws DigestException {
byte[] checksum = world.getChecksum();
if (!world.model.statistics.compareChecksum(checksum)) {
world.model.statistics.markAsAlteredSavegame();
}
}
private static void onWorldLoaded(Resources res, WorldContext world, ControllerContext controllers) {
controllers.actorStatsController.recalculatePlayerStats(world.model.player);
controllers.mapController.resetMapsNotRecentlyVisited();
@@ -268,15 +284,15 @@ public final class Savegames {
}
}
private static void writeCheatCheck(Context androidContext, long savedVersion, String playerId) throws IOException {
File cheatDetectionFolder = AndroidStorage.getStorageDirectory(androidContext, Constants.CHEAT_DETECTION_FOLDER);
ensureDirExists(cheatDetectionFolder);
File cheatDetectionFile = new File(cheatDetectionFolder, playerId);
FileOutputStream fileOutputStream = new FileOutputStream(cheatDetectionFile);
DataOutputStream dataOutputStream = new DataOutputStream(fileOutputStream);
CheatDetection.writeToParcel(dataOutputStream, savedVersion);
dataOutputStream.close();
fileOutputStream.close();
private static void writeCheatCheck(Context androidContext, long savedVersion, String playerId) throws IOException {
File cheatDetectionFolder = AndroidStorage.getStorageDirectory(androidContext, Constants.CHEAT_DETECTION_FOLDER);
ensureDirExists(cheatDetectionFolder);
File cheatDetectionFile = new File(cheatDetectionFolder, playerId);
FileOutputStream fileOutputStream = new FileOutputStream(cheatDetectionFile);
DataOutputStream dataOutputStream = new DataOutputStream(fileOutputStream);
CheatDetection.writeToParcel(dataOutputStream, savedVersion);
dataOutputStream.close();
fileOutputStream.close();
fileOutputStream = androidContext.openFileOutput(playerId, Context.MODE_PRIVATE);
dataOutputStream = new DataOutputStream(fileOutputStream);
@@ -287,26 +303,26 @@ public final class Savegames {
private static final Pattern savegameFilenamePattern = Pattern.compile(Constants.FILENAME_SAVEGAME_FILENAME_PREFIX + "(\\d+)");
public static List<Integer> getUsedSavegameSlots(Context context) {
try {
final List<Integer> result = new ArrayList<Integer>();
AndroidStorage.getStorageDirectory(context, Constants.FILENAME_SAVEGAME_DIRECTORY).listFiles(new FilenameFilter() {
@Override
public boolean accept(File f, String filename) {
Matcher m = savegameFilenamePattern.matcher(filename);
if (m != null && m.matches()) {
result.add(Integer.parseInt(m.group(1)));
return true;
}
return false;
}
});
Collections.sort(result);
return result;
} catch (Exception e) {
return new ArrayList<Integer>();
}
}
public static List<Integer> getUsedSavegameSlots(Context context) {
try {
final List<Integer> result = new ArrayList<Integer>();
AndroidStorage.getStorageDirectory(context, Constants.FILENAME_SAVEGAME_DIRECTORY).listFiles(new FilenameFilter() {
@Override
public boolean accept(File f, String filename) {
Matcher m = savegameFilenamePattern.matcher(filename);
if (m != null && m.matches()) {
result.add(Integer.parseInt(m.group(1)));
return true;
}
return false;
}
});
Collections.sort(result);
return result;
} catch (Exception e) {
return new ArrayList<Integer>();
}
}
private static final class CheatDetection {
public final int fileversion;
@@ -326,16 +342,17 @@ public final class Savegames {
}
public static final class FileHeader {
public final int fileversion;
public final String playerName;
public final String displayInfo;
public final int iconID;
public boolean skipIcon = false;
public final boolean isDead;
public final boolean hasUnlimitedSaves;
public final String playerId;
public final long savedVersion;
public static final class FileHeader {
public final int fileversion;
public final String playerName;
public final String displayInfo;
public final int iconID;
public final boolean isAlteredSavegame;
public boolean skipIcon = false;
public final boolean isDead;
public final boolean hasUnlimitedSaves;
public final String playerId;
public final long savedVersion;
public String describe() {
return (fileversion == AndorsTrailApplication.DEVELOPMENT_INCOMPATIBLE_SAVEGAME_VERSION ? "(D) " : "") + playerName + ", " + displayInfo;
@@ -344,18 +361,18 @@ public final class Savegames {
// ====== PARCELABLE ===================================================================
public FileHeader(DataInputStream src, boolean skipIcon) throws IOException {
int fileversion = src.readInt();
if (fileversion == 11)
fileversion = 5; // Fileversion 5 had no version identifier, but the first byte was 11.
this.fileversion = fileversion;
if (fileversion >= 14) { // Before fileversion 14 (0.6.7), we had no file header.
this.playerName = src.readUTF();
this.displayInfo = src.readUTF();
} else {
this.playerName = null;
this.displayInfo = null;
}
public FileHeader(DataInputStream src, boolean skipIcon) throws IOException {
int fileversion = src.readInt();
if (fileversion == 11)
fileversion = 5; // Fileversion 5 had no version identifier, but the first byte was 11.
this.fileversion = fileversion;
if (fileversion >= 14) { // Before fileversion 14 (0.6.7), we had no file header.
this.playerName = src.readUTF();
this.displayInfo = src.readUTF();
} else {
this.playerName = null;
this.displayInfo = null;
}
if (fileversion >= 43) {
int id = src.readInt();
@@ -380,9 +397,14 @@ public final class Savegames {
this.playerId = "";
this.savedVersion = 0;
}
if(fileversion >= 81){
this.isAlteredSavegame = src.readBoolean();
}else{
this.isAlteredSavegame = false;
}
}
public static void writeToParcel(DataOutputStream dest, String playerName, String displayInfo, int iconID, boolean isDead, boolean hasUnlimitedSaves, String playerId, long savedVersion) throws IOException {
public static void writeToParcel(DataOutputStream dest, String playerName, String displayInfo, int iconID, boolean isDead, boolean hasUnlimitedSaves, String playerId, long savedVersion, boolean isAlteredSavegame) throws IOException {
dest.writeInt(AndorsTrailApplication.CURRENT_VERSION);
dest.writeUTF(playerName);
dest.writeUTF(displayInfo);
@@ -391,6 +413,7 @@ public final class Savegames {
dest.writeBoolean(hasUnlimitedSaves);
dest.writeUTF(playerId);
dest.writeLong(savedVersion);
dest.writeBoolean(isAlteredSavegame);
}
}
}

View File

@@ -1,5 +1,7 @@
package com.gpl.rpg.AndorsTrail.util;
import com.gpl.rpg.AndorsTrail.model.ChecksumBuilder;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
@@ -47,4 +49,9 @@ public final class Coord {
dest.writeInt(x);
dest.writeInt(y);
}
public void addToChecksum(ChecksumBuilder builder) {
builder.add(x);
builder.add(y);
}
}

View File

@@ -1,11 +0,0 @@
package com.gpl.rpg.AndorsTrail.util;
// Should really use android.util.Pair<> instead, but it is not available for API level 4 (Android 1.6).
public final class Pair<T1, T2> {
public final T1 first;
public final T2 second;
public Pair(T1 a, T2 b) {
this.first = a;
this.second = b;
}
}

View File

@@ -1,5 +1,7 @@
package com.gpl.rpg.AndorsTrail.util;
import com.gpl.rpg.AndorsTrail.model.ChecksumBuilder;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
@@ -99,4 +101,9 @@ public final class Range {
dest.writeInt(max);
dest.writeInt(current);
}
public void addToChecksum(ChecksumBuilder builder) {
builder.add(max);
builder.add(current);
}
}

View File

@@ -13,7 +13,6 @@ import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.view.Window;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.TextView;
@@ -76,11 +75,8 @@ public class CustomDialogFactory {
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
dialog.setContentView(R.layout.custom_dialog_title_icon);
dialog.getWindow().setBackgroundDrawableResource(android.R.color.transparent);
if (((AndorsTrailApplication)context.getApplicationContext()).getPreferences().fullscreen) {
dialog.getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
} else {
dialog.getWindow().setFlags(0, WindowManager.LayoutParams.FLAG_FULLSCREEN);
}
boolean fullscreen = ((AndorsTrailApplication) context.getApplicationContext()).getPreferences().fullscreen;
AndorsTrailApplication.setFullscreenMode(fullscreen, dialog.getWindow());
setTitle(dialog, title, icon);

View File

@@ -20,13 +20,17 @@ public final class ItemEffectsView extends LinearLayout {
private final ActorConditionEffectList itemeffect_onequip_conditions;
private final ItemEffectsView_OnUse itemeffect_onuse;
private final ItemEffectsView_OnUse itemeffect_onhit;
private final ItemEffectsView_OnUse itemeffect_onmiss;
private final ItemEffectsView_OnUse itemeffect_onkill;
private final ItemEffectsView_OnHitReceived itemeffect_onhitreceived;
private final ItemEffectsView_OnHitReceived itemeffect_onmissreceived;
private final ItemEffectsView_OnDeath itemeffect_ondeath;
private final TextView itemeffect_onuse_title;
private final TextView itemeffect_onhit_title;
private final TextView itemeffect_onmiss_title;
private final TextView itemeffect_onkill_title;
private final TextView itemeffect_onhitreceived_title;
private final TextView itemeffect_onmissreceived_title;
private final TextView itemeffect_ondeath_title;
public ItemEffectsView(Context context, AttributeSet attr) {
@@ -41,13 +45,17 @@ public final class ItemEffectsView extends LinearLayout {
itemeffect_onuse = (ItemEffectsView_OnUse) findViewById(R.id.itemeffect_onuse);
itemeffect_onhit = (ItemEffectsView_OnUse) findViewById(R.id.itemeffect_onhit);
itemeffect_onmiss = (ItemEffectsView_OnUse) findViewById(R.id.itemeffect_onmiss);
itemeffect_onkill = (ItemEffectsView_OnUse) findViewById(R.id.itemeffect_onkill);
itemeffect_onhitreceived = (ItemEffectsView_OnHitReceived) findViewById(R.id.itemeffect_onhitreceived);
itemeffect_onmissreceived = (ItemEffectsView_OnHitReceived) findViewById(R.id.itemeffect_onmissreceived);
itemeffect_ondeath = (ItemEffectsView_OnDeath) findViewById(R.id.itemeffect_ondeath);
itemeffect_onuse_title = (TextView) findViewById(R.id.itemeffect_onuse_title);
itemeffect_onhit_title = (TextView) findViewById(R.id.itemeffect_onhit_title);
itemeffect_onmiss_title = (TextView) findViewById(R.id.itemeffect_onmiss_title);
itemeffect_onkill_title = (TextView) findViewById(R.id.itemeffect_onkill_title);
itemeffect_onhitreceived_title = (TextView) findViewById(R.id.itemeffect_onhitreceived_title);
itemeffect_onmissreceived_title = (TextView) findViewById(R.id.itemeffect_onmissreceived_title);
itemeffect_ondeath_title = (TextView) findViewById(R.id.itemeffect_ondeath_title);
}
@@ -55,8 +63,10 @@ public final class ItemEffectsView extends LinearLayout {
ItemTraits_OnEquip effects_equip,
Collection<ItemTraits_OnUse> effects_use,
Collection<ItemTraits_OnUse> effects_hit,
Collection<ItemTraits_OnUse> effects_miss,
Collection<ItemTraits_OnUse> effects_kill,
Collection<ItemTraits_OnHitReceived> effects_hitreceived,
Collection<ItemTraits_OnHitReceived> effects_missreceived,
ItemTraits_OnUse effects_death,
boolean isWeapon
) {
@@ -90,6 +100,12 @@ public final class ItemEffectsView extends LinearLayout {
} else {
itemeffect_onhit_title.setVisibility(View.GONE);
}
itemeffect_onmiss.update(effects_miss);
if (effects_miss != null) {
itemeffect_onmiss_title.setVisibility(View.VISIBLE);
} else {
itemeffect_onmiss_title.setVisibility(View.GONE);
}
itemeffect_onkill.update(effects_kill);
if (effects_kill != null) {
@@ -104,6 +120,12 @@ public final class ItemEffectsView extends LinearLayout {
} else {
itemeffect_onhitreceived_title.setVisibility(View.GONE);
}
itemeffect_onmissreceived.update(effects_missreceived);
if (effects_missreceived != null) {
itemeffect_onmissreceived_title.setVisibility(View.VISIBLE);
} else {
itemeffect_onmissreceived_title.setVisibility(View.GONE);
}
itemeffect_ondeath.update(effects_death);
if (effects_death != null) {

View File

@@ -1,6 +1,68 @@
I put both (release notes + forum announcement) into this source, so it will be easier to maintain them parallel:
APK 81 (0.8.14)
Release notes
=============
* New quests:
- "Darkness in the Daylight" and "Shadows" (from RaptorNXT)
- "A familiar shadow"
- "The exploded star"
- "The swamp healer"
- "Restless in the grave"
* 90 new maps, most of them in the new area "Mt.Galmore" south of Stoutford
* Many minor map fixes, typos and other little things
* Translations
Forum announcement //2025-07-20
==================
Hello, fellow adventurers!
Today we have the beta of one of the largest and most highly anticipated releases for you - Mt. Galmore and its devastated wastelands. Come take the next step in our journey and one step closer to finding the elusive heartstone.
This project was a collaborative effort between some great members of the community and the development team as once again we got contributions from RaptorNXT on multiple quests, Kutsu was instrumental in the construction of a lot of other content and quest ideas.
Travel south of Stoutford and west of the Sutdover River to explore and discover three quests and continue your epic search for Andor.
Another quest starts on the walls of Stoutford castle or in the Sullengard church - what you prefer.
Oh, and go exploring the Crossroads Guard shack and the southern regions of the Duleian Road and you will be rewarded or punished for some of your choices made in the past.
These quests and many little things along the way can be discovered.
You will be surprised.
[list]
6 New Quests
[list]"Darkness in the Daylight" and "Shadows"
(You must have completed Beer Bootlegging before as well as a few beginners quests.)[/list]
[list]"A familiar shadow"[/list]
[list]"The exploded star"
(Sullengard must have been visited)[/list]
[list]"The swamp healer"[/list]
[list]"Restless in the grave"[/list]
[/list]
[list]Continue "Search for Andor" and "Lost Treasure"[/list]
[list]Explore 90 new maps![/list]
[list]Bug Fixes and current translations[/list]
And now try it out, enjoy the new quests! And pay attention to inconsistencies, errors or other opportunities for improvement.
Please post your comments here in this thread or on Discord in the Beta testing channel.
This helps us to create again a release version in good quality.
This download will create a separate beta APK, so that you are independent of your productive game. Your saved games will remain untouched there.
Tip: You can export a saved game and import it into the beta (but never the other way around).
The first start might take long, maybe even minutes. Be patient, world map files are generated - but only on first start.
Here is is the link on our server: [url]https://andorstrail.com/static/AndorsTrail_v0.8.14_beta.apk[/url]
Don't forget to give us feedback about mistakes, what is good and what you don't like.
Happy playing! :D
APK 80 (0.8.13)
Release notes
@@ -18,7 +80,7 @@ Hey fellow adventurers - we have a new release now!
There have been many requests for a sequel to quests in the Thieves' World. Now here it is!
This is essentially a co-production between RaptorNXT and us. RaptorNXT created and formulated the quest log and dialogue, and we translated it into the game. It's a good division of labor, since a good story requires a surprising amount of detail work.
The quest starts in the Thieves' Guild. You will find it easily when you have completed all Thieves Guild quests up to, and including, The Ruthless Crackshot and Wanted Men.
The quest starts in the Thieves' Guild. You will find it easily when you have completed all Thieves' Guild quests up to, and including, The Ruthless Crackshot and Wanted Men.
[list]A new quest "Troubling Times"[/list]
[list]3 new maps (2 of which don't even have any connection to the new quest)[/list]
@@ -30,6 +92,8 @@ Google Play, F-droid, Github and Itch will follow soon.
Note: The first start might take long, maybe even minutes. Be patient, world map files are generated. This is only on first start.
Nevertheless please check this beta and try if it is working as expected: [url]https://andorstrail.com/static/AndorsTrail_v0.8.12.1beta.apk[/url]
And now: Happy playing! :D
@@ -153,7 +217,7 @@ Tip: You can export a saved game and import it into the beta (but never the othe
Here is is the link on our server: [url]https://andorstrail.com/static/AndorsTrail_v0.8.12_beta.apk[/url]
And dont forget to give us feedback about mistakes, what is good and what you dont like.
And don't forget to give us feedback about mistakes, what is good and what you don't like.
Happy playing!

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -5,6 +5,7 @@
android:layout_height="match_parent"
android:padding="@dimen/dialog_margin"
android:orientation="vertical"
android:id="@+id/about_root"
>
<LinearLayout
android:layout_width="match_parent"

View File

@@ -2,7 +2,8 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
android:orientation="vertical"
android:id="@+id/actorconditioninfo_root">
<TextView
android:id="@+id/actorconditioninfo_title"

View File

@@ -3,7 +3,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:orientation="vertical" >
android:orientation="vertical" android:id="@+id/bulkselection_root">
<TextView
android:id="@+id/bulkselection_itemname"

View File

@@ -4,6 +4,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:id="@+id/conversation_root"
>
<ListView

View File

@@ -2,7 +2,8 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
android:orientation="vertical"
android:id="@+id/worldmap_root">
<LinearLayout
android:layout_width="match_parent"

View File

@@ -12,19 +12,19 @@
android:layout_height="wrap_content"
/>
<com.gpl.rpg.AndorsTrail.view.AbilityModifierInfoView
android:id="@+id/itemeffect_onequip_abilitymodifierinfo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="10sp"
/>
<com.gpl.rpg.AndorsTrail.view.AbilityModifierInfoView
android:id="@+id/itemeffect_onequip_abilitymodifierinfo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="10sp"
/>
<com.gpl.rpg.AndorsTrail.view.ActorConditionEffectList
android:id="@+id/itemeffect_onequip_conditions"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="10sp"
/>
<com.gpl.rpg.AndorsTrail.view.ActorConditionEffectList
android:id="@+id/itemeffect_onequip_conditions"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="10sp"
/>
<TextView
android:text="@string/iteminfo_effect_works_when_used"
@@ -32,12 +32,12 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
<com.gpl.rpg.AndorsTrail.view.ItemEffectsView_OnUse
android:id="@+id/itemeffect_onuse"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="10sp"
/>
<com.gpl.rpg.AndorsTrail.view.ItemEffectsView_OnUse
android:id="@+id/itemeffect_onuse"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="10sp"
/>
<TextView
android:text="@string/iteminfo_effect_works_when_hitting_target"
@@ -45,12 +45,25 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
<com.gpl.rpg.AndorsTrail.view.ItemEffectsView_OnUse
android:id="@+id/itemeffect_onhit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="10sp"
/>
<com.gpl.rpg.AndorsTrail.view.ItemEffectsView_OnUse
android:id="@+id/itemeffect_onhit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="10sp"
/>
<TextView
android:text="@string/iteminfo_effect_works_when_missing_target"
android:id="@+id/itemeffect_onmiss_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
<com.gpl.rpg.AndorsTrail.view.ItemEffectsView_OnUse
android:id="@+id/itemeffect_onmiss"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="10sp"
/>
<TextView
android:text="@string/iteminfo_effect_works_when_killing_target"
@@ -58,12 +71,12 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
<com.gpl.rpg.AndorsTrail.view.ItemEffectsView_OnUse
android:id="@+id/itemeffect_onkill"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="10sp"
/>
<com.gpl.rpg.AndorsTrail.view.ItemEffectsView_OnUse
android:id="@+id/itemeffect_onkill"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="10sp"
/>
<TextView
android:text="@string/iteminfo_effect_works_when_hit_by_attacker"
@@ -71,23 +84,36 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
<com.gpl.rpg.AndorsTrail.view.ItemEffectsView_OnHitReceived
android:id="@+id/itemeffect_onhitreceived"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="10sp"
/>
<com.gpl.rpg.AndorsTrail.view.ItemEffectsView_OnHitReceived
android:id="@+id/itemeffect_onhitreceived"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="10sp"
/>
<TextView
android:text="@string/iteminfo_effect_works_when_missed_by_attacker"
android:id="@+id/itemeffect_onmissreceived_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
<com.gpl.rpg.AndorsTrail.view.ItemEffectsView_OnHitReceived
android:id="@+id/itemeffect_onmissreceived"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="10sp"
/>
<TextView
android:text="@string/iteminfo_effect_works_when_killed_by_attacker"
android:id="@+id/itemeffect_ondeath_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
<com.gpl.rpg.AndorsTrail.view.ItemEffectsView_OnDeath
android:id="@+id/itemeffect_ondeath"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="10sp"
/>
<com.gpl.rpg.AndorsTrail.view.ItemEffectsView_OnDeath
android:id="@+id/itemeffect_ondeath"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="10sp"
/>
</merge>

View File

@@ -2,7 +2,8 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
android:orientation="vertical"
android:id="@+id/iteminfo_root">
<TextView
android:id="@+id/iteminfo_title"

View File

@@ -3,7 +3,8 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="@dimen/dialog_margin" >
android:padding="@dimen/dialog_margin"
android:id="@+id/levelup_root">
<TextView
android:id="@+id/levelup_title"

View File

@@ -5,6 +5,7 @@
android:layout_height="wrap_content"
android:padding="@dimen/dialog_margin"
android:orientation="vertical"
android:id="@+id/loadsave_root"
>
<TextView

View File

@@ -2,7 +2,8 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" >
android:orientation="vertical"
android:id="@+id/monsterencounter_root">
<TextView
android:id="@+id/monsterencounter_title"

View File

@@ -2,7 +2,8 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
android:orientation="vertical"
android:id="@+id/monsterinfo_root">
<TextView
android:id="@+id/monsterinfo_title"

View File

@@ -2,7 +2,8 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
android:orientation="vertical"
android:id="@+id/skillinfo_root">
<TextView
android:id="@+id/skillinfo_title"

View File

@@ -348,10 +348,12 @@
<string name="iteminfo_effect_works_on_target">On target</string>
<string name="iteminfo_effect_works_on_attacker">On attacker</string>
<string name="iteminfo_effect_works_when_hitting_target">When hitting target</string>
<string name="iteminfo_effect_works_when_missing_target">When missing target</string>
<string name="iteminfo_effect_works_when_killing_target">On every kill</string>
<string name="iteminfo_effect_works_when_used">When used</string>
<string name="iteminfo_effect_works_when_equipped">When equipped</string>
<string name="iteminfo_effect_works_when_hit_by_attacker">When hit by attacker</string>
<string name="iteminfo_effect_works_when_missed_by_attacker">When missed by attacker</string>
<string name="iteminfo_effect_works_when_killed_by_attacker">When killed by attacker</string>
<string name="iteminfo_effect_decrease_current_hp">Drains %1$s HP</string>

View File

@@ -0,0 +1,258 @@
import re
import os
import argparse
from xml.etree import ElementTree as ET
def get_string_value_and_specifiers(filepath, key_name):
"""
Parses an XML strings file to find a specific key and its format specifiers.
Args:
filepath (str): Path to the strings.xml file.
key_name (str): The name of the string resource to find.
Returns:
tuple: (string_value, set_of_specifiers) or (None, None) if key not found.
Specifiers are returned as a set, e.g., {"%1$s", "%2$d"}.
"""
try:
tree = ET.parse(filepath)
root = tree.getroot()
for string_tag in root.findall('string'):
if string_tag.get('name') == key_name:
value = string_tag.text if string_tag.text else ""
# Regex to find format specifiers like %s, %d, %1$s, %2$d, etc.
# It handles optional positional arguments (e.g., 1$) and type characters.
specifiers = set(re.findall(r'%(?:(?:\d+\$)?(?:[sdfeoxXgGaAbhHc]|(?:\.\d[fd])))', value))
return value, specifiers
except ET.ParseError:
print(f"Warning: Could not parse XML file: {filepath}")
except FileNotFoundError:
print(f"Warning: File not found: {filepath}")
return None, None
def find_res_directories(project_root):
"""
Finds all 'res' directories within a project, typically in module roots.
"""
res_dirs = []
for root_dir, dirs, _ in os.walk(project_root):
if 'res' in dirs:
res_dirs.append(os.path.join(root_dir, 'res'))
if not res_dirs and os.path.basename(project_root) == 'res': # If project_root itself is a res dir
res_dirs.append(project_root)
return res_dirs
def find_strings_files(res_dir_path, base_filename="strings.xml"):
"""
Finds all strings.xml files (or variants like strings-es.xml)
within a given 'res' directory.
"""
strings_files = {} # lang_code -> filepath
for dirpath, _, filenames in os.walk(res_dir_path):
if "values" in os.path.basename(dirpath).lower(): # e.g., values, values-es, values-en-rGB
for filename in filenames:
if filename.startswith(os.path.splitext(base_filename)[0]) and filename.endswith(".xml"):
full_path = os.path.join(dirpath, filename)
# Determine language code from directory name (e.g., "values-es" -> "es")
# or default if it's just "values"
dir_name_parts = os.path.basename(dirpath).split('-')
lang_code = "default" # For the base "values" folder
if len(dir_name_parts) > 1:
lang_code = "-".join(dir_name_parts[1:]) # Handles values-en-rUS correctly
strings_files[lang_code] = full_path
return strings_files
def find_non_escaped_percent(value):
"""
Finds non-escaped % characters in a string (not part of %% or a valid format specifier).
Returns a list of indices where such % occur.
"""
# Find all % positions
percent_indices = [m.start() for m in re.finditer(r'%', value)]
# Find all valid format specifiers and %% positions
valid_specifier_pattern = r'%(?:%|(?:\d+\$)?(?:[sdfeoxXgGaAbhHc]|(?:\.\d[fd])))'
valid_matches = [m.span() for m in re.finditer(valid_specifier_pattern, value)]
# Mark all indices covered by valid specifiers or %%
covered_indices = set()
for start, end in valid_matches:
covered_indices.update(range(start, end))
# Only report % indices not covered by valid specifiers or %%
return [idx for idx in percent_indices if idx not in covered_indices]
def get_all_keys(filepath):
"""
Returns a list of all string resource keys in the given XML file.
"""
try:
tree = ET.parse(filepath)
root = tree.getroot()
return [string_tag.get('name') for string_tag in root.findall('string') if string_tag.get('name')]
except Exception:
return []
def find_used_keys_in_java(project_root):
"""
Scans all .java files under project_root for usages of string resource keys.
Returns a set of keys found.
"""
key_pattern = re.compile(r'R\.string\.([a-zA-Z0-9_]+)')
used_keys = set()
for root, _, files in os.walk(project_root):
for fname in files:
if fname.endswith('.java'):
try:
with open(os.path.join(root, fname), encoding='utf-8') as f:
content = f.read()
used_keys.update(key_pattern.findall(content))
except Exception:
pass
return used_keys
def main():
parser = argparse.ArgumentParser(
description="Check format specifier consistency across language files for a given string key."
)
parser.add_argument(
"project_root",
help="Path to the Android project's root directory (or a specific module's root directory)."
)
parser.add_argument(
"res_root",
help="Path to the some 'res' directory."
)
parser.add_argument(
"key_name",
nargs="?",
default=None,
help="The name of the string resource to check (e.g., 'skill_longdescription_evasion'). If omitted, checks all keys in the base language file."
)
parser.add_argument(
"--base_lang",
default="default",
help="The language code for the base/reference strings file (e.g., 'en', 'default' for values/strings.xml). Default is 'default'."
)
parser.add_argument(
"--strings_filename",
default="strings.xml",
help="The base name of your strings files (default: strings.xml)."
)
args = parser.parse_args()
print(f"Using base language: '{args.base_lang}' from file '{args.strings_filename}'")
print(f"Project root path: {args.project_root}\n")
print(f"Project res path: {args.res_root}\n")
res_directories = find_res_directories(args.res_root)
if not res_directories:
print(f"Error: No 'res' directory found under {args.res_root}")
return
all_strings_files = {}
for res_dir in res_directories:
all_strings_files.update(find_strings_files(res_dir, args.strings_filename))
if not all_strings_files:
print(f"Error: No '{args.strings_filename}' files found in any 'res/values-*' directories under {args.res_root}.")
return
base_file_path = all_strings_files.get(args.base_lang)
if not base_file_path:
if args.base_lang != "default" and all_strings_files.get("default"):
print(f"Warning: Base language '{args.base_lang}' not found. Using 'default' (values/{args.strings_filename}) as base.")
base_file_path = all_strings_files.get("default")
args.base_lang = "default"
else:
print(f"Error: Base strings file for language '{args.base_lang}' not found.")
print(f"Available language files found: {list(all_strings_files.keys())}")
return
# If no key_name is provided, check only keys used in .java files
if args.key_name is None:
used_keys = find_used_keys_in_java(args.project_root)
all_keys = set(get_all_keys(base_file_path))
keys_to_check = sorted(list(all_keys & used_keys))
if not keys_to_check:
print('no keys to check')
return
else:
keys_to_check = [args.key_name]
total_issues_found = 0
file_error_counts = {}
file_warning_counts = {}
for key_name in keys_to_check:
base_value, base_specifiers = get_string_value_and_specifiers(base_file_path, key_name)
if base_specifiers is None:
continue
for lang_code, file_path in all_strings_files.items():
if lang_code == args.base_lang:
continue
current_value, current_specifiers = get_string_value_and_specifiers(file_path, key_name)
if current_specifiers is None:
continue
error_count = 0
warning_count = 0
non_escaped_percent_indices = find_non_escaped_percent(current_value)
if non_escaped_percent_indices:
error_count += 1
print(f"--- Language: {lang_code} (NON-ESCAPED % FOUND) ---")
print(f"Key: {key_name}")
print(f"File: {file_path}")
print(f"Value: \"{current_value}\"")
print(f"Non-escaped % at positions: {non_escaped_percent_indices}")
print("-" * 20)
if current_specifiers != base_specifiers:
missing_in_current = base_specifiers - current_specifiers
extra_in_current = current_specifiers - base_specifiers
if extra_in_current:
error_count += 1
print(f"--- Language: {lang_code} (ISSUE FOUND) ---")
print(f"Key: {key_name}")
print(f"File: {file_path}")
print(f"Value: \"{current_value}\"")
print(f"Specifiers: {sorted(list(current_specifiers)) if current_specifiers else 'None'}")
print(f"Expected specifiers (from base): {sorted(list(base_specifiers)) if base_specifiers else 'None'}")
print(f" EXTRA in '{lang_code}': {sorted(list(extra_in_current))}")
print("-" * 20)
if missing_in_current:
warning_count += 1
print(f"--- Language: {lang_code} (WARNING: MISSING SPECIFIERS) ---")
print(f"Key: {key_name}")
print(f"File: {file_path}")
print(f"Value: \"{current_value}\"")
print(f"Specifiers: {sorted(list(current_specifiers)) if current_specifiers else 'None'}")
print(f"Expected specifiers (from base): {sorted(list(base_specifiers)) if base_specifiers else 'None'}")
print(f" MISSING in '{lang_code}': {sorted(list(missing_in_current))}")
print("-" * 20)
if error_count:
file_error_counts[file_path] = file_error_counts.get(file_path, 0) + error_count
if warning_count:
file_warning_counts[file_path] = file_warning_counts.get(file_path, 0) + warning_count
if file_error_counts or file_warning_counts:
print("\nSummary of errors and warnings per file:")
for file_path in sorted(set(list(file_error_counts.keys()) + list(file_warning_counts.keys()))):
error_str = f"{file_error_counts.get(file_path, 0)} error(s)"
warning_str = f"{file_warning_counts.get(file_path, 0)} warning(s)"
print(f"{file_path}:\t {error_str:>5}, {warning_str:>5}")
total_errors = sum(file_error_counts.values())
total_warnings = sum(file_warning_counts.values())
print(f"\nTOTAL: {total_errors} error(s), {total_warnings} warning(s)")
if file_error_counts:
exit(1)
if __name__ == "__main__":
main()

5
useful_links.html Normal file
View File

@@ -0,0 +1,5 @@
[b][color=#0080FF]v0.8.13[/color] (Troubling Times) active on Google Play[/b]
[b]Useful links[/b]
[size=80] [url=https://github.com/AndorsTrailRelease/andors-trail][b]Source code of the game[/b][/url] - [url=https://andorstrail.com/viewtopic.php?f=6&t=4560][b]Contribution guide[/b][/url] - [url=https://andorstrail.com/viewtopic.php?f=6&t=4806][b]ATCS Editor[/b][/url] - [url=https://hosted.weblate.org/projects/andors-trail/][b]Translate the game on Weblate[/b][/url] - [url=https://andorstrail.com/viewtopic.php?f=12&t=4583][b]Example walkthrough[/b][/url] - [url=https://andorstrail-directory.github.io/][b]Andor's Trail Directory[/b][/url] - [url=https://discord.gg/FgwXdy6][b]Join the Discord[/b][/url][/size]
[size=80][b]Get the game (v0.8.13) from[/b] [url=https://andorstrail.app]Google[/url], [url=https://github.com/AndorsTrailRelease/andors-trail/releases]Github[/url], [url=https://f-droid.org/packages/com.gpl.rpg.AndorsTrail/]F-Droid[/url], [url=https://andorstrail.com/static/AndorsTrail_v0.8.13.apk]our server[/url], [b]or [/b][url=https://andorstrailteam.itch.io/andors-trail] itch.io[/url]