From d239b6c3f7f5cf208ccf8408106b96f12973cb55 Mon Sep 17 00:00:00 2001 From: "oskar.wiksten" Date: Mon, 14 Nov 2011 19:48:18 +0000 Subject: [PATCH] Added LruCache for better caching of item tile images. Corrected bug that caused the player to move without user input after combat. Added safeguards to model state when buying and selling items (Issue 273, thanks chitul!) git-svn-id: https://andors-trail.googlecode.com/svn/trunk@203 08aca716-68be-ccc6-4d58-36f5abd142ac --- .../controller/InputController.java | 1 + .../controller/ItemController.java | 16 +- .../AndorsTrail/model/item/ItemContainer.java | 6 +- .../AndorsTrail/resource/tiles/TileCache.java | 22 +- .../gpl/rpg/AndorsTrail/util/LruCache.java | 323 ++++++++++++++++++ 5 files changed, 348 insertions(+), 20 deletions(-) create mode 100644 AndorsTrail/src/com/gpl/rpg/AndorsTrail/util/LruCache.java diff --git a/AndorsTrail/src/com/gpl/rpg/AndorsTrail/controller/InputController.java b/AndorsTrail/src/com/gpl/rpg/AndorsTrail/controller/InputController.java index 3b71274..b128fd9 100644 --- a/AndorsTrail/src/com/gpl/rpg/AndorsTrail/controller/InputController.java +++ b/AndorsTrail/src/com/gpl/rpg/AndorsTrail/controller/InputController.java @@ -67,6 +67,7 @@ public final class InputController implements OnClickListener, OnLongClickListen @Override public void onClick(View arg0) { + if (!model.uiSelections.isInCombat) return; onRelativeMovement(lastTouchPosition_dx, lastTouchPosition_dy); } diff --git a/AndorsTrail/src/com/gpl/rpg/AndorsTrail/controller/ItemController.java b/AndorsTrail/src/com/gpl/rpg/AndorsTrail/controller/ItemController.java index c11a9f8..23d9b33 100644 --- a/AndorsTrail/src/com/gpl/rpg/AndorsTrail/controller/ItemController.java +++ b/AndorsTrail/src/com/gpl/rpg/AndorsTrail/controller/ItemController.java @@ -214,17 +214,19 @@ public final class ItemController { } public static void sell(Player player, ItemType itemType, ItemContainer merchant, int quantity) { int price = getSellingPrice(player, itemType) * quantity; - player.inventory.gold += price; - player.inventory.removeItem(itemType.id, quantity); - merchant.addItem(itemType, quantity); + if (player.inventory.removeItem(itemType.id, quantity)) { + player.inventory.gold += price; + merchant.addItem(itemType, quantity); + } } public static void buy(ModelContainer model, Player player, ItemType itemType, ItemContainer merchant, int quantity) { int price = getBuyingPrice(player, itemType) * quantity; if (!canAfford(player, price)) return; - player.inventory.gold -= price; - player.inventory.addItem(itemType, quantity); - merchant.removeItem(itemType.id, quantity); - model.statistics.addGoldSpent(price); + if (merchant.removeItem(itemType.id, quantity)) { + player.inventory.gold -= price; + player.inventory.addItem(itemType, quantity); + model.statistics.addGoldSpent(price); + } } public void quickitemUse(int quickSlotId) { diff --git a/AndorsTrail/src/com/gpl/rpg/AndorsTrail/model/item/ItemContainer.java b/AndorsTrail/src/com/gpl/rpg/AndorsTrail/model/item/ItemContainer.java index 95f44a4..e0a1211 100644 --- a/AndorsTrail/src/com/gpl/rpg/AndorsTrail/model/item/ItemContainer.java +++ b/AndorsTrail/src/com/gpl/rpg/AndorsTrail/model/item/ItemContainer.java @@ -71,10 +71,12 @@ public class ItemContainer { } } if (index < 0) return false; - if (e.quantity <= quantity) { + if (e.quantity == quantity) { items.remove(index); - } else { + } else if (e.quantity > quantity) { e.quantity -= quantity; + } else { + return false; } return true; } diff --git a/AndorsTrail/src/com/gpl/rpg/AndorsTrail/resource/tiles/TileCache.java b/AndorsTrail/src/com/gpl/rpg/AndorsTrail/resource/tiles/TileCache.java index 7563c2c..09a8fd4 100644 --- a/AndorsTrail/src/com/gpl/rpg/AndorsTrail/resource/tiles/TileCache.java +++ b/AndorsTrail/src/com/gpl/rpg/AndorsTrail/resource/tiles/TileCache.java @@ -2,13 +2,14 @@ package com.gpl.rpg.AndorsTrail.resource.tiles; import java.lang.ref.Reference; import java.lang.ref.ReferenceQueue; -import java.lang.ref.SoftReference; +import java.lang.ref.WeakReference; import java.util.Collection; import java.util.HashMap; import java.util.Map.Entry; import com.gpl.rpg.AndorsTrail.AndorsTrailApplication; import com.gpl.rpg.AndorsTrail.util.L; +import com.gpl.rpg.AndorsTrail.util.LruCache; import android.content.res.Resources; import android.graphics.Bitmap; @@ -17,7 +18,8 @@ public final class TileCache { private final ReferenceQueue gcQueue = new ReferenceQueue(); private ResourceFileTile[] resourceTiles = new ResourceFileTile[1]; - private HashMap> tileIDsPerTilesetAndLocalID = new HashMap>(); + private final HashMap> tileIDsPerTilesetAndLocalID = new HashMap>(); + private final LruCache cache = new LruCache(1000); public int getMaxTileID() { return resourceTiles.length-1; } public void allocateMaxTileID(int maxTileID) { @@ -43,7 +45,7 @@ public final class TileCache { private static final class ResourceFileTile { public final ResourceFileTileset tileset; public final int localID; - public SoftReference bitmap; + //public WeakReference bitmap; public ResourceFileTile(ResourceFileTileset tileset, int localID) { this.tileset = tileset; this.localID = localID; @@ -84,8 +86,7 @@ public final class TileCache { int tileID = j.getKey(); ResourceFileTile tile = j.getValue(); - Bitmap bitmap = null; - if (tile.bitmap != null) bitmap = tile.bitmap.get(); + Bitmap bitmap = cache.get(tileID); if (bitmap == null) { if (cutter == null) { @@ -98,7 +99,8 @@ public final class TileCache { } bitmap = cutter.createTile(tile.localID); - tile.bitmap = new SoftReference(bitmap, gcQueue); + cache.put(tileID, bitmap); + new WeakReference(bitmap, gcQueue); } result.setBitmap(tileID, bitmap); } @@ -112,10 +114,8 @@ public final class TileCache { public Bitmap loadSingleTile(int tileID, Resources r) { cleanQueue(); ResourceFileTile tile = resourceTiles[tileID]; - if (tile.bitmap != null) { - Bitmap bitmap = tile.bitmap.get(); - if (bitmap != null) return bitmap; - } + Bitmap bitmap = cache.get(tileID); + if (bitmap != null) return bitmap; if (AndorsTrailApplication.DEVELOPMENT_DEBUGMESSAGES) { L.log("Loading single tile from tileset " + tile.tileset.tilesetName); @@ -123,7 +123,7 @@ public final class TileCache { TileCutter cutter = new TileCutter(tile.tileset, r); Bitmap result = cutter.createTile(tile.localID); cutter.recycle(); - tile.bitmap = new SoftReference(result, gcQueue); + cache.put(tileID, result); return result; } } diff --git a/AndorsTrail/src/com/gpl/rpg/AndorsTrail/util/LruCache.java b/AndorsTrail/src/com/gpl/rpg/AndorsTrail/util/LruCache.java new file mode 100644 index 0000000..a744c6b --- /dev/null +++ b/AndorsTrail/src/com/gpl/rpg/AndorsTrail/util/LruCache.java @@ -0,0 +1,323 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.gpl.rpg.AndorsTrail.util; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Static library version of {@code android.util.LruCache}. Used to write apps + * that run on API levels prior to 12. When running on API level 12 or above, + * this implementation is still used; it does not try to switch to the + * framework's implementation. See the framework SDK documentation for a class + * overview. + */ +public class LruCache { + private final LinkedHashMap map; + + /** Size of this cache in units. Not necessarily the number of elements. */ + private int size; + private int maxSize; + + private int putCount; + private int createCount; + private int evictionCount; + private int hitCount; + private int missCount; + + /** + * @param maxSize for caches that do not override {@link #sizeOf}, this is + * the maximum number of entries in the cache. For all other caches, + * this is the maximum sum of the sizes of the entries in this cache. + */ + public LruCache(int maxSize) { + if (maxSize <= 0) { + throw new IllegalArgumentException("maxSize <= 0"); + } + this.maxSize = maxSize; + this.map = new LinkedHashMap(0, 0.75f, true); + } + + /** + * Returns the value for {@code key} if it exists in the cache or can be + * created by {@code #create}. If a value was returned, it is moved to the + * head of the queue. This returns null if a value is not cached and cannot + * be created. + */ + public final V get(K key) { + if (key == null) { + throw new NullPointerException("key == null"); + } + + V mapValue; + synchronized (this) { + mapValue = map.get(key); + if (mapValue != null) { + hitCount++; + return mapValue; + } + missCount++; + } + + /* + * Attempt to create a value. This may take a long time, and the map + * may be different when create() returns. If a conflicting value was + * added to the map while create() was working, we leave that value in + * the map and release the created value. + */ + + V createdValue = create(key); + if (createdValue == null) { + return null; + } + + synchronized (this) { + createCount++; + mapValue = map.put(key, createdValue); + + if (mapValue != null) { + // There was a conflict so undo that last put + map.put(key, mapValue); + } else { + size += safeSizeOf(key, createdValue); + } + } + + if (mapValue != null) { + entryRemoved(false, key, createdValue, mapValue); + return mapValue; + } else { + trimToSize(maxSize); + return createdValue; + } + } + + /** + * Caches {@code value} for {@code key}. The value is moved to the head of + * the queue. + * + * @return the previous value mapped by {@code key}. + */ + public final V put(K key, V value) { + if (key == null || value == null) { + throw new NullPointerException("key == null || value == null"); + } + + V previous; + synchronized (this) { + putCount++; + size += safeSizeOf(key, value); + previous = map.put(key, value); + if (previous != null) { + size -= safeSizeOf(key, previous); + } + } + + if (previous != null) { + entryRemoved(false, key, previous, value); + } + + trimToSize(maxSize); + return previous; + } + + /** + * @param maxSize the maximum size of the cache before returning. May be -1 + * to evict even 0-sized elements. + */ + private void trimToSize(int maxSize) { + while (true) { + K key; + V value; + synchronized (this) { + if (size < 0 || (map.isEmpty() && size != 0)) { + throw new IllegalStateException(getClass().getName() + + ".sizeOf() is reporting inconsistent results!"); + } + + if (size <= maxSize || map.isEmpty()) { + break; + } + + Map.Entry toEvict = map.entrySet().iterator().next(); + key = toEvict.getKey(); + value = toEvict.getValue(); + map.remove(key); + size -= safeSizeOf(key, value); + evictionCount++; + } + + entryRemoved(true, key, value, null); + } + } + + /** + * Removes the entry for {@code key} if it exists. + * + * @return the previous value mapped by {@code key}. + */ + public final V remove(K key) { + if (key == null) { + throw new NullPointerException("key == null"); + } + + V previous; + synchronized (this) { + previous = map.remove(key); + if (previous != null) { + size -= safeSizeOf(key, previous); + } + } + + if (previous != null) { + entryRemoved(false, key, previous, null); + } + + return previous; + } + + /** + * Called for entries that have been evicted or removed. This method is + * invoked when a value is evicted to make space, removed by a call to + * {@link #remove}, or replaced by a call to {@link #put}. The default + * implementation does nothing. + * + *

