Pull Request #86: Anti cheat checksum

This commit is contained in:
Nut.andor
2025-02-25 15:32:58 +01:00
19 changed files with 550 additions and 126 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 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();

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

@@ -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

@@ -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

@@ -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

@@ -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,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);
}
}