Implement checking for savegame edits with a hash

This commit is contained in:
OMGeeky
2024-01-26 17:19:03 +01:00
parent 3fbc7cf65f
commit 4c2113c58c
21 changed files with 417 additions and 18 deletions

View File

@@ -32,7 +32,7 @@ public final class AndorsTrailApplication extends Application {
public static final boolean IS_RELEASE_VERSION = !CURRENT_VERSION_DISPLAY.matches(".*[a-d].*"); 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 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 DEVELOPMENT_INCOMPATIBLE_SAVEGAME_VERSION = 999;
public static final int CURRENT_VERSION = DEVELOPMENT_INCOMPATIBLE_SAVEGAMES ? DEVELOPMENT_INCOMPATIBLE_SAVEGAME_VERSION : 70; public static final int CURRENT_VERSION = DEVELOPMENT_INCOMPATIBLE_SAVEGAMES ? DEVELOPMENT_INCOMPATIBLE_SAVEGAME_VERSION : 71;
private final AndorsTrailPreferences preferences = new AndorsTrailPreferences(); private final AndorsTrailPreferences preferences = new AndorsTrailPreferences();
private WorldContext world = new WorldContext(); private WorldContext world = new WorldContext();

View File

@@ -134,8 +134,9 @@ public final class WorldSetup {
onSceneLoadedListener = null; onSceneLoadedListener = null;
if (o == null) return; if (o == null) return;
if (loadResult == Savegames.LoadSavegameResult.success) { if (loadResult == Savegames.LoadSavegameResult.success
o.onSceneLoaded(); || loadResult == Savegames.LoadSavegameResult.editDetected) {
o.onSceneLoaded(loadResult);
} else { } else {
o.onSceneLoadFailed(loadResult); o.onSceneLoadFailed(loadResult);
} }
@@ -161,7 +162,7 @@ public final class WorldSetup {
public static interface OnSceneLoadedListener { public static interface OnSceneLoadedListener {
void onSceneLoaded(); void onSceneLoaded(Savegames.LoadSavegameResult loadResult);
void onSceneLoadFailed(Savegames.LoadSavegameResult loadResult); void onSceneLoadFailed(Savegames.LoadSavegameResult loadResult);
} }
public interface OnResourcesLoadedListener { public interface OnResourcesLoadedListener {

View File

@@ -141,13 +141,18 @@ public final class LoadingActivity extends AndorsTrailBaseActivity implements On
@Override @Override
public void onSceneLoaded() { public void onSceneLoaded(Savegames.LoadSavegameResult loadResult) {
synchronized (semaphore) { synchronized (semaphore) {
if (progressDialog != null) progressDialog.dismiss(); if (progressDialog != null) progressDialog.dismiss();
loaded =true; loaded =true;
} }
startActivity(new Intent(this, MainActivity.class));
this.finish(); if (loadResult == Savegames.LoadSavegameResult.editDetected) {
showLoadingWarnDialog(R.string.dialog_loading_warning_edit);
}else{
startActivity(new Intent(this, MainActivity.class));
this.finish();
}
} }
@Override @Override
@@ -165,6 +170,19 @@ public final class LoadingActivity extends AndorsTrailBaseActivity implements On
} }
} }
private void showLoadingWarnDialog(int messageResourceID) {
final CustomDialog d = CustomDialogFactory.createDialog(this, getResources().getString(R.string.dialog_loading_warning_title), null, getResources().getString(messageResourceID), null, true);
CustomDialogFactory.addDismissButton(d, android.R.string.ok);
CustomDialogFactory.setDismissListener(d, new OnDismissListener() {
@Override
public void onDismiss(DialogInterface dialog) {
startActivity(new Intent(LoadingActivity.this, MainActivity.class));
LoadingActivity.this.finish();
}
});
CustomDialogFactory.show(d);
}
private void showLoadingFailedDialog(int messageResourceID) { private void showLoadingFailedDialog(int messageResourceID) {
final CustomDialog d = CustomDialogFactory.createDialog(this, getResources().getString(R.string.dialog_loading_failed_title), null, getResources().getString(messageResourceID), null, true); final CustomDialog d = CustomDialogFactory.createDialog(this, getResources().getString(R.string.dialog_loading_failed_title), null, getResources().getString(messageResourceID), null, true);
CustomDialogFactory.addDismissButton(d, android.R.string.ok); CustomDialogFactory.addDismissButton(d, android.R.string.ok);

View File

@@ -1,5 +1,8 @@
package com.gpl.rpg.AndorsTrail.context; package com.gpl.rpg.AndorsTrail.context;
import android.os.Build;
import android.support.annotation.RequiresApi;
import com.gpl.rpg.AndorsTrail.model.ModelContainer; import com.gpl.rpg.AndorsTrail.model.ModelContainer;
import com.gpl.rpg.AndorsTrail.model.ability.ActorConditionTypeCollection; import com.gpl.rpg.AndorsTrail.model.ability.ActorConditionTypeCollection;
import com.gpl.rpg.AndorsTrail.model.ability.SkillCollection; import com.gpl.rpg.AndorsTrail.model.ability.SkillCollection;
@@ -12,6 +15,10 @@ import com.gpl.rpg.AndorsTrail.model.quest.QuestCollection;
import com.gpl.rpg.AndorsTrail.resource.ConversationLoader; import com.gpl.rpg.AndorsTrail.resource.ConversationLoader;
import com.gpl.rpg.AndorsTrail.resource.VisualEffectCollection; import com.gpl.rpg.AndorsTrail.resource.VisualEffectCollection;
import com.gpl.rpg.AndorsTrail.resource.tiles.TileManager; import com.gpl.rpg.AndorsTrail.resource.tiles.TileManager;
import com.gpl.rpg.AndorsTrail.util.ByteUtils;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public final class WorldContext { public final class WorldContext {
//Objectcollections //Objectcollections
@@ -62,4 +69,13 @@ public final class WorldContext {
public void resetForNewGame() { public void resetForNewGame() {
maps.resetForNewGame(); maps.resetForNewGame();
} }
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
public String createHash() throws NoSuchAlgorithmException {
MessageDigest digest = MessageDigest.getInstance("MD5");
this.maps.createHash(digest);
this.model.createHash(digest);
byte[] hash = digest.digest();
return ByteUtils.toHexString(hash);
}
} }

View File

@@ -3,6 +3,7 @@ package com.gpl.rpg.AndorsTrail.model;
import java.io.DataInputStream; import java.io.DataInputStream;
import java.io.DataOutputStream; import java.io.DataOutputStream;
import java.io.IOException; import java.io.IOException;
import java.security.MessageDigest;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
@@ -12,6 +13,8 @@ import java.util.Map.Entry;
import java.util.Set; import java.util.Set;
import android.content.res.Resources; import android.content.res.Resources;
import android.os.Build;
import android.support.annotation.RequiresApi;
import com.gpl.rpg.AndorsTrail.R; import com.gpl.rpg.AndorsTrail.R;
import com.gpl.rpg.AndorsTrail.context.WorldContext; import com.gpl.rpg.AndorsTrail.context.WorldContext;
@@ -19,6 +22,7 @@ import com.gpl.rpg.AndorsTrail.model.actor.MonsterType;
import com.gpl.rpg.AndorsTrail.model.item.ItemType; import com.gpl.rpg.AndorsTrail.model.item.ItemType;
import com.gpl.rpg.AndorsTrail.model.map.PredefinedMap; import com.gpl.rpg.AndorsTrail.model.map.PredefinedMap;
import com.gpl.rpg.AndorsTrail.model.quest.Quest; import com.gpl.rpg.AndorsTrail.model.quest.Quest;
import com.gpl.rpg.AndorsTrail.util.ByteUtils;
import com.gpl.rpg.AndorsTrail.util.HashMapHelper; import com.gpl.rpg.AndorsTrail.util.HashMapHelper;
public final class GameStatistics { public final class GameStatistics {
@@ -214,4 +218,21 @@ public final class GameStatistics {
dest.writeInt(startLives); dest.writeInt(startLives);
dest.writeBoolean(unlimitedSaves); dest.writeBoolean(unlimitedSaves);
} }
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
public void createHash(MessageDigest digest) {
digest.update(ByteUtils.toBytes(deaths));
for (Entry<String, Integer> e : killedMonstersByTypeID.entrySet()) {
digest.update(ByteUtils.toBytes(e.getKey()));
digest.update(ByteUtils.toBytes(e.getValue()));
}
for (Entry<String, Integer> e : usedItems.entrySet()) {
digest.update(ByteUtils.toBytes(e.getKey()));
digest.update(ByteUtils.toBytes(e.getValue()));
}
digest.update(ByteUtils.toBytes(spentGold));
digest.update(ByteUtils.toBytes(startLives));
digest.update(ByteUtils.toBytes(unlimitedSaves));
}
} }

View File

@@ -3,8 +3,10 @@ package com.gpl.rpg.AndorsTrail.model;
import java.io.DataInputStream; import java.io.DataInputStream;
import java.io.DataOutputStream; import java.io.DataOutputStream;
import java.io.IOException; import java.io.IOException;
import java.security.MessageDigest;
import com.gpl.rpg.AndorsTrail.model.actor.Monster; import com.gpl.rpg.AndorsTrail.model.actor.Monster;
import com.gpl.rpg.AndorsTrail.util.ByteUtils;
import com.gpl.rpg.AndorsTrail.util.Coord; import com.gpl.rpg.AndorsTrail.util.Coord;
public final class InterfaceData { public final class InterfaceData {
@@ -51,4 +53,12 @@ public final class InterfaceData {
} }
dest.writeUTF(selectedTabHeroInfo); dest.writeUTF(selectedTabHeroInfo);
} }
public void createHash(MessageDigest digest) {
digest.update(ByteUtils.toBytes(isMainActivityVisible));
digest.update(ByteUtils.toBytes(isInCombat));
if(selectedPosition != null){
selectedPosition.createHash(digest);
}
}
} }