The method is called without synchronization: other threads may + * access the cache while this method is executing. + * + * @param evicted true if the entry is being removed to make space, false + * if the removal was caused by a {@link #put} or {@link #remove}. + * @param newValue the new value for {@code key}, if it exists. If non-null, + * this removal was caused by a {@link #put}. Otherwise it was caused by + * an eviction or a {@link #remove}. + */ + protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {} + + /** + * Called after a cache miss to compute a value for the corresponding key. + * Returns the computed value or null if no value can be computed. The + * default implementation returns null. + * + *

The method is called without synchronization: other threads may + * access the cache while this method is executing. + * + *

If a value for {@code key} exists in the cache when this method + * returns, the created value will be released with {@link #entryRemoved} + * and discarded. This can occur when multiple threads request the same key + * at the same time (causing multiple values to be created), or when one + * thread calls {@link #put} while another is creating a value for the same + * key. + */ + protected V create(K key) { + return null; + } + + private int safeSizeOf(K key, V value) { + int result = sizeOf(key, value); + if (result < 0) { + throw new IllegalStateException("Negative size: " + key + "=" + value); + } + return result; + } + + /** + * Returns the size of the entry for {@code key} and {@code value} in + * user-defined units. The default implementation returns 1 so that size + * is the number of entries and max size is the maximum number of entries. + * + *

An entry's size must not change while it is in the cache. + */ + protected int sizeOf(K key, V value) { + return 1; + } + + /** + * Clear the cache, calling {@link #entryRemoved} on each removed entry. + */ + public final void evictAll() { + trimToSize(-1); // -1 will evict 0-sized elements + } + + /** + * For caches that do not override {@link #sizeOf}, this returns the number + * of entries in the cache. For all other caches, this returns the sum of + * the sizes of the entries in this cache. + */ + public synchronized final int size() { + return size; + } + + /** + * For caches that do not override {@link #sizeOf}, this returns the maximum + * number of entries in the cache. For all other caches, this returns the + * maximum sum of the sizes of the entries in this cache. + */ + public synchronized final int maxSize() { + return maxSize; + } + + /** + * Returns the number of times {@link #get} returned a value. + */ + public synchronized final int hitCount() { + return hitCount; + } + + /** + * Returns the number of times {@link #get} returned null or required a new + * value to be created. + */ + public synchronized final int missCount() { + return missCount; + } + + /** + * Returns the number of times {@link #create(Object)} returned a value. + */ + public synchronized final int createCount() { + return createCount; + } + + /** + * Returns the number of times {@link #put} was called. + */ + public synchronized final int putCount() { + return putCount; + } + + /** + * Returns the number of values that have been evicted. + */ + public synchronized final int evictionCount() { + return evictionCount; + } + + /** + * Returns a copy of the current contents of the cache, ordered from least + * recently accessed to most recently accessed. + */ + public synchronized final Map snapshot() { + return new LinkedHashMap(map); + } + + @Override public synchronized final String toString() { + int accesses = hitCount + missCount; + int hitPercent = accesses != 0 ? (100 * hitCount / accesses) : 0; + return String.format("LruCache[maxSize=%d,hits=%d,misses=%d,hitRate=%d%%]", + maxSize, hitCount, missCount, hitPercent); + } +} -- 2.49.0