diff --git a/jme3-core/src/main/java/com/jme3/asset/AssetEventListener.java b/jme3-core/src/main/java/com/jme3/asset/AssetEventListener.java index 5e391ba5ff..84559f242c 100644 --- a/jme3-core/src/main/java/com/jme3/asset/AssetEventListener.java +++ b/jme3-core/src/main/java/com/jme3/asset/AssetEventListener.java @@ -35,8 +35,9 @@ * AssetEventListener is an interface for listening to various * events happening inside {@link AssetManager}. For now, it is possible * to receive an event when an asset has been requested - * (one of the AssetManager.load***() methods were called), or when - * an asset has been loaded. + * (one of the AssetManager.load***() methods were called), when + * an asset has been loaded, or when an asset has been reloaded via + * {@link AssetManager#reloadAsset(com.jme3.asset.AssetKey)}. * * @author Kirill Vainer */ @@ -73,4 +74,16 @@ public interface AssetEventListener { */ public void assetDependencyNotFound(AssetKey parentKey, AssetKey dependentAssetKey); + /** + * Called after {@link AssetManager#reloadAsset(com.jme3.asset.AssetKey) } + * has successfully reloaded an asset from its locators. + *

+ * Default implementation is a no-op so existing listeners do not need to + * be updated. + * + * @param key the key of the reloaded asset + */ + public default void assetReloaded(AssetKey key) { + } + } diff --git a/jme3-core/src/main/java/com/jme3/asset/AssetManager.java b/jme3-core/src/main/java/com/jme3/asset/AssetManager.java index 73762a1234..1747030cd7 100644 --- a/jme3-core/src/main/java/com/jme3/asset/AssetManager.java +++ b/jme3-core/src/main/java/com/jme3/asset/AssetManager.java @@ -427,4 +427,24 @@ public default List getClassLoaders() { * Clears the asset cache. */ public void clearCache(); + + /** + * Removes the asset for the given key from the cache (if present) and loads + * it again from the configured locators. Registered + * {@link AssetEventListener}s receive {@link AssetEventListener#assetReloaded(com.jme3.asset.AssetKey) } + * after a successful reload. + *

+ * This method is intended for development workflows such as refreshing + * content after a file change on disk. It fires {@link AssetEventListener#assetRequested(com.jme3.asset.AssetKey) } + * and {@link AssetEventListener#assetLoaded(com.jme3.asset.AssetKey) } through + * the normal {@link #loadAsset(com.jme3.asset.AssetKey) } path in addition to + * {@code assetReloaded}. + * + * @param The asset type + * @param key The asset key to reload (not null) + * @return The reloaded asset + * @throws IllegalArgumentException If the key is null or specifies no cache + * @throws AssetNotFoundException If the asset cannot be located + */ + public T reloadAsset(AssetKey key); } diff --git a/jme3-core/src/main/java/com/jme3/asset/DesktopAssetManager.java b/jme3-core/src/main/java/com/jme3/asset/DesktopAssetManager.java index a8ff73f2c4..74a6037cd3 100644 --- a/jme3-core/src/main/java/com/jme3/asset/DesktopAssetManager.java +++ b/jme3-core/src/main/java/com/jme3/asset/DesktopAssetManager.java @@ -253,6 +253,19 @@ public void clearCache() { } } + @Override + public T reloadAsset(AssetKey key) { + if (key == null) { + throw new IllegalArgumentException("key cannot be null"); + } + deleteFromCache(key); + T asset = loadAsset(key); + for (AssetEventListener listener : eventListeners) { + listener.assetReloaded(key); + } + return asset; + } + /** * Loads an asset that has already been located. * @param The asset type diff --git a/jme3-core/src/test/java/com/jme3/asset/DesktopAssetManagerReloadTest.java b/jme3-core/src/test/java/com/jme3/asset/DesktopAssetManagerReloadTest.java new file mode 100644 index 0000000000..2baa7b0d56 --- /dev/null +++ b/jme3-core/src/test/java/com/jme3/asset/DesktopAssetManagerReloadTest.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2009-2026 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.jme3.asset; + +import com.jme3.asset.plugins.ClasspathLocator; +import com.jme3.texture.Texture; +import java.util.ArrayList; +import java.util.List; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +/** + * Tests {@link DesktopAssetManager#reloadAsset(com.jme3.asset.AssetKey)}. + */ +public class DesktopAssetManagerReloadTest { + + private static final String TEXTURE_PATH = "Textures/Terrain/Pond/Pond.jpg"; + + private DesktopAssetManager assetManager; + private TextureKey textureKey; + + private static final class TrackingListener implements AssetEventListener { + + private final List> requested = new ArrayList<>(); + private final List> loaded = new ArrayList<>(); + private final List> reloaded = new ArrayList<>(); + + @Override + public void assetLoaded(AssetKey key) { + loaded.add(key); + } + + @Override + public void assetRequested(AssetKey key) { + requested.add(key); + } + + @Override + public void assetDependencyNotFound(AssetKey parentKey, AssetKey dependentAssetKey) { + } + + @Override + public void assetReloaded(AssetKey key) { + reloaded.add(key); + } + + public int requestCount() { + return requested.size(); + } + + public int loadedCount() { + return loaded.size(); + } + + public int reloadedCount() { + return reloaded.size(); + } + } + + @Before + public void setUp() { + assetManager = new DesktopAssetManager(true); + assetManager.registerLocator("/", ClasspathLocator.class); + textureKey = new TextureKey(TEXTURE_PATH, false); + } + + /** + * reloadAsset should invalidate the cache entry, parse again, and notify listeners. + */ + @Test + public void reloadAssetReparsesAndNotifiesListeners() { + TrackingListener listener = new TrackingListener(); + assetManager.addAssetEventListener(listener); + + Texture initial = assetManager.loadTexture(textureKey); + Assert.assertNotNull(initial); + Assert.assertEquals(1, listener.loadedCount()); + + Texture reloaded = assetManager.reloadAsset(textureKey); + + Assert.assertNotNull(reloaded); + Assert.assertEquals(2, listener.requestCount()); + Assert.assertEquals(2, listener.loadedCount()); + Assert.assertEquals(1, listener.reloadedCount()); + Assert.assertNotSame(initial, reloaded); + } + + /** + * reloadAsset must reject a null key. + */ + @Test(expected = IllegalArgumentException.class) + public void reloadAssetRejectsNullKey() { + assetManager.reloadAsset(null); + } +}