View File

@@ -1,12 +1,17 @@
package com.gpl.rpg.AndorsTrail.model; package com.gpl.rpg.AndorsTrail.model;
import android.os.Build;
import android.support.annotation.RequiresApi;
import java.io.DataInputStream; import java.io.DataInputStream;
import java.io.DataOutputStream; import java.io.DataOutputStream;
import java.io.IOException; import java.io.IOException;
import java.security.MessageDigest;
import com.gpl.rpg.AndorsTrail.context.ControllerContext; import com.gpl.rpg.AndorsTrail.context.ControllerContext;
import com.gpl.rpg.AndorsTrail.context.WorldContext; import com.gpl.rpg.AndorsTrail.context.WorldContext;
import com.gpl.rpg.AndorsTrail.model.actor.Player; import com.gpl.rpg.AndorsTrail.model.actor.Player;
import com.gpl.rpg.AndorsTrail.util.ByteUtils;
public final class ModelContainer { public final class ModelContainer {
@@ -49,4 +54,14 @@ public final class ModelContainer {
statistics.writeToParcel(dest); statistics.writeToParcel(dest);
worldData.writeToParcel(dest); worldData.writeToParcel(dest);
} }
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
public void createHash(MessageDigest digest) {
player.createHash(digest);
digest.update(ByteUtils.toBytes(currentMaps.map.name));
uiSelections.createHash(digest);
statistics.createHash(digest);
worldData.createHash(digest);
}
} }

