Compare commits

...

15 Commits

Author SHA1 Message Date
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
34 changed files with 725 additions and 178 deletions

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

@@ -26,13 +26,13 @@ public final class AndorsTrailApplication extends Application {
public static final boolean DEVELOPMENT_FORCE_CONTINUEGAME = false;
public static final boolean DEVELOPMENT_DEBUGBUTTONS = false;
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_VALIDATEDATA = true;
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();

View File

@@ -67,8 +67,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

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

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

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

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

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

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 74 KiB

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

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

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]