View File

@@ -1,8 +1,14 @@
package com.gpl.rpg.AndorsTrail.model; package com.gpl.rpg.AndorsTrail.model;
import android.os.Build;
import android.support.annotation.RequiresApi;
import com.gpl.rpg.AndorsTrail.util.ByteUtils;
import java.io.DataInputStream; import java.io.DataInputStream;
import java.io.DataOutputStream; import java.io.DataOutputStream;
import java.io.IOException; import java.io.IOException;
import java.security.MessageDigest;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
@@ -56,4 +62,13 @@ public final class WorldData {
dest.writeLong(e.getValue()); dest.writeLong(e.getValue());
} }
} }
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
public void createHash(MessageDigest digest) {
digest.update(ByteUtils.toBytes(worldTime));
for (Map.Entry<String, Long> e: timers.entrySet() ) {
digest.update(ByteUtils.toBytes(e.getKey()));
digest.update(ByteUtils.toBytes(e.getValue()));
}
}
} }

View File

@@ -1,10 +1,15 @@
package com.gpl.rpg.AndorsTrail.model.ability; package com.gpl.rpg.AndorsTrail.model.ability;
import android.os.Build;
import android.support.annotation.RequiresApi;
import java.io.DataInputStream; import java.io.DataInputStream;
import java.io.DataOutputStream; import java.io.DataOutputStream;
import java.io.IOException; import java.io.IOException;
import java.security.MessageDigest;
import com.gpl.rpg.AndorsTrail.context.WorldContext; import com.gpl.rpg.AndorsTrail.context.WorldContext;
import com.gpl.rpg.AndorsTrail.util.ByteUtils;
public final class ActorCondition { public final class ActorCondition {
public static final int MAGNITUDE_REMOVE_ALL = -99; public static final int MAGNITUDE_REMOVE_ALL = -99;
@@ -45,4 +50,12 @@ public final class ActorCondition {
dest.writeInt(magnitude); dest.writeInt(magnitude);
dest.writeInt(duration); dest.writeInt(duration);
} }
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
public void createHash(MessageDigest digest) {
digest.update(ByteUtils.toBytes(conditionType.conditionTypeID));
digest.update(ByteUtils.toBytes(magnitude));
digest.update(ByteUtils.toBytes(duration));
}
} }

View File

@@ -1,8 +1,12 @@
package com.gpl.rpg.AndorsTrail.model.actor; package com.gpl.rpg.AndorsTrail.model.actor;
import android.os.Build;
import android.support.annotation.RequiresApi;
import java.io.DataInputStream; import java.io.DataInputStream;
import java.io.DataOutputStream; import java.io.DataOutputStream;
import java.io.IOException; import java.io.IOException;
import java.security.MessageDigest;
import com.gpl.rpg.AndorsTrail.context.WorldContext; import com.gpl.rpg.AndorsTrail.context.WorldContext;
import com.gpl.rpg.AndorsTrail.controller.Constants; import com.gpl.rpg.AndorsTrail.controller.Constants;
@@ -13,6 +17,7 @@ import com.gpl.rpg.AndorsTrail.model.item.ItemContainer;
import com.gpl.rpg.AndorsTrail.model.item.Loot; import com.gpl.rpg.AndorsTrail.model.item.Loot;
import com.gpl.rpg.AndorsTrail.model.map.MonsterSpawnArea; import com.gpl.rpg.AndorsTrail.model.map.MonsterSpawnArea;
import com.gpl.rpg.AndorsTrail.savegames.LegacySavegameFormatReaderForMonster; import com.gpl.rpg.AndorsTrail.savegames.LegacySavegameFormatReaderForMonster;
import com.gpl.rpg.AndorsTrail.util.ByteUtils;
import com.gpl.rpg.AndorsTrail.util.Coord; import com.gpl.rpg.AndorsTrail.util.Coord;
import com.gpl.rpg.AndorsTrail.util.CoordRect; import com.gpl.rpg.AndorsTrail.util.CoordRect;
import com.gpl.rpg.AndorsTrail.util.Range; import com.gpl.rpg.AndorsTrail.util.Range;
@@ -193,4 +198,28 @@ public final class Monster extends Actor {
dest.writeBoolean(false); dest.writeBoolean(false);
} }
} }
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
public void createHash(MessageDigest digest) {
digest.update(ByteUtils.toBytes(getMonsterTypeID()));
digest.update(ByteUtils.toBytes(attackCost));
digest.update(ByteUtils.toBytes(attackChance));
digest.update(ByteUtils.toBytes(criticalSkill));
digest.update(ByteUtils.toBytes(criticalMultiplier));
damagePotential.createHash(digest);
digest.update(ByteUtils.toBytes(blockChance));
digest.update(ByteUtils.toBytes(damageResistance));
ap.createHash(digest);
health.createHash(digest);
position.createHash(digest);
for (ActorCondition c: conditions){
c.createHash(digest);
}
digest.update(ByteUtils.toBytes(moveCost));
digest.update(ByteUtils.toBytes(forceAggressive));
if (shopItems != null) {
shopItems.createHash(digest);
}
}
} }

View File

@@ -3,6 +3,7 @@ package com.gpl.rpg.AndorsTrail.model.actor;
import java.io.DataInputStream; import java.io.DataInputStream;
import java.io.DataOutputStream; import java.io.DataOutputStream;
import java.io.IOException; import java.io.IOException;
import java.security.MessageDigest;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
@@ -13,6 +14,8 @@ import java.util.List;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.UUID; import java.util.UUID;
import android.os.Build;
import android.support.annotation.RequiresApi;
import android.util.SparseIntArray; import android.util.SparseIntArray;
import com.gpl.rpg.AndorsTrail.AndorsTrailApplication; import com.gpl.rpg.AndorsTrail.AndorsTrailApplication;
@@ -27,6 +30,7 @@ import com.gpl.rpg.AndorsTrail.model.item.Loot;
import com.gpl.rpg.AndorsTrail.model.quest.Quest; import com.gpl.rpg.AndorsTrail.model.quest.Quest;
import com.gpl.rpg.AndorsTrail.model.quest.QuestProgress; import com.gpl.rpg.AndorsTrail.model.quest.QuestProgress;
import com.gpl.rpg.AndorsTrail.savegames.LegacySavegameFormatReaderForPlayer; import com.gpl.rpg.AndorsTrail.savegames.LegacySavegameFormatReaderForPlayer;
import com.gpl.rpg.AndorsTrail.util.ByteUtils;
import com.gpl.rpg.AndorsTrail.util.Coord; import com.gpl.rpg.AndorsTrail.util.Coord;
import com.gpl.rpg.AndorsTrail.util.Range; import com.gpl.rpg.AndorsTrail.util.Range;
import com.gpl.rpg.AndorsTrail.util.Size; import com.gpl.rpg.AndorsTrail.util.Size;
@@ -55,9 +59,11 @@ public final class Player extends Actor {
public String id = UUID.randomUUID().toString(); public String id = UUID.randomUUID().toString();
public long savedVersion = 1; // the version get's increased for cheat detection everytime a player with limited saves is saved public long savedVersion = 1; // the version get's increased for cheat detection everytime a player with limited saves is saved
public boolean wasEditingDetected;
public String hash;
// Unequipped stats // Unequipped stats
public static final class PlayerBaseTraits { public static final class PlayerBaseTraits {
public int iconID; public int iconID;
public int maxAP; public int maxAP;
@@ -416,6 +422,13 @@ public final class Player extends Actor {
this.id = src.readUTF(); this.id = src.readUTF();
this.savedVersion = src.readLong(); this.savedVersion = src.readLong();
} }
if (fileversion >= 71){
this.hash = src.readUTF();
this.wasEditingDetected = src.readBoolean();
}else{
this.hash = "";
this.wasEditingDetected = false;
}
} }
public void writeToParcel(DataOutputStream dest) throws IOException { public void writeToParcel(DataOutputStream dest) throws IOException {
@@ -474,6 +487,58 @@ public final class Player extends Actor {
} }
dest.writeUTF(id); dest.writeUTF(id);
dest.writeLong(savedVersion); dest.writeLong(savedVersion);
dest.writeUTF(hash);
dest.writeBoolean(wasEditingDetected);
}
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
public void createHash(MessageDigest digest) {
//skipping icon ID so that it can be changed without hash difference
digest.update(ByteUtils.toBytes(baseTraits.maxAP));
digest.update(ByteUtils.toBytes(baseTraits.maxHP));
//skipping name so that it can be changed without hash difference
digest.update(ByteUtils.toBytes(moveCost));
digest.update(ByteUtils.toBytes(baseTraits.attackCost));
digest.update(ByteUtils.toBytes(baseTraits.attackChance));
digest.update(ByteUtils.toBytes(baseTraits.criticalSkill));
digest.update(ByteUtils.toBytes(baseTraits.criticalMultiplier));
baseTraits.damagePotential.createHash(digest);
digest.update(ByteUtils.toBytes(baseTraits.blockChance));
digest.update(ByteUtils.toBytes(baseTraits.damageResistance));
digest.update(ByteUtils.toBytes(baseTraits.moveCost));
ap.createHash(digest);
health.createHash(digest);
position.createHash(digest);
for(ActorCondition c : conditions){
c.createHash(digest);
}
lastPosition.createHash(digest);
nextPosition.createHash(digest);
digest.update(ByteUtils.toBytes(level));
digest.update(ByteUtils.toBytes(totalExperience));
inventory.createHash(digest);
digest.update(ByteUtils.toBytes(baseTraits.useItemCost));
digest.update(ByteUtils.toBytes(baseTraits.reequipCost));
for (int i = 0; i<skillLevels.size(); i++){
digest.update(ByteUtils.toBytes(skillLevels.keyAt(i)));
digest.update(ByteUtils.toBytes(skillLevels.valueAt(i)));
}
digest.update(ByteUtils.toBytes(spawnMap));
digest.update(ByteUtils.toBytes(spawnPlace));
for (Entry<String, LinkedHashSet<Integer> > e:questProgress.entrySet() ) {
digest.update(ByteUtils.toBytes(e.getKey()));
for(int progress: e.getValue()){
digest.update(ByteUtils.toBytes(progress));
}
}
digest.update(ByteUtils.toBytes(availableSkillIncreases));
for (Entry<String, Integer> e:alignments.entrySet() ) {
digest.update(ByteUtils.toBytes(e.getKey()));
digest.update(ByteUtils.toBytes(e.getValue()));
}
digest.update(ByteUtils.toBytes(id));
// digest.update(ByteUtils.toBytes(savedVersion));
// digest.update(ByteUtils.toBytes(wasEditingDetected));
} }
} }

View File

@@ -1,14 +1,19 @@
package com.gpl.rpg.AndorsTrail.model.item; package com.gpl.rpg.AndorsTrail.model.item;
import android.os.Build;
import android.support.annotation.RequiresApi;
import java.io.DataInputStream; import java.io.DataInputStream;
import java.io.DataOutputStream; import java.io.DataOutputStream;
import java.io.IOException; import java.io.IOException;
import java.security.MessageDigest;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import com.gpl.rpg.AndorsTrail.context.WorldContext; import com.gpl.rpg.AndorsTrail.context.WorldContext;
import com.gpl.rpg.AndorsTrail.model.actor.Player; import com.gpl.rpg.AndorsTrail.model.actor.Player;
import com.gpl.rpg.AndorsTrail.util.ByteUtils;
public class ItemContainer { public class ItemContainer {
public final ArrayList<ItemEntry> items = new ArrayList<ItemEntry>(); public final ArrayList<ItemEntry> items = new ArrayList<ItemEntry>();
@@ -23,7 +28,8 @@ public class ItemContainer {
return result; return result;
} }
public static final class ItemEntry {
public static final class ItemEntry {
public final ItemType itemType; public final ItemType itemType;
public int quantity; public int quantity;
public ItemEntry(ItemType itemType, int initialQuantity) { public ItemEntry(ItemType itemType, int initialQuantity) {
@@ -42,6 +48,12 @@ public class ItemContainer {
dest.writeUTF(itemType.id); dest.writeUTF(itemType.id);
dest.writeInt(quantity); dest.writeInt(quantity);
} }
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
public void createHash(MessageDigest digest) {
digest.update(ByteUtils.toBytes(itemType.id));
digest.update(ByteUtils.toBytes(quantity));
}
} }
public void addItem(ItemType itemType, int quantity) { public void addItem(ItemType itemType, int quantity) {
@@ -280,4 +292,12 @@ public class ItemContainer {
e.writeToParcel(dest); e.writeToParcel(dest);
} }
} }
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
public void createHash(MessageDigest digest) {
for (ItemEntry e :
items) {
e.createHash(digest);
}
}
} }

View File

@@ -1,11 +1,16 @@
package com.gpl.rpg.AndorsTrail.model.item; package com.gpl.rpg.AndorsTrail.model.item;
import android.os.Build;
import android.support.annotation.RequiresApi;
import java.io.DataInputStream; import java.io.DataInputStream;
import java.io.DataOutputStream; import java.io.DataOutputStream;
import java.io.IOException; import java.io.IOException;
import java.security.MessageDigest;
import com.gpl.rpg.AndorsTrail.context.WorldContext; import com.gpl.rpg.AndorsTrail.context.WorldContext;
import com.gpl.rpg.AndorsTrail.savegames.LegacySavegameFormatReaderForItemContainer; import com.gpl.rpg.AndorsTrail.savegames.LegacySavegameFormatReaderForItemContainer;
import com.gpl.rpg.AndorsTrail.util.ByteUtils;
import com.gpl.rpg.AndorsTrail.util.Coord; import com.gpl.rpg.AndorsTrail.util.Coord;
public final class Loot { public final class Loot {
@@ -88,4 +93,13 @@ public final class Loot {
position.writeToParcel(dest); position.writeToParcel(dest);
dest.writeBoolean(isVisible); dest.writeBoolean(isVisible);
} }
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
public void createHash(MessageDigest digest) {
digest.update(ByteUtils.toBytes(exp));
digest.update(ByteUtils.toBytes(exp));
items.createHash(digest);
position.createHash(digest);
digest.update(ByteUtils.toBytes(isVisible));
}
} }

View File

@@ -1,8 +1,12 @@
package com.gpl.rpg.AndorsTrail.model.map; package com.gpl.rpg.AndorsTrail.model.map;
import android.os.Build;
import android.support.annotation.RequiresApi;
import java.io.DataInputStream; import java.io.DataInputStream;
import java.io.DataOutputStream; import java.io.DataOutputStream;
import java.io.IOException; import java.io.IOException;
import java.security.MessageDigest;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
@@ -88,15 +92,27 @@ public final class MapCollection {
return false; return false;
} }
public void writeToParcel(DataOutputStream dest, WorldContext world) throws IOException { private List<PredefinedMap> getAllSavedMaps(WorldContext world) {
List<PredefinedMap> mapsToExport = new ArrayList<PredefinedMap>(); List<PredefinedMap> mapsToExport = new ArrayList<PredefinedMap>();
for(PredefinedMap map : getAllMaps()) { for(PredefinedMap map : getAllMaps()) {
if (shouldSaveMap(world, map)) mapsToExport.add(map); if (shouldSaveMap(world, map)) mapsToExport.add(map);
} }
return mapsToExport;
}
public void writeToParcel(DataOutputStream dest, WorldContext world) throws IOException {
List<PredefinedMap> mapsToExport = getAllSavedMaps(world);
dest.writeInt(mapsToExport.size()); dest.writeInt(mapsToExport.size());
for(PredefinedMap map : mapsToExport) { for(PredefinedMap map : mapsToExport) {
dest.writeUTF(map.name); dest.writeUTF(map.name);
map.writeToParcel(dest, world); map.writeToParcel(dest, world);
} }
} }
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
public void createHash(MessageDigest digest) {
for (PredefinedMap map : getAllMaps()) {
map.createHash(digest);
}
}
} }

View File

@@ -1,8 +1,12 @@
package com.gpl.rpg.AndorsTrail.model.map; package com.gpl.rpg.AndorsTrail.model.map;
import android.os.Build;
import android.support.annotation.RequiresApi;
import java.io.DataInputStream; import java.io.DataInputStream;
import java.io.DataOutputStream; import java.io.DataOutputStream;
import java.io.IOException; import java.io.IOException;
import java.security.MessageDigest;
import java.util.List; import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArrayList;
@@ -10,6 +14,7 @@ import com.gpl.rpg.AndorsTrail.context.WorldContext;
import com.gpl.rpg.AndorsTrail.controller.Constants; import com.gpl.rpg.AndorsTrail.controller.Constants;
import com.gpl.rpg.AndorsTrail.model.actor.Monster; import com.gpl.rpg.AndorsTrail.model.actor.Monster;
import com.gpl.rpg.AndorsTrail.model.actor.MonsterType; import com.gpl.rpg.AndorsTrail.model.actor.MonsterType;
import com.gpl.rpg.AndorsTrail.util.ByteUtils;
import com.gpl.rpg.AndorsTrail.util.Coord; import com.gpl.rpg.AndorsTrail.util.Coord;
import com.gpl.rpg.AndorsTrail.util.CoordRect; import com.gpl.rpg.AndorsTrail.util.CoordRect;
import com.gpl.rpg.AndorsTrail.util.Range; import com.gpl.rpg.AndorsTrail.util.Range;
@@ -140,4 +145,13 @@ public final class MonsterSpawnArea {
m.writeToParcel(dest); m.writeToParcel(dest);
} }
} }
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
public void createHash(MessageDigest digest) {
digest.update(ByteUtils.toBytes(isSpawning));
for (Monster m :
monsters) {
m.createHash(digest);
}
}
} }

View File

@@ -1,8 +1,12 @@
package com.gpl.rpg.AndorsTrail.model.map; package com.gpl.rpg.AndorsTrail.model.map;
import android.os.Build;
import android.support.annotation.RequiresApi;
import java.io.DataInputStream; import java.io.DataInputStream;
import java.io.DataOutputStream; import java.io.DataOutputStream;
import java.io.IOException; import java.io.IOException;
import java.security.MessageDigest;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
@@ -16,6 +20,7 @@ import com.gpl.rpg.AndorsTrail.model.actor.Monster;
import com.gpl.rpg.AndorsTrail.model.item.ItemType; import com.gpl.rpg.AndorsTrail.model.item.ItemType;
import com.gpl.rpg.AndorsTrail.model.item.Loot; import com.gpl.rpg.AndorsTrail.model.item.Loot;
import com.gpl.rpg.AndorsTrail.model.map.MapObject.MapObjectType; import com.gpl.rpg.AndorsTrail.model.map.MapObject.MapObjectType;
import com.gpl.rpg.AndorsTrail.util.ByteUtils;
import com.gpl.rpg.AndorsTrail.util.Coord; import com.gpl.rpg.AndorsTrail.util.Coord;
import com.gpl.rpg.AndorsTrail.util.CoordRect; import com.gpl.rpg.AndorsTrail.util.CoordRect;
import com.gpl.rpg.AndorsTrail.util.L; import com.gpl.rpg.AndorsTrail.util.L;
@@ -340,6 +345,7 @@ public final class PredefinedMap {
} }
} }
} else { } else {
currentColorFilter = null;
activeMapObjectGroups.clear(); activeMapObjectGroups.clear();
activeMapObjectGroups.addAll(initiallyActiveMapObjectGroups); activeMapObjectGroups.addAll(initiallyActiveMapObjectGroups);
activateMapObjects(); activateMapObjects();
@@ -360,16 +366,20 @@ public final class PredefinedMap {
} }
public boolean shouldSaveMapData(WorldContext world) { public boolean shouldSaveMapData(WorldContext world) {
if (!hasResetTemporaryData()) return true;
if (this == world.model.currentMaps.map) return true; if (this == world.model.currentMaps.map) return true;
return mapDataNeedsToBeSaved();
}
private boolean mapDataNeedsToBeSaved() {
if (!hasResetTemporaryData()) return true;
if (!groundBags.isEmpty()) return true; if (!groundBags.isEmpty()) return true;
for (MonsterSpawnArea a : spawnAreas) { for (MonsterSpawnArea a : spawnAreas) {
if (this.visited && a.isUnique) return true; if (this.visited && a.isUnique) return true;
if (a.isSpawning != a.isSpawningForNewGame) return true; if (a.isSpawning != a.isSpawningForNewGame) return true;
} }
if (!activeMapObjectGroups.containsAll(initiallyActiveMapObjectGroups) if (!activeMapObjectGroups.containsAll(initiallyActiveMapObjectGroups)
|| !initiallyActiveMapObjectGroups.containsAll(activeMapObjectGroups)) return true; || !initiallyActiveMapObjectGroups.containsAll(activeMapObjectGroups)) return true;
if (currentColorFilter != null) return true; if (currentColorFilter != null && !currentColorFilter.equals(initialColorFilter)) return true;
return false; return false;
} }
@@ -398,4 +408,23 @@ public final class PredefinedMap {
dest.writeBoolean(visited); dest.writeBoolean(visited);
dest.writeUTF(lastSeenLayoutHash); dest.writeUTF(lastSeenLayoutHash);
} }
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
public void createHash(MessageDigest digest) {
if (mapDataNeedsToBeSaved()) {
for (MonsterSpawnArea a : spawnAreas) {
a.createHash(digest);
}
for (String g : activeMapObjectGroups) {
digest.update(ByteUtils.toBytes(g));
}
for (Loot l : groundBags) {
l.createHash(digest);
}
digest.update(ByteUtils.toBytes(currentColorFilter));
//skip lastVisitTime since it is too volatile
}
digest.update( ByteUtils.toBytes(visited));
digest.update( ByteUtils.toBytes(lastSeenLayoutHash));
}
} }

View File

@@ -12,6 +12,7 @@ import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.io.StringWriter; import java.io.StringWriter;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@@ -20,6 +21,7 @@ import java.util.regex.Pattern;
import android.content.Context; import android.content.Context;
import android.content.res.Resources; import android.content.res.Resources;
import android.os.Build;
import android.os.SystemClock; import android.os.SystemClock;
import com.gpl.rpg.AndorsTrail.AndorsTrailApplication; import com.gpl.rpg.AndorsTrail.AndorsTrailApplication;
@@ -42,6 +44,7 @@ public final class Savegames {
public static enum LoadSavegameResult { public static enum LoadSavegameResult {
success success
, editDetected
, unknownError , unknownError
, savegameIsFromAFutureVersion , savegameIsFromAFutureVersion
, cheatingDetected , cheatingDetected
@@ -78,7 +81,7 @@ public final class Savegames {
} }
return true; return true;
} catch (IOException e) { } catch (IOException | NoSuchAlgorithmException e) {
L.log("Error saving world: " + e.toString()); L.log("Error saving world: " + e.toString());
return false; return false;
} }
@@ -210,14 +213,18 @@ public final class Savegames {
} }
public static void saveWorld(WorldContext world, OutputStream outStream, String displayInfo) throws IOException { public static void saveWorld(WorldContext world, OutputStream outStream, String displayInfo) throws IOException, NoSuchAlgorithmException {
DataOutputStream dest = new DataOutputStream(outStream); DataOutputStream dest = new DataOutputStream(outStream);
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
world.model.player.hash = world.createHash();
}
FileHeader.writeToParcel(dest, world.model.player.getName(), FileHeader.writeToParcel(dest, world.model.player.getName(),
displayInfo, world.model.player.iconID, displayInfo, world.model.player.iconID,
world.model.statistics.isDead(), world.model.statistics.isDead(),
world.model.statistics.hasUnlimitedSaves(), world.model.statistics.hasUnlimitedSaves(),
world.model.player.id, world.model.player.id,
world.model.player.savedVersion); world.model.player.savedVersion,
world.model.player.wasEditingDetected);
world.maps.writeToParcel(dest, world); world.maps.writeToParcel(dest, world);
world.model.writeToParcel(dest); world.model.writeToParcel(dest);
dest.close(); dest.close();
@@ -239,6 +246,15 @@ public final class Savegames {
onWorldLoaded(res, world, controllers); onWorldLoaded(res, world, controllers);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
try {
String worldHash = world.createHash();
if(!worldHash.equals(world.model.player.hash)){
world.model.player.wasEditingDetected = true;
return LoadSavegameResult.editDetected;
}
} catch (NoSuchAlgorithmException ignored) { }
}
return LoadSavegameResult.success; return LoadSavegameResult.success;
} }
@@ -334,6 +350,7 @@ public final class Savegames {
public final boolean hasUnlimitedSaves; public final boolean hasUnlimitedSaves;
public final String playerId; public final String playerId;
public final long savedVersion; public final long savedVersion;
public final boolean wasEditingDetected;
public String describe() { public String describe() {
return (fileversion == AndorsTrailApplication.DEVELOPMENT_INCOMPATIBLE_SAVEGAME_VERSION ? "(D) " : "") + playerName + ", " + displayInfo; return (fileversion == AndorsTrailApplication.DEVELOPMENT_INCOMPATIBLE_SAVEGAME_VERSION ? "(D) " : "") + playerName + ", " + displayInfo;
@@ -378,9 +395,14 @@ public final class Savegames {
this.playerId = ""; this.playerId = "";
this.savedVersion = 0; this.savedVersion = 0;
} }
if (fileversion >= 70){
this.wasEditingDetected = src.readBoolean();
}else{
this.wasEditingDetected = 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 wasEditingDetected) throws IOException {
dest.writeInt(AndorsTrailApplication.CURRENT_VERSION); dest.writeInt(AndorsTrailApplication.CURRENT_VERSION);
dest.writeUTF(playerName); dest.writeUTF(playerName);
dest.writeUTF(displayInfo); dest.writeUTF(displayInfo);
@@ -389,6 +411,7 @@ public final class Savegames {
dest.writeBoolean(hasUnlimitedSaves); dest.writeBoolean(hasUnlimitedSaves);
dest.writeUTF(playerId); dest.writeUTF(playerId);
dest.writeLong(savedVersion); dest.writeLong(savedVersion);
dest.writeBoolean(wasEditingDetected);
} }
} }
} }

View File

@@ -1,6 +1,17 @@
package com.gpl.rpg.AndorsTrail.util; package com.gpl.rpg.AndorsTrail.util;
import android.os.Build;
import android.support.annotation.RequiresApi;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
public final class ByteUtils { public final class ByteUtils {
private static int bytes;
public static String toHexString(byte[] bytes) { return toHexString(bytes, bytes.length); } public static String toHexString(byte[] bytes) { return toHexString(bytes, bytes.length); }
public static String toHexString(byte[] bytes, int numBytes) { public static String toHexString(byte[] bytes, int numBytes) {
if (bytes == null) return ""; if (bytes == null) return "";
@@ -21,4 +32,57 @@ public final class ByteUtils {
array[i] ^= mask[i]; array[i] ^= mask[i];
} }
} }
public static byte[] toBytes(long l){
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
bytes = Long.BYTES;
}else{
bytes = 8;
}
ByteBuffer buffer = ByteBuffer.allocate(bytes);
buffer.putLong(l);
return buffer.array();
}
public static byte[] toBytes(int i){
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
bytes = Integer.BYTES;
}else{
bytes = 4;
}
ByteBuffer buffer = ByteBuffer.allocate(bytes);
buffer.putInt(i);
return buffer.array();
}
public static byte[] toBytes(float f) {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
bytes = Float.BYTES;
}else{
bytes = 4;
}
ByteBuffer buffer = ByteBuffer.allocate(bytes);
buffer.putFloat(f);
return buffer.array();
}
public static byte[] toBytes(boolean bool){
byte b;
if(bool){
b = 0;
}else{
b = 1;
}
return new byte[] {b};
}
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
public static byte[] toBytes(String x) {
if (x == null) {
return new byte[]{};
}
return x.getBytes(StandardCharsets.UTF_8);
}
} }

View File

@@ -3,6 +3,7 @@ package com.gpl.rpg.AndorsTrail.util;
import java.io.DataInputStream; import java.io.DataInputStream;
import java.io.DataOutputStream; import java.io.DataOutputStream;
import java.io.IOException; import java.io.IOException;
import java.security.MessageDigest;
public final class Coord { public final class Coord {
public int x; public int x;
@@ -47,4 +48,9 @@ public final class Coord {
dest.writeInt(x); dest.writeInt(x);
dest.writeInt(y); dest.writeInt(y);
} }
public void createHash(MessageDigest digest) {
digest.update(ByteUtils.toBytes(x));
digest.update(ByteUtils.toBytes(y));
}
} }

View File

@@ -1,8 +1,11 @@
package com.gpl.rpg.AndorsTrail.util; package com.gpl.rpg.AndorsTrail.util;
import com.gpl.rpg.AndorsTrail.context.WorldContext;
import java.io.DataInputStream; import java.io.DataInputStream;
import java.io.DataOutputStream; import java.io.DataOutputStream;
import java.io.IOException; import java.io.IOException;
import java.security.MessageDigest;
public final class Range { public final class Range {
public int max; public int max;
@@ -99,4 +102,9 @@ public final class Range {
dest.writeInt(max); dest.writeInt(max);
dest.writeInt(current); dest.writeInt(current);
} }
public void createHash(MessageDigest digest) {
digest.update(ByteUtils.toBytes(max));
digest.update(ByteUtils.toBytes(current));
}
} }

View File

@@ -16,10 +16,12 @@
<string name="savegame_currenthero_displayinfo">level %1$d, %2$d exp, %3$d gold</string> <string name="savegame_currenthero_displayinfo">level %1$d, %2$d exp, %3$d gold</string>
<string name="dialog_loading_message">Loading resources…</string> <string name="dialog_loading_message">Loading resources…</string>
<string name="dialog_loading_warning_title">Loading completed with warnings</string>
<string name="dialog_loading_failed_title">Load Failed</string> <string name="dialog_loading_failed_title">Load Failed</string>
<string name="dialog_loading_failed_message">Andor\'s Trail was unable to load the savegame file.\n\n:(\n\nThe file may be damaged or incomplete.</string> <string name="dialog_loading_failed_message">Andor\'s Trail was unable to load the savegame file.\n\n:(\n\nThe file may be damaged or incomplete.</string>
<string name="dialog_loading_failed_incorrectversion">Andor\'s Trail was unable to load the savegame file. This savegame file is created with a newer version than what is currently running.</string> <string name="dialog_loading_failed_incorrectversion">Andor\'s Trail was unable to load the savegame file. This savegame file is created with a newer version than what is currently running.</string>
<string name="dialog_loading_failed_cheat">Andor\'s Trail was unable to load the savegame file. This savegame file has already been continued.</string> <string name="dialog_loading_failed_cheat">Andor\'s Trail was unable to load the savegame file. This savegame file has already been continued.</string>
<string name="dialog_loading_warning_edit">Andor\'s Trail detected that this savegame has been edited. Please do not share screenshots of this savegame or at least add a note that it has been edited. Thanks for being fair.</string>
<string name="dialog_recenter">Recenter</string> <string name="dialog_recenter">Recenter</string>
<string name="dialog_close">Close</string> <string name="dialog_close">Close</string>