From 9dfb1a3328a2b8f7e93f6d32edd1c77b19c494ec Mon Sep 17 00:00:00 2001 From: mondogo24 <167080436+mondogo24@users.noreply.github.com> Date: Thu, 7 May 2026 18:00:02 +0200 Subject: [PATCH 01/21] Deprecate CursorLoader CursorLoader is not crossplatform and the cursor file formats that support are not common anymore. Also his class rute has been deleted from the Desktop.cfg file. --- jme3-core/src/main/resources/com/jme3/asset/Desktop.cfg | 1 - .../main/java/com/jme3/cursors/plugins/CursorLoader.java | 7 +++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/jme3-core/src/main/resources/com/jme3/asset/Desktop.cfg b/jme3-core/src/main/resources/com/jme3/asset/Desktop.cfg index b350b696f6..7d339204ec 100644 --- a/jme3-core/src/main/resources/com/jme3/asset/Desktop.cfg +++ b/jme3-core/src/main/resources/com/jme3/asset/Desktop.cfg @@ -1,4 +1,3 @@ INCLUDE com/jme3/asset/General.cfg # Desktop-specific loaders -LOADER com.jme3.cursors.plugins.CursorLoader : ani, cur, ico diff --git a/jme3-desktop/src/main/java/com/jme3/cursors/plugins/CursorLoader.java b/jme3-desktop/src/main/java/com/jme3/cursors/plugins/CursorLoader.java index 7d530ad960..ed421d1ecd 100644 --- a/jme3-desktop/src/main/java/com/jme3/cursors/plugins/CursorLoader.java +++ b/jme3-desktop/src/main/java/com/jme3/cursors/plugins/CursorLoader.java @@ -51,9 +51,16 @@ import javax.imageio.ImageIO; /** + * Support loads of .ico, .ani and .cur cursor file formats. + * * Created Jun 5, 2012 9:45:58 AM * @author MadJack + * + * @deprecated This class is not crossplatform and the + * supported file formats are not commonly used anymore. + * Which leads to it deprecation. */ +@Deprecated public class CursorLoader implements AssetLoader { final private static int FDE_OFFSET = 6; // first directory entry offset From e78b01e707bba6439b41802e621dfeba25c71049 Mon Sep 17 00:00:00 2001 From: mondogo24 <167080436+mondogo24@users.noreply.github.com> Date: Thu, 7 May 2026 18:51:37 +0200 Subject: [PATCH 02/21] Drop Desktop.cfg file and change references for General.cfg Since there is no more use for Desktop.cfg file. It has been deleted and all references to that file now point to /com/jme3/asset/General.cfg. --- jme3-core/src/main/resources/com/jme3/asset/Desktop.cfg | 3 --- jme3-core/src/test/java/com/jme3/asset/TestLocators.java | 2 +- jme3-core/src/test/java/com/jme3/audio/AudioNodeTest.java | 2 +- .../java/com/jme3/test/PreventCoreIssueRegressions.java | 2 +- jme3-desktop/src/main/java/com/jme3/app/AppletHarness.java | 2 +- .../src/main/java/com/jme3/system/JmeDesktopSystem.java | 2 +- .../main/java/jme3test/material/TestMaterialCompare.java | 2 +- .../java/com/jme3/material/plugin/TestMaterialDefWrite.java | 4 +--- .../java/com/jme3/material/plugin/TestMaterialWrite.java | 2 +- .../java/com/jme3/scene/plugins/gltf/GltfLoaderTest.java | 2 +- .../test/java/com/jme3/terrain/collision/BaseAWTTest.java | 6 +----- 11 files changed, 10 insertions(+), 19 deletions(-) delete mode 100644 jme3-core/src/main/resources/com/jme3/asset/Desktop.cfg diff --git a/jme3-core/src/main/resources/com/jme3/asset/Desktop.cfg b/jme3-core/src/main/resources/com/jme3/asset/Desktop.cfg deleted file mode 100644 index 7d339204ec..0000000000 --- a/jme3-core/src/main/resources/com/jme3/asset/Desktop.cfg +++ /dev/null @@ -1,3 +0,0 @@ -INCLUDE com/jme3/asset/General.cfg - -# Desktop-specific loaders diff --git a/jme3-core/src/test/java/com/jme3/asset/TestLocators.java b/jme3-core/src/test/java/com/jme3/asset/TestLocators.java index a3355f1cee..3e77210801 100644 --- a/jme3-core/src/test/java/com/jme3/asset/TestLocators.java +++ b/jme3-core/src/test/java/com/jme3/asset/TestLocators.java @@ -16,7 +16,7 @@ public class TestLocators { @Test public void testAbsoluteLocators() { - AssetManager am = JmeSystem.newAssetManager(TestLocators.class.getResource("/com/jme3/asset/Desktop.cfg")); + AssetManager am = JmeSystem.newAssetManager(TestLocators.class.getResource("/com/jme3/asset/General.cfg")); am.registerLocator("/", ClasspathLocator.class); am.registerLoader(WAVLoader.class, "wav"); am.registerLoader(AWTLoader.class, "jpg"); diff --git a/jme3-core/src/test/java/com/jme3/audio/AudioNodeTest.java b/jme3-core/src/test/java/com/jme3/audio/AudioNodeTest.java index a34f3091df..0f6b083083 100644 --- a/jme3-core/src/test/java/com/jme3/audio/AudioNodeTest.java +++ b/jme3-core/src/test/java/com/jme3/audio/AudioNodeTest.java @@ -15,7 +15,7 @@ public class AudioNodeTest { @Test public void testAudioNodeClone() { - AssetManager assetManager = JmeSystem.newAssetManager(AudioNodeTest.class.getResource("/com/jme3/asset/Desktop.cfg")); + AssetManager assetManager = JmeSystem.newAssetManager(AudioNodeTest.class.getResource("/com/jme3/asset/General.cfg")); AudioNode audio = new AudioNode(assetManager, "Sound/Effects/Bang.wav", AudioData.DataType.Buffer); diff --git a/jme3-core/src/test/java/com/jme3/test/PreventCoreIssueRegressions.java b/jme3-core/src/test/java/com/jme3/test/PreventCoreIssueRegressions.java index c5af2b3038..2a0c5463fa 100644 --- a/jme3-core/src/test/java/com/jme3/test/PreventCoreIssueRegressions.java +++ b/jme3-core/src/test/java/com/jme3/test/PreventCoreIssueRegressions.java @@ -111,7 +111,7 @@ public void simpleInitApp() { } */ @Test public void testIssue1138() { - AssetManager am = JmeSystem.newAssetManager(PreventCoreIssueRegressions.class.getResource("/com/jme3/asset/Desktop.cfg")); + AssetManager am = JmeSystem.newAssetManager(PreventCoreIssueRegressions.class.getResource("/com/jme3/asset/General.cfg")); Node cgModel = (Node)am.loadModel("Models/Elephant/Elephant.mesh.xml"); cgModel.rotate(0f, -1f, 0f); cgModel.scale(0.04f); diff --git a/jme3-desktop/src/main/java/com/jme3/app/AppletHarness.java b/jme3-desktop/src/main/java/com/jme3/app/AppletHarness.java index eddf09b333..3fdeb5d703 100644 --- a/jme3-desktop/src/main/java/com/jme3/app/AppletHarness.java +++ b/jme3-desktop/src/main/java/com/jme3/app/AppletHarness.java @@ -152,7 +152,7 @@ public void init(){ assetCfg = new URL(getParameter("AssetConfigURL")); } catch (MalformedURLException ex){ System.out.println(ex.getMessage()); - assetCfg = Resources.getResource("/com/jme3/asset/Desktop.cfg",this.getClass()); + assetCfg = Resources.getResource("/com/jme3/asset/General.cfg", this.getClass()); } createCanvas(); diff --git a/jme3-desktop/src/main/java/com/jme3/system/JmeDesktopSystem.java b/jme3-desktop/src/main/java/com/jme3/system/JmeDesktopSystem.java index 81c197a3c5..451f81b535 100644 --- a/jme3-desktop/src/main/java/com/jme3/system/JmeDesktopSystem.java +++ b/jme3-desktop/src/main/java/com/jme3/system/JmeDesktopSystem.java @@ -72,7 +72,7 @@ public JmeDesktopSystem() { @Override public URL getPlatformAssetConfigURL() { - return Resources.getResource("com/jme3/asset/Desktop.cfg"); + return Resources.getResource("com/jme3/asset/General.cfg"); } private static BufferedImage verticalFlip(BufferedImage original) { diff --git a/jme3-examples/src/main/java/jme3test/material/TestMaterialCompare.java b/jme3-examples/src/main/java/jme3test/material/TestMaterialCompare.java index a37855185c..6d6c3cdcff 100644 --- a/jme3-examples/src/main/java/jme3test/material/TestMaterialCompare.java +++ b/jme3-examples/src/main/java/jme3test/material/TestMaterialCompare.java @@ -43,7 +43,7 @@ public class TestMaterialCompare { public static void main(String[] args) { AssetManager assetManager = JmeSystem.newAssetManager( - TestMaterialCompare.class.getResource("/com/jme3/asset/Desktop.cfg")); + TestMaterialCompare.class.getResource("/com/jme3/asset/General.cfg")); // Cloned materials Material mat1 = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); diff --git a/jme3-plugins/src/test/java/com/jme3/material/plugin/TestMaterialDefWrite.java b/jme3-plugins/src/test/java/com/jme3/material/plugin/TestMaterialDefWrite.java index c5522c2cb8..9cd2a939ed 100644 --- a/jme3-plugins/src/test/java/com/jme3/material/plugin/TestMaterialDefWrite.java +++ b/jme3-plugins/src/test/java/com/jme3/material/plugin/TestMaterialDefWrite.java @@ -54,9 +54,7 @@ public class TestMaterialDefWrite { @BeforeEach public void init() { assetManager = JmeSystem.newAssetManager( - TestMaterialDefWrite.class.getResource("/com/jme3/asset/Desktop.cfg")); - - + TestMaterialDefWrite.class.getResource("/com/jme3/asset/General.cfg")); } diff --git a/jme3-plugins/src/test/java/com/jme3/material/plugin/TestMaterialWrite.java b/jme3-plugins/src/test/java/com/jme3/material/plugin/TestMaterialWrite.java index 298fb85f6c..d7aaf257bd 100644 --- a/jme3-plugins/src/test/java/com/jme3/material/plugin/TestMaterialWrite.java +++ b/jme3-plugins/src/test/java/com/jme3/material/plugin/TestMaterialWrite.java @@ -59,7 +59,7 @@ public class TestMaterialWrite { @BeforeEach public void init() { - URL configFile = TestMaterialWrite.class.getResource("/com/jme3/asset/Desktop.cfg"); + URL configFile = TestMaterialWrite.class.getResource("/com/jme3/asset/General.cfg"); assetManager = JmeSystem.newAssetManager(configFile); } diff --git a/jme3-plugins/src/test/java/com/jme3/scene/plugins/gltf/GltfLoaderTest.java b/jme3-plugins/src/test/java/com/jme3/scene/plugins/gltf/GltfLoaderTest.java index d2662b9ec0..fc87938193 100644 --- a/jme3-plugins/src/test/java/com/jme3/scene/plugins/gltf/GltfLoaderTest.java +++ b/jme3-plugins/src/test/java/com/jme3/scene/plugins/gltf/GltfLoaderTest.java @@ -63,7 +63,7 @@ public class GltfLoaderTest { @BeforeEach public void init() { assetManager = JmeSystem.newAssetManager( - TestMaterialWrite.class.getResource("/com/jme3/asset/Desktop.cfg")); + TestMaterialWrite.class.getResource("/com/jme3/asset/General.cfg")); } diff --git a/jme3-terrain/src/test/java/com/jme3/terrain/collision/BaseAWTTest.java b/jme3-terrain/src/test/java/com/jme3/terrain/collision/BaseAWTTest.java index 5a70a7e6c5..5fe536293b 100644 --- a/jme3-terrain/src/test/java/com/jme3/terrain/collision/BaseAWTTest.java +++ b/jme3-terrain/src/test/java/com/jme3/terrain/collision/BaseAWTTest.java @@ -27,11 +27,7 @@ public AssetManager getAssetManager() { } private AssetManager createAssetManager() { - /* Desktop.cfg supports the following additional file formats at the time of writing: - LOADER com.jme3.texture.plugins.AWTLoader : jpg, bmp, gif, png, jpeg - LOADER com.jme3.audio.plugins.OGGLoader : ogg - */ - return JmeSystem.newAssetManager(BaseTest.class.getResource("/com/jme3/asset/Desktop.cfg")); + return JmeSystem.newAssetManager(BaseTest.class.getResource("/com/jme3/asset/General.cfg")); } } From 7b0ff9dcbda67c43ed69a1e62836f0f7f7bec391 Mon Sep 17 00:00:00 2001 From: mondogo24 <167080436+mondogo24@users.noreply.github.com> Date: Thu, 7 May 2026 19:23:45 +0200 Subject: [PATCH 03/21] Javadoc improvements in CursorLoader Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .../main/java/com/jme3/cursors/plugins/CursorLoader.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/jme3-desktop/src/main/java/com/jme3/cursors/plugins/CursorLoader.java b/jme3-desktop/src/main/java/com/jme3/cursors/plugins/CursorLoader.java index ed421d1ecd..101f4c5894 100644 --- a/jme3-desktop/src/main/java/com/jme3/cursors/plugins/CursorLoader.java +++ b/jme3-desktop/src/main/java/com/jme3/cursors/plugins/CursorLoader.java @@ -51,14 +51,14 @@ import javax.imageio.ImageIO; /** - * Support loads of .ico, .ani and .cur cursor file formats. + * Supports loading of .ico, .ani, and .cur cursor file formats. * * Created Jun 5, 2012 9:45:58 AM * @author MadJack * - * @deprecated This class is not crossplatform and the - * supported file formats are not commonly used anymore. - * Which leads to it deprecation. + * @deprecated This class is not cross-platform, and the supported file formats are no longer commonly used. + * + * */ @Deprecated public class CursorLoader implements AssetLoader { From dd3335c1415a100aaa188a88dee0a764073767eb Mon Sep 17 00:00:00 2001 From: mondogo24 <167080436+mondogo24@users.noreply.github.com> Date: Thu, 7 May 2026 19:28:08 +0200 Subject: [PATCH 04/21] Revert "Drop Desktop.cfg file and change references for General.cfg" This reverts commit e78b01e707bba6439b41802e621dfeba25c71049. Desktop.cfg will be kept since it serve as good a entry point to add desktop only configs in the future --- jme3-core/src/main/resources/com/jme3/asset/Desktop.cfg | 3 +++ jme3-core/src/test/java/com/jme3/asset/TestLocators.java | 2 +- jme3-core/src/test/java/com/jme3/audio/AudioNodeTest.java | 2 +- .../java/com/jme3/test/PreventCoreIssueRegressions.java | 2 +- jme3-desktop/src/main/java/com/jme3/app/AppletHarness.java | 2 +- .../src/main/java/com/jme3/system/JmeDesktopSystem.java | 2 +- .../main/java/jme3test/material/TestMaterialCompare.java | 2 +- .../java/com/jme3/material/plugin/TestMaterialDefWrite.java | 4 +++- .../java/com/jme3/material/plugin/TestMaterialWrite.java | 2 +- .../java/com/jme3/scene/plugins/gltf/GltfLoaderTest.java | 2 +- .../test/java/com/jme3/terrain/collision/BaseAWTTest.java | 6 +++++- 11 files changed, 19 insertions(+), 10 deletions(-) create mode 100644 jme3-core/src/main/resources/com/jme3/asset/Desktop.cfg diff --git a/jme3-core/src/main/resources/com/jme3/asset/Desktop.cfg b/jme3-core/src/main/resources/com/jme3/asset/Desktop.cfg new file mode 100644 index 0000000000..7d339204ec --- /dev/null +++ b/jme3-core/src/main/resources/com/jme3/asset/Desktop.cfg @@ -0,0 +1,3 @@ +INCLUDE com/jme3/asset/General.cfg + +# Desktop-specific loaders diff --git a/jme3-core/src/test/java/com/jme3/asset/TestLocators.java b/jme3-core/src/test/java/com/jme3/asset/TestLocators.java index 3e77210801..a3355f1cee 100644 --- a/jme3-core/src/test/java/com/jme3/asset/TestLocators.java +++ b/jme3-core/src/test/java/com/jme3/asset/TestLocators.java @@ -16,7 +16,7 @@ public class TestLocators { @Test public void testAbsoluteLocators() { - AssetManager am = JmeSystem.newAssetManager(TestLocators.class.getResource("/com/jme3/asset/General.cfg")); + AssetManager am = JmeSystem.newAssetManager(TestLocators.class.getResource("/com/jme3/asset/Desktop.cfg")); am.registerLocator("/", ClasspathLocator.class); am.registerLoader(WAVLoader.class, "wav"); am.registerLoader(AWTLoader.class, "jpg"); diff --git a/jme3-core/src/test/java/com/jme3/audio/AudioNodeTest.java b/jme3-core/src/test/java/com/jme3/audio/AudioNodeTest.java index 0f6b083083..a34f3091df 100644 --- a/jme3-core/src/test/java/com/jme3/audio/AudioNodeTest.java +++ b/jme3-core/src/test/java/com/jme3/audio/AudioNodeTest.java @@ -15,7 +15,7 @@ public class AudioNodeTest { @Test public void testAudioNodeClone() { - AssetManager assetManager = JmeSystem.newAssetManager(AudioNodeTest.class.getResource("/com/jme3/asset/General.cfg")); + AssetManager assetManager = JmeSystem.newAssetManager(AudioNodeTest.class.getResource("/com/jme3/asset/Desktop.cfg")); AudioNode audio = new AudioNode(assetManager, "Sound/Effects/Bang.wav", AudioData.DataType.Buffer); diff --git a/jme3-core/src/test/java/com/jme3/test/PreventCoreIssueRegressions.java b/jme3-core/src/test/java/com/jme3/test/PreventCoreIssueRegressions.java index 2a0c5463fa..c5af2b3038 100644 --- a/jme3-core/src/test/java/com/jme3/test/PreventCoreIssueRegressions.java +++ b/jme3-core/src/test/java/com/jme3/test/PreventCoreIssueRegressions.java @@ -111,7 +111,7 @@ public void simpleInitApp() { } */ @Test public void testIssue1138() { - AssetManager am = JmeSystem.newAssetManager(PreventCoreIssueRegressions.class.getResource("/com/jme3/asset/General.cfg")); + AssetManager am = JmeSystem.newAssetManager(PreventCoreIssueRegressions.class.getResource("/com/jme3/asset/Desktop.cfg")); Node cgModel = (Node)am.loadModel("Models/Elephant/Elephant.mesh.xml"); cgModel.rotate(0f, -1f, 0f); cgModel.scale(0.04f); diff --git a/jme3-desktop/src/main/java/com/jme3/app/AppletHarness.java b/jme3-desktop/src/main/java/com/jme3/app/AppletHarness.java index 3fdeb5d703..eddf09b333 100644 --- a/jme3-desktop/src/main/java/com/jme3/app/AppletHarness.java +++ b/jme3-desktop/src/main/java/com/jme3/app/AppletHarness.java @@ -152,7 +152,7 @@ public void init(){ assetCfg = new URL(getParameter("AssetConfigURL")); } catch (MalformedURLException ex){ System.out.println(ex.getMessage()); - assetCfg = Resources.getResource("/com/jme3/asset/General.cfg", this.getClass()); + assetCfg = Resources.getResource("/com/jme3/asset/Desktop.cfg",this.getClass()); } createCanvas(); diff --git a/jme3-desktop/src/main/java/com/jme3/system/JmeDesktopSystem.java b/jme3-desktop/src/main/java/com/jme3/system/JmeDesktopSystem.java index 451f81b535..81c197a3c5 100644 --- a/jme3-desktop/src/main/java/com/jme3/system/JmeDesktopSystem.java +++ b/jme3-desktop/src/main/java/com/jme3/system/JmeDesktopSystem.java @@ -72,7 +72,7 @@ public JmeDesktopSystem() { @Override public URL getPlatformAssetConfigURL() { - return Resources.getResource("com/jme3/asset/General.cfg"); + return Resources.getResource("com/jme3/asset/Desktop.cfg"); } private static BufferedImage verticalFlip(BufferedImage original) { diff --git a/jme3-examples/src/main/java/jme3test/material/TestMaterialCompare.java b/jme3-examples/src/main/java/jme3test/material/TestMaterialCompare.java index 6d6c3cdcff..a37855185c 100644 --- a/jme3-examples/src/main/java/jme3test/material/TestMaterialCompare.java +++ b/jme3-examples/src/main/java/jme3test/material/TestMaterialCompare.java @@ -43,7 +43,7 @@ public class TestMaterialCompare { public static void main(String[] args) { AssetManager assetManager = JmeSystem.newAssetManager( - TestMaterialCompare.class.getResource("/com/jme3/asset/General.cfg")); + TestMaterialCompare.class.getResource("/com/jme3/asset/Desktop.cfg")); // Cloned materials Material mat1 = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); diff --git a/jme3-plugins/src/test/java/com/jme3/material/plugin/TestMaterialDefWrite.java b/jme3-plugins/src/test/java/com/jme3/material/plugin/TestMaterialDefWrite.java index 9cd2a939ed..c5522c2cb8 100644 --- a/jme3-plugins/src/test/java/com/jme3/material/plugin/TestMaterialDefWrite.java +++ b/jme3-plugins/src/test/java/com/jme3/material/plugin/TestMaterialDefWrite.java @@ -54,7 +54,9 @@ public class TestMaterialDefWrite { @BeforeEach public void init() { assetManager = JmeSystem.newAssetManager( - TestMaterialDefWrite.class.getResource("/com/jme3/asset/General.cfg")); + TestMaterialDefWrite.class.getResource("/com/jme3/asset/Desktop.cfg")); + + } diff --git a/jme3-plugins/src/test/java/com/jme3/material/plugin/TestMaterialWrite.java b/jme3-plugins/src/test/java/com/jme3/material/plugin/TestMaterialWrite.java index d7aaf257bd..298fb85f6c 100644 --- a/jme3-plugins/src/test/java/com/jme3/material/plugin/TestMaterialWrite.java +++ b/jme3-plugins/src/test/java/com/jme3/material/plugin/TestMaterialWrite.java @@ -59,7 +59,7 @@ public class TestMaterialWrite { @BeforeEach public void init() { - URL configFile = TestMaterialWrite.class.getResource("/com/jme3/asset/General.cfg"); + URL configFile = TestMaterialWrite.class.getResource("/com/jme3/asset/Desktop.cfg"); assetManager = JmeSystem.newAssetManager(configFile); } diff --git a/jme3-plugins/src/test/java/com/jme3/scene/plugins/gltf/GltfLoaderTest.java b/jme3-plugins/src/test/java/com/jme3/scene/plugins/gltf/GltfLoaderTest.java index fc87938193..d2662b9ec0 100644 --- a/jme3-plugins/src/test/java/com/jme3/scene/plugins/gltf/GltfLoaderTest.java +++ b/jme3-plugins/src/test/java/com/jme3/scene/plugins/gltf/GltfLoaderTest.java @@ -63,7 +63,7 @@ public class GltfLoaderTest { @BeforeEach public void init() { assetManager = JmeSystem.newAssetManager( - TestMaterialWrite.class.getResource("/com/jme3/asset/General.cfg")); + TestMaterialWrite.class.getResource("/com/jme3/asset/Desktop.cfg")); } diff --git a/jme3-terrain/src/test/java/com/jme3/terrain/collision/BaseAWTTest.java b/jme3-terrain/src/test/java/com/jme3/terrain/collision/BaseAWTTest.java index 5fe536293b..5a70a7e6c5 100644 --- a/jme3-terrain/src/test/java/com/jme3/terrain/collision/BaseAWTTest.java +++ b/jme3-terrain/src/test/java/com/jme3/terrain/collision/BaseAWTTest.java @@ -27,7 +27,11 @@ public AssetManager getAssetManager() { } private AssetManager createAssetManager() { - return JmeSystem.newAssetManager(BaseTest.class.getResource("/com/jme3/asset/General.cfg")); + /* Desktop.cfg supports the following additional file formats at the time of writing: + LOADER com.jme3.texture.plugins.AWTLoader : jpg, bmp, gif, png, jpeg + LOADER com.jme3.audio.plugins.OGGLoader : ogg + */ + return JmeSystem.newAssetManager(BaseTest.class.getResource("/com/jme3/asset/Desktop.cfg")); } } From ba7e586deb340517af7522c938a38f1b81e1b151 Mon Sep 17 00:00:00 2001 From: mondogo24 <167080436+mondogo24@users.noreply.github.com> Date: Fri, 22 May 2026 12:14:54 +0200 Subject: [PATCH 05/21] Adapted TestCursor for the deprecation of CursorLoader Since .ico, .ani and .cur formats will no longer be supported... The cursor files used for testing has been converted to pngs and the new class, CursorConverter, will handle the convertions from Texture2D to JmeCursor. --- .../jme3/cursors/plugins/CursorConverter.java | 75 ++++++++++++++++++ .../main/java/jme3test/gui/TestCursor.java | 37 ++++++++- .../main/resources/Textures/Cursors/meme.cur | Bin 4286 -> 0 bytes .../main/resources/Textures/Cursors/meme.png | Bin 0 -> 561 bytes .../resources/Textures/Cursors/monkey.ani | Bin 18292 -> 0 bytes .../resources/Textures/Cursors/monkey.png | Bin 0 -> 2379 bytes .../resources/Textures/Cursors/nyancat.ico | Bin 83024 -> 0 bytes .../resources/Textures/Cursors/nyancat.png | Bin 0 -> 1969 bytes 8 files changed, 109 insertions(+), 3 deletions(-) create mode 100644 jme3-core/src/plugins/java/com/jme3/cursors/plugins/CursorConverter.java delete mode 100644 jme3-testdata/src/main/resources/Textures/Cursors/meme.cur create mode 100644 jme3-testdata/src/main/resources/Textures/Cursors/meme.png delete mode 100644 jme3-testdata/src/main/resources/Textures/Cursors/monkey.ani create mode 100644 jme3-testdata/src/main/resources/Textures/Cursors/monkey.png delete mode 100644 jme3-testdata/src/main/resources/Textures/Cursors/nyancat.ico create mode 100644 jme3-testdata/src/main/resources/Textures/Cursors/nyancat.png diff --git a/jme3-core/src/plugins/java/com/jme3/cursors/plugins/CursorConverter.java b/jme3-core/src/plugins/java/com/jme3/cursors/plugins/CursorConverter.java new file mode 100644 index 0000000000..89bdaefa81 --- /dev/null +++ b/jme3-core/src/plugins/java/com/jme3/cursors/plugins/CursorConverter.java @@ -0,0 +1,75 @@ +package com.jme3.cursors.plugins; + +import java.nio.IntBuffer; + +import com.jme3.math.ColorRGBA; +import com.jme3.texture.Image; +import com.jme3.texture.Texture2D; +import com.jme3.texture.image.ImageRaster; +import com.jme3.util.BufferUtils; + +/** + * Convert any image like object to a {@link JmeCursor}. + */ +public class CursorConverter { + /** + * Convert a {@link Texture2D} to a {@link JmeCursor}. + * Doesn't support cursor animations. + * The coordinate system used is the same specified in {@link JmeCursor}. The start + * point is 0, 0 being lower left. + * + * @param cursorImage The texture to convert. No modification will be done to the object. + * + * @return The {@link JmeCursor} using a deep copy of {@link Texture2D.getImage}. + */ + public static JmeCursor fromTexture(Texture2D cursorImage) { + Image image = cursorImage.getImage().clone(); + + if (image == null) { + throw new NullPointerException("There is not an image set to the Texture2D"); + } + + int imageHeight = image.getHeight(); + int imageWidth = image.getWidth(); + + IntBuffer adaptedImageData = getDataAsIntBuffer(image); + + JmeCursor jmeCursor = new JmeCursor(); + jmeCursor.setWidth(imageWidth); + jmeCursor.setHeight(imageHeight); + jmeCursor.setxHotSpot(0); + jmeCursor.setyHotSpot(imageHeight); + jmeCursor.setNumImages(1); + jmeCursor.setImagesDelay(null); + jmeCursor.setImagesData(adaptedImageData); + return jmeCursor; + } + + private static IntBuffer getDataAsIntBuffer(Image image) { + int width = image.getWidth(); + int height = image.getHeight(); + + ImageRaster raster = ImageRaster.create(image); + + IntBuffer data = BufferUtils.createIntBuffer(width * height); + + //ARGB color system is needed to show cursors correctly. + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + ColorRGBA color = raster.getPixel(x, y); + + int a = (int) (color.a * 255) & 0xFF; + int r = (int) (color.r * 255) & 0xFF; + int g = (int) (color.g * 255) & 0xFF; + int b = (int) (color.b * 255) & 0xFF; + + int argb = (a << 24) | (r << 16) | (g << 8) | b; + + data.put(argb); + } + } + + data.flip(); + return data; + } +} \ No newline at end of file diff --git a/jme3-examples/src/main/java/jme3test/gui/TestCursor.java b/jme3-examples/src/main/java/jme3test/gui/TestCursor.java index 3ba14216d7..812ab4e397 100644 --- a/jme3-examples/src/main/java/jme3test/gui/TestCursor.java +++ b/jme3-examples/src/main/java/jme3test/gui/TestCursor.java @@ -1,7 +1,12 @@ package jme3test.gui; import com.jme3.app.SimpleApplication; +import com.jme3.cursors.plugins.CursorConverter; import com.jme3.cursors.plugins.JmeCursor; +import com.jme3.texture.Image; +import com.jme3.texture.image.ImageRaster; +import com.jme3.texture.Texture2D; +import com.jme3.math.ColorRGBA; import java.util.ArrayList; /** @@ -48,15 +53,41 @@ public void simpleInitApp() { * * The animated cursor has been made by Pointer Adic and can be found here: * http://www.rw-designer.com/cursor-set/monkey + * + * At date of 2026/05/22, the three cursor examples has been converter to png formats. + * Checking and following the licences restrictions in the process. */ - cursors.add((JmeCursor) assetManager.loadAsset("Textures/Cursors/meme.cur")); - cursors.add((JmeCursor) assetManager.loadAsset("Textures/Cursors/nyancat.ico")); - cursors.add((JmeCursor) assetManager.loadAsset("Textures/Cursors/monkey.ani")); + + Image[] textureCursors = { + (Image) assetManager.loadAsset("Textures/Cursors/meme.png"), + (Image) assetManager.loadAsset("Textures/Cursors/nyancat.png"), + (Image) assetManager.loadAsset("Textures/Cursors/monkey.png") + }; + + for (Image cursor : textureCursors) { + flipVertically(cursor); + cursors.add(CursorConverter.fromTexture(new Texture2D(cursor))); + } sysTime = System.currentTimeMillis(); inputManager.setMouseCursor(cursors.get(count)); } + private void flipVertically(Image image) { + int height = image.getHeight(); + int width = image.getWidth(); + + ImageRaster raster = ImageRaster.create(image); + + for (int i = 0; i < width; i++) { + for (int j = 0; j < height / 2; j++){ + ColorRGBA reserve = raster.getPixel(i, j); + raster.setPixel(i, j, raster.getPixel(i, height - j -1)); + raster.setPixel(i, height - j -1, reserve); + } + } + } + @Override public void simpleUpdate(float tpf) { long currentTime = System.currentTimeMillis(); diff --git a/jme3-testdata/src/main/resources/Textures/Cursors/meme.cur b/jme3-testdata/src/main/resources/Textures/Cursors/meme.cur deleted file mode 100644 index 89c8a68e797691375155eecfba1fc96b7c1bf1c5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4286 zcmcgwJ9ZO65Nz22p@>MOkQ)%nSKhar1W)F6-^VzC>eQ)-`vybj)_a}Fs`d_nWpWVrd^F+TB^^m6NmBf;Jh#wMe98{h} zvTI$h*Ag87xoe|b2Wy<2SW-ZHX3HHN`ASjg)_7HOhTW;(XlI-av?OcTjW<*_aINv} z)dzm%hE3Xg@6tOb`VBVYf9tG~dy?Fs0uvu?;-= z_W6^Rf1OADOp_v*-CG~rdIL0OH~edTz7;Xv@ju?Lx=|l*=ld~>bG==2?r2YQ{r{ss z;>~z?2cz;b&-OS&jQTp$e2oWK-oc38Y)`a3HNaB?N90*{d4G*uraofaBL*?(4{rcR zrZ?)T#z;TWp6ZWyR4d+t*=TQ%{)|WUk?Rf6&h!V`-kyI0)KU%hG(wXY?IU0M9#V_& zRy(@oOxWW)HR9Wg27Ideryo~pO&vxwN4XLJ6Wq(xh{I4RTCRP48~cn{K)$$#O;&Xr zS@)XgAW?llut!@sHi-&|>%62(ZulADx0~0|58gZ69Je_3j_#)?=RMn_(;wYW)gSCq4!1AGUQ77}_(J`j diff --git a/jme3-testdata/src/main/resources/Textures/Cursors/meme.png b/jme3-testdata/src/main/resources/Textures/Cursors/meme.png new file mode 100644 index 0000000000000000000000000000000000000000..4be43cc96bde30dc6a13225a52e6f2513c0cb041 GIT binary patch literal 561 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 bwZ9J3UJb!=@&)JX;$Fz_h1d;ond1Kghoa$0 diff --git a/jme3-testdata/src/main/resources/Textures/Cursors/monkey.png b/jme3-testdata/src/main/resources/Textures/Cursors/monkey.png new file mode 100644 index 0000000000000000000000000000000000000000..fdb5a0e9942a02c1b08eb7a4dd5a92af19e2417c GIT binary patch literal 2379 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 KG?H}LG5SB9J`6hm literal 0 HcmV?d00001 diff --git a/jme3-testdata/src/main/resources/Textures/Cursors/nyancat.ico b/jme3-testdata/src/main/resources/Textures/Cursors/nyancat.ico deleted file mode 100644 index ab26e914cb5b0c8ad131d8f286076691111d4de5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 83024 zcmeFZ2UJu|(;$3j0Leko5flUyNRk{StK=joIZDn!V1_(okf_8VisYPgl&It+83rYT z?(t*)->?&`kXb*p;j&K&>%0ptJxP+%!8pe6zU zCs>Yy<8R*L1^~#40suYz-~2il0Loth02S3gJO}^~(%9A%fAi0^D?SkDK2!FK>;Nz1&0npL=TK;a9%YP>ALgzR z_eG?avLm{*`3gl8*hUAwIe$hDmD_0Xy=alw4G?PX&b%yTMebN%$urIScogG#Q^OqB z@x~z&dN0|V4MN^r@yDhU`nNW(f?IJ_E307oDote#cA71a2@xkgEnZ&ATJQjh zQy(&0NKzO<_wV+^-E8v+6ZTFwo!!kQ>kAAyS}2tb7Xq{@5?to_KVmY6!0Dneiaj46 zAZIy&Hb;swkcQl;r>kM3uZQ(RyX9FYoY@<5zt5EeAhbv(RF7m+a-|qY?PT08XYiV= zi{j;w0RGm4b@I!bdm1S$ua3BnShkUtr2Mh>%*OfbblCe6(O;FfisMR$IcfxTUnI?M z<<=BhPJWm;=B3URykRd)43Qwr#@m}Mg4_SfOqT%$=;KO zv}}xMC<47r)n6LoS^k)lr02tb^t({otNPC-P1DAt9EOOEWeG9}O8x*0YOa&n&fBjZ6Y5CKvZmXCP+Q+zAElu_wF7 zblkUzg6z>(m8lHhohP+7go*{zXynO~f{7aCOS_dU}- z@FcFr+^Z=5es9#Piebif#nWsZcc!FuPv!aw{$R4 zX31g%W=!V=EJz}rS24nfovN?obi`hFKoec!g5qj461@<7E@Skgy$z?fI&jSQu;vsa zZuQ^u#|)9Y)}AcN_iZes8+8V(HguogEm&Lr>g9a{Ub<3=U*TzsiT9G7v0Gl7!XvGd zFC(=zV5)YS)stbh9e1uenh{$wrwKT@6svvu(yc^yO|FYG6v-TGtF>Zosjq{~Fj{d> zjzx^vj(jOKcP@$S+R7}^d$Txbj%ib$U|qWjI1k}o62Tzw+z1cK={*pYS()WcDo9(! zNvq2XIUEml_>S60K`rfx@Ot^&QlfnNL?fo=qi-_Lq$s(|!teZIejb=@vDH*ud6C7z z&jS`7;Zc5V;2>b5)&`UV0j$R*?sJWgL92CtHb^Q=WDpfIom*_EYrqgi}z?=(iB8fZ7WD_gJ-cNrA_d96B;i~tSAqk4JxSoI^z7Uv|op9vwFqSyf974 zBh#q3#Y9l~v4?GOY^_~af5B(##vq%A@qVaOB*7D~4saoMdh~}`2o!zvxOp@-K1Z+v zc3Bf#ej5BpDM+UFyh!pFq)>)ifSA5IZXNeFw_VRLv^l?Ndwm1hy>DZ@um?LzU8@_3 z6ph6ze%;z0GSQyKxw5+K0e;WmX?C2}>+Tlc_Iagg-veC~n_m5ka841jN}~SE%~oH7 zIVz44Zcv9;MrYrwcbL3v;Kk8{eDs~IJ8^AtiKjhr0Xv%%6Y*c;e=cnfRL$6a}RWMxmOlWo`7py-|~?XAB@NlgU&v(&MY(qTrxd1 zJP>ktyn8l7lllGa*_!O4?fhCL)VOz07(k`=e8>!<`}I~NuYZ?RcQVipBf#qKAsVlf zZQD*=FL7oU?(OVTdvnsI@k)Jt^or`|h>h?AuivMqavIjidkK>*yy{ys$-((!@7!#5 zRSg@ra=&GfFHUBNmJl@?*gNJ^%UVr%JhZduXDmimiH-4&@_4{QQ9C=;O9|0S+)@0m z=cZ*OH%-mY}0s0r^%yI9*I zG}hc6_^oU!9OqWhsH)uQ(Px=~-i_C~^Mr%Qu&K6n0DsuGIyQUXB+XmPtbkfCr;)BV z)Y&qSs@=r!)k6;Teuu)A?e*p4{Ef7%gy^->fy{fY`bHE#lojT*0Hrf(e~*=x(QdW# zg5fhcjIx(!bwqB(Sof^RgfiIMoVab)h1xqib?|3r1dlllJuPi>CYb=!i(25#PD^EF zPN7exsEhe(pY!8ZVVO4g4esYzF(uHc4DZ7EoQ#OGCx%F!MlU*B-TApM)sHU( zB?RmwQP0~2ZZ6R_(EUzPnHli%?qAh~nYx@?)#p0l_00o5win1M^@Z=AcW_l#lLr~s4xml%!bVVfNnq1h1l0X{PBh{ z+KTt1veBSEFcq(3DO*pC$G{fKR)>bF&$Yj#I8-a$3%SO`M%MR{bB1EZpnE9hoc$Sc zfj<02)q9Xa^=CD^_?DRkq7?T@mv`NhtvR^MT}`_ln4Gn+lX)kufKMmm@h>zMvZ|E^ zDWiMf+{BBrOG7PY%~pg{Lm-06_@0dFEeREf_}G-lp*4#*7yslZg@%0huOg39ctnkd zdlxp(E^ME5m>-S4qOU-or|Uv9PQ6VT%oEya(YqM*5v1OT!hK>Z?{`)W2Ik zx}Jy_t(elwFL5smyQNq7%rJBOsRiNZ^t63vTydmXk@yl>4mxES=sVQV@}Ntfd2#&A zh*3OXmCcsdwy|1B64E9h12+K}=(k!p$;}s^o%tD*r^PDJ|jQpVx!}Z{? z(n))K&@AfDtHKsvL|O6(J;FupSGd0QgU0;=-2IFM8^o&1q6opMGfy6GYrsUaJVLl+dS0 zX~Q+qa$oxhgQ|cB)vej4E$y+~IagMV4BU#nR2$lqgWir>7r0ZFQxEW6*UPE2f}n4> zxQ@Di4ZPG)dKzM&yvh##u?vdjoNud~sI8HTr-y}MNSIJ{$h9AvsqnX`Ww;EBBkvc2QdH|krEB_nq85{sn6jjJX3o}`_t(CP=py$8-5;lhAcxhu8=F5! zhP|XS4`(P5-M*%z1HG|I;qg-8;I{F0ar;qXHih^1RUm=}>9{obK~-Ao)o%g;Sx`3b zg7a zYC|Jktw~M+gOd*p%&tCtNpo{c+9B=WTS_C>7VK#1VA;ioBzVW$g?yA?YlX2Jnua)= z^_OT4wYZhV%lP&tJEpK~Mko81(K8`$RV`sW7VW`VvhRFC-!>wC<*vF=rE?mhIfm23 zh}B6fszONZ^_%MRK1hnku$WA{0<6S<`!3S@s{6A3xRfi{^Uf*hG84QBXXyj}Ho^1c zMKsN_v4CTiX3K(h^YQyAtCp5OoxB3O0(mb8*FEpY@dTe~@RYr?dv`cgl|313u4+B7 ztIamfyCqm&6%^yR4N_+Mm38PwQfOS!yMCaj$F_a%u<|GLmM;e6;PX>E(lYSO^lXmR z#nz334Sr|bSbU;o#cQoQMRL={&1vG&Nj=$o<89I^=6lBGs5M4giqH! zs)M&U^O@5g<{%*T^Mk$kGjq`x1Re(gTvzM!(;qalS23$%@Pj9D_`N>A^SVAfa@*@& zi_Z_AeD6t1SK3GkY!S92@b)^FmBH`d!4(clwt5+Ay1D#$Qey5~vRyIy8WQ^%yDcnx zfJW2XwyTTx@iiJcy#gLTdnHxhbI`$uq(-UHh~Fu>5cPRf)<^&K0;XCDUPIu)v(F}f zv)rpPf3)*KN#j9>jhN-PF%FVPlJ|-LQXK*g#Z)@+@bc$vj1TwpwlP^N<}Rl{YTo1N zZdfgBzspd?&OvjVlf#dT^4`;8+V++xb_l0Gw;JQZ&bu`lLF@;b^ain(R^Z&DUuPtE z6-fXR@zyeMt^9+dX?i>2n|T>cdUo|*;T2d=P*|d}E=6Xr@PjHuhJC%Ltl0I01wibh zy!e9=2!rY^&e+;g{nUkA^e#`7WuFc_ie{*+hri_Yv==fMUCLM_l3~ah(%G`+@jVxJ z&FyyPX`~ra@AvEE&74za(Tx1c1Po>G0LcQRO^Ml#%4;`Ip^XC zf5mk8j@}jzTTGD<_ru>S^U2H}V#E(~n+sg-WObRYX!Q@zGuG}bJZI2H-BAd zS3f)%zQMM=)72$PGzVph+ zBU={sior!%fVzcW1zwnnl8tykcyulRIa^G_8iDy10?t zOXSzd@w8YDFsx9#8y2bKlCYEOXJ*1Ojvfq%ew{v9TK!zmz6)&l-TDI2e$$`4ZNYw) z!PN&HhZgNwYtKl{)`5t(wu=ypzc+K@bar0=EmvN~R>-!dX04a=Z4MCSLB!=U?-63t7tr6$1j}#+k6+3cC zxfZOyGWy)WinZL#EbYuRbb#S(dZzJU=^!{Lq9BTe)IA&>0g!8Q66aOqIo-2JQ*RGItf75c0ae#G8 z$9ABq!e;X8iseB6?T2yh(&B$4 z`Eu^ivzY}`{mHD6u|Qqc-3ftTKC_s=L6VN!xJJVF;g6Q(<(N@&h>pqTMoNw4vP3r9 zc;>kQc^JsVV#Te$sQl5Q57@wV?0#eaVE|s))dRrL>*n|QF&uXZ90b#fTB@~UQv4^$ zkg=)D-N6rxR+|C@D9|6KUjpz(0=zbTm9$vz&kF$XD8(Gp&K;0?)cwGUHwzm5A#PDeOx;PSE&LtNffNMM%I!WNcx}zRTy! zrv3(4wPvuX7>=j>!9#mY{(`BfBr|Tfqc^GFf)QY86*N2t{$#dIOsoT0=M^MuT^f=v zN-XzlDIPcD(ixN!ZLYg{eCuIfNut$VDbT_ORQppvVF+UrwY@t3oA&HI;rihPBFE=| zNqGKaD2Dz8c%%Ap^e;K`hwUZor!f@MFT5GQE{&FKF{HRL_8m+h2xvEcMzZYV;q7 zwXle3t!^VVyomI#6giKuIQ_$BUnSgq+-l=jNyPy1AvjN33j}jhG?@ z0N)-ppl!5h)Wm#MK?6*i_b?S#bpvg9;4(OQ++!`2%y7I>x%3nK>|Lv|yIWXsL}4pK z4bgCw56W5sZb;REZz#B=?gfZ%=U$DDc%h`j?Pn?jK5HNgS$?OZ{k@P~khUPG4=bqL z-GEoUSfubzKNzOXt~}w-Z)bbQ;Zz-P7KO*&QvH2nmGs?1l|&cJA1~OS-O==RWBkF2 zI|3TeFp-cvIo~Qq_<2|L`y4lZgW8Ao1jJ>YA3+}MBKqO7+;s33byKl);v3Qhh4=CSZVpt3K8Ll~%yXK@d z)@ruO9`K}<$ZE7oyml(n+z9Tfr=37-|>=xE;3t zKvz%Wp`j))FW%Djr%fbF?HEL$O>Y^)^J22ARUG=GUY^OwZ?LL9PL%TY9N&-)N-`Qo zpXavf+q+Q`2mkb`$QBGabe=QT&T}s8C+g5S)&kgVa9D2~?tD|T__;r-&Qr}uvP>mB zU|($%A>CnMZ%yR|T*ipeteS0GTS4FfCc40s7~LHF$K1T3=!>0-_q(dfUyZjOC-1+d z3B5?GQ2R7nQ9_y}5?8)lqX8MTc^Gj^&>nQq!&F87#laQDJKczf9GQ}5#Eo%P$8C@K zaPZ9%md!FGw*KaWZ1>Wzaarj?hgQ;^Ny{{jvh}LX5`tvLVdKyTselX<6JrY9sH_$c zTSYzUVuT}CUrglRSajpcL8{?a&F9u%-6BOwR{iV&;Wf9_OI(DM=X?D=!Ft!&#O3n6 zM(VhHW{H+n9Fo`2KBN8I44xHKW+c{eI@w)JF{%aOyOz%fPK9i9Xhr&YP?U$3Imert zrs?NnUYn<5i98m+_;29TwG}WaugHoI;RADn#4r2j+SJw0f{F{%17B0#8^sr=s-fysZ69c6fBDem#F#e4DFR=vg+&yrl4y2b%9@}c)%MCf+U0VU^| zrqU0@ERRq>z^}Eb8&aw)ma~2gC(q^Hr&oO83b=a1zu~uegQg&D8X~B)uDyvqjLFlt z+Ozi~R_zrj4n-r}z@IGzs&LC(=t2m3ycPxQXnK5?S{&(u9h^}*QHa$vPSnRQhIQ>< zhaKk-6OF@#Ta=#)bxj%zBCYXAbl`|HB(IewQ2UH!OY`DPf2p!~*n!Y@ln#e)@7^Co zi{){n_Cv38hJe7^H-7$@;>$^ey8k>nfRJ*FV%|D05b1V@oab4BY&MfYTq%~vcebx` zM4+w6OPT|`tS#oqR=goGLt$Y z-DW!K)f43G`isnSz!LC|B8D-n6*D-`0rcVacKIC-(1>0QBultqINDv%|>N%c7M8E_U`gh4^uX)+SaF%D8^z8L9Sz|av^$G z6F7G_dkO=KGfNGDqj_{B`wKc8*Z}+8(r^4um_Kc@Q-F$_QsS0&hD{^#`SQD1gWP$c z-}mopC$(C-7axD9r&Xj0zXHn>RM9=mYOqSo8O;kL|4{@#TIo$(^yS)h(U&*Gm8rsfmCG~7J z##b$@ee8Q{l4*lcn>e?>asRB$qg;fap4YPBxeeVHH~Op}$oEY@^A`M+ccEnpugR6; z#kU%oGR1@mIx{Lf3v8zLrn95#lIh`U^OmEE32v1qX11D=xZ7wM6r3hef zJN@Ns+;lHeAw;+ff4gzlcHm2q*4K)dEISfCy$3Oe$)QfP+Fm+U)Q8Tyo57Mo#Nr)7 zq(B9Q1%qzPFRqAe{l2k>)cahPJ@b0}6Kk=>A8wgpBqzdWzTxw5%gx}8Za?EE?cjrZ zvhns$c3!R}zwrX*9Pu+w*m}}A&|8IF+!5Q^S{vL3@HHIR%stIn!mm3E9(xK_h}N&$ zTv3#5;9L%LKYm&7rFv5hd#u4u23`3BI;I8zMT0HD5lKU{z=~R$XaJxkyZ$YJwq*yz zaD|`@m1yplM98>9SO#mKwq2%De3Eg99W$(e7PZ=GM;*(&BK{nL*y*CMavW0(zX`r`XY zOAoeM(O=8+1ws^v!ZSVz=Y4?(qnylJ${MYvDynx)guKqQ8~Ymf%K)`4L!u-YuJ&ee-X#qAf^pl>xrgkgoDQ*Vo3Su|GGEH}6R`SKaNB zXj1^b2NtmbL*p)N;9{+Oj)YT`iDYSKz0yXB9mop#wW%ks!ZWfax>v=7wxg476{pyg za(+AapbgG5x=#Pa8%Z|b`4jzpZH%h5s$m}CD3(6-B^0}7(KpD}owxgFtq&hD zZ=GlKBvqgzbB-cAtS(WLRvT3r)28{tJjX*?_?_RlrzIgBuhDnM^o!BhJVw8{Ub)tA zd#L$b2%Udvg(e)VXf0Oo%fD&JHEVSWNmFr)>E3wv#@UZCSh;X|v*UjGFs9c}n1F9qmLz;kbJkoIf7okr3XUG! zi1YXT5C9CL&ZR?eEqX#288Pvy2bPOOhQ{o)GUlKBx@ z6JI~57cd!qR___Hi~W|zbElZg=Sm8m^6W@~XIdlK!+D@geH!6VAJL$0uuxqP<3pst zg&V+8_RTtqn#Jdb3{el45AUmi*3lKNh#qm4iNKyzgfL59`kiF(}&j! zDMoMA?qAE}!n)2zEvSxo&IVsnp~KDh*C=q#*)X4gcNRbIm)1I%LN(4;|0G>XNZ>OEsT|ZdbCwZvz)=+yt|+ z?!*8P5_vUtV!QJqXzg6}rKomPvO}UTN~7lsy&h@5*NYX_2)4tz*4P=o*OB&Ag;XK8 z4O!ocdbrWSJI;yr0xGX$xeX>p_TM#8Fs#)_sJG$pRPqhro8nb%3D>4G9g1-tG5d2b z5Jc>*Huk>cq$w>|YnFao3t&e4fqRFUiO5NpUoATB-a`3J@48g=IB}!54n8pJ8n_3! z6~CvfBDR6$N81#4UXkJ1su;7YZ4#G;%JQaQ})im$tZ7tP8uNXhG^$XsIDr@k&PYyczH*`Fn#i)e&;`nv(a}&xo>d4Hzj*mRqE_g^&vQJ#J3N>3yHPQAWv}9l5Zxqogt+GbnD}xplAVQal4eA%GPyB~J@;&bJPOBc2tloNAunYXmN}LxN34;ySkGBRDl+&y@}SYB=gq}Z`S`InYWs()iJK(|C}V@~ z)Pi2Wo_2uxho36MEIqVmP-x&OpGqqt4SVoDC-++h%R_`L*RMEL{o#4V1b(0JAq9{5 z+=S=l1zqM~bI7E#W_w00FbvU~KjgFi(!wLn!ASskhe!+UXE_*2DKPz%(wg19U}QgW zin)R36crC2UtlG%qJ|JL5z{g+o&G_Kwi?5}!Bwf$WfmGB==M!>P=-Sn#HUQ#B?(hF)&Ks9U< znJ3MS+4@97yUiqSbX9q>9B2?Im0@0ni`yY1k5ke7NSDCvN7-IoKvV|TylrP6IJRB2 zr*vzx2;O`uc+jM4%huQ8=CllZ<>R6oME-^QVuhvoY4Kol*J%5HE!V|W7^tPTX8>6nHygXM7caZt|cE+rxBQRC5EfP z-IM6oafh=(>q}*m4Tc>_-P_r*oBk39<6kCX4s~xG3mh?ZJF3_hh%2z3y1^M>dhN%0 zO%Z!cL;h-fbn`o2ODHvCujJX9P-!QmFj;{2PSK&U%cC|%L2g^3DUWS_OBOff30wni zkW=nUWu?FlNHjN5QymQ5mrJe%n@8ow-L$S7oBEnc ze2|4B9+8etJW+Vjqn7bvLpU}V?&^4DzaXAd6k#;Hs=7foqwT;>?l2KFwr%XAucQ8~ zcgK?58Z>}QuP}n>^lWL~=f3m$i#}nx$eOPhw+59ww;t;D@AeJ?ZZ@TAiMPJ(k}ICH{l=eb+Fe3|!r~wC*>;cfvna*M zwWqie1Y`RjBjmY@@crhiv-gWH=R!s&;Id;X%GFQVh%B%A_(am%-Bc?VUK_Z?N^)3| zfa$Yqf}2O**+1Cr0)@l?<-^tn_$QtIf(=S6hiyz6hX*IWDOOBl}Jxt^f5 z@$8W^H6kq_>Nm4@n7ec$geUjOSnIAwwiw-q6@jtmJ-jqH+9bGN_|=AcXt$ARgvfKg zG*3W3DQ*XGsvPe>dxbz*u?u{2H?5GRB<~0IwCSGI z7OQFLTEJeRVqeMf(2TxipC7Y^54FtfFLH*`ApJdV%0@{mn`cG81wt0a4Fymj|=BsmC@0gVLU|GevWUYkc!qGM#ozRQUFZMMeD@8!?> zUQVYLgQ1y3Beo+(3HrnPncSJ2hjG0D4Fm*M&9MCggZsf$A*>`-ysWog7`5?0k`imC z6PxN)B+U%H+uW&ThS?Fmp<6f9xu%6zyOjLgd>Kr$o<;{sw6mQnZfwqcc1)|_`;rzb zSNPS7DmY})G2e0X#FrtUfvv6O@it+;CJrkJ@OCwKDKE#~)Z3eiAmZ$ddo3L6RKmWQ z)>YX1>^)9YxXLNMtKT086YGMf>2hJ%3?&>$sX&->?G;o++|>okf-Z@0<5aN!cTu=i`DD}Ch7bGdF! z&a0W_xHs?WrOfE^&jjo)#$SPN_Ie2>tXE3;4?d6HTzVgG$M_9q3u05)oi=AerVKM zt7qKurpq*%chzd;8NY5dNvLafw(qF@O%Jc6$h)AW?ZfJ_t?p-?`MGG&R=K zB2V_)Et)1?ri)_+f@F$l)5Hw^vax(kzQFy%ahhrc##H`&`slMppzSRI!1uD*Q1~$9 zjU@3MDpL_P%n%bErKUnV{SJ;K)v{8u_@nVI^+PQS60mr}QuWKH zN-;3&(%g)$Y>LC*?(o8P=Q3dC>Qb$mi#yN6ERAj_sQQHtkouH9#TSvU=9ZB$SyXEw zuZ?%`^j3>UzeB%a1*w^wt)dJ=oC_@Cv%_Nf&kj8&A$xhJvl>OCUtN-G5Yy!scSMYD z)4_l63hMhPc?KfPSLjCp`Gu(~?5{oCDETk((hpifb^izkNrUL`w%nOyaAbQ)F_U;< zh1Tky;dZml;%vBm1LID1$HHFn0yC#<97Jaz19~u-nNoXc>*_T=bDubPv0&2nQ2k2u!6ohP*sVx z6wj(1I}7Sbjb#+yQ-79sI8W8)--8>^J1p2OE@O!wvME&J_UgDQbQhDRYO3j-BdU}8 z)(0=-!dl(B;A8B?_w7}oIsR8-&yreBn0hxkEgmvHjH|GueTIEn{L->}HBf9zzri^d zH=ykHaFrv=Q1##~qaN+e#Fzo69A8DJ9aZAH_1nY8uD?PP7%n96`JS4P*_1+$eW z{y4F8fc-BLES+iIX;#(5;W+%p8QxFw$+RO)Eax%i@^{f8u%SKF+7OEH@$9rx4r)jt za}n7}y6XYi8v?ianG;%ck7$o^WeX1QR=#Ux#Xc)0Yi@C&mfzaCBx`^F%rtj~PUS!- z&K`V#@J71&1TYrSQ$#Y;ET1*obO;fRll0q~L?BP*B0?r|QQ^KtJ+=dpj4PAI$CV^D z3peMZMg7zw;>MZ}ecZ;#zP3*`q3~^9JYXiv?4KRBGUnaBF!C7V%&o3vnLSQBdGA`u zqzmCq_7(%!Zm0cLo`m?9#`+Yzu(pTK2`x?^nPb1qWLDy~T;1v7e8yWn{{5{dj;z3x z^Zgm-XdS!~e4a7Aom96{B{#$BlDQn27_n_x0U)a2VX*)vfcgcY?*S);kd=>%JpHD8 z?xV?#8liTvH%MIEP41bF`@XAY%^#@X2$Ol7+_7S=g&>UWth#%8DIK&%N%TPXPB(kG zRnW~ATw5-ADearMx`UO>JjPai-;C`P!*YIn9Rm}~SWU|JlMD2~;m+Zy^9#*syJ}}q zPHjru8+fITjs;&15%q$Dky45;b@0cl*#M})jUX@T*-#JAOjjO(SEFBDqzQXrNE+-)7TOZyTzeyA&EfSC(=2jz@5quxwl6P z)5B=nb+jO&N!e1*6y%k>O*boZCVAckjcMMOnF%@1Y>`W!@{u>EB___VpYAm>Qc(tA z4!AZpD}VAwNSxdKhFNbn!|ID!vJdnx)T@0^zAC?{Vsr7gU(yt(g-=F(-828{e7@$t z(@~uvFe+1%iS}uK?huP2Jy@pFTES z_5C(qU2xACPWLP!QH@&{smVxVqY@R>o;AatD;ecqCy$c>WnYxH+D}F07hqq==E6#kb{U$9{OzlA=cI=_{a{Uh_gSRo{SU2Gx!`<9NfFj(DS`kD+&{cReb?CQo7eN+5qW?HS6s_+{|(i`tvD zqOF%f?`Kev8@xD|YSA99wlhP$TXh<%yotFDS*Y+KDpp`i(2KdpWIHcXQUBY~_;1w# z6iNFA?U|Cm>SV#wP+m+%Vymcch}j(uv{R!>IxmMOxR|Aa?PkWOC#)Y-YJJACC2NYpCk4l$NXCA9NH+n*n@Nd#CjC2R%M}g1E&nJxE`{id4ivgPMha@ zy7Gmm>mR0{f(iY~6sX1EVz8lI8k`{8W6kX%ky($Qv`Cn<(Ys0I4b zVeC6YnKUy)l0O=brDflGS%xm3=I*uSd4hF`y!gJ$p;HKs(~`T%WKr-~HQ$#!?%xt}6sxD(xgY3S1J;ZrWa=XeCYWd-1Os zI*x}HSo)2CC~W=8-(6AAAzCxL47~{lj+~;tr#B6WM zSSU8PPl^4woN!%ZOr%qR04harIbq)fSWNk6`q7_0DdM@dzl7c=sZ@gsUP~&f{Ctz= zoP98zw4vbJva6_vR7DHBeM~=z?U6X{p;@WLQF??DvNslJS)#8x{p2INDsC`gQ+kwY z$?T$hulEBTBd^eqkjD6N>+Wl7`+%hfJq(uS+s`+CyE-+F>+vggqDX&VVFbf~Q%0T^ zl_sx+sDrp^s(f~w=5Jy@P7Bi6$4iHqBtFWP)@)FENvVNx&XU7poGii=d&t1>^v z{}dTX)-olNKpEHU{!Tok!NNF44hL~mYnC59OYFz@99|sIN+?S?1eG0Gx#Cd7R0r1a~op>mD~{1)ky1eGVe+b_>Gj{$k7V z)wA?_)j65rp&Xl+Vi_um#PoZ&VsY$2gGC_P$m7}fdQm!B$fR^+){ju{dF@j z8B2*Fp*jRt8;L0vBqLsw76&}(;JUHu-gaNvgp4*sxqK2{=oAHp*F=+GCS(*aXZHP~_o%lnuyx=x z{94Tq=)}O}g{u9VPq>`)&RDE`7UnWUBF`IERiI!_GEdyLPa1w25%_QxIyLon_ z`Y5Vxr~im<3K!#gN2tAih}%XESL|d|bFc#CiR>h-b8MDaT>1Wpu>FpFB}dpLVl_Av z19{LRQ0w5jRv&aJpRj~N{gmzvV(zl^5OT zcX58zDohd}4Xw01x!At?$osiRgC7iZJN9=!J$$0(u_S+xP?4>+3=$<3*={w}?*N}N zEJv8cjF9D9`94hTpmdlJej8kch-*a z^SNi8Gmpo&gLq^J6}8dyB4VWW-uFM;+QfABM`0bvZY8Hrs)=ep(oe(dO7@cZJy^_t zm*}e(vehK;lIb#HA6+`QQTq7VUS+hpFZ4FyrRYEv(W3@7<)%BBd`@?--WKnMq5nOB*UvT|8^*=y?rPug3zySbz z0&w`>1e;>BScC&@ZEe^B%wH#f!CdFCh&`@F*UA5d|7r%HZ~*Fnl|UWvq5rM`9}2~u z0Dw)Pf0h4Jh7WVW3b1Wp4*0=;H3)Y2s}c-)GsKryL-o48G*%(LhMJ2DR6`mX78)8F z78ZIfzt)5w=n4HE4(sx2ZpA`fFcb=dViN$r=lkS4J(mzz>nR3}M)SiQjU1h><-r;n z`jH_$no&q3@)QM2yjDXM?0_E`vc8Uq3_-PGkQn4KG$b(u8XS`7KoGJX5)v5_6@nFG zkioE!;NXxDScn6DZpga($B)p^R;(h-84T(Y;s}KXI}m-$!$K|$7Ij^J5(-7ZfT5l9$%%>e zVTtw4m}^MHpfMNNaboM&)z@PSLa?)g!CnCp+dV&l13-{~KqMlt(25-<1~Y}l*wii}K%}-}PX9SBV1F$xc3!b&0IjQQ{pYB=wu^rQ z>>~XO{!R9OGylyqaJ@+X_@e*Ll*WhZ+Q(fHhyp&>6rjQGa)2H}a8GxS&# z$Kt=@3>^EnB>#fz7;}w(1N`rSW8(@ohehmhEy5=MOU4;|2YjrDb-=phzv23Z1%e%* z*c%+{-LZ@#%<&DrFVq2B%UMIi8J1Yz74#bId^oBbKV52OSG3STDkeEqqjCSy-tqaD6`0E(!5Y|}d5rsJojtYS} zgksGt9f^&DQ&0yttYu-_A~6^a06!tY%_FMrBI+L_BKRww|23ZfE(vx`{@Qo`SJa#x zxVnLlon@>e#jei(1K0U~kJokZ0Suh{|E=R6z1Qn}y^i#MwZz4y|M=Lya(|co4_;>i z0Ko1;2%vZQ9if1WhDJyT6iN{p5)y^P-sn*GVC?QG7#ddRfktCj%Og4L^fUzJ;Ff^E zPRSv*DLz&j^3Q1hbMkj=xQ-71aD@VZFaL4suA5#@9acVt#eZM_I{)ADmrbc|i8N5R zwN@{UcLqOu=D0`0$>eKHSxE?HeX0B_O$eVnd3gW9f#LV|q^A${?Y~P%rVUhwx*a$O zL=I$B6S5SiE>>59BtyP+f4lLpuRGpFb3Nsg)Q4jQWhCm&g|zbVTMZ2t4VQOxpOHg{ z>m$xC(4VD^Tj;9=~q*F?yr5mJFM3nAEKt#IZ+|Bp*{NC}rb;teZy<^;QpEcO$Y|fc$oweuM zYt7GRuC?YqngoT!+hl*}jwW#x*w|{ei+R7N71!VzM}~I(jc5uJDxO&XMb6?F?adOX z$><(J1ueqg{pf=HY$pc4Pka9r`IO+yU}nIOra_%`gEXRTYTcXy_jCMLiTKB5v6W`| z*9k9ox2(?MYT@I4d{BbJivA?yFW^Zr9C6%QXKljlVxLkz-NUKEnanrWUWav0S4zt| z#)KZ1t#*|P>zN(aaKN@pOejbVAoIRc)8NLiPH4nF@5{$d;b)gpHf#Mg^2_EOuZ=7=Hu`!I=a$1|zf*-A zoqV|}TF2a!>fd9cq{;n5)tj+Xz1q%YyTgS`;Wb{iq@Gv1U?Z%g8rv%u39!PZa8(yQ z{;M=opF3P9`$c9Uqe_nlOz4of9Z&9Vz`bb1Nh&R*ute(KlpUH2?& zT4R)ULq7|G_0=SDul*7^uB#LKk}82A#OdLqi~te1wYf`IG1vmte{5U}ZEf)o^j4R0BUo!yq%EoW9bAyx`t_CHI)AHihR zp{EP{BE;T;rPVyUO75MTgj$>k3eJ?j7}x2+8D7Fl#ATEJcz@3Zi-O2v1;gt!x`g2E zU5Y*j0iZ6RX`gd2NHT71Di%{qJ>2MY-EXs-DeQkayGelUCdb-`MoH4V9wn7RixO*;xV*RWb@b`U2-0sazp8AHiawr_lkkT*uu?nNf|>f zg^x3_$7%iOx&kc+&tTQm9L{ zk4|N_=s#p};<~w% zuNii!^8E`&Z1!5~K80BRI&|Gn%Z-4r_5++(wD_ciRiSuqZ_4$T;b#-A8TS*_Nx6#S zx6enST@`qqs_O52G(FK-?Ol91G{VWMNp`C8%&u4rZ}|8|7L4F>3EqJBj#tp#OsH!u zpQ2agEJCLeoWD=j;uAx~o|8Pcw;kPG3Z_3yH{AWGP)2$95*}VM*Y2L$qA*{M***5d z8IlwiZW5gEESAl41lZYxv;|bAdFKCciaYU>0 zX+oKp!E=eHW#ODXkBJ$Bac15#ZKTRPQ_|Oab2Z+6v_IWsB+A&EW}F$fVE1I~{x8vA zgzu76vfh0CH0zuZ%hUR6(d6mGsVyx>Ybh}A>6VsieY#a$pj(Ry;;i#!!AWz~vmbqa zm9p~jmwoBrVeu5&1=X~mjh=@y8kN*Gc&I{GoNso`ldth(wX~U7rGg_$`n>qebRc&K z`{a*JBg3?#P(b2C7E&yiCe;?5mV_-G9IWRp+U!Gvq6S1aIBB+c@1A`66`*QPyMql+ z6@+N*=W@mW4mj6-tb8CCH|#0l<)Bx5H{i2}_HS0j(~+npzv{*&HNLv7=cO$*-C>ot zm|u0@j3~-Y-)OOu581A-vYIr#}|8vVF*@^?0lg z|EtzgFnf&(1%`u4TCc0?HJW%V^KEUkBSAkmq5WhXT`h-Nn<`M^TW~K15a&(Bc~zlqKH~z& z>~mQDeT~GkQDGvxf8^E!)cp742Q>$pZKhpAQXXvU^z8!+q|dj&Ul#9A+TKIna;wws z58JlMKdxS&^?ml%W%L&12symFsUoJ*^@Ch+6XQjRLlCYC8@dZiAxEQVn7VW{WjVV> z*_ak9k%s7pe}+2u=ehkG$`|y*Zc2)<6IG3yY&(8lFND`BQSKP8nuULLi5wD}$D41q zZ6j3;mTnul_K|45Z63dlMp)&(=+LLkERv{#o2;c$t_dza<%dUH##D1JCFVAubH;il7t`KvtoqGmDkMQ_wmw#d% z2X!psziMz|hL&w<`e2OikM^~eRk6I(Ci!t?HO#H8Ax#%7iXsPJX=Y zBTV*s8|~#$W*m*7Wo2bYk>3P~>4W{c7eDDeC3!)&Wbd&kI#st?L{FMPb^N^LGJ#vN zft{uig^oZJdLF~{Z{Nj}@jp7;g)=j}ctSlY^OMI@B=Nhy_u1K|(5Q`XFM6>? z{%j6L7`LzQbH#Ua)YW0f{fzrQ+%H+&V=AA?ipOpgAi!;yrMAM!}y08{Gz34a?g#q0km2)(dJI#>VomCZ^4$HvL<+Z^% zrK92kpPxQmm=zuvv`mYb2|X)s`|FGZrZ$2UFz}2eLx;(lS?!wtt_6=r?~3879qU%k z&YqIt3p6?sK3j?t6q0--exlVWFy~mSeua9*v9`rt_ClZJg6P5*dRkPD4n?i zkGam(ok9$9AbVp7Bbd{fGd|pM%=>dJM@zx_i-QMf{>`CuHA9a|=!{rwcE zlk0J{*2j2;9>h$bm(sjT{pRtgX{({{vxdC)aceDwv;79npC^4*%p6pB74Ksky=sWi z5e`nDKHjz`JMah?@nF5ds#7CP_I>Tf=UyJh=1~55JSdf#X?;-yr(ZZaUlS^AF7?d+N(or^4T_ zr%LSpzPk4(a~JMkJA<2&_&Im^uE+K+wSFvo*UFoD3G+p*Br`Gucx~{xk~ZMO`>6e# zR$2MRrFGoMUHHSCCL4{-lz#ha-dQe$M=nXR#JHZ#8#rOAHB0b5zxiT3jp_Jl%BUED znAx}1viqQdbI{g2=06f%GN*PX?C09pTt4~2Chlw<>sg?sY(QUT`TZ`#>c4}&!g)Bu zD%M^3Bl$;}2K?IVyJPMcsE_zH2K?>w4io$9`?K*bryAPA_!w7IX*6dotVkl-UMXCU zie6cpnK00wkMF{%AHA9ekg<5wyUFgn^RKl6*EF7!u!aHACtj}2RnYMViZ@h z{7d7(OEXOIS7f{&@&+pV6!_z@R&_ctCu-2Xmp6;|U4P6JpziYM-%EWv$Z};ZWm&>m zEcuQ&3C0dPou8MiF&U}P{Ej%t$KXHNVtC@naBGg@36<`*CGmi6vXi5x?HY*lMhcM72sxOlDOXH-(|Enweqt!) zciCMNRj0&=?+%h@N7<#E;i{ITbe2q=6zmS)eoTQ>F2zVPvbI+8LLfHne0ZtUn*9j# z?zVE76-0}1J29rqPM%P6)EIn-f3mftgjk?V~#RJIuNBHY$=#tGkNr@B;U+<0`N zb_y7LZ{Lu^62(XLJa0>r;kM8{^Z8?ev3Zf`I`2lG`?_(mjs1sC76X^NYmJl%X=$Bm z{LPT@^XVf-+={HX_$hbNTjSG@D8XcJj`>}O#VI3k8_=3q7uLQH>Z=^DysTY3v#=5_ zL6G5=O&drg^#=^xY8#liOm9?gTHQGLbLiD%dIbjJ2&xrKU;i1RAFyI2`<*8u&|=;= z6lg^ifO?a;x{JA>tk^;(_pZ$Os(?U%g9v^NR+ts(!1pY?l>)1Go$rV<)GJMfZ)N4@ zGc+?d-gvM%mePh<(C8hB;&l>IUt-1nDTxzQhZRvBkYVwlr8wG9IbImwRJpQxcoheF zbOSg~lya6-JzSz)Ho1f{@A#7JWEKhE5Y~ag?9Ac4@@3buqnU7)3U^uZ7|GAhl z9^ievH@m8)Rl3z{6j92(@M3kNcI}{DX6cIH`M(wM8dTM1iPX!xgylWbaU6=y7J^LPpw25)>XJtl?aJst{UO%z<40MaAV*)+hA1Sy^8T z4u1XmMIPQ^f2NR@i?=&hfm&)*?yy`e&rzDnC*r4kc?-{pD zAcf3K7QJEtnplO*(v>@sMWy3-da%%G`)V^Pf?9CVs3kF7Oh9#RqVoE#`2yZ@hxy99 zwoJyj(|WxRo2twvtnWG#P95(UG1Or4~)&t4|3bN^VU3D!IMfQ50fn+#X{aeI=D za&2TId0x-U`{-ofvu?PlTh1FJ=iL#V&*mEvsZ61#LzWuOck^)Gvh@b&5&MZ*dnIF`!Ia zzKswAO8*(kK5KQt_P0Q^a)$a9(Ql2s49&Dz6eQ|a{;v~1E8e(|Pg(hm`Dhp#D$Hl zs^>SQqg!g3hSd|o<|lvh7!CI-ePuwgI7~z^Jv<@S@UQaTX=gGFpS8L%w!QatW}IW6 z6lKn#PI>JCF+ZPD*;DCkZUTID3Xm~7LMClOTn<_>X6g*6pRhb6;1Z`$T`2Saygk8C4R-iC%>+T z9kP81Dx|zs%cG%er5qh*71V`}z?fCTR=e zt2VDvoj;fmvbR+WFb(&;7s)y@A%VSIxMcU1Ivej=^iRfCaZb{A$81qu$saFaw9MLD zBw+wG_RKi?0ED&_}u5%zunx*G zOa5%$XQHch&6w5Vq-l5W#PX-B%x5X$N`;Fay~C=+dL%^IH#r<=+s*rBrw1;8+^7$3 z$v17Nn7;7f^M(4Pu6WfPnvR+5VZZcOk^Xm9=^bOC;qYqU2&PLqi=yPXL~_gVDZ*ZG z^`$e2!&-R-8=dRUs>dEP9?q*q7_X%0sf&1I-BGZ-c3K(gXMhdtwBa0eRfOA8Wbrs5-_^z3tna?{N7c!Ux6-6encbS%QpO*u5ey~Q zPZM$t2BSI$=51+zsZ(NKTi`@MIPw&NQtvL+6ggMg91Vhw6fwKJJ0L8cEm+K9YVP1d z(6RVe>2&axiI+L?nb3=V+wK*@!rq^x@{J4ae+Z;c)Rqj9kdOA0=Pd4O%_QxAp9@%YwR8e#4@p|v5 zQOv8d%7TGb*_aFtPv|LEn47jbjsv4khh&Zj{V&AL^4O!7j_li$c#96W`H;Q4m)FlO<7TT2i+kXuF(|-NK zTjD}e@lssr%{KJ7I+^?}D={Ofn}$Y4on}!o!Rl8=>{H!1{VXct+$y~F`KUHoTu)Lm z7n{vD0@zOA_ABQ6YVoG%!^>5V7J`{-?#l7~^n)EWyigQrUZ6mJF8qw(z``B|Y10Syo^!@f6>O1b5 zcL-Ft;kBxd?%PS)Z&;cPRT_3U`*c+}k$7sOQ~de&%5_}C>LPV$-LnyqIPhhC?s3-1 zWLJnjU5HVei_^D#aO#s?aCv+5iFdO7H35?uoDA)a=HFd?RNFdIm6BLZ2FJdS*l|DU2$SC?7hU-RWNr)Rl${U>>N754a^^_>=|9FeqF0KJg$;-d(kFZV9N7_G*3s* z^5U$1Cx~6g&d%2ot1*2#;kI>n*>nk6OPt?T&_{Ks-OaFEg_oiEzMWV&u}6>PZaLPL z_3fWT%GobIFTTzDLqa6Kr%93bgL!iecTs1*n*DObJUt^Obrn(ff`~-9JuB;NJHnQ* zLLiH1)VX1v9>rfgXMh^b8Lc}O$YYvbEt|i*Q5NM*EFvPZYu4S} zZMb@B$WQk@T~3>=B>Yy^YK_whgNmA30ipaA>h{^Tkj;EG?+;BO81U`8<@7@%Zplzn z`Vnib*e%HpQq@fxk|%r8 zZ5y*!oyo&4;pO*;QhP2Zj}$s(YZB-XdF1_m#U}S^zALIgcHO0(iMZH!!$*$aly&;> zRn=CXUI@(`akn04lxfiUk0nf%k#Xr)SNmp~a(-U&U~1q#{z&g*$~Gaj$056q)&96F z_->nr?TjP;D~bR`jM{n{v0vnudG4xDRC&C1`X~(Jao57n+kDKP{llya5=dZ zu=L~Wu68%Mropa-GiUmJp5kC@NT7r_R_Mj*NR}7YKIKvRP!SS?mOFULK5?@VkHc zseBOoo0rQ;XAeJ(Jx=v}Zbp-0W+rLP8EqkWMbc5bF@0g%y@Ok}F_u~jQ&^Q(qm0;b z4H-;qF(z7mN5x{{Y6Q~bRS6Skm^TD9e7x89y|j|u`OtsQG)3k~SM9?!#jjU74$M_s zY|xA4Iz~02{g(%*g7kzc_oVC62(RwqW=G*+P${Vo#o|nHV9cFh8o#zNGn{a8aP3u@ zJ6-qn9cP5T3l~I$F}k`g+poOwaJIO%_jhueJGK0*gg98lsD_yp;lt zS9Kg@A8JKliGEoj=|*Ezjf{(4yRMsPb)2Noq)vyiGVSrY{<>X@FK#SW?ytufZ}ubs zy7h$0&2Wbe#QTqAcno}m32%AC5=w_EyoO(9owqzgUVmblGEBxB3rw{ZA5$;0PP$Xi zBdgT!M-+ruSv`)t+a@9kU(Qm)vki2*`L8`^KLjavk&NNcb;Ds!*sV6454eMC;^edQ|$ z9p*|ID^;1Iy7F7bkG$iiP9Yh0%lgy_`w9;awEm1$61h%$SI@mLUxk@8Xq|lLJl0H9X z%ghtsFq_o@WI9}PTm_5qx;xwQ{@9vd%V!DWDEdN-AQ8gwXxlLY-hSCPvVRzqM#SIM z;h~Z?nOJa_6?riGy0ov4sxV^sZm$pYSAw|*25K`SlCjp{(XK={sl}*Z7KYq4p%J&@ z3x%J2Ov+Yl-lkF|VFM|`c67qNVKEHwLiM<7@B7Vop0`I88Cna@aJ)kD87nrG6l3ls z_NC6-Nw35wdd&IsZGxN{ik~iLc%-pnJ1vwJK6=EglJ_I?N9+ak^$w2Xy#2Z@Unb)E zkk!G292%59M^mxRUu%|7qxpEwNz_fz7NbemyTf8(Z9h-UNKY)~uPx-ndggq8hfiF1 znLQ`IAH`TKBqKxe-Y$LQt(*L1RupqBp2?GATdrYDR9M>cD+J7|{ypYaV?s3ot`5y) zdmaGP7tcdBiND)xR5vf%XS0}(-=BIMobjNf1=}U^kA`${Y$2CS?x4PU+j1~F=a$jv z)%kd$yW-bH@?!jMmx`^?Pl%Jr7aUL9zBl=`bD#It{Q498*WDO2R#CyER+XD|)OZ5A zN#mvq)20jxF3(;haX1jUNf`7!1uMnI z#a{FX5@c*$MvpBFEy}u+jHF5>$$-pba@-oVrmi0Yv!&1I#Z{U~yOv%JMpY7S-Ra2G z=v!)9$g(2e|3K5b-FHu5_RHmTYvB*mCq2`e6H8?@2p*slFltAj%h1k5i0lprF&WwF z7?U6}KRV^Pj`PD}$F`cub<$ZoMwxb~{D(jLfKgYiOqdid6D?}j5qmirzuhQBaVBz@ zTXy(9#d+8KB59Tw^6-=7Oy^$7YC@#4cUJ(_#bXU&d#+7vywdd}ZxT%QcTZ1z$8g&D zQ?0JsR!hF#{o-SH`88dVon7bDu{hZyv7U$|ixl!8iQZ5a|s? z%8~ld3G6Nl<=^7WypL_bS~$iVX%wwa#%I0q~hN|fVEp4Jv83u8fcG0#i;10fh&Ijwh% zP!nA5WABEw1zXlm><6+G<Ac#c`8@mngRY z-nT?}v3bArY7doMNwkDdv~I{ZDh%G?uJe+Oa+7|Az}z2uEE}5`Cr5T(HO`M_9`pN| z{NnW4`fU<>-&|64^FYn#gamtS(valJW63&PmtGU90nas&I^E!T6B z=ma}@2UfA}a&3E<;|%l#?t1vD$(?G(h*J`hRr1kPRa9h;n5AELCUkhqnKE~r-Nu&T zo!O^_nLBiRdswUW_;kiQ<=rva6=Dp>kl+@+3ITjfD4SsX(Xr{vpZtg#<6-9l7+Wj#zWh|S!_{bZ>KSvwI}2t-%OJuY}OnoKXqHq0MMZR~vMoT?tQM)j3x zg$Gsq+TeYQ^N^R${7K3*(bl2Y&2=_H914rsFUaI!43XKDs^lJ^iXWsWANMR9IRvA}7v1g6$C30UI7{L0zSsz=wvf#j$mv$gH6~t?E#`g{? znvlN^Z+WCh>$bGJ-rh}AGBY~(3?b3?TZQ05V{;PudmP;A^%31^ii5ifHhh6QHOof= z25ES!rKXR)u$wkH2Fsd1({O-gql0(|1O#eg7mIxy<^1c@E#vn0_(3~WaxKO?j_yc< zdxGjcO!Q1lZRexi;Qp>ul)P%e6V8kCa=w49y~lTFGX*gl)OJrfNibhO8-|@{$3?2k zF3cI@ns;d|Qp?B?EeMzFJ;sVVv!D6pI@|ACkGdEB04gV(4|mv6xuY0|FElEMiL#0B zG6>293!F7qR0NhSvEuFT8Ml3(;((5h?zQ*oajCZ@ zk^e(8J}h62VjYavjE$q5uOH%n#;~wDv0TT3D{e(%UGVJBZU96HoP1fU+20m0akwu@ zmt*+BzQATYTPZtlAi--fpIXL0vw%N6ZYsH&VuH>@Wjglq_Ov_-}*Hy%u&YZ!bF66x%pBadmJk5#3pVx)wkAh6gW zjG$&+)8b&vX-Tgb#`|G$GS8L3%J#`_`qZ=TR~QLVi^5;(A6&hnsaC2Xe}eEEC=dBh zh<@v5TwpH8H#Tdd6%P^izu!CFwVlvw-BvajKf5&`+h*|^>*M=y6!O^03s)~ax>NUN zR+p~n-IcF#yg#>Ll!47 zRP;l-{eY}gq0D!_pdsf!J?=G6$08(gukz@KHGT7gK!+pojVejyQ5y#j&k=kL#Q zM{1rMxpe$<+r1Z)a`DF>(fn=ag(RFC4RN~fGR*%J8F+7&iFPA?a#4onzQ^e~XqQ(rUvA?zz~`^!cL+N_NJW1^GI2Z3*9ZeExVW9I87eYnF| zQDgK5MAis+kitDyu8Rw2-f3Ii{bc71DJ)v`XVz7gwV_VMp>D$AoatIN>1lst8aZ2W z`S9%=I6g=D?R%P*>ptLl^7>mW(_XzD9W`Op{Ve<09FME=)PB3SXd~K;wsAvoi<1fF zr%Y(cyyeCOFT7Tzu}r)~{a}kQyLZJ9@kd)culIkyWyJP=O}^oU8-5dM=ZPaDb_V0O znFf3M)ummg=8^djg&PqeYxu;50ro9;Y;4dbXu^<}>wf1ND>4^P9^OUay6Et`KGo)D zQGB2o?Pm-hJMt4J2K~;P^o)t}?CSTlxEEj4G*I7gGMwI8>Bx7*+*bRvX!<)1P<7(+ zSGIpv+jZ0rm)kUA$WL;C9vRMppV-Q31g)Trwug%C0U#3zTm zpJreG%5TyVO`Rd~-F2U>?}}5u{vy*yh0$Hxe_9uJwUlFK{}Uat=>=v~6d8NVF)>3U z<@xf8Pqk)5Zint`Ypc{zQKLIgKHW@Uo)wa=lk7U$IKvCnR_lE{yCq#b{UYG+r#Br2 znEG*PxL}_+7LzG&yr#MM<>wKxClQT=V)dR%i5PDey#H#3~T2>}b-g((dG4s3j^z4@! zig!rVnn{%Yvc@)1EJCnI$*c-9!lEu=cb?<`$RM28ont7zzg?No=thldC z*BTTf3KeAiMM2NlxuHH%VPqUO_k~ra)Ht*wMmbO9{iSkc{iN`RZrE1dD!sJ^GYzb5 zwoR<(>k|<}$ewzK!w@z1Jx^()R8?n9)lJ$~kT~n8qZ_Bi;`(!0hM+;y+m)J19@m0L zKTBgK5|NZ_x-*4X@QXzb;+Os&A*}BYPMqaNt(y2>)*o_eG>TEN!BsN*YZI1k#~0WB zY3{Dfb^dp^iQDHVCko_?hNFrHzjnL~7N|R(+{wjw%bwwuSt7S}EmMR0ord)kxx$2L zwl=Q7ZG*)fM8}xWA?6Ym-D@st6T4}1b^3|l;1kxTf(K_ocNTiSaa)UdIfro}CvEt( z!HO3n);}8hlrg(M;s^OV2T;wkHAG<*%A{nzr+tvG`TA}SV)rF{O=)4K+wlUpw6yaU7yT!ve@rwNw&`WsZ;TL&+8v)5N}~-1{)Pu z+d-o z4$I)CEeVN>hior>e{6Hje*<|gtzGhS`)%)(dYjo-wl^?MIJd6K*S1zu`6G2+HCz9Z zPcCf$h?CdlXo<_!`;+6kvDetZ)yG4o0m$t0@Dj}6{cY$zub;}Hb>H;mXyum03h{_W zjOP=Xaict>^$N3@xw>h{fVfXwLA;&aDJLUo`rFAU?=|cI5^0o^I=uCo>??LTtzH@Y z;1DOiO;N1y$>N%HFFC<_66V!U8|wI6S!N@`te8vryCXSaIEU6B|K2gI)7<`wv<0jF zYpzqi#c{800_?=3jHt++!Gbpmm?o5Z?^^Wy{j028Oe`IVv$#xER9|t|odgP{sOH+9 z+%yxRXCoLebP@VRu3IsHBY(@rJkK!VcDv;)`9?j7jmR^x3*enGNFpLShsX9l$Memm zE7|!Kvwb>H&frczYdx0ZjTN=NUA!mtfe+t|MNER3bYv~S%q5`z%`p32@vxD58x83Z z&EYi?Wc1(jbRX9i%yu&bn!Z);8vjDYlprns{rD7VnUom904F9jMfcgk z5_D$>Uy)YP*Xj%bYQm%lVVAB)ED7Zt)1(uB{YrS4RW=r);gio;l)@yY`Nlqu2|GB?O+`Fpu&SFHt1g<9J?+*`K2 zZa;XuLXzCQc%4UsqgZKPhfbn8kBj3sF0u)Lzq>L0=;myC+Ls*4)pdC-Um-a zRy>4X(*C}u!$OO#bV-Pj)Ng;agrr4S^}HWWbaaOn7k$XBs8XQj%!|3fP7dSqyPHMy zQ({5sd=n8ddEaMFYRh%>ep}BESzjXgZi<0Cio&OfY{ET$@x1L82x+eJ^8<>*Z)t+n z?Uch!`mZ1PBMM5OBIo+TI`pzqhcZ!xcF>V@@iC4IjjNj1cgtt9E-&!wB@Q}6$Y;D+ z_&y!Aq`K;!PeNfQ2Xr9gi7f}FSciqvg^03fRBlm@M^M*V>&^l;Gb!HCa9p(L#-Ud$ z`KMWv@m{(a?;6{Eu>)!1IPIa=J=qUF=SCyXV|M2;ig6yb%x^Z>?#w=YViLP-Idi_8 z4pCz*NqmWht#?-Fyi1&^$`?HJ`9jK}cUH)nJ)Q82s~s8@?0kPHNrzqDu4((!F+NTW zTbSd~`pEsH-=QZx>V-~=hj*Ra^!Y-^!ou4W;TKKI!34t%e!O7hd`~&&n+P}H5EcbLqStZ2NKiYftHRg#Kgn^ z6Z-|Ya)lcrk^B!eH6bQ31>Aj5U}Nt9Z^B=~3wvw0z$XUsa`KRp7zbe?fuQop5OhqR zgOaK?h>J^t*^?)b9GeWURP4cB&KRV5uYta?DMUSZ0TD{pz|C?I%-?o_QRV{Zr_OI6gW<7nu#Ook*=4|pC}0j^zWh+jrS z#wr?eHqa2*J_e@7hG1rHfvgsO3LX(%5IBT}&x=QZ&P5(06t@Wa2D*?E7YW^Ye(*8I z7T(@*fRT?8@cOnLScbGhz$hBl+D_oK{S3AnPT{Nl37qwu!PyryBt#}cc2*W-B}Kz# zR|?Emzk!vu1Q^T@hSQ-B5c%#iG?^Vi?3+TU{fLIr4+oHwlLPtr`H-2JiRe2ClHv#9 zLpB;>U(dkaw=6)*M^I|N2P#YIpu435&(5BKb&d_><-7xz#IG=$ zcmm7I%P=r70M1G7AXB9TLYX(f?7$E-MpQt(RvjKC)kF9M8lD%G04uWfjc#Tccq#+n z(t9_!vULo+XK2v;+5`EC@8Nx39;nAf!~KL-7!E&%@sHzh8Og)GFb_g!XK-nI4~)tx z;1=@P!;&%hoc$RnJH7+&-WD(pOoCBX2DIv)0Qv_SKE{89;L6Wn=NSQ)ZICh^P5@nL zKhW3Bg0ROiNcx?CRKO<4hi-$k^CECH&VgD|AMAGQf=WOg@VK7>=hF$`wOod4emfxZ z3Jr46Xi$zvgRK8L?6#i5x3X`b6WIz@&hLO$c^=5GcLF~O4I+VP;CZnOOiF#gp*{n$ zFE_yGO*>fGRsavrF>rD$03GiTun5e6i0~;$T|WkS*+Wpeg$7Q+e&7~E>Jr=sN1so@ z-Ma}S#F6U_ozM24fxqBm{(s>4V2+W~NU}pFVwRX7=={IU=vCXK1MV z%s^L-)Z)1%7p0Z8jkUFvt)0CC4kG_V!_?97spgZ%j~_pAeDT5wkw>Hr4fPD2xm^%> zS8Ho)H%covcMp3z764B#Zw((G9}O>GUtcd2Bb%_cjvrF|dGZgSq6=ii$9J<1VxVK> zCbA+%n%UDgIK($J)XNtM;}S+_6&|6j6ZtQBJ$-RfI!eY@h&-}goid8R26iuZQN{J zkSt_ebad8kWas)%NGe9e-^Hgq7UO{P<@#*|r_h#E|I%})*Tkb}CM|*vJeP`F_ zFJBwGzkU@%74ty|2H2U~s5rXmI!+c|(1@ zf8_nj%k{F63NJ?xp^Q~&jXyVX@e502t4G!33o1Ehc|caAxX^@QXY7K-eqJLJi?2EqY}WX za0RSuH^IDO4J@kuf>q5rJk6a0Lvv>ceHjXok&%Fm;&4|*72e)-gc|)=*|v>-fVw3>&=19PiZh+8U-88 zDX`F#0O*x!Xl?2M^j9>j79eFK{LIzP6<&q~LvYO0AK-|e196W%kcvdZ<(JbSlJ^&G zCr!eS@?O~NLxW|*I;bcsLQ+x^@ahZzlja!kd80uH;iUrZ+i+258u*^9f{4=sC?Q-{ zHRczbHlbnf(<$tI*@NXAG}zYv2Gs`-K}_^FaIr7Lr7P>e&9e@dE-!(k*fHFYMT4RO z8U*=&ft17s+|I5Z$3W&~Vm<1_$RKAR_t&`1nqcytRmb=rgFQe+Eg(DY%bt z<5a}I^7$_sz9ad5{6a(R3G%y@%MhQM@sHmqC?*-4@`u1Se-y0F+as>*KYIWFZ{9Ev zXodC95QvQfxc@r)PZy7egO5i*NQCe!5>i4kazdhijh2{*M;I`usIaJMXwNAW6Eial zD;qlzLYDsFUKcJRY>E4v9q|hY3JD1diU^8c!x0k~7Z*c@M*!Y)7KD%>X}NO>bnDj5 zn+W?synCFScf{}91wd$ysv1IO9%?@NH-#}WHZgt7X-0tk!(j`?HxJi-#Z}RcRJV` zoa1&j#K?4i>-mmg^PheF138104h9DLL&GBuLk3-=W0>by7eV3Y5PWWaVe!|Jot~cF z;IiHd;sxs({a0YC->ZNA{8{_EzA?ACg#hT?e-eVD_x6#>{gZHs56<5YkB(3N6Z}2- zw-@$bLjU#pzt+INaN7Uo_5bxqcK^aD8UJvg|EMo=3;q6&(>Dpp;O!p-=YI8+tlWR> zL1c6cczbz)@@*w}j@ao)#P_YG`2ZZ95Fd7UIH;?ugORcEKNv$%`3{)NYk`=UBp8}L z25U_-FqhGUC(;_AtacC5GSa~+_yZ`|r$9hj6KHy7gOqh5NZTgEEt^CL2nYguB)X!z zyE}3o0ElM*ZYe53T0}CW+;)RpbrihU2!J#d52!PG14UY)P_F+H@t2oCDl#SyBewr1 zatwS&!w_P#fBr;+H7X3j8GeAc4!{G!PUl$56Tudtp&`f^e;W#O)1fEV4_YGwp(8p3 zKBoo1-}V%ks*Z-9q)5ciT?wC&^3EF0V5b7X0*K8%Lj0MniH#5x6al%}14yJlA3(T0 z1P2Era|dtW&6_L;4X%ga@K=zQp8#nepTSa1EUY%fL3Oz+RQ7s7Zo31FHKameSp#(T zB4ySi*ag8GX9$it$1(-4KftTkiIA5y1Q8KoP*hX|QBhHlo}LC#uiimi%op$rehHz) zks#kG1sd!3L2LT~-08asnyZ>%+v))CQZf;HTn=9mJG)nnl>hDA?xW!Zv3Xw-M!+u+ zh1lH$C@3g^4<9~2Zf*{Om7ar7Z38f`?*RYQR}h^32A7r=;aS@kC`e0(u#_ZFZs>;| zq|Sdz{)M;Dh<=vqmmuO*1SBOVLCo72NKQ?LjPz7UPfLY7#7}ODw52zKlbDO7$xOsCD3GVI= z32p%b0fKw55ZnfLceh|6xC}0X1cJNEKnU*cu#@-O@7t|9yLJ9JwWrRh`gK)T_jJuv z_47>kecjJ>-2ywt!0yGyCk~KV+5m{jtpr5>-E%4dDf$04$^UCx^Z)0oFf;TIV+)T6 zfRTg$*^ptpEE)`DF)*>PasJX`02t%?Z%5&;p8#_cU>*Vh@IUzo+&sK|{QuCSqGAB? zf4K$!_ym6`&;O+_U}kOsa|2*r0L%&a9}WNv{{I1gf4_i0Sad54{lg<7qoPef4-`hNbyxcv*Y$0x_8 zCSbt*cU-eD*#3tzTZ6&%`uf^m#QuBz-y00Q|F!Mz{rl4YmyUnQi2uv}f2>mHzi>kW{S5C!=A`2duZ6#l_BM+XOhiHQl| z>({RUn2QKVwhaaN37Z1Eg^XZ&D+rJ%`vs5)$^&E!-vX*S&H+`O7l7{_rvPj3Sb!hM z=bttEUtH1E)dn;d!~v%ABLOqHv4E4VOu%n&BA}t%m`03!w%1QxThzp1VvY99^GG75WfETl|Tba zs~7sOlmE>mEWr20I7r44zx|Dn@A%32D+cgE*G6&`(ufg~4Y%SU4^g?3{~b*=kVO=_ zORyf2=XQH=L2vo{ZoSp%i$9(^GP6mGq~v4qmoAy-=(R&m4QP|NN5@zQp}n*2`d9>=Ee2K+bIi z>?n7&ZVnD+vB+co+HOOgKYK0c?z#!?^4%WMdC8MAd>WdrHtGPta=jqjHLCgK?M(4J z{s&QLoT$oA*Ag$Wd2J`D23;;5^N6#G0SSppWq+he5GE#dVg-1D0Y{r)zR|{kG9_Dd zsMpbX-qW$4=eDg{Ub`^%2tH>Z0et;>s977aKZeBok|j8YE%HE)AkgC_bgvL=oDEn0 zx$Na)2YTa7>Gcrs0{Wp>^odoSsqL2r+elP=VOcUN)_F@W)Bp|9 zmwD1@4AU9<)?-Y*J7KfyzW3&_&)Wg|R935)VA@^$DPE9{Xb6chP116;G5SczsM$%L zK!W5G3iUD-;yXldH?5P~tvKk_s>I{D3^snWK98F~-RBm=1vCWC>%(7I#4V^L?U33J zb8wTARCkRSxQdS!r>c@M>&SDc4(nbY9crHh>Lp=SS6hhak+k@t85!l{Tm4>m3#O|v zw`AN;x?YY8>+?vMh`tyoNTFqaLyrv(MEHq;NfnVBwgQO#QpxVwD_=Me@&Hx{U*CLK zM-Sdy|JHJ_d+0tEt7F(+r{{MPzX96wnS7bGnoA9t8X zk(Wc#ost+x>Jxn6e-Nne|CO!^&fL(1WeMUx`s_BrifoC4K#}gbfP>M^2^dGe0mrk2 zt!mOd_}!sELG#sOr&oN&5wS9-DuaNcD+MehmHa8~fC#nd=*QokwYAs1f z+%;ME+0!5Oeoyryx2>Ost;;xug>_;XdE$*B=mfljw^TRtwkt>Dd!bQ`pRoGk4s zrpFAQ2A4uh>Z}qKC%au~k>q~Ak}l0VkZ!@)G+S4kc+-4g(Dd$dPN>0kwX=0z0w>9o z=PkNZ;d^tka_)WH*AIx@JdA`0$m=X$$ko*uh1(w*#oXHhbwo!A0=e3xac~#d3;1{w zF(jvI_f$+3r)o=4i(dMCYiuht?%21t!@2zbI4(6xUF&w$t;*Wlts_tzF2LnI68oH1 zo)TgAY^DX0w!Y0LNY84`*svpBVdT^e!Pa&=3heOi?P%RB6R_~sm+eY*C3O$WUzh(R zr{O{#OapNWgyRv%ny!cAnHaPzb(JZ4B%ku*JbgYAw!*QYYh3Ip zcpoH-uq4ii#4Z@y+nr;E;&<=uAaVPE-Qf`cq5ecY0l)SNLlSRXk(4I>{9S8?Bx?Gn zfK|nVsZ_oAew?XTRL7cg)a7$JnlVmF-FnbGC;`Z8n%>lf$mqKOZ>k&j|9adj%aV;!o}>f zehEA9ydWj>zCsIdM1j+wA|I4rbdqqn2{7w=nTX=Ri>^ish_}DCe&h=o1w=}2nKYl! z-!=qlK-#`&92!g_-=H$yaw54PgU*}g#UF@^g{OF1#slGx5s!E65+x8N9`#S9;Bql$ z{Mh)b;nEKTs{{QS@n?TIm{oVeTMKGl#%sJCOOX6^fLOb1COJ8|3BIx&(x<-GdNB3( z)g8!c#6+7eF$oe#IL--B;r1V~*L^80m2di*JqZs7-#ugPlq%UqMu$^W^$>gj;oDC(m_^p69AeuM=!M8d|@qPm1I={H>E&_|)1x2To|za4N{*G^{J|zz%#YQYooX@`hY02(_N8ukvu@c>Bdzg$^OFS390zkbd&e(}hr%VxfIGAh7c;r!sEZj_ik zQU~&ebbHzW(I=osCN9L^PIvI_%T_jq5A?p9e1R(0)u)XPN z!^Cgi;1zpi7o`DQCGhVAEnqld_I^a}*4~%5_3Kj(E#o1Eg|V(FnwTJRNq^77{TTa` zqEvRC{P)+6%rnc34AJb2Rnp}R23`zFT+z7c&6*PcnRqw2cCw1Rs#3|O?Gesqp+bn8 zaa#VzlgwM3M&uZy@_csf1WVbF_fIO3+7fmswe5?Di*75=M~XtHK*j-O6&83RtM$`C zl}~^A-}qiKd{{U9%;i%0YpL%<;S24i~R-f}+3pdLMdSTQY)?Is;`{6F)X z_;$3}4}P5!XTwSARaRR-D}0kHMq3E>O+P1%VWf0``OQrGim=yUpr z=md@>X(JG*AZ5f!2cqadlU$r_A5%Yhqq zLwX3c@%lL{mT)k=8!EA3g?MpL;}xr4Z{vO9}L`icda=-iYn(rUU+KaQz zk1?!_=uvJP-p}|={arAI93ajaC zE!n2`&~xFE2-Jw^yLr#vezgvC?65%|$My`ni&bqpP4h>M*|Z=ikE=ZKSy1P%$|4*yu7J!XP_C5=w0dMx57p`@H$Xbps+9_t;BZ>t z1Y?6nLZNcALwrGNPUGXml0TpzFz$>MOIk}I1zP1W*~@f@D6q0;)3+6}cD2Si`?4Ig zx{LvlsBK+p)~l|Lfy9n%M;S98$d<=2cYAO* zEVlU@cHNsRXY=0)-`=wwnQY&WhN{DmSOpdg@-aO)FRN5E^T5!I+p9!Dv{4Rw#r#lKOowZzpEu9dqSL{<|5 zt=MNjUhJQN+|K+F-e6ukaHhq8PSe=JiY;7XdJQ6$O$2d_}LRs?d( z^(1N)7J7xhOb_NPd#_Ku(@gYGmw49F-W-SBb(Hmlh5`k^qInDM@(p!41c|D(qYHp&gODp*x+-%`C zXWp|yDfY#GvKQs|&eF*md1^~~5uS+Oj@~#u8AYQ1!su6D8cENASn@(=XXg)2LF&bF5(@Lb62wkzv~Hzaf&MRS&@(6uy69R}2Qt`wnLwxrPo z7+>t@O|H+JFQ?$WzLgp-;yYLb=SW;VtmTeC~gn&!C0C>s)>^^hK{au!MdyLDiJ>wn^ym^B@-zBKWXAt9>eNP+Vg>ALF*2 z)sklhStyVRbAvh!kV8cT1a4?x!P;R1{r$wRUl(XGzh@;(&{%77-WhxNF}qLcyUmk# z_a=wvB7ILWBayz5!hcRLGF?h2+c!5>JlIb~){HWA>SSTY#UZEL?jEP}Fnf(Mi`X@D z!-&n~qM7t3knHv`O?-Uo+QLoc@S7GZtU`Lch6a1edz#4NIXolLvv;||7q3pbuAUCF z7UvDd>5J=d;F*!Wf4tVMmov3H7tV-N#Qv&oSq}vIy&}#_PfzdM$@La*b%b{2t<{KnU)w2vS+(0mp3>SoqfK~{v89gVh*j_2c_&5`4b zcZw0P#+y7AfO%IY0;LMR`P)ODysd>9uFi+Claf!Y?!UFO-VgHiG8rK7WT-5nln^9B zIw=w8kJ~TCnpS--M!T9}Q+kq~on53?sZplI2)kjlknmgb0V!a>TxLQ7uIISejn8R8 z7)2(-DO|;rCz9@G$4yp_?N3wnFwGs#8V_=VU& zXV{a-wd3)Uo003T5n4vTsjGWuzdDJmv=>&tF>Ow4H`#a2(WPqlF|E%-6M9{O`=~x5)fbCX!lvQ2MD#S}m-Wg3!FRcqT-jeG zw=3cxr_+Z-4!Q4}YuM>Xb&HW~&+GmPYyR7n`n=7m#kO0BJtf2`EZy7$x899>PL9aw z?atPeSDsq&r?pTltUd_BfVWlI{5f0G%GbXy>U}vZi2DQoLQi>Lyh(HftPGODW5fG*5x-D^KnJ4L2=e00+E@up(3Si|N-;5}(O>BOcK^*Hs*g#Aa2 z_D{SiP;2?^+y#pPr@oPq5mvafxCzfyd6#5*&p8Fzy2KLHA{`)^IjW{x%{UoMB_CVDLh%np_dyyM z?AU8_n#S=+ck6ow9kRiTntI{2sB}bm2%ms&>b{y(p$FX2cq>- z(A`p~3IaaB(lC&RZh|GX(te2{dhgA*w-CsQvAF*!f{8TO}kJcmSrQU%l z{#Kgo+67>iXlaOrrgmt{=%JBSrN`Pd%4GBOv&T(J6%YnTVNwQnjut_f*4}Kei z&#Q2I&9K)@Mo9^kdfcMP;Gn9tLSWut+L~Q%j(zu*cE~>&epXvO&>b`{$eH!speQ+M zp7J<7&>45!=-Za z<@-Sp7lE2FdHnn|ommt)i{RT68iX}*WC~wWRn+}`ujn_HC#FLFepcKlkZd604yj=d z7Y;C49fQi%9kPT3I+=uSGC;4Al(iTf0A~j?rYP{m?NgmHa&arZ^EuwIvXMm!`&@>g zc~>*$E)y4pHt1aZHzl|1vcxgbfLt~DegK6?y`En~UF%2OkxP;vi4cl;8ui!Cz!A)c z<13ouKaHRVMCoQArY(md*i%BU^euUqI_}dyic2`qv!2OW3;G6bRy}|)B!5e7A zm(_wdX(5UMz0P5to*P@lXXRgJEulANdj2Y>3YAp*mh!}4uNdgl)XDjvxA@@3YFJp* zcdX18(_h24XF-G?QCy6)0I?Gl{Ak7A$DoJK#f&oc6lmj({b1e<;LzLjID=e4*U-?A zL1Uwhgh<9t%%36ky;1s~YN%JR-=gD>$_e#m(?ipw4<-jub>odDH%M7v^zkm9FF9_- zt>_;s$$tlDZ!A2oMr$z}WOK8Sr8OETDwNNfcJiu~gU5En-9xbbnkCE3Wg|1-ESl!m zEZdSEOn_p39|v>4vV;W!z>T37cdprk3L^{WRfc_S791f4B4JfiH$I}{2c`gewDkwj zNFx{wDArP=Ul+vkJD;m)aGEOMg8|qtC*7ncNma{stI(u$?mGqd} z7@6f^bztSzrr0cL$x_!V@QpcWBcl*gPI%Kc?CsRfC#(EZ1DWFMWJ@kiU>C?TVmyyE zs%7!-kk&2ztK^t3qB89rkU{^>*9+}sp<4WZhSE$rUkXO5-u z*gzU>ai)-qE6ITGy%dY!9KF@!hI=3j6~&eWHqbhAk2vjR)4j*)hnf5XZ-8eXWG*0Y|9St*w5K0xJiOf zWP9tv4sRP`UE*VlHu#!dwqaamF@e$cFk|Iu%D2lp^K)CeSh5`Rld3n~*(4&xx|X+9iuTl zcDinr#%Dt$O6ay(9~tW@Yl~t&_ih+}93}5rE9`eZ}$>BXx6Gd~1qrt8A4@w9>iX9td%;R7HF&IZ# zRr&lfrL^UA7M*uKD_d8vcWcRFsF)k6NwS*_Mr4kWiS6`9^mp$Z0hiNeJSTtE6%)hqy! zima3|Bpv6;ULR%LwqQn&5zw0Kgq#%KYXj7i0W&9j4W;U$9_?eAG9lg*s5nrZgVYVb zmJ93BPq=;h5|iOC6h}QaOH%MQ{_J(8XisSr4)RZH&t$i@oX^O_a>id4n|UTcx%~K? zFJ=a>0u{Ihi&KQolv*S6Jby9B03JTYwdb_Y_lK) z#;8B|+~#-P^9xV8R}4<6_aicG+5lhECN$;w8RbgM9uvGF3g z@w>G?+(yI2T2SHikJv|Cnr`TD`*H912Cmyx7>_+hmtho6Uyq%g{af<4PwWH05o=+ri4%rZgw!N&Q6`aHGpzW)>d>_N@Y$$qeLP0U%Y8KYLyUU1zO>y`Y3pN{H zH{c)dt2s`I{5H7xEdzVAI&^%O_~h%GNY0h!%=Wn#N(PLWQ;C_PC+q}hB>bs?^R8{% z$h0znv71H0PNUZ;DFoWu+O3H;b~c~!b+|j`ssu*4R-#bT`4^NSk4u!(utv7JMn-Y; z0E^;#>98}4;1}zrWiMafRaQjPbciuVe$c3VZH05=K8CW4+wOZ{p5^rBP)d14b@f2F zf$b>lUZbb1TDf^289guz1S>YmZw-*H~_z~46}%Rp;oY8!b()JXFfO)W*-i9Lo6{Ju0}ce&d_sD`*O3C3s&jBM#7%x{5pk=i?ejR$|ftf z>bGq@?6qW9j0(LTLQ9kAkp|v_Rwse*43P+72$NBw~Jo1yvyU zB9Xr>8?*!t%b_LUE)~Is4qjZ0%k?>e#Kxm=J1Rk>rZuIQq`X-wRfZjkg_E4NTX3sx z;(c_TZ1&UX0WEIlUY6dO{PBd9oXXvtLRqhY)Nf>K8XF0@;5TUxTek=`Yi0Zko}i`| zB$Cy*mVJ@<{P1IW)y{u@S+xHWvSSD>o#okWR5g$WYHRZ^Ecm<4*C8vgNuG;z)kA`? ztIik95@TZ4KZG4M7A=ZS%x5HCa~>Vp6#RscdgE4#`9S3#RmG~xCq5JQX~{|?`5p&J zrO=^}#7U^_>8DM^0;nfCb~RtiPZ~($`EfLm^>x)HLVKq#leI4kz3Z*mIZSPT8bha z8tzhNu*&21T4hBAD?W~2QZpqLff%npYf5L~R4cpB(*b6K!}_QC8*0QfyP1uV*Tj@M zsr2y_{RFNg?Q7bzc+$Jo#Yev>+`% zAu}zvp=ZibvYk&UwM|%lpyhqFFTYWkGJc!5y}m|4;GhwM>?O`j9ijH=ec<{kmR+@) z`$xuwdS`4p-NUwxKP zch^)-Es`0Z^NB98A^r{XX{rvmg@s9M5U$0zFq~mw?lEJ_0nym`j`URiggqEc%lraw zXUJcO0AGXg>qR_1H@rpfV0-_K7BS!VyG>jZ;T)d#VO-8D+R7tsZ&t^BOR)dP<>2*S z2~QQ(l;-x8n}X+}aa&8=I}Re_=PI_3$};_HNoldvCv%(>eg(acOt@dXL_T%{c8n0B zQ{^%!a!S^!tV$4;VUH6RSTDw_BX&+WvH-y#Ld;U1WP?<}F3UwBkar%8DlklBd?HYz z^6nv3W-T>^a{72N&M|2MkVV)%K$!67uxLX%0Lev4 zH3cw8kIf(=?ev#;KHE=6e}1u#bfzStkm_$C0jqktXZ}?}1X4+Gakr(V)5rXY5~PQuvnq-Q3=|b?~C(^1cA0 zLVJ;)mT!>fqh7x<@v_U*EH7;ZLSq5Mlv3rh-xlp;-8vKQ2QgZUda-|-7-yZ@TNx2f zmfrbH#fy$#FD%#e2D=GPQy973jytq*d6UE-!(J9)Bc^vSA00-hs-98+mQmtSz4`NB zO8xZopGsdE+r zYZ>wc2ZdcJ#Hc+FYdtO98aA)puz{#7EMaA_pEE}Z!FrWSnWV8I!or#NkR)*cHDa6F z(GVF~KdAu@5>_1fx3BUAMwH3T0)vQDRhS;@KJ)gh63$rJr-0%wafik6?d~1YzVngP zjLn-6ax%Yj4^|?G&xo%81_Z}TZ=1}e$d04TktfuAaV?b>+pYvUeBo#d=ui%Ba3e-G zCCh2tGZW#0gLSd=^%X;N4V?^%fmv^*7k*#lBohF|M5h+1c1gs2`=c!Q8~es9p9!&yvuns4vj17z ziT}fKIOQ}4M*1r~*!ZlQJ)xt#l<0{8OGo@6(gAL5F>{%g^{*9d@UfKeKpcB{bXN~t z{=59N%7IcD5Gr}wFlta?$O3L0EdmdvJ_{`UX(=E7M#p6;fCpC}2XO+Lx%l44!n}f0 z180h0@lXbk3pLlWy$&Iuoh1rgEFsV+qO@vK|8}Qc+^0mdsYS*WTrtz^o?|&s?kdY3 zgXmZob8X$PXcirdEIU$DBB^IRk%~z2t_w-qG-u_{F;D>v`iNqt?Tf5mIo`we;E4JL zZes-E)<(Vx{iCta5iY@2DsdO!(o>J>r9SAW^f3As>CkO~UP^;hp8j5PWrlNL{=juC zS8SOo_inPugp0x0#&HmOK~pdBM=M80Y#`JBS|barBS1X3`$@y=df76qZN^!68wn}B z(~r!31^kV0^+*X?)#CN=;jP%%Vsc^g)^HP*%S}WOXMo|7bZM@kyOw*li|&AZf0kEe zw$C!be!}TOR2-MkYt}@Q@fd+VN2OEV9yOZn1~u+KR$~e@w}1GlE+kxb;4o%fZ7XP> z6ctJ=jGL5nuN@220TYhC2l}Okpu4T}GR@K<#B%sFpf=K&kGHv<@ab{fVbyV)FZBKU zhMDtxoCgl_!NuyFcLIFU`fh59KCK))@=-Qn%fSs_3uNfmqlL*j4>(iKj9WM&qL)e| zUIga%nWJuNTfc97m`D@G=8nn_vw#n9*G7tMG7QE~`XPp#91)16IH6#_E*tWc82{xl zW)r3=`Ss0=Xz}vR86qz~FMJlq;X>Nwc&h*X)KLV|+vSMb704noUcTSPNtI%f0t*DgyfRm1Jsa&&}4 zM8H>Q3c^xF4#R>2c0BWOu^d49Qij(#XtS=o8ZM>FSerpOz*O0^Z=|bAPik*Gv&A@* zHMAJYJ^YGwDQCl5USS!R@?kJ*HgM7at;$eQgdLAaFXD)sh)=WM%X3!e zjd$10E>YL*Pjo~jt16tgNc{4zjk%EX#SCT?OK>`vj^(NOs9c zg-43#n5X1@w%jwY6r80C6E>72TVtKg58<%zNk%@G|3k53k&6e<1x8N4J>an!rwX9X z0NBOi)(l=eNJ$@o=gS@!S$0ipM6=XM{HmjF6K>XDMXp0=)WmzDE;zVFc$Tk9^!0zs zv<5dta7W#$M3en~>o~@gq6nn^21K}2eNu6L#N0%Yr!XT9#OW3VBw<)A--qHBZJ`oF z12SatU2E1L9XD$aYt_C-w$&9?RZAQwd>H{;eT0aUifvvz$STP93Xf7Mp{y7OR=Z_6 zyI5gOFqz2GsGbNvJp2XL9K$C-!xr=Nj@QHup^P7=D76tyrR2FD*_Gim$Yuz7?nl9FE zI-8WD_-@M8899Z7S}suhPEoox15hYFQW*ovC1a9nj7_C**x(j*_KTxuLCnr`&mj6B zeqCEamv3+~1|0A3C#{!6mMcWTJy9H;>35J=3b7$rdZ(nau^lYNjBt`bc&X8kLqEff`V;ha8s>r;*+tPiyKu6Ed;xTTMel z-|ej+w&>~KEP+XlPghULeg||g4kyJ)sxO6ej0zSAlru8~fT}c#s^%i;I-sO|Pk@QOlkF|6O4NP4qb!Q{v z{-H=uUff&Un*m+(pft?o^GUfCyMp%FhCAby0>A@ZKbx(@xGT?YR5n+>Qgu=cX@7le z5<{V4FCzid`#taG@#wui`g&_2bcG?caB(~8bj_n6JbHb&C;%O1Uz-6SxtLO0c~S-7lD=85+qO{RHeQUhwXYNI8H(T!;m=>zU1gj!*k_kE|dU_t*kB#(R@ z7v{~^$*91^onq(SV7SrjD5-(nE36lr?}zCH=VB)ZOCR*B&qnc~MV}5UCbE*R-R81X z)~}7#Xr5_w>r6d*}{_O~?Z>Mtabke>BE*{xD{CXv6G9gvU3w zy^%yb8(&|uC9sAQ50_$#vL%16FWAeqr7{MYcykxj*u(_k^Icva-HK4@+n4bK`_#qH}CNLxy!mtMz=NXG`Jv z2luaC8W~aE2g8QVVswYrbzj~!G&7^+G6HiIOqGfls;!9MPE~N;e1Ub!4THU0Jgyr( zA*g|U`P@<;YcTgXe2ys}Vi8-%vz;Z})BKX9%SNt|a5i!Lb?oZ3+eY5)yyiW~@eR7g zyFWZxA}m6K&qQk@cESRjt@t-shZyKoJiVzg@LKj(mhEN& zNeLz=-kI8fd2Jq?FQ5(4-0Q%bqhgN}dnQX;xJLYm)aUEQ2JeGb_HpqCRyDCJEtmEq zoEbgMO6`p7>}Vi^N_=1Iq*d4GZPN1ek~MGq;GhD#QRj#Zw%9BEBZlGkHd$|FlWHHV z(;>fp$zRRsJFYsz1~-q&>yH<(vG5aC2~+y#q5X7%xh}E=<_`BXS^ZZMFyX$4;+^n4d_m#5MX#w`2w65J{AI^LIO-x18S^jBP9k(I%w1*s(Oo zyCS@fKEUpPtWv}Z>O;zP(lz^x>ld`hMhk~ZdHqsPDbu1InuBF89=9C*i#bv3A-w9LJ`{A^i_W-dhNE#fE`1P|b_dFXRHUkDMOt9mqIuB0bQg z;lVFimXXGxpDMKvrf}>?mkc|-)rr5U*tW4nOjp?|*?Q+`Fzc1RSyX-KI%t(2h?tU4 z9+5|iK^+soQlXXvx{Vw!plbERWl+&ld2H|m6E6Sc_*4<0->12?9~&oVt_CK6f9wa+ zb@|6*#k%0cQj3~^xQF_CDU(rz`4558nxDuwR*YCK|48p2c2stM{lN$qWmbc%S>Gmj z?fk-HW#pYuthDo+%;2L7jj05=Ap1|97mCZWwY0f1KjZ;7ujgrpBdX`n3fuiLjgK8F z1*aMcWh{t4Ws@@0^R21lH4u{+izPFmUtaXBEbkI1@2NkJTC&#CXB_Rcvcm`!w^0y? zyq;}QwUQ4;hyJNUOF>sU!XzmVdFjwaAZqAre9s{yuZ&B46FW}s8Ou3E=Xp_dv|Q+ zj#uIGL&GtPo~q`0h&v>Lne-=@s7g`IM~_q<@sr3Y^=YiOQX9DwLUvW z&$gtr)bqp}x(pW6ozu{GUfP89=xLj5L?dSE-S*>T%o?RnBV^Ai>!yQ8UdM|@BhbfQ zKg~IDiS2w^rDXmf>m_^{jKgXq6gO`d=0j^8i3eZyN_yyfbh69kbUv#(BXt6^M{39T z^a9N|Tk4|U@G@gebiiCjNp(W}sKo12yz2r)ag=gZLG99L)5HLZUD)lSz z%?dA!6a!|Yad1EZ{>H z>m3m}>^&!gv<+t{06k5z*IRSTP(@ce_)0b;I1^lp0e76Ux0A0QmY5mtf@@V^4RxA* zo?Vl#AiwRghdx@+wcRXRdZ}S3u7u)Z$o$f6d;(h{4_bzMU@71=GKgo);1_9G<=R}a za1yuM_cCwX8PYxDz9DH-NIV}XO(f^`f~V}CkCEfaxs2S)V)8)$jG>{HIYEBv zi{6jr7Vg~~^QYknLE#$@z2ryyNz8Cu-+2Nl^yE1~R6emIff!Uk*$Z_R?xl2U(Fs^W z_EDPHHa(p5nhMmjf=f-N%bpgqOJ!MVK+NVP&Yt zxgeH9(cJCr7igtmisH$EH8E36jk^~I(PtvlMUdr}Aq(8jX zo|3cnHWdRk(9Xd;T=-X6{m@g974n7r8C-fNl0Dg{q?!+I_YDkDh!41+R2v*Wb*A0c zbsXkiH%|CAiZ*rx)aQM)^ElOwXf@;92tz$dXd{tT(NxpmPw9luEs;yR?N0-$&MKeb zw!1uiwcpN^iNADKMu=3N?@C7VXh!s-XkVlhRAh+J7HzM2o&QwKnOJlY-=zUhfGg|V?x7A2VcZ zhPfP3m)X310XGIG9m_m+b6w5Ox(3b|T~tA=6%DQ@ILQ^{)sy9;5pany-w^M2$L)G_ zjb7KVPIR9MSF(PK|HhPMyD;AytA3e$tGl(9DVc~!Q^~!;gv=CX`GbEH+&YoeXOGDm z^>7u)))G@bkUEhSq7CF|=o*f=Vkn3IO3JU6)&AKEuOeA#|N*%>QNAME;{{t_Zq+R4J)&mwE(Mrt)NLrV=zH!l0P9{u}lPY zk5o8TaRuAcH0|?grhnzSJhe@e?-E-`#tD_ML3m2#u|BEf5BMQXCJ~;AsPFfjNF5D< z*1A!h))PEQKmiPuE2nD<15NGniWO~{it4cO4}~|1^vpCtTyU7)R@dByWO0TFmOOCY zu9UFIl_(=wtlnQ3obrky-p-`8a+1R8Qqt)o*Rm=xc$(|=wox{Hw z>A`~^BlE**`q-~_a0oysWKbY}gC7V^q5FI=Y+UpMzAyi?pLXz@8ALo~NNs<{`LgIk zUt#;%eZQ!DJqX@BjfWBHFJw^`q-T51EIjVOzJY>F<}AztVw z&=r}DUEGV))&rO~64A3DnACL+4YtRiHeWX;jfXqTT>Ht^=D2bD+ld)OcV!RZPgoo_ zwy8Wz#s-rNWok~%Ca6PhW009IiL3)N0$}diw?OjCPEvg$VI@*9i zh(O5xbKRz3wupP|IOw{*_MNDHC1Kv&4JNc>!L0yWp_?lMu~^qBaoSUFqW32_ogpgfWBjB?^ZA6v3eUBZRsKMCDbG7ytz zDOdo4Ak&Frn>#pd_Yo^_5ra6jDiADKn`?U_`XA&xgKW}s*%7Rk#>N6Vu;NDvr!N>z zO6Kpso{Zj{U#8`j2y3WV0AQhw#T65o_$l0ZA}_+yf>du$$wGkJNa@^EArNOo+hXqE z4HKDPFco|fW~*XkgJ4{9TdMC7D;pqx)uFzo1}Cd?*_ziHtmmto){JayY#3|G<1br? zTdl;D3jNCCw>2wSE;)=1Hk05DOFlna76-MBrYNQHKcCO)5TecjzE65OSrZ{lSY}{G z?|eYqZ#FsasoNRN?)o}xvHi$c2mK+SA6zQxqC&MTY#B}nj|=8w%zCZ9VZIIttsCCp zI{P|cd9rXooutX>)G7TTxkm1RYI6;P0GbxwByu^HIa&5IH@wW}TCGP()Xv~Zb+tls zGR&RK8wFeR99Hlt%Ct6o$FugQq@Bnzt6LdZ_Or^cQ}cT2H?!$FWcw>aJWhn=eq>Wz zfPQaHhFaonlAfeSNYk(lzJH>8uGyNu-MFgqHr!j>Xw!)DkTJfIt3v^nCY$N0$Wr!E zqfAHsIbbUMGSvk9x6q&T2ny7_NTqMhWr91fLNtxb%Ne!uGwjrA-H$mzA4`kOMwWYP z%#Km&PudFXOC_yD731~3Slqo~iTU!}$&cNWooQYEDm@stGR99z?g)Dc#}ddKlUjnI zm&qvkYj(qSUBoro1_ zlHj^;nG*qzbu9X;BkW`=1ASbbJ*^PVwrfXRAshF-b-_=;QpxjB`&w z&YQ*Q@HzT5URmQY6@!cJ)>s2a!oq1z?^j0`k#qd0InD z-@1_GJO~w2}hKZIK3_&GFgo@orn#l2p@mRLyO0e(#kX`KfZ|%K# zTu)!uH~uN15;CL^icpD4A*4x~L&4UUh&GSfUp63}&RH8JQDx_4Tq9jzBi)dWG zwLjmh>-KzK*Zq5*`?;@w?$=}UZtt@{=j^@qI(uAeoxL6S%g44Tsb?3~Q79Yuh%JCi zkDV?1s$etavtN2goDFK#FD#@p*L?WoXuY$?r3Y<4C@R5M=~He8-Cb_HcYX7|2;=N* z!F{&-e3p06KEDwaS-3V@;Ook+w4{7RwjXJg>fIvhIxj6Um$jFu2`PA3F&6sojJtj9 zwe(gk%Pj@fU0?IPrqXsyy4eEd28?zK@2Kld=P+!&{n>rZy)~2#)p5G(8ZM@^u9I9t zvsE|Exs7Y<1EAP>Frg7Y|Vr+++t?DN8@xweUg7lp~A2?2qJrb%H z6;jIQ`o*|m5p(=%J=|PsHu>4RfsVzQGPbkk!$mX;+kT!{zjK6X`!dUr%@Z4|y8OyY z`RJ;?J(g+^EvPJjA9-xC9S=-m9XX7%a`--JX?xvK9#0k$RM7a6bVqkUTEpPL^V%dD z*X(;t?kYCyb8lo%4663HD|8<}X1lqfJz`b9JgW0XHn7L+fAVzdh`-nQfeR8>CbU$R z2zadbrGI8fyGSjhwZkHl-KFoS&Ur^iQ5kDAK|Nv3p;qx=&T9#4NiKi-=b{#E+>Cwe zp71mDRWmc9xFNaVc^!52ln}X__d?U|uFQCO@oV?ASVGThW8v=wDBkeyv0FmNzetcN zNmu8wfNN#u(6s5mewS=bi!1a87KmLqtLCA$A%@PaK!9y(TkYbo#+o2b{YXjg$%4Kw zR*tcE?FEX|eC}vgFG;wb zNI(2Ne;JcP=*bT&)qN6aUR7-5j_{wkHayKb)0Q~1uI^Gn|Dc3?`}?#7vD49-Ld)-V z(oilg<>1NLP~G+E>9sg<7qKg^j`Ej<@l;*NP|i_J9aP{!N!bmtDdPcq6S=kt6@_IR zMcLXTUJA+Wrn$T+Oy#x1!T^3<+_z`eZ+n8s(GfAqxPSiAm4R7Nt44ZXYCSz9wC}_4 z0Y8I0#ne=t+VGL0IO)K{ zkQw@7mtTFyuf5NlriZ&1$cj~__H0Z)%bl^Ca**cPHa3l^_6jw>y|rFK7or5CB3?({ z(>(i)E?)Xxf?vDQhFGpSuJOBmoioKw6lNn@!wSwh`?A~w=c&F(r_KsG&20@7RfaSd z-UUIF(u|*LCWgh%+Qdmom2J&fArkmv%vS6`X00@>s&Pm#G)VVVkITqryzktmn7!UNXEz6f_+_Xcc_Ti_`@_EM1)W@5hgN2$ zfE!<0YrTBRhvyTLfpu+f^Taf!wnw{epb>b|i3e0){PbjIU&UlyQ>EyJMfu02W__Mm zpIfLxpBx(Kn`FLhiSrS2iFS(~;ZGZ{bLz)$@}Q6BlalTl`xO;C?*w=-_nu!TzqImQwF#%>w5?FP!-jTCjl3zv z&Veu!jZi_S-B(<0ziHgDW5=M+Cg)#UKZ;rfyVCHj_>B9)N{lX)wEq6;W`kY0#f$df zZO3GNm+l%YeKVu~;Bw%yx9$xmTsP)O_Wimff3fs^*!1L@PhZZ7$rmdu^4un2>u}IB zaog-tkKV&~?gdz^TUK*Ev~SoY&I@J&T^Yh?Pfu=QT#&SI)BcGS31YzK;_}6^M0WvO2CMJD+Gz+4(=ffF>hoI$~vpA_Jf^jE1lVGes#QwC$%pq*j zZPkXX)*B)(9)C&KTgc^Hr=1~fDbtkxXq7;G;SR%urZ21l(-i$1=Z;*mlIA@&(r=d5 zuK%^W^Yinb;Uik{kAiD|dbl_o=9ujH>FD3>GP`d4HfxlE+@(+f{m0A|cV5@cDmi%F zpX5$|S7gR2K3yQbUhrC4Mr`in6^BuW*h@7Zp5n9CgUf4S|WkOAnGUyl<{A7bjT0+-edN^Om4h<$x$SnvuC?p)izo+61t!XFBCv0r>vj3P@lIeaPEAy>EF4`?pl9QDqI z5}SYc{H4-Cu&6y0wfvmGwx9@?jrw&@9T!HwG++ze@|2D_sqlif_3c@ybvE@`xrP)5 ziIY~h4jJwMN+2=BDdEeR~-fvW7Mbt>l@bqfNg>8dZ!m5mfXy|7s&)zDH z@02L^R0^&1=6q)`9i_e2MgCp1b3+~FdLgTnwb!(HZKBK!Zj)(Av7{K$$GI#?domkZ zqfe1fQ94<>C+1$KeroxOm(q_m-iQxW=&uNz^D%g_z>@L6#br_r6Y9oaR`89l-o0d@ z!WAJm3x@bF@k-*`P1&m|-x-_dm)u*Ov#Y5#Yso2V&W|tK#vk4rJEm@*y2DSwO!D=K zfwy$cf0-i zwVM7FAA7agq_mph*U0>kU&2Lh4ST#jllofJeyRqme3M{M<)D-DJGHGr>-g24(pQ2^ zQe7guA7)GNB(f@rRrM8YZw&1d{#CH#`cRJ0zCASdyVGUYr7z6OX?dq+man}_g_o{l z;BKCk6N~=r9`!yZj?k>!$S!B64RoP_pZ7V>TyX~RvPZ0mItKQ4*KMKSvas&jUQgx~ zCk3XjD^)tPMC;OLB-u4!1alVBRH?|*!K-C5+h2=mU*Mk8VlrBT+J+6RN$^rR#uX=XFdH1 zR$r^FA!$l3%=B)mL-r02?z)WKTYK&R<4MN-9IJd|DOn9BQNa(J9tdk{i$6VW7kIDp z`}&S&cO`PSNUu-daN@)phYsf#1LcQ&c6ZL^<}iOfKfCtXx}E-B=fbz7Upv)sUtx>r zjk@~x58SjLnz=IY{EFOk=uuan&`hU}M%B2J4DXkk<>!prTSLTm-sbOnQ@n*jN$7d% zxLC#`-zBvEjrk_Jy>3RW)AEbL49APrccQJf7>=iX{4SF(>MhO9<2EZ#cXDvd*)7VS zzVJlFZN=-D`%yG3#q|qpR5kWKkodS`xl-|KDYr#RMeRis-)_F>&wO?DR+*vzYb&2( zui$&*J&d*sqeMlWs>>=1=~vmf^Br_+s5+&qm)FiN_U0#t`;q#_35FFeg0~v|?aiL* zWXHSc-4Sz1;3&AouzKj7?R4Fj^9EPU&99?+`Bb>TI&-PJ5AD}Ctyel}6<>KL%OhX! zhza9!MU^?9RbSf~8Kp)%hTqrS(PEjKeLNngF&dSvdfroa+e#KjFS%9uYb6_R#(rI0 z$k55Wz2QeJm%Y$_o@*=K@m3t0dB$~TPmHipVqfZ=pOKM~>-is!Ji3kIgXY_)-dXMT zcvyHesI&6Pa=%tr+P+BzPDhqt+xODLY{Lguf3BKw^pfhb%fZh??42KLIO(NdeCo;h z@|1MJx?02cN0%ut%m_R0(=%E8tDKc@&TYslv|-7{;5^D}HRJq~%b5vCzCM> zoZPg)qT|qWOBkUM`}u3@+$S%CXU}e3Kc#@?`0u9Nw#nBR(ly*^4e&g6Z5bQwOaGNo z_Klw=p7~HNCc4^1T)FqjP;?fT{^LFMI`-dgW@_)Z`PR%sE8Dr2)}ixyHH$Zwv#$^Z zW**gkIdfdR^s}68#+J0w#c9&{syd5=6($R3Ra4(;A1c*M6hCQ}r^TgwWEagDhO{s1 zXcidX*}sUx?c%xW@PLA$%Xv=SkGC3ebv#^M)tVS~_r^$AwTwe}z=XoI>EQcS*M3~v z-ph4XlXE0D@L=F&?zOk3p#5NE$KKo>fiOJ6BgXaZ2+x%PF83wP$6WWy1q<&i>CLD~ zDYYo&ZrF8g*BFmKt(Hx@&1)s)LWzcBtn#lp?rEw8YP2SE2ivQxTh{qF`hnK_{c$f= z?B8sncFBEw!J;(okjp(|376{+-8e|`UhOudxtj9LOq^~RFqC$Beo(5qqSCvcK|gfJ z>H@7$C39uH_}QmFhlEEX>yL@rx@=s+nv_@_t1ntPJ9jvbeXcFQt(lDmuQ+TnWeWM|BQ%?_cY{35eLQsX zYm>~j@?X>Drd=&6&)Gj3q=oTN1ev@uzQ52AZM|Ekefx}f;XsqZi`+y>hUuG~0=$Q_ z5A#rNNypguzo5;Zcv+nz&G(~J@%-Et3B#@W>I%`!RfpZmf=$Le*H%IC%KM4 z11;BF=J^=8Y1g^S#aT3@ituwm1tGs)W$cxkJ|-PDKo3Az=#unV^_QS2x$C~uSR zvU~AZf$v2ZOwh!ZyjHkQ2Z%x z>7E?mcuSMb=*HeWcq31N<|@Tp$$lsIp#36)*9|9pR1)&aU~amC7`9cfU%#I9iu)VA zr>WZO&C?t21c|oGmi&nA;+<~VGXeJEGj3ld{f?a6?D_mCn_8vyDhmd| zPdtN~Jg0Ol&RlR*r!n8+p=h|QR{zqug}sYemea5)urZ$51$Ilw~@pC zZOigD-m+Hnl5~}uGrg}<^YA+phG`o2M;pr?7B9Hqkf+wLkFIY@XWino9-2dyhaxN4 zxmL32XEG_A(d14Zl8pX&493^EFQq>?=V1Kn<$>#5VG(AJ>Q#e-_HwMJEK8SOB>Cg@ zHEtTy#KmtcR4mqnNo`4g>oz8Tb$20mv(ePZI;;0)mv-Ig=ogo0v0E;axr}~~+$*IU z7oF{|PqdcDjq~v^y3QIoYmT_9P z>41-uwv@uV1l^=1oF(4OPMq7zmXEyQJY8^J<+*+_|D~PLe)aro1S<;t%`IpegXB6` zevNKOa_o>yzvNm_Ae7eBSGBD4ltI=G+L#YE3n_HvZuzwI8Z?z78C<$4yQiP+pZxa2 zJ`+F5-z4Vv>!Ehu$qnMiM5>jA%F7Gt6}P#t9b!B;t;4s1+hc!LbhzgdwlynTKy$eQ zcW>|cvm<%5Dk`px_7}L_6sKE~i${;p@3^1qva3z?!L=oqEi3PDrIamV+CiHxKGS=I zZ_#Jtdi&)&3w-o4-T0S_jj zmbV^Lj5Ll8S|8_Thm+oz>bkgWN>^dXQ8ml2yJx}DyG-MN_3#Y0*}1Tl4Ga!5eS)UX zhJV<1w6?ZR4h;=;OiWB@S9In{sx`v>!%5-1Nc* zRoc}ZsnwAN0wwe>6P7iHbn|*8HiT2OSU;RF5OGX$ky~|tq$E*$fmA`o-m}e}a}psd zX}^ucIUknui3|Gdk(B?9-8Ed1m$O@NR9n=LBieUMG~4mfD6Wzq-)d$dIu*aOO7s^w z()lDO4+SPoO&wryr36uwDawk^0&Mjbjg3aRNsD@?B`#(*ym)cN(W6JN_QcyN-+rTf z^OV)4OL$y7hbNo*=7zfxXJ}I+7E)w(Eqvs;F!7ORn>l~!hg_o=!&y9kElG3Nnd8Tg z%T?AVdBz6{(Uneye5`-?c9X2gsvyM+v)Z3dY?1X2?5{j*b=l+gni&CWtw@<&GDm1+ z7(`u@qaWS7aEdp4NrxWKR`r}Ud@mlMf+M28L@8NH{4{rHgS(2H9UdE?`&rf?e}%5m z>E?B+3avlu?<@~DxVLx@#|2HxDczNojFhAFugBZhMEl8RJYc3p%OgB~kcJ|>ZfRUx z+{eDAOc8N$S<#J=wfZVSE9rN*b8k%&Z!ud?Bbimq8LjweTr?_!XOp<|`)t+Q22H|0 zMm9{oP#nxUJX?MNu9}2?yRYurzq(f~dF@pA`NplTf!!kbaee-qL$h*SS3g)3>*T0z z8W>wI3=6L8iad&)&V#F>8?RM&`<>-m zBgAxgNoaH-@$57Sq2h|yJrr%OY zJ9e}eN(?^stnO8Pb4FPq$(_R?Z-GH;W4uGI0j=RetEY;)$9>|De2>v(J6_P5EmgNn zhcjz1BaK&`{|fWxQ{QPsqy{^$=r%AJa0W&RZi%4X7}gNH+|y!*Q+#iEB*eS`No6~B3g@!G|Y z^S;wb%zYmIEANj?8yik^wtKCV{?^0m&OA=*sxf+1bnV>nLtku99h51O>_53&Qclfw zOzYjvp9e5~uU-WToA~&R@rA!IZIz)dQCqF~N74(d-kI3c+PmDn=CE*W%LKPs<-Y0- z+_LQqr^JURU6y2J9cFz#$#(PmGlsdSmaXm&*RI#&i|zMjsbmrAU+F5gY|o2C$)(3{ zrQf$Y?DL{6Nhn~!NWXiB?a3IB2s+s){jpm_mYMO!dhZkKr@Q*-TpB(%&}b$ew&Bah z199|yt=hJ_&Xs`HV1uBwP_^HBR>sI ziu%Hs9=ov4W6@!u2fSpy@=EWg@JIhh4-1J?*|-1P56|o+jeA~YUTk$>{vMh8{=4Ft z#S)zYDLV>M=)~h%y2Q78jX&q6QKb}g(^%aI4BUu&TO>90Ro=}G7Ls1=tiWyF?rjot zkbdHLQdoUf)$V*LrpLbTjlbMg6W8bb?!|nFXKT*MyblKlg=(HzYyCV*>16zA`Rl;5 zA5PrKJ}U3zYrpAirnw(~yvJBqH+eBfs*9}1rOU}xdSPPjbbDE=%3>_pF>lENY25DN z^w5XWpZhqU$fEPur+AAlEet>CeAnJSt@J?C*^{HCBA_;#vfQ=FSa@Q3w!E)oNTu|H z_l5RVOBT=OwGTeJ$bL5Bbg)Xu=FQdJ$C%!^*>zYHgKqmoy=Dsw_6T)`m(6){19t_@ zIwwxNZn=HRXp~7z?k{+nJd)@Wi=ddHcN|n;*mu zG;yA#dAaP(7Gbf;&!OTSrMAOOj$&}Hzhq(k_$4XdFetxxZ|~9rrK-0F&)hzJsLRLV ztDN7y6Za_tO-)U0wY9a0O?}@j+Xg2T0}oYS#9<*DkM!hcHWyxz+xzgiXqH$n`^JqH zuj=c8GqK)fKqT;@m6bpf>pk_Hj;~+8_R#b8KZ=Oxu6VmE@`cOdp!mc@2oGDCVIspg zB~tPsx3r*O+>+^M-!XO71s3m@OzwH+s&jLZ%MDt}?YU07tbvEVrPdN0+z&Tf3WvL> zu)Is3E;-CIO6zJvDIV%5n;y#1D0bfLc{W_-l|0YmH*vbgYhM@h-B#ZfnTUHu?RM8s zyVV&nv32mOS{e->?fr!xf1JPb3%{2w7MIIxvP}=;;W41aT}NfKy3a*7_y0JaDDEcwrgok<8SRq*^?!5g@!j# zEO1nQPvNVf$(}T3u5j8!sp#B&ECtJUwAsK#hFcX)l?~1;x;0(0)AH9##CWqee^OR( zTqv2iCVA@e#;Re5*8S7;&Mk)rMSkd%j(saP>DpR(O;TJSoF`UU2n>YiBp=D@W{o&A zhH}i-tL$|3kO(pnU%W26TP)A#5g!~dUaOeqhyu)^EbMS{!If6eUd-^C)cd+=sC z=eMb>`H=;tmUPa;`wrT7yYW}g_l$1R!lbHPU`*UQVZ;%!#yy}GGk8yXjSrLM8w zuDjpGm8J5_``ue9Qeb5KOFd<&jE7Q3<34vncs@D z2*V5r;{W}(-)^J+PENrh_su(Ua2knQjQIb1cK-hV8-D>lv>tH)A&U?~-~q1GvT&e} zC$3U-5poC)1iAlrN3J&`jv~$@N}E2O2RFXO?Ibvima z?J6oN#h5Cgnf>6wgO_Y=ZToF(Y(~*as*8pOSC@G3+~0C##vOv}u3Wi7?i<6yD!Udh zUTnE?CGG%bZvGi};T^;HL4JOITYPTq-=6(fm!yp3eUZBGcSm?N|31k*pNH z6#Cj48XB(&{V_4o5FB_7E?Jt*EM{K!ZokO>vH0i+h>wm0e;-d^T)Dn~8QbPt%hqm= zL_`rVv2f%I@C%L+`a?oO0N)p!K7AVS8)QKK10Li?gsBy`kK|8topU_S31+|E#xs&HEfr*JR zT)lb~@ZfjAgG0d=zf1S?^8-Arof>v^bp_mdad_+2tw~4^X-~h`#id5Ol|1g8@qsW| zD~OW642j3>s7O(EgzHkKkf-4b5A{N!+#n1p4I`-6u}ZdZQ_~A#bnU^!qZpj(0Ahv! z@NjEL{05MOJ{1E1$$vxo2!Q94Hqga&u)Wi*rPfDMcL>P)A#vn4 zQa-G6xk$ehV*YT)rJ6i4-f0R>;45kbxo5ngNaGq5X$8Ooog0v(Y!8Ww4v>A)8?sOP zK>itjz(Zf5;9LMyTO>lAc^ueZuLjSz05xBx08%FbGSI(`ApYNxH3I_?UqD+!9h{w= zsC*{%$Hm13|2io}zkCo8<~`^8KJC{~ELw1V9a52gzDO(n$wMJz)!` zK2_k}3NTnX1Mmo7ymSU?uVVNm9-~-;s72uWp@z}-zj6(1Y;BNsUkJJpO69Mkqa)y6 zV}Lt3Q`;^Ipr~ad{Kwse0LA%`mYNDtkx_77*MQV<(#Mj%W@*4>4WA!1@$fRk13s69 zz`&CT7$mnBxf})d>J^ZUel{0cgvE{-+7hRT%-l-e$q652JZNVg7x^o2_u&nbF-$nk^HkV`Q7G!A~f zNP(%kG?;sx2^~c@pgF@Ex(h?#GeL1Uyw31~(U++J9}58^AvnF3%Kw2UNdM#ipc|_H z3gbb%@S_aF&jI=$^n;$E9{BirLqu2(JSYITbNeTxru{`Pp`Fl-+aXc=RZ<=j&d$CG zDM_#3b{arF9@gUtAZUviwe9KWd@#Pep z{CXNx`_6*wdj)W}vxXZ10T2@C4`yLc!K)2m=-DiEgipfH^0~k245>Gy{vh2{yg>ff zPJ;G%ZEC*~78Z(|1ja+a^+2$+w50Z%M4OA&O>p<_UBIunspG=+>({}^$O!AvRY*@u z2Uj<@UZh(YLE7-r-Ts1}szayYNVPO5_9%nOuqvDyISXgLYk*9%0%-MXg5j(I7|$7j z3H~!rzW~=TP2AoMGSgGwO7wH^ZU-2CHUpm`rr~GBZ!{CyNjZ@A@8vV_!VK%bqc76n zO6~VaA4BNFeJ-#cHKFpF&`)@to104o8OxfRn*qmaz|SE7H*Y250THhd$Fy0W-T6?d z90(>$g4%!zXpNo$?Qu2G`=tdc?-W7ojx-#3qy%!0l|cD{GQ@_&LPmN9+`4%ag8h8p zbWADuBK?D;{$t&z@|v{WSAWq>XeXG%vdrN7=q~C8vkPX__8AcoL9Hi=iHX$uPU;ID z=t-?Zq#UF!&9{NHv@~#ccb{FbV1WnHP3Bs}F|7!R@Ysl1uzu_To;+9JE%t&P_q~7* zu`TN=aF0y`&n&>P>Q|5zn@r_F7UtvZ;Q`7K!Eh|93jF5jC-ooOX=lPTyz#^G)Z;ZN z%RI15odum>eZ`vE*N}c4Kli5g^P~(ZDJekcByDycB#!h$q)p@4Hi009LGeGb_S-6ho6E@em+RzJty7Qlu?%T;LCo`iDU*}42iwO!So!o7?7%E`Ap{Y^h#eDSu&((7Xcatw zl&D}xhzWzVxCqcrO#*5BT^?~VzwLjef1cO>D8pp+EL5ps*&6X?q`xS#2!cX_s5Ftf z5|3@THnSG;BJ&_V0OxH1v5*;_4csA@p`Wd^53)< z_PBkALxBL|!2pLt0rtC(f|&a-D1-wXjRZIq(gxQ)VE;qv&oh9Z&!%DaCHA3&ZbHxR zz<1Dcw*{i2qu}KoEGQ}0Kgw5QG6Q2}SkJKj{lIZBtQX*fU*!N=h@LukP^)ijXq4W_rlsoabmM&nI zX#z375s;6?`V)ushtS{g5Bjl>sk5Ge(FaHeX}ka6C)LO6w&3Z%>JifYy%etl@tTx@ zytDbTU>Xxhe<;$ga{Wm+(s&v{<~t$?vd&{c%)6zmYofe(vEgp!_zY}(?ZCbZY2^P1 zn*_RmmA4W02~UEUD1gKv0BLNnq9VUwvp^H@hMR_>XZ|F47M0da7m?MjzsPBEOM$KtRfY{Qp^r<1C^7+G9dLS-+5V3IUn({O*>l zU7L29mlt5mmd~)04|y+$ycYrx*@t}*?ZU7#hAHc+vIlN*;Zt2REMLDZh6=gFqrIZ{z zI6*Co43nD1TTeRfOSpp0*VeUBQ7CKF`mp-xe;p-YzUMIoX7cx9-f%1 z=^%X8P((9n|$QdjCg`l!XC7{~H$kizi7O zxsT|5IG3#6B)3Ds^db-Lc)}xNVE5kwNki7~BBm>k*o63doyVwuyPGdJ>DT^Um|r9Q z+ll>?;2#eB;lLjb{NcbK4*cQ39}fIaKA&Bj4W%0}Lt&?g|A$}giX0*{ zN#cmqmALs??flAQ-v8^@|NQU#eg9Q|KEL_==jlN@T}f`}kBI)|<4OdH|9A4`U-OwP zN;nV_h=1B?G2_J__lE<2IPgD_16)Wn(N7au7tzV@LXfqz5`wJN=N*w>6CH>ZLJ&dZ zVnkMO0x>V!#^8U+Ejf4YTp`h2qt3A#pFD41VDO0OP1V%Y`te#CLDup#3utMSQ0F~^ z`T!yiZAJTqZgL;m1dQVEgs#6k@*F9X1wszNi6DCVGYDsd6T-&!s_pm0gm`dvu!Xg( zYo;%l7(se+JeV5j0flDa#|4WR3uqTH7A~S^{2Y$@Bci`Ja^wi;>+4hX)kOD>dpUl= zv`{ufmFb{Ib*ua&jb7XAIRi2qh34co?dg?R&p#NVUS-`rEVGq zQN}oc^7nqkVC^gn)y%>B$D?3uWd`0}?huSR1EP0AJCT3t&`6ljVrpuPBM`;@ui{hK zo~AhVJP5r4c~|Y=F4}8kUb+nLiUOdgG6Hp~v5@a+3*}y@OTShCX(;~#)S-S(nt`bb zlyRaw>c<0=kD`1t#WM-@UrrDo`vhX5Z=>GM43d+R!NS6Vs@rjLazve8C?v+efW5*h zME*u}6>Kk)+bwrI?Klq7HxoepjK_P-o7kJM ziPcHQWMS5`jlQNtcSBEnb>R=S451~P!U>{ZjS)Is#|7ofofU5sP znKUW;WI5{p>;m9=z;&w5I4v!is^2EM4Wb(&dVE5IqoX~fXQaTPLkCJQZ_;jA4;9LP z7ArdnYU3wB;|Ji!6Pp@hw}JUyK@i8fp>L>XLsvI!YkCaZo1ehe zmk(jb6!NI@8Dzz$z)hsv1a(nI@?Sy@$)C#qS?Ef@{O4ui=(nN1$k*Q&h`ucZbvrR} zF;pE^GCrG{@)tcs_vYl}MDjPF=8pVd`?C4#`VU=H-5QFxfgk2z(}%ARgSI!xsF!kz zi-f}%fBf|~$eDlt^EwdJ9dyKC{X-f2v(#tQJTq@)!kwf$kQ0qMw2(wdPs|3#h}-yK zM<~{T96)`?@8xIXGVJT&G($Z`!T@ZE8vxG8Pq5E^0`^}As(xm>UndA<4uNdU2gpTz z29^IrhcPeLCvtwwzxrl1#3aT*LjnGX*F(7&kHPQF0HzOzK~7roC$THSzG)WAt}h{q zBW+@i=xR|`O$F-RMw(D&UHXmW@Bfc7|L)PTvDG=RU0d^x=m+?B&BD%|lfcei+|R}J zw0hgNr#qx+jaG-rwuo@A=P%S73mQ zm68ZC1Q~CM9GnipjySk+&tZM-#FmC{bYZ%%7CwkNd-cKOFeOfq!xUd+Gn&RSGNzSL!%HBpgI4Lh!qr zkNe#x!f%5UAWB3$Vhc4&e zu|4oQ+NY!ud`M&PiV*nsHwE7UzJk zaqegC>W^zeH?%8u277yZ>inD7<@@^jz`=teUom+mPqUN8bs26@72pX^gMFYT!2^b# z#6d@KFx15aKx#}8)L@!(XxH++aE?0Ho5FQKRdgA6d7;f@S~IRIoT=*zVk=H;yFx-z z!PmQ#%)3tMU6L|RxN8JG_rsw7VFVNx*g{dgBV;_e2yGAJA^Xllc!u-XsZv~1;F^lq zwvxH!?Vwu_7!(besc+Gq(w{nizj5ORt}zneMqmm=gw~Su)p?w_3?-f&L>tdzpo;4# z`NqR=s{a&NR9%FWxMZCF7r=A0&zUI3^l?s1)=k77xHjq?xOuvwt$!5NzI1-QL~I;0 zGSYGVY&?zWuUQclYs5bM9d>m*2Z7F)u&w6{7(K3q+X;!_kLw({hfPq2^b^_#@-UBo ztWSDNd*FKDb%;S5?a*+vM~p{%!^C(XvJ7HViQgIwWBQ9%ngkfL=b+u%;CI-L;Hc?= z%ONko@FrR_aF=#1hhgqD@kKV&LoZnZ!2iU4;)op#zA|B;J#O95%jy380w#{R(ZTA@P z3XcIl|1e01;`(X-B#4NOfb8WjP~V2nleGczo!GMxf0nEzNgP?LkT~=oe?(hFRr}W@ z-FZh(@5a1jNxbV;u8$4e+uPb$S+i?bt%~wnzC74lWY5Ps$@9c20W3%*s4z44nnk+mrM23`fG4*Xj< zfGvaQ(*76BEm}+|`QLs@M}|D2m|Bc#C&b^B>vx7F_Lkf*oFxs zRwIbph}enPid@2foN4bU2jpNO1=^V^eV7HxGi#FmF_-#`X z%9_F0m! zod1qG$C!DGw${$clWs1KZ}EA~4Qx+tY~9`uJ9dqM5dRn0v-=Ck8Vy5RH_E_TaEwBG z(P5M~jbeBVllY49vd$Ugo&4s6@x96&!3ZLcS8Dh%a#%gvs z=;&C>IFv#zn^ewHDdb)|sW+E$V&?a*f8NjM{p0-c{XU=P`8?0(`RmC?`+7k1P4oc( zfS^3x{51&sc3ZSHA2+sDssU4ir@s#XP%Qv}#sGjdjYLxdK#Ior9tQwURRCZ_s%by# z3;@SZN@oVm9QfBub03D92 zoKCE|Ak;IXpcDk#L2z`oIU)9ijnSm--e<4_)Am*hrk}d&%SMkH90E5UZ>+PY)Uq}8 zy=73o(NhvWHJ)*p5^O+6@i%x>YrxvqMe9Ceem5j1E?7`cEU98}c;By%n z)Y8(@nwn}b9{NmdG}XR)o$k1thnP!op6>WT!SRz+O1NCENF=IfH+<>+(w^-dhQ&8D zG-Oc7O}Ao{lSWT^cYqr<8yj{@D>mc9Ce63Qi#dV{b`4Ju4uWp4n=tDkT{5AJ&8m_~ znS~jNjg5`9wY70^u^fJT zaGnP)h+g+FYS^VD?;`9e3f#sSj}48%2PdDsYQEQMwl;j48>$rH<4DQ&hs+SC5RBX_ zW~O_>0=x-ZHJL8Nep8zb^(ryd8=DMk2gC+%^h{GM>!FaqBH;;fgQ0w1cM*4g9X?*IN8CSU`K6W zFeezyE~)h1{{`X`BV(c|e;+{D+GzZM<^LMUG4WB!WPCj7?=wbD`V#=4O+&f426*-p zaJzK`xCOx7B6@pgPZpn1&f;=e+C9^eGt+OUdbQH43TKSuqr+h2qp|H8|pm#?N%^BU1MdS0Us`xuRvn5xoP0(jcE9fWAgdxIz#9%N#8rGjb>lGU6 zaXQEkJJE^2Vkgkp3E6JhG1wpF_BpXQhoi0HW{ITbw)Bqlb{dIPMmloI-X7Rwl$l8g z%GAyj>pJL$q=|J6w)t(dwY5#$hdreS0%F~hVPdMbx|4_FQHcZ|^V*PjVGF4<-R(6J z>EYo)>HXT@hwSZ#0&qmx9wpSmyiS-kLRG~4Ib|$on?gcfIkvS?D62X-$X=wmNEC&J zQ0LZk^QVlBQ9G^5%dG;RPs!oZ6ddFWqz$o`Me%tommkc*K`acR#sSU4FNRx~xq3P~ zb90Hqo27JkGcgTtB?cv6&cvtrCGL~y8U=}&og1dS_rlkaruC4UT-bL)ykgc0Q|D9d zLRcDP--(72k3FT2;Iy=o=Peje^C&YS4Uv@e$oU#`6+=7>ybnH}@&_MTP-$ix)-om^ z+TA!dLP2_^D+U5n;Kpz`9QCUz_F<>VuHQoLCXBaBx7GP_+ezwyX!e}%VMB<;9(&&{ zSA#?JhQ%?-Uk481H+BDuBsKcoMi zG#zq;qvY`1f9!LlZWrQJi{nt=xQUj`cTc!b-w$Jy9rdmT7joWMX?Hw3gn;y-t3 zx9pHGaX)!xkp=z!oP)s)aMZ*Z3i|7Yx0!(E#{hxvv2D;N_z=T+!2ORII&+j0~tC`5dd zh+7SP`LtVXcCc{*GWcohwr!6-eEMV%+Z8C2CDE34dKnBNU0z) UxV4U?^=-f?q_5j=r*T>T1w&!Ewg3PC literal 0 HcmV?d00001 From 85de9779e7b3d3ea63f0c48442595540d2f38f3b Mon Sep 17 00:00:00 2001 From: mondogo24 <167080436+mondogo24@users.noreply.github.com> Date: Wed, 27 May 2026 18:49:19 +0200 Subject: [PATCH 06/21] feature: CursorConverter now support cursor animations --- .../jme3/cursors/plugins/CursorConverter.java | 132 +++++++++++++++++- .../main/java/jme3test/gui/TestCursor.java | 26 +++- .../Textures/Cursors/monkey/frame_0001.png | Bin 0 -> 697 bytes .../Textures/Cursors/monkey/frame_0002.png | Bin 0 -> 707 bytes .../Textures/Cursors/monkey/frame_0003.png | Bin 0 -> 699 bytes .../Textures/Cursors/monkey/frame_0004.png | Bin 0 -> 695 bytes .../Textures/Cursors/monkey/frame_0005.png | Bin 0 -> 685 bytes .../Textures/Cursors/monkey/frame_0006.png | Bin 0 -> 656 bytes .../Textures/Cursors/{ => monkey}/monkey.png | Bin 9 files changed, 150 insertions(+), 8 deletions(-) create mode 100644 jme3-testdata/src/main/resources/Textures/Cursors/monkey/frame_0001.png create mode 100644 jme3-testdata/src/main/resources/Textures/Cursors/monkey/frame_0002.png create mode 100644 jme3-testdata/src/main/resources/Textures/Cursors/monkey/frame_0003.png create mode 100644 jme3-testdata/src/main/resources/Textures/Cursors/monkey/frame_0004.png create mode 100644 jme3-testdata/src/main/resources/Textures/Cursors/monkey/frame_0005.png create mode 100644 jme3-testdata/src/main/resources/Textures/Cursors/monkey/frame_0006.png rename jme3-testdata/src/main/resources/Textures/Cursors/{ => monkey}/monkey.png (100%) diff --git a/jme3-core/src/plugins/java/com/jme3/cursors/plugins/CursorConverter.java b/jme3-core/src/plugins/java/com/jme3/cursors/plugins/CursorConverter.java index 89bdaefa81..09a2545941 100644 --- a/jme3-core/src/plugins/java/com/jme3/cursors/plugins/CursorConverter.java +++ b/jme3-core/src/plugins/java/com/jme3/cursors/plugins/CursorConverter.java @@ -1,6 +1,40 @@ +/* + * 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.cursors.plugins; import java.nio.IntBuffer; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; import com.jme3.math.ColorRGBA; import com.jme3.texture.Image; @@ -13,12 +47,10 @@ */ public class CursorConverter { /** - * Convert a {@link Texture2D} to a {@link JmeCursor}. - * Doesn't support cursor animations. - * The coordinate system used is the same specified in {@link JmeCursor}. The start - * point is 0, 0 being lower left. + * Convert a {@link Texture2D} to a {@link JmeCursor}. The coordinate system used is the same specified + * in {@link JmeCursor}. The start point is 0, 0 being lower left. * - * @param cursorImage The texture to convert. No modification will be done to the object. + * @param cursorImage The texture to convert. No modifications will be applyed. * * @return The {@link JmeCursor} using a deep copy of {@link Texture2D.getImage}. */ @@ -72,4 +104,94 @@ private static IntBuffer getDataAsIntBuffer(Image image) { data.flip(); return data; } + + /** + * Convert a {@link Texture2D} array to a {@link JmeCursor} object that will represent an animated cursor, + * interpreting each {@link Texture2D} object as a frame of the animated cursor. + * The coordinate system used for each frame is the same specified in {@link JmeCursor}. The start point + * is 0, 0 being lower left. + * + * @param frameDelay The time delay that will take for a cursor to change from one frame to another. + * @param cursorFrames The frames that will make up the cursor animation. No modifications will be applyed. + * + * @return A {@link JmeCursor} object that contains the data for an animated cursor. + */ + public static JmeCursor fromTextureFrames(int frameDelay, Texture2D[] cursorFrames) { + int[] frameRates = new int[cursorFrames.length]; + Arrays.fill(frameRates, frameDelay); + return fromTextureFrames(frameRates, cursorFrames); + } + + /** + * Convert a {@link Texture2D} array to a {@link JmeCursor} object that will represent an animated cursor, + * interpreting each {@link Texture2D} object as a frame of the animated cursor. + * The coordinate system used for each frame is the same specified in {@link JmeCursor}. The start point + * is 0, 0 being lower left. + * + * @param frameDelays The time delay that will take each frame to change to the next frame. Because of it, + * it must contains as many delays as frames (lenghts of cursorFrames and frameDelays + * arrays must be equals). + * @param cursorFrames The frames that will make up the cursor animation. No modifications will be applyed. + * + * @return A {@link JmeCursor} object that contains the data for an animated cursor. + */ + public static JmeCursor fromTextureFrames(int[] frameDelays, Texture2D[] cursorFrames) { + if (frameDelays.length != cursorFrames.length) { + throw new IllegalArgumentException("The lenghts of cursorFrames and frameDelays arrays must be equals"); + } + + List imageFrames = Arrays.stream(cursorFrames) + .map((frame) -> frame.getImage()) + .collect(Collectors.toList()); + + boolean missingImage = imageFrames + .stream() + .anyMatch((frame) -> frame == null); + + if (missingImage) { + throw new NullPointerException("Some Texture2D objects does not have a setted imaged"); + } + + //Avoid working and accidentally modifying original values + imageFrames = imageFrames + .stream() + .map((image) -> image.clone()) + .collect(Collectors.toList()); + + List imageFrameHeights = imageFrames + .stream() + .map((image) -> image.getHeight()) + .distinct() + .collect(Collectors.toList()); + + List imageFrameWidths = imageFrames + .stream() + .map((image) -> image.getWidth()) + .distinct() + .collect(Collectors.toList()); + + if (imageFrameHeights.size() > 1 || imageFrameWidths.size() > 1) { + throw new IllegalArgumentException("Some images from the Texture2D objects has different sizes"); + } + + int imageHeight = imageFrameHeights.get(0); + int imageWidth = imageFrameWidths.get(0); + + IntBuffer framesData = BufferUtils.createIntBuffer(imageHeight * imageWidth * cursorFrames.length); + + framesData = imageFrames + .stream() + .map((image) -> getDataAsIntBuffer(image)) + .reduce(framesData, (previous, next) -> previous.put(next)); + + JmeCursor jmeCursor = new JmeCursor(); + jmeCursor.setWidth(imageWidth); + jmeCursor.setHeight(imageHeight); + jmeCursor.setxHotSpot(0); + jmeCursor.setyHotSpot(imageHeight); + jmeCursor.setNumImages(cursorFrames.length); + jmeCursor.setImagesDelay(BufferUtils.createIntBuffer(frameDelays)); + jmeCursor.setImagesData(framesData); + return jmeCursor; + } } \ No newline at end of file diff --git a/jme3-examples/src/main/java/jme3test/gui/TestCursor.java b/jme3-examples/src/main/java/jme3test/gui/TestCursor.java index 812ab4e397..f9c1005aa2 100644 --- a/jme3-examples/src/main/java/jme3test/gui/TestCursor.java +++ b/jme3-examples/src/main/java/jme3test/gui/TestCursor.java @@ -7,7 +7,10 @@ import com.jme3.texture.image.ImageRaster; import com.jme3.texture.Texture2D; import com.jme3.math.ColorRGBA; + import java.util.ArrayList; +import java.util.Arrays; +import java.util.stream.Collectors; /** * This test class demonstrate how to change cursor in jME3. @@ -58,17 +61,34 @@ public void simpleInitApp() { * Checking and following the licences restrictions in the process. */ - Image[] textureCursors = { + Image[] staticCursors = { (Image) assetManager.loadAsset("Textures/Cursors/meme.png"), (Image) assetManager.loadAsset("Textures/Cursors/nyancat.png"), - (Image) assetManager.loadAsset("Textures/Cursors/monkey.png") }; - for (Image cursor : textureCursors) { + for (Image cursor : staticCursors) { flipVertically(cursor); cursors.add(CursorConverter.fromTexture(new Texture2D(cursor))); } + int monkeyFramesDelay = 60; + String[] monkeyFramePaths = { + "Textures/Cursors/monkey/frame_0001.png", + "Textures/Cursors/monkey/frame_0002.png", + "Textures/Cursors/monkey/frame_0003.png", + "Textures/Cursors/monkey/frame_0004.png", + "Textures/Cursors/monkey/frame_0005.png", + "Textures/Cursors/monkey/frame_0006.png" + }; + + Texture2D[] monkeyFrames = Arrays.stream(monkeyFramePaths) + .map(framePath -> (Image) assetManager.loadAsset(framePath)) + .peek(frameImage -> flipVertically(frameImage)) + .map(frameImage -> new Texture2D(frameImage)) + .toArray(Texture2D[]::new); + + cursors.add(CursorConverter.fromTextureFrames(monkeyFramesDelay, monkeyFrames)); + sysTime = System.currentTimeMillis(); inputManager.setMouseCursor(cursors.get(count)); } diff --git a/jme3-testdata/src/main/resources/Textures/Cursors/monkey/frame_0001.png b/jme3-testdata/src/main/resources/Textures/Cursors/monkey/frame_0001.png new file mode 100644 index 0000000000000000000000000000000000000000..c9c1cadf2d424f769c71af91c85b8e724bff2494 GIT binary patch literal 697 zcmeAS@N?(olHy`uVBq!ia0vp^8X(NV3?%C=ER6$F3dtTpz6=aiY77hwEes65fI?NMQuI#T^rTG-~ zC6!M|0fkgcTq875Jpiad18ze}W^QV6Nn&mRZas=m)cS#rPV{tf4AD5hchcUxW&@Gd zkJ1I*y^HeB-Z;CdlA*;jz5CY7ELRKv2^Z8?7^R;+5%Fzb{Q84WmY=+{S)tahVEuVT zMvuwXwtH3hA225~U2bNGIcQ>Z?tk5!0M(^h`I@1NPf0Hp(pYjmEQnR($yQMZpIw`p zPWl)}p8lMabGRm(M@}QnC+4Eov*xIU*XCXqzH|Ccx$LE7_cU+!g>BMYdzp7iw8M-ogzxg#E(C}z1L8ly=L#d-!FV5_}<$I zUXxBvpK&wOKl0YPtHpAyoI56}8vlx65M}KX;^GLXvI9q@O0xV+ z9=WKGot6qR&C3i98P-0U&~&<@(u>(A`RO`-_VdRXmODOi-yO#-v2VI&m_+ue-q(&F z9`0Z{k)mVrUCL;2`(y33PwuE2+nnDk?PvRsui)mKi1qD?aSz_P{a+K`emPYqt3K%g QFoGC7UHx3vIVCg!01dPSYybcN literal 0 HcmV?d00001 diff --git a/jme3-testdata/src/main/resources/Textures/Cursors/monkey/frame_0002.png b/jme3-testdata/src/main/resources/Textures/Cursors/monkey/frame_0002.png new file mode 100644 index 0000000000000000000000000000000000000000..790e325001c19172b3bc947a666452ec6240eb9c GIT binary patch literal 707 zcmeAS@N?(olHy`uVBq!ia0vp^8X(NV3?%C=ER6$F3dtTpz6=aiY77hwEes65fIcptHiCL>?3;sP=f~ChLX(O)Z&uF+ydNs6rZT|109|1>EaloaenJ0+q`B6f!3SS z28?SLFX2kvb#+33a+}58o6HK<-n|=UFsn?w^PEj@?y0r4JKrQFZ)m!dIzhv3@6F23 zj~Rd5Tfd|7gW7|Z3(cI*d^!Jfq&qUi*s3PYZF+L8Cv(PxNoS`0Qck?4p=Fd7%x7@i zh~sn94%ve~cbA>E&=->X#L6~X+=b_P@`@M1!mFA6vv>o~veE-?sge z+S9@txm5rQZSZ-x${<{fBo`n8S b|HE*6adMxZQhq%!su(<7{an^LB{Ts5gaZ=Q literal 0 HcmV?d00001 diff --git a/jme3-testdata/src/main/resources/Textures/Cursors/monkey/frame_0003.png b/jme3-testdata/src/main/resources/Textures/Cursors/monkey/frame_0003.png new file mode 100644 index 0000000000000000000000000000000000000000..8bf8c1b8a4006d250a8729f53ed4462bb91ab668 GIT binary patch literal 699 zcmeAS@N?(olHy`uVBq!ia0vp^8X(NV3?%C=ER6$F3dtTpz6=aiY77hwEes65fI;FR6S& z3Miyn;u=wsl30>zm0Xkxq!^40jLdWmOm&USLkvx+tPBj;*X)0Tq9Hdw zB{QuOw}!Hh>;XUx8gLs*GILXlOA>PnaO+WgqSg;|bh4+5V~EE2t&{fV9ySnYJ<9t< z>5$75t*l^96^ZRk{^uCX*g0~7w+ckaDCq5Ji>hp{*ctF|s<2}0x<4QKYQDcyQs}F@ zdbrqx$-PC;Ey6_Y&teI;IL}kAG7s;`8f!CGJb%sT>yaHdiSG;B&pl4(bkAzlPcG_R zIU(h2z^o@(*-Os+lzoyS?#5O0?^(j3l{_W8)@FJBl;rYDd7Jb-a^7(kX2pIO!Zps~$l z&T1RY)xP|e6?fRR_Ou&S)oy9w?3d&ce3a6uV*qlRdwL#NLzj>+%(T#k_p1&b$*C OMGT&sCwCg$1(237_J>}&SFLD7(#pOTqY ziCaV2NA>`q1`W6kC7HRY#U+Wk1-SJnK2hrjIoi|3F+}71)=3-v4jTwKytO}($h|tJ zBxSSAnVhFVrTID=-B!-A%<4=(a>+F3A^)uPM>0JpJ6-9Oo_}xd`*ZFLN}tr<$(`hx zBglEiz)1aG*G{>N9NvWt;rs=?LN2jYGrBw{CQsXOsXsJhl{Q;u+qrOMpQ3O3x-a|q zhpf6GH8p6%EGNn3VpH#|KlSPo<5|Jn1?^MQ3qL)+#AUd(toDNSmPk7uk?&u=9?pEf zR^}7;8)tFXmG|_1ZspRsc>8(I_f4ICrxmX3zA&-uwuyELzovN$M}gugTdga#R_1MP zQ@U?i?)SfRHthSdDK<=M2dd8p?DUzJHihkjT+fF9%`MN5?=<3mCKGb7)oJeSCmPWg zS{bCcc>W&>xyQmR_Q2IPNAr(wvcSUT2h*ls)s<>Hd3Y1&-(7;+cn_pEgg@JuyZdRd zi;L|W-H^Ypm~5D?EWYu)Job7;xZnClr`y{;ymtEa_w2nV_Q%b0UM9M`*HnU2m8Ywp J%Q~loCIIqx48H&X literal 0 HcmV?d00001 diff --git a/jme3-testdata/src/main/resources/Textures/Cursors/monkey/frame_0005.png b/jme3-testdata/src/main/resources/Textures/Cursors/monkey/frame_0005.png new file mode 100644 index 0000000000000000000000000000000000000000..f39c59cde533a1351c4c8c3cfe0017ab46192cce GIT binary patch literal 685 zcmeAS@N?(olHy`uVBq!ia0vp^8X(NV3?%C=ER6$F3dtTpz6=aiY77hwEes65fIWo&F^Vy9{IBw4d5^7A#4n5Qx8}TNNtpKM z;$?3mM)e*}pA9LeB<>3DZWK_e&Tw|Quy?xG(yc-_=LTkJZr!(CNK>XbZ2Mi`t?5yo zX}d4_l$MCCzBg@p)+-;mSzF^`*7Uqm2o#T~wLj$dBCn+(v@xPnb=%j7dAH9+WMAEN z@5)ulPkT0QtK9nbRc*4~2h03>Ro<_!Z8`eYV?%1k)92d^9)2)%*>+#)QC+xU$$rlg zxreLXzrDVWYvQhoJyUP4=ob(c)Y4Hi5HVC-@p9$}KB&X2&WPFuaoEvfnq8 zZ>?nk*PQ7lwr6T~C59QqUkIqT+BrRh@tmpIo-0ocWNNw!>l$vzF#1}_{QdD*(xd(C zfu2%RdP`(kYX@0Ff!9MFx53O4>2^cGB&m{G1oRQure@UU$g%WiiX_$ zl+3hB+#1S0vIhV)XuxeK$;?eHE=kNSz^zB|iCRC<(bk?Wjv*T7*G}@yJ7gf>W^11C z>WI*3xr3W589rTDr(2L9y>(t(gIajPX`#LYn%UM4KTl4Iv^e#7^5mL$mPal&XK(M8 zXjEQwKv3=2k#HUbJ6rxGhR;4q8yUOZ?{3qXXjr~V<$1@Qy;^e@nz-FhVZIjF_?h*0 zy~oilzW0xIcWvufQm+#m@`1VJt69r=_Z8E=CD*oIWsttDvx__Csy3J6jIV3@_kTA2 z6DBj`r+4meiSq#=T8bxQj7_hHGcEnKZf>dF&!-+9)*B|iJW(ZX_`{PkDE)=-oi~px zgFJT~x%XZ6{USX>#+@auq1t(Z)2+gpM4u*vE;_hfXZdo^v`CQ%O`T#JCABcA6&hQ9 zN9ZnVpL}NRlsLtfZw;kU-*)a>nBDQyt|00|6o@{e^AG?4 literal 0 HcmV?d00001 diff --git a/jme3-testdata/src/main/resources/Textures/Cursors/monkey.png b/jme3-testdata/src/main/resources/Textures/Cursors/monkey/monkey.png similarity index 100% rename from jme3-testdata/src/main/resources/Textures/Cursors/monkey.png rename to jme3-testdata/src/main/resources/Textures/Cursors/monkey/monkey.png From b9e60e1acde7e894c0b66e8e8876357b7e44f4dc Mon Sep 17 00:00:00 2001 From: mondogo24 <167080436+mondogo24@users.noreply.github.com> Date: Wed, 27 May 2026 19:36:57 +0200 Subject: [PATCH 07/21] doc: update CursorLoader deprecation javadoc --- .../src/main/java/com/jme3/cursors/plugins/CursorLoader.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/jme3-desktop/src/main/java/com/jme3/cursors/plugins/CursorLoader.java b/jme3-desktop/src/main/java/com/jme3/cursors/plugins/CursorLoader.java index 101f4c5894..b60fa7c1a7 100644 --- a/jme3-desktop/src/main/java/com/jme3/cursors/plugins/CursorLoader.java +++ b/jme3-desktop/src/main/java/com/jme3/cursors/plugins/CursorLoader.java @@ -57,8 +57,7 @@ * @author MadJack * * @deprecated This class is not cross-platform, and the supported file formats are no longer commonly used. - * - * + * Use {@link com.jme3.cursors.plugins.CursorConverter} instead. */ @Deprecated public class CursorLoader implements AssetLoader { From ba390ac709f0bf025e6494a146c5dcc0acded47d Mon Sep 17 00:00:00 2001 From: mondogo24 <167080436+mondogo24@users.noreply.github.com> Date: Mon, 1 Jun 2026 16:39:00 +0200 Subject: [PATCH 08/21] Fix cached assets related problems --- jme3-examples/src/main/java/jme3test/gui/TestCursor.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/jme3-examples/src/main/java/jme3test/gui/TestCursor.java b/jme3-examples/src/main/java/jme3test/gui/TestCursor.java index f9c1005aa2..03dca12c8d 100644 --- a/jme3-examples/src/main/java/jme3test/gui/TestCursor.java +++ b/jme3-examples/src/main/java/jme3test/gui/TestCursor.java @@ -67,8 +67,9 @@ public void simpleInitApp() { }; for (Image cursor : staticCursors) { - flipVertically(cursor); - cursors.add(CursorConverter.fromTexture(new Texture2D(cursor))); + Image copyCursor = cursor.clone(); + flipVertically(copyCursor); + cursors.add(CursorConverter.fromTexture(new Texture2D(copyCursor))); } int monkeyFramesDelay = 60; @@ -82,7 +83,7 @@ public void simpleInitApp() { }; Texture2D[] monkeyFrames = Arrays.stream(monkeyFramePaths) - .map(framePath -> (Image) assetManager.loadAsset(framePath)) + .map(framePath -> ((Image) assetManager.loadAsset(framePath)).clone()) .peek(frameImage -> flipVertically(frameImage)) .map(frameImage -> new Texture2D(frameImage)) .toArray(Texture2D[]::new); From 40ffe746ff883bcea8edfc4b627ebb8d6d13a608 Mon Sep 17 00:00:00 2001 From: mondogo24 <167080436+mondogo24@users.noreply.github.com> Date: Mon, 1 Jun 2026 17:09:10 +0200 Subject: [PATCH 09/21] Deleted manual null checks in CursorConverter Since methods like Texture2D() or Image.clone() already provide a null check with a decent message. Manual null check has been deleted for simplicity and solve an unreachable Exception throw --- .../jme3/cursors/plugins/CursorConverter.java | 21 ++----------------- 1 file changed, 2 insertions(+), 19 deletions(-) diff --git a/jme3-core/src/plugins/java/com/jme3/cursors/plugins/CursorConverter.java b/jme3-core/src/plugins/java/com/jme3/cursors/plugins/CursorConverter.java index 09a2545941..1de82a83e8 100644 --- a/jme3-core/src/plugins/java/com/jme3/cursors/plugins/CursorConverter.java +++ b/jme3-core/src/plugins/java/com/jme3/cursors/plugins/CursorConverter.java @@ -57,10 +57,6 @@ public class CursorConverter { public static JmeCursor fromTexture(Texture2D cursorImage) { Image image = cursorImage.getImage().clone(); - if (image == null) { - throw new NullPointerException("There is not an image set to the Texture2D"); - } - int imageHeight = image.getHeight(); int imageWidth = image.getWidth(); @@ -141,21 +137,8 @@ public static JmeCursor fromTextureFrames(int[] frameDelays, Texture2D[] cursorF } List imageFrames = Arrays.stream(cursorFrames) - .map((frame) -> frame.getImage()) - .collect(Collectors.toList()); - - boolean missingImage = imageFrames - .stream() - .anyMatch((frame) -> frame == null); - - if (missingImage) { - throw new NullPointerException("Some Texture2D objects does not have a setted imaged"); - } - - //Avoid working and accidentally modifying original values - imageFrames = imageFrames - .stream() - .map((image) -> image.clone()) + //Avoid working and accidentally modifying original values + .map((frame) -> frame.getImage().clone()) .collect(Collectors.toList()); List imageFrameHeights = imageFrames From ee832cef2b9548e3102d8f33bff61020f28fbe12 Mon Sep 17 00:00:00 2001 From: mondogo24 <167080436+mondogo24@users.noreply.github.com> Date: Mon, 1 Jun 2026 17:16:26 +0200 Subject: [PATCH 10/21] Fix misspell mistake: from 'applyed' to 'applied' --- .../java/com/jme3/cursors/plugins/CursorConverter.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/jme3-core/src/plugins/java/com/jme3/cursors/plugins/CursorConverter.java b/jme3-core/src/plugins/java/com/jme3/cursors/plugins/CursorConverter.java index 1de82a83e8..3d55f06456 100644 --- a/jme3-core/src/plugins/java/com/jme3/cursors/plugins/CursorConverter.java +++ b/jme3-core/src/plugins/java/com/jme3/cursors/plugins/CursorConverter.java @@ -50,7 +50,7 @@ public class CursorConverter { * Convert a {@link Texture2D} to a {@link JmeCursor}. The coordinate system used is the same specified * in {@link JmeCursor}. The start point is 0, 0 being lower left. * - * @param cursorImage The texture to convert. No modifications will be applyed. + * @param cursorImage The texture to convert. No modifications will be applied. * * @return The {@link JmeCursor} using a deep copy of {@link Texture2D.getImage}. */ @@ -108,7 +108,7 @@ private static IntBuffer getDataAsIntBuffer(Image image) { * is 0, 0 being lower left. * * @param frameDelay The time delay that will take for a cursor to change from one frame to another. - * @param cursorFrames The frames that will make up the cursor animation. No modifications will be applyed. + * @param cursorFrames The frames that will make up the cursor animation. No modifications will be applied. * * @return A {@link JmeCursor} object that contains the data for an animated cursor. */ @@ -127,7 +127,7 @@ public static JmeCursor fromTextureFrames(int frameDelay, Texture2D[] cursorFram * @param frameDelays The time delay that will take each frame to change to the next frame. Because of it, * it must contains as many delays as frames (lenghts of cursorFrames and frameDelays * arrays must be equals). - * @param cursorFrames The frames that will make up the cursor animation. No modifications will be applyed. + * @param cursorFrames The frames that will make up the cursor animation. No modifications will be applied. * * @return A {@link JmeCursor} object that contains the data for an animated cursor. */ From db22e5809f904b12665614d846cd74eb2b0506f1 Mon Sep 17 00:00:00 2001 From: mondogo24 <167080436+mondogo24@users.noreply.github.com> Date: Mon, 1 Jun 2026 17:32:45 +0200 Subject: [PATCH 11/21] Add Flip() to IntBuffer data and swap reduce() with a for loop --- .../com/jme3/cursors/plugins/CursorConverter.java | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/jme3-core/src/plugins/java/com/jme3/cursors/plugins/CursorConverter.java b/jme3-core/src/plugins/java/com/jme3/cursors/plugins/CursorConverter.java index 3d55f06456..73d78e7d1d 100644 --- a/jme3-core/src/plugins/java/com/jme3/cursors/plugins/CursorConverter.java +++ b/jme3-core/src/plugins/java/com/jme3/cursors/plugins/CursorConverter.java @@ -160,12 +160,17 @@ public static JmeCursor fromTextureFrames(int[] frameDelays, Texture2D[] cursorF int imageHeight = imageFrameHeights.get(0); int imageWidth = imageFrameWidths.get(0); - IntBuffer framesData = BufferUtils.createIntBuffer(imageHeight * imageWidth * cursorFrames.length); + IntBuffer imagesData = BufferUtils.createIntBuffer(imageHeight * imageWidth * cursorFrames.length); - framesData = imageFrames + List framesData = imageFrames .stream() .map((image) -> getDataAsIntBuffer(image)) - .reduce(framesData, (previous, next) -> previous.put(next)); + .collect(Collectors.toList()); + + for (IntBuffer frameData : framesData) { + imagesData.put(frameData); + } + imagesData.flip(); JmeCursor jmeCursor = new JmeCursor(); jmeCursor.setWidth(imageWidth); @@ -174,7 +179,7 @@ public static JmeCursor fromTextureFrames(int[] frameDelays, Texture2D[] cursorF jmeCursor.setyHotSpot(imageHeight); jmeCursor.setNumImages(cursorFrames.length); jmeCursor.setImagesDelay(BufferUtils.createIntBuffer(frameDelays)); - jmeCursor.setImagesData(framesData); + jmeCursor.setImagesData(imagesData); return jmeCursor; } -} \ No newline at end of file +} From 14c41e7d01e3d28dd83c548b6ef49c184d058c47 Mon Sep 17 00:00:00 2001 From: mondogo24 <167080436+mondogo24@users.noreply.github.com> Date: Mon, 1 Jun 2026 18:21:34 +0200 Subject: [PATCH 12/21] Fix some spelling/grammar issues in javadoc and error messages --- .../java/com/jme3/cursors/plugins/CursorConverter.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/jme3-core/src/plugins/java/com/jme3/cursors/plugins/CursorConverter.java b/jme3-core/src/plugins/java/com/jme3/cursors/plugins/CursorConverter.java index 73d78e7d1d..96dd21548d 100644 --- a/jme3-core/src/plugins/java/com/jme3/cursors/plugins/CursorConverter.java +++ b/jme3-core/src/plugins/java/com/jme3/cursors/plugins/CursorConverter.java @@ -125,15 +125,15 @@ public static JmeCursor fromTextureFrames(int frameDelay, Texture2D[] cursorFram * is 0, 0 being lower left. * * @param frameDelays The time delay that will take each frame to change to the next frame. Because of it, - * it must contains as many delays as frames (lenghts of cursorFrames and frameDelays - * arrays must be equals). + * it must contains as many delays as frames (lengths of cursorFrames and frameDelays + * arrays must be equal). * @param cursorFrames The frames that will make up the cursor animation. No modifications will be applied. * * @return A {@link JmeCursor} object that contains the data for an animated cursor. */ public static JmeCursor fromTextureFrames(int[] frameDelays, Texture2D[] cursorFrames) { if (frameDelays.length != cursorFrames.length) { - throw new IllegalArgumentException("The lenghts of cursorFrames and frameDelays arrays must be equals"); + throw new IllegalArgumentException("The lengths of cursorFrames and frameDelays arrays must be equal"); } List imageFrames = Arrays.stream(cursorFrames) @@ -154,7 +154,7 @@ public static JmeCursor fromTextureFrames(int[] frameDelays, Texture2D[] cursorF .collect(Collectors.toList()); if (imageFrameHeights.size() > 1 || imageFrameWidths.size() > 1) { - throw new IllegalArgumentException("Some images from the Texture2D objects has different sizes"); + throw new IllegalArgumentException("Some images from the Texture2D objects have different sizes"); } int imageHeight = imageFrameHeights.get(0); From 596ea87a676a7676b8c56a4974dbcc5cf7e51d76 Mon Sep 17 00:00:00 2001 From: mondogo24 <167080436+mondogo24@users.noreply.github.com> Date: Mon, 1 Jun 2026 18:53:09 +0200 Subject: [PATCH 13/21] Update lincense disclaimer and javadoc in TestCursor --- .../main/java/jme3test/gui/TestCursor.java | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/jme3-examples/src/main/java/jme3test/gui/TestCursor.java b/jme3-examples/src/main/java/jme3test/gui/TestCursor.java index 03dca12c8d..8af9bb5693 100644 --- a/jme3-examples/src/main/java/jme3test/gui/TestCursor.java +++ b/jme3-examples/src/main/java/jme3test/gui/TestCursor.java @@ -42,23 +42,25 @@ public void simpleInitApp() { /* * To make jME3 use a custom cursor it is as simple as putting the - * .cur/.ico/.ani file in an asset directory. Here we use + * image file in an asset directory. Here we use * "Textures/GUI/Cursors". * - * For the purpose of this demonstration we load 3 different cursors and add them - * into an array list and switch cursor every 8 seconds. + * For the purpose of this demonstration we load 3 different cursors and + * switch cursor every 8 seconds. * - * The first ico has been made by Sirea and the set can be found here: + * At date of 2026/06/01: + * + * The nyan cat cursor has been made by Sirea. Under the Attribution Required (CC by) license: * http://www.rw-designer.com/icon-set/nyan-cat * - * The second cursor has been made by Virum64 and is Public Domain. + * The second cursor has been made by Virum64. Released to Public Domain. * http://www.rw-designer.com/cursor-set/memes-faces-v64 * - * The animated cursor has been made by Pointer Adic and can be found here: + * The animated cursor has been made by Pointer Adic. Released to Public Domain: * http://www.rw-designer.com/cursor-set/monkey * - * At date of 2026/05/22, the three cursor examples has been converter to png formats. - * Checking and following the licences restrictions in the process. + * The three cursor examples have been converted to png format. + * Checking and following the license restrictions in the process. */ Image[] staticCursors = { @@ -72,6 +74,10 @@ public void simpleInitApp() { cursors.add(CursorConverter.fromTexture(new Texture2D(copyCursor))); } + /* + * For animated cursors. Each frame must be loaded. + */ + int monkeyFramesDelay = 60; String[] monkeyFramePaths = { "Textures/Cursors/monkey/frame_0001.png", From e002d5d67e89fd8caaa53b20d290af8c8c43a48e Mon Sep 17 00:00:00 2001 From: mondogo24 <167080436+mondogo24@users.noreply.github.com> Date: Mon, 1 Jun 2026 19:03:45 +0200 Subject: [PATCH 14/21] Added missing extra information in license disclaimer --- jme3-examples/src/main/java/jme3test/gui/TestCursor.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jme3-examples/src/main/java/jme3test/gui/TestCursor.java b/jme3-examples/src/main/java/jme3test/gui/TestCursor.java index 8af9bb5693..bc106ae4ee 100644 --- a/jme3-examples/src/main/java/jme3test/gui/TestCursor.java +++ b/jme3-examples/src/main/java/jme3test/gui/TestCursor.java @@ -53,10 +53,10 @@ public void simpleInitApp() { * The nyan cat cursor has been made by Sirea. Under the Attribution Required (CC by) license: * http://www.rw-designer.com/icon-set/nyan-cat * - * The second cursor has been made by Virum64. Released to Public Domain. + * The meme face cursor has been made by Virum64. Released to Public Domain. * http://www.rw-designer.com/cursor-set/memes-faces-v64 * - * The animated cursor has been made by Pointer Adic. Released to Public Domain: + * The animated monkey cursor has been made by Pointer Adic. Released to Public Domain: * http://www.rw-designer.com/cursor-set/monkey * * The three cursor examples have been converted to png format. From f8688be67ce30d1041285c601abdacd3e0a99ce1 Mon Sep 17 00:00:00 2001 From: mondogo24 <167080436+mondogo24@users.noreply.github.com> Date: Tue, 2 Jun 2026 19:35:58 +0200 Subject: [PATCH 15/21] Add deep clone to Image --- .../src/main/java/com/jme3/texture/Image.java | 24 +++++++++++++++++++ .../jme3/cursors/plugins/CursorConverter.java | 2 +- .../main/java/jme3test/gui/TestCursor.java | 4 ++-- 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/jme3-core/src/main/java/com/jme3/texture/Image.java b/jme3-core/src/main/java/com/jme3/texture/Image.java index 3181abd4f9..3850db8359 100644 --- a/jme3-core/src/main/java/com/jme3/texture/Image.java +++ b/jme3-core/src/main/java/com/jme3/texture/Image.java @@ -720,6 +720,8 @@ public long getUniqueId() { /** * @return A shallow clone of this image. The data is not cloned. + * + * @see #deepClone() Deep clone. */ @Override public Image clone(){ @@ -731,6 +733,28 @@ public Image clone(){ return clone; } + /** + * @return A deep clone of this image. The data is cloned. + * + * @see #clone() Shallow clone. + */ + public Image deepClone() { + Image clone = (Image) super.clone(); + clone.mipMapSizes = mipMapSizes != null ? mipMapSizes.clone() : null; + clone.data = data != null ? dataDeepClone() : null; + clone.lastTextureState = new LastTextureState(); + clone.setUpdateNeeded(); + return clone; + } + + private ArrayList dataDeepClone() { + ArrayList clone = new ArrayList(data.size()); + for (ByteBuffer originalBuffer : data) { + clone.add(BufferUtils.createByteBuffer(originalBuffer, originalBuffer.capacity())); + } + return clone; + } + /** * Constructor instantiates a new Image object. All values * are undefined. diff --git a/jme3-core/src/plugins/java/com/jme3/cursors/plugins/CursorConverter.java b/jme3-core/src/plugins/java/com/jme3/cursors/plugins/CursorConverter.java index 96dd21548d..dc2362150d 100644 --- a/jme3-core/src/plugins/java/com/jme3/cursors/plugins/CursorConverter.java +++ b/jme3-core/src/plugins/java/com/jme3/cursors/plugins/CursorConverter.java @@ -52,7 +52,7 @@ public class CursorConverter { * * @param cursorImage The texture to convert. No modifications will be applied. * - * @return The {@link JmeCursor} using a deep copy of {@link Texture2D.getImage}. + * @return The {@link JmeCursor} object that contains the data for a cursor. */ public static JmeCursor fromTexture(Texture2D cursorImage) { Image image = cursorImage.getImage().clone(); diff --git a/jme3-examples/src/main/java/jme3test/gui/TestCursor.java b/jme3-examples/src/main/java/jme3test/gui/TestCursor.java index bc106ae4ee..2dd0119746 100644 --- a/jme3-examples/src/main/java/jme3test/gui/TestCursor.java +++ b/jme3-examples/src/main/java/jme3test/gui/TestCursor.java @@ -69,7 +69,7 @@ public void simpleInitApp() { }; for (Image cursor : staticCursors) { - Image copyCursor = cursor.clone(); + Image copyCursor = cursor.deepClone(); flipVertically(copyCursor); cursors.add(CursorConverter.fromTexture(new Texture2D(copyCursor))); } @@ -89,7 +89,7 @@ public void simpleInitApp() { }; Texture2D[] monkeyFrames = Arrays.stream(monkeyFramePaths) - .map(framePath -> ((Image) assetManager.loadAsset(framePath)).clone()) + .map(framePath -> ((Image) assetManager.loadAsset(framePath)).deepClone()) .peek(frameImage -> flipVertically(frameImage)) .map(frameImage -> new Texture2D(frameImage)) .toArray(Texture2D[]::new); From 0708ab11dd4620e0db1046e2b3c2f1361c99962f Mon Sep 17 00:00:00 2001 From: mondogo24 <167080436+mondogo24@users.noreply.github.com> Date: Thu, 4 Jun 2026 13:21:59 +0200 Subject: [PATCH 16/21] Added custom hot spot and other improvements --- .../src/main/java/com/jme3/texture/Image.java | 6 +- .../jme3/cursors/plugins/CursorConverter.java | 239 +++++++++++++----- .../main/java/jme3test/gui/TestCursor.java | 45 +++- 3 files changed, 207 insertions(+), 83 deletions(-) diff --git a/jme3-core/src/main/java/com/jme3/texture/Image.java b/jme3-core/src/main/java/com/jme3/texture/Image.java index 3850db8359..d2e3a0b58a 100644 --- a/jme3-core/src/main/java/com/jme3/texture/Image.java +++ b/jme3-core/src/main/java/com/jme3/texture/Image.java @@ -719,9 +719,8 @@ public long getUniqueId() { } /** - * @return A shallow clone of this image. The data is not cloned. - * * @see #deepClone() Deep clone. + * @return a shallow clone of this image. The data is not cloned. */ @Override public Image clone(){ @@ -734,9 +733,8 @@ public Image clone(){ } /** - * @return A deep clone of this image. The data is cloned. - * * @see #clone() Shallow clone. + * @return a deep clone of this image. The data is cloned. */ public Image deepClone() { Image clone = (Image) super.clone(); diff --git a/jme3-core/src/plugins/java/com/jme3/cursors/plugins/CursorConverter.java b/jme3-core/src/plugins/java/com/jme3/cursors/plugins/CursorConverter.java index dc2362150d..c630f7d1f7 100644 --- a/jme3-core/src/plugins/java/com/jme3/cursors/plugins/CursorConverter.java +++ b/jme3-core/src/plugins/java/com/jme3/cursors/plugins/CursorConverter.java @@ -46,112 +46,179 @@ * Convert any image like object to a {@link JmeCursor}. */ public class CursorConverter { + /** + * Convert a {@link Texture2D} to a {@link JmeCursor}. + * + * The coordinate system used is the same specified in {@link JmeCursor}, the start point is 0, 0 being + * lower left. + * + * The cursor's hot spot will be the top left of the texture's image. + * + * {@param cursorTexture the texture to convert. No modifications will be applied.} + * {@return the {@link JmeCursor} object that contains the data for a cursor.} + */ + public static JmeCursor fromTexture(Texture2D cursorTexture) { + return fromTexture(cursorTexture, 0, cursorTexture.getImage().getHeight()); + } + /** - * Convert a {@link Texture2D} to a {@link JmeCursor}. The coordinate system used is the same specified - * in {@link JmeCursor}. The start point is 0, 0 being lower left. + * Convert a {@link Texture2D} to a {@link JmeCursor}. * - * @param cursorImage The texture to convert. No modifications will be applied. + * The coordinate system used is the same specified in {@link JmeCursor}, the start point is 0, 0 being + * lower left. * - * @return The {@link JmeCursor} object that contains the data for a cursor. + * {@param cursorTexture the texture to convert. No modifications will be applied.} + * {@param xHotspot same specification as {@link JmeCursor#setxHotSpot(int)}: the cursor's X axis + * coordinate for its hotspot. Note that the coordinate system is the same as + * OpenGL. 0, 0 being lower left. Must be between 0 and image width.} + * {@param yHotspot same specification as {@link JmeCursor#setyHotSpot(int)}: the cursor's Y axis + * coordinate for its hotspot. Note that the coordinate system is the same as + * OpenGL. 0, 0 being lower left. Must be between 0 and image height.} + * {@return the {@link JmeCursor} object that contains the data for a cursor.} */ - public static JmeCursor fromTexture(Texture2D cursorImage) { - Image image = cursorImage.getImage().clone(); + public static JmeCursor fromTexture(Texture2D cursorTexture, int xHotspot, int yHotspot) { + Image image = cursorTexture.getImage(); int imageHeight = image.getHeight(); int imageWidth = image.getWidth(); + if (!hotspotInsideImageLimits(xHotspot, yHotspot, imageWidth, imageHeight)) { + throw new IllegalArgumentException( + "Cursor's hot spot must be inside image limits (width and height)" + ); + } + IntBuffer adaptedImageData = getDataAsIntBuffer(image); JmeCursor jmeCursor = new JmeCursor(); jmeCursor.setWidth(imageWidth); jmeCursor.setHeight(imageHeight); - jmeCursor.setxHotSpot(0); - jmeCursor.setyHotSpot(imageHeight); + jmeCursor.setxHotSpot(xHotspot); + jmeCursor.setyHotSpot(yHotspot); jmeCursor.setNumImages(1); jmeCursor.setImagesDelay(null); jmeCursor.setImagesData(adaptedImageData); return jmeCursor; } - private static IntBuffer getDataAsIntBuffer(Image image) { - int width = image.getWidth(); - int height = image.getHeight(); - - ImageRaster raster = ImageRaster.create(image); - - IntBuffer data = BufferUtils.createIntBuffer(width * height); - - //ARGB color system is needed to show cursors correctly. - for (int y = 0; y < height; y++) { - for (int x = 0; x < width; x++) { - ColorRGBA color = raster.getPixel(x, y); - - int a = (int) (color.a * 255) & 0xFF; - int r = (int) (color.r * 255) & 0xFF; - int g = (int) (color.g * 255) & 0xFF; - int b = (int) (color.b * 255) & 0xFF; + /** + * Convert a {@link Texture2D} array to a {@link JmeCursor} object that will represent an animated cursor, + * interpreting each {@link Texture2D} object as a frame of the animated cursor. + * + * The coordinate system used is the same specified in {@link JmeCursor}. The start point is 0, 0 being + * lower left. + * + * The cursor's hot spot will be the top left of the texture's image. + * + * {@param cursorFrames the frames that will make up the cursor animation. + * No modifications will be applied.} + * {@param frameDelay the time delay that will take for a cursor to change from one frame to another.} + * {@return a {@link JmeCursor} object that contains the data for an animated cursor.} + */ + public static JmeCursor fromTextureFrames(Texture2D[] cursorFrames, int frameDelay) { + int yHotspot = 0; + if (cursorFrames.length > 0) { + yHotspot = cursorFrames[0].getImage().getHeight(); + } + return fromTextureFrames(cursorFrames, frameDelay, 0, yHotspot); + } - int argb = (a << 24) | (r << 16) | (g << 8) | b; - - data.put(argb); - } + /** + * Convert a {@link Texture2D} array to a {@link JmeCursor} object that will represent an animated cursor, + * interpreting each {@link Texture2D} object as a frame of the animated cursor. + * + * The coordinate system used is the same specified in {@link JmeCursor}. The start point is 0, 0 being + * lower left. + * + * The cursor's hot spot will be the top left of the texture's image. + * + * {@param cursorFrames the frames that will make up the cursor animation. No modifications will be applied.} + * {@param frameDelay the time delay that will take for a cursor to change from one frame to another.} + * {@return a {@link JmeCursor} object that contains the data for an animated cursor.} + */ + public static JmeCursor fromTextureFrames(Texture2D[] cursorFrames, int[] frameDelays) { + int yHotspot = 0; + if (cursorFrames.length > 0) { + yHotspot = cursorFrames[0].getImage().getHeight(); } - - data.flip(); - return data; + return fromTextureFrames(cursorFrames, frameDelays, 0, yHotspot); } /** * Convert a {@link Texture2D} array to a {@link JmeCursor} object that will represent an animated cursor, * interpreting each {@link Texture2D} object as a frame of the animated cursor. - * The coordinate system used for each frame is the same specified in {@link JmeCursor}. The start point - * is 0, 0 being lower left. * - * @param frameDelay The time delay that will take for a cursor to change from one frame to another. - * @param cursorFrames The frames that will make up the cursor animation. No modifications will be applied. + * The coordinate system used is the same specified in {@link JmeCursor}. The start point is 0, 0 being + * lower left. * - * @return A {@link JmeCursor} object that contains the data for an animated cursor. + * {@param cursorFrames the frames that will make up the cursor animation. + * No modifications will be applied.} + * {@param frameDelay the time delay that will take for a cursor to change from one frame to another.} + * {@param xHotspot same specification as {@link JmeCursor#setxHotSpot(int)}: the cursor's X axis + * coordinate for its hotspot. Note that the coordinate system is the same as + * OpenGL. 0, 0 being lower left. Must be between 0 and image width.} + * {@param yHotspot same specification as {@link JmeCursor#setyHotSpot(int)}: the cursor's Y axis + * coordinate for its hotspot. Note that the coordinate system is the same as + * OpenGL. 0, 0 being lower left. Must be between 0 and image height.} + * {@return a {@link JmeCursor} object that contains the data for an animated cursor.} */ - public static JmeCursor fromTextureFrames(int frameDelay, Texture2D[] cursorFrames) { + public static JmeCursor fromTextureFrames( + Texture2D[] cursorFrames, + int frameDelay, + int xHotspot, + int yHotspot) { int[] frameRates = new int[cursorFrames.length]; Arrays.fill(frameRates, frameDelay); - return fromTextureFrames(frameRates, cursorFrames); + return fromTextureFrames(cursorFrames, frameRates, xHotspot, yHotspot); } /** - * Convert a {@link Texture2D} array to a {@link JmeCursor} object that will represent an animated cursor, + * Convert a {@link Texture2D} array to a {@link JmeCursor} object that will represent an animated cursor, * interpreting each {@link Texture2D} object as a frame of the animated cursor. - * The coordinate system used for each frame is the same specified in {@link JmeCursor}. The start point - * is 0, 0 being lower left. * - * @param frameDelays The time delay that will take each frame to change to the next frame. Because of it, - * it must contains as many delays as frames (lengths of cursorFrames and frameDelays - * arrays must be equal). - * @param cursorFrames The frames that will make up the cursor animation. No modifications will be applied. + * The coordinate system used is the same specified in {@link JmeCursor}. The start point is 0, 0 being + * lower left. * - * @return A {@link JmeCursor} object that contains the data for an animated cursor. + * {@param cursorFrames the frames that will make up the cursor animation. + * No modifications will be applied.} + * {@param frameDelays the time delay that will take each frame to change to the next frame. Because of it, + * it must contains as many delays as frames (lengths of cursorFrames and frameDelays + * arrays must be equal).} + * {@param xHotspot tame specification as {@link JmeCursor#setxHotSpot(int)}: the cursor's X axis + * coordinate for its hotspot. Note that the coordinate system is the same as + * OpenGL. 0, 0 being lower left. Must be between 0 and image width.} + * {@param yHotspot same specification as {@link JmeCursor#setyHotSpot(int)}: the cursor's Y axis + * coordinate for its hotspot. Note that the coordinate system is the same as + * OpenGL. 0, 0 being lower left. Must be between 0 and image height.} + * {@return a {@link JmeCursor} object that contains the data for an animated cursor.} */ - public static JmeCursor fromTextureFrames(int[] frameDelays, Texture2D[] cursorFrames) { + public static JmeCursor fromTextureFrames( + Texture2D[] cursorFrames, + int[] frameDelays, + int xHotspot, + int yHotspot) { if (frameDelays.length != cursorFrames.length) { - throw new IllegalArgumentException("The lengths of cursorFrames and frameDelays arrays must be equal"); + throw new IllegalArgumentException( + "The lengths of cursorFrames and frameDelays arrays must be equal" + ); } List imageFrames = Arrays.stream(cursorFrames) - //Avoid working and accidentally modifying original values - .map((frame) -> frame.getImage().clone()) - .collect(Collectors.toList()); + //Avoid working and accidentally modifying original values + .map((frame) -> frame.getImage()) + .collect(Collectors.toList()); List imageFrameHeights = imageFrames - .stream() - .map((image) -> image.getHeight()) - .distinct() - .collect(Collectors.toList()); + .stream() + .map((image) -> image.getHeight()) + .distinct() + .collect(Collectors.toList()); List imageFrameWidths = imageFrames - .stream() - .map((image) -> image.getWidth()) - .distinct() - .collect(Collectors.toList()); + .stream() + .map((image) -> image.getWidth()) + .distinct() + .collect(Collectors.toList()); if (imageFrameHeights.size() > 1 || imageFrameWidths.size() > 1) { throw new IllegalArgumentException("Some images from the Texture2D objects have different sizes"); @@ -160,12 +227,18 @@ public static JmeCursor fromTextureFrames(int[] frameDelays, Texture2D[] cursorF int imageHeight = imageFrameHeights.get(0); int imageWidth = imageFrameWidths.get(0); + if (!hotspotInsideImageLimits(xHotspot, yHotspot, imageWidth, imageHeight)) { + throw new IllegalArgumentException( + "Cursor's hot spot must be inside image limits (width and height)" + ); + } + IntBuffer imagesData = BufferUtils.createIntBuffer(imageHeight * imageWidth * cursorFrames.length); List framesData = imageFrames - .stream() - .map((image) -> getDataAsIntBuffer(image)) - .collect(Collectors.toList()); + .stream() + .map((image) -> getDataAsIntBuffer(image)) + .collect(Collectors.toList()); for (IntBuffer frameData : framesData) { imagesData.put(frameData); @@ -175,11 +248,47 @@ public static JmeCursor fromTextureFrames(int[] frameDelays, Texture2D[] cursorF JmeCursor jmeCursor = new JmeCursor(); jmeCursor.setWidth(imageWidth); jmeCursor.setHeight(imageHeight); - jmeCursor.setxHotSpot(0); - jmeCursor.setyHotSpot(imageHeight); + jmeCursor.setxHotSpot(xHotspot); + jmeCursor.setyHotSpot(yHotspot); jmeCursor.setNumImages(cursorFrames.length); jmeCursor.setImagesDelay(BufferUtils.createIntBuffer(frameDelays)); jmeCursor.setImagesData(imagesData); return jmeCursor; } + + private static IntBuffer getDataAsIntBuffer(Image image) { + int width = image.getWidth(); + int height = image.getHeight(); + + ImageRaster raster = ImageRaster.create(image); + + IntBuffer data = BufferUtils.createIntBuffer(width * height); + + //ARGB color system is needed to show cursors correctly. + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + ColorRGBA color = raster.getPixel(x, y); + + int a = (int) (color.a * 255) & 0xFF; + int r = (int) (color.r * 255) & 0xFF; + int g = (int) (color.g * 255) & 0xFF; + int b = (int) (color.b * 255) & 0xFF; + + int argb = (a << 24) | (r << 16) | (g << 8) | b; + + data.put(argb); + } + } + + data.flip(); + return data; + } + + private static boolean hotspotInsideImageLimits( + int xHotspot, + int yHotspot, + int imageWidth, + int imageHeight) { + return xHotspot >= 0 && xHotspot <= imageWidth && yHotspot >= 0 && yHotspot <= imageHeight; + } } diff --git a/jme3-examples/src/main/java/jme3test/gui/TestCursor.java b/jme3-examples/src/main/java/jme3test/gui/TestCursor.java index 2dd0119746..84614be13f 100644 --- a/jme3-examples/src/main/java/jme3test/gui/TestCursor.java +++ b/jme3-examples/src/main/java/jme3test/gui/TestCursor.java @@ -14,6 +14,8 @@ /** * This test class demonstrate how to change cursor in jME3. + * + * The different methods of {@link com.jme3.cursors.plugins.CursorConverter} will be tested. * * NOTE: This will not work on Android as it does not support cursors. * @@ -46,7 +48,8 @@ public void simpleInitApp() { * "Textures/GUI/Cursors". * * For the purpose of this demonstration we load 3 different cursors and - * switch cursor every 8 seconds. + * switch cursor every 5 seconds, applying differents configurations with + * {@link com.jme3.cursors.plugins.CursorConverter}. * * At date of 2026/06/01: * @@ -63,22 +66,26 @@ public void simpleInitApp() { * Checking and following the license restrictions in the process. */ - Image[] staticCursors = { - (Image) assetManager.loadAsset("Textures/Cursors/meme.png"), - (Image) assetManager.loadAsset("Textures/Cursors/nyancat.png"), - }; + Image memeImage = ((Image) assetManager.loadAsset("Textures/Cursors/meme.png")).deepClone(); + flipVertically(memeImage); + Texture2D memeTexture = new Texture2D(memeImage); - for (Image cursor : staticCursors) { - Image copyCursor = cursor.deepClone(); - flipVertically(copyCursor); - cursors.add(CursorConverter.fromTexture(new Texture2D(copyCursor))); - } + Image nyancatImage = ((Image) assetManager.loadAsset("Textures/Cursors/nyancat.png")).deepClone(); + flipVertically(nyancatImage); + Texture2D nyancatTexture = new Texture2D(nyancatImage); + + //Shows a cursor with default hot spot. + cursors.add(CursorConverter.fromTexture(memeTexture)); + + //Shows a cursor with custom hot spot. + cursors.add(CursorConverter.fromTexture(nyancatTexture, 10, 10)); /* * For animated cursors. Each frame must be loaded. + * + * Differents configuration with the monkey animated cursor will be + * showed by changing the frame delays. */ - - int monkeyFramesDelay = 60; String[] monkeyFramePaths = { "Textures/Cursors/monkey/frame_0001.png", "Textures/Cursors/monkey/frame_0002.png", @@ -94,7 +101,17 @@ public void simpleInitApp() { .map(frameImage -> new Texture2D(frameImage)) .toArray(Texture2D[]::new); - cursors.add(CursorConverter.fromTextureFrames(monkeyFramesDelay, monkeyFrames)); + //Shows monkey cursor with same frame delay for each frame and default hot spot. + cursors.add(CursorConverter.fromTextureFrames(monkeyFrames, 60)); + + //Shows monkey cursor with custom frame delay for each frame and default hot spot. + cursors.add(CursorConverter.fromTextureFrames(monkeyFrames, new int[]{40, 40, 40, 200, 200, 200})); + + //Shows monkey cursor with same frame delay for each frame and custom hot spot. + cursors.add(CursorConverter.fromTextureFrames(monkeyFrames, 150, 5, 7)); + + //Shows monkey cursor with custom frame delay for each frame and hot spot. + cursors.add(CursorConverter.fromTextureFrames(monkeyFrames, new int[]{40, 200, 40, 200, 40, 200}, 10, 20)); sysTime = System.currentTimeMillis(); inputManager.setMouseCursor(cursors.get(count)); @@ -119,7 +136,7 @@ private void flipVertically(Image image) { public void simpleUpdate(float tpf) { long currentTime = System.currentTimeMillis(); - if (currentTime - sysTime > 8000) { + if (currentTime - sysTime > 5000) { count++; if (count >= cursors.size()) { count = 0; From be9f260ed1e16d59bd01efb89988d9f0e7dbefdb Mon Sep 17 00:00:00 2001 From: mondogo24 <167080436+mondogo24@users.noreply.github.com> Date: Thu, 11 Jun 2026 10:19:38 +0200 Subject: [PATCH 17/21] Fix javadoc brackets misuse --- .../src/main/java/com/jme3/texture/Image.java | 4 +- .../jme3/cursors/plugins/CursorConverter.java | 65 ++++++++++--------- 2 files changed, 35 insertions(+), 34 deletions(-) diff --git a/jme3-core/src/main/java/com/jme3/texture/Image.java b/jme3-core/src/main/java/com/jme3/texture/Image.java index d2e3a0b58a..82f94acb00 100644 --- a/jme3-core/src/main/java/com/jme3/texture/Image.java +++ b/jme3-core/src/main/java/com/jme3/texture/Image.java @@ -719,8 +719,8 @@ public long getUniqueId() { } /** - * @see #deepClone() Deep clone. * @return a shallow clone of this image. The data is not cloned. + * @see #deepClone() Deep clone. */ @Override public Image clone(){ @@ -733,8 +733,8 @@ public Image clone(){ } /** - * @see #clone() Shallow clone. * @return a deep clone of this image. The data is cloned. + * @see #clone() Shallow clone. */ public Image deepClone() { Image clone = (Image) super.clone(); diff --git a/jme3-core/src/plugins/java/com/jme3/cursors/plugins/CursorConverter.java b/jme3-core/src/plugins/java/com/jme3/cursors/plugins/CursorConverter.java index c630f7d1f7..e70f930215 100644 --- a/jme3-core/src/plugins/java/com/jme3/cursors/plugins/CursorConverter.java +++ b/jme3-core/src/plugins/java/com/jme3/cursors/plugins/CursorConverter.java @@ -54,8 +54,8 @@ public class CursorConverter { * * The cursor's hot spot will be the top left of the texture's image. * - * {@param cursorTexture the texture to convert. No modifications will be applied.} - * {@return the {@link JmeCursor} object that contains the data for a cursor.} + * @param cursorTexture the texture to convert. No modifications will be applied. + * @return the {@link JmeCursor} object that contains the data for a cursor. */ public static JmeCursor fromTexture(Texture2D cursorTexture) { return fromTexture(cursorTexture, 0, cursorTexture.getImage().getHeight()); @@ -67,14 +67,14 @@ public static JmeCursor fromTexture(Texture2D cursorTexture) { * The coordinate system used is the same specified in {@link JmeCursor}, the start point is 0, 0 being * lower left. * - * {@param cursorTexture the texture to convert. No modifications will be applied.} - * {@param xHotspot same specification as {@link JmeCursor#setxHotSpot(int)}: the cursor's X axis + * @param cursorTexture the texture to convert. No modifications will be applied. + * @param xHotspot same specification as {@link JmeCursor#setxHotSpot(int)}: the cursor's X axis * coordinate for its hotspot. Note that the coordinate system is the same as - * OpenGL. 0, 0 being lower left. Must be between 0 and image width.} - * {@param yHotspot same specification as {@link JmeCursor#setyHotSpot(int)}: the cursor's Y axis + * OpenGL. 0, 0 being lower left. Must be between 0 and image width. + * @param yHotspot same specification as {@link JmeCursor#setyHotSpot(int)}: the cursor's Y axis * coordinate for its hotspot. Note that the coordinate system is the same as - * OpenGL. 0, 0 being lower left. Must be between 0 and image height.} - * {@return the {@link JmeCursor} object that contains the data for a cursor.} + * OpenGL. 0, 0 being lower left. Must be between 0 and image height. + * @return the {@link JmeCursor} object that contains the data for a cursor. */ public static JmeCursor fromTexture(Texture2D cursorTexture, int xHotspot, int yHotspot) { Image image = cursorTexture.getImage(); @@ -110,10 +110,10 @@ public static JmeCursor fromTexture(Texture2D cursorTexture, int xHotspot, int y * * The cursor's hot spot will be the top left of the texture's image. * - * {@param cursorFrames the frames that will make up the cursor animation. - * No modifications will be applied.} - * {@param frameDelay the time delay that will take for a cursor to change from one frame to another.} - * {@return a {@link JmeCursor} object that contains the data for an animated cursor.} + * @param cursorFrames the frames that will make up the cursor animation. + * No modifications will be applied. + * @param frameDelay the time delay that will take for a cursor to change from one frame to another. + * @return a {@link JmeCursor} object that contains the data for an animated cursor. */ public static JmeCursor fromTextureFrames(Texture2D[] cursorFrames, int frameDelay) { int yHotspot = 0; @@ -132,9 +132,10 @@ public static JmeCursor fromTextureFrames(Texture2D[] cursorFrames, int frameDel * * The cursor's hot spot will be the top left of the texture's image. * - * {@param cursorFrames the frames that will make up the cursor animation. No modifications will be applied.} - * {@param frameDelay the time delay that will take for a cursor to change from one frame to another.} - * {@return a {@link JmeCursor} object that contains the data for an animated cursor.} + * @param cursorFrames the frames that will make up the cursor animation. + * No modifications will be applied. + * @param frameDelay the time delay that will take for a cursor to change from one frame to another. + * @return a {@link JmeCursor} object that contains the data for an animated cursor. */ public static JmeCursor fromTextureFrames(Texture2D[] cursorFrames, int[] frameDelays) { int yHotspot = 0; @@ -151,16 +152,16 @@ public static JmeCursor fromTextureFrames(Texture2D[] cursorFrames, int[] frame * The coordinate system used is the same specified in {@link JmeCursor}. The start point is 0, 0 being * lower left. * - * {@param cursorFrames the frames that will make up the cursor animation. - * No modifications will be applied.} - * {@param frameDelay the time delay that will take for a cursor to change from one frame to another.} - * {@param xHotspot same specification as {@link JmeCursor#setxHotSpot(int)}: the cursor's X axis + * @param cursorFrames the frames that will make up the cursor animation. + * No modifications will be applied. + * @param frameDelay the time delay that will take for a cursor to change from one frame to another. + * @param xHotspot same specification as {@link JmeCursor#setxHotSpot(int)}: the cursor's X axis * coordinate for its hotspot. Note that the coordinate system is the same as - * OpenGL. 0, 0 being lower left. Must be between 0 and image width.} - * {@param yHotspot same specification as {@link JmeCursor#setyHotSpot(int)}: the cursor's Y axis + * OpenGL. 0, 0 being lower left. Must be between 0 and image width. + * @param yHotspot same specification as {@link JmeCursor#setyHotSpot(int)}: the cursor's Y axis * coordinate for its hotspot. Note that the coordinate system is the same as - * OpenGL. 0, 0 being lower left. Must be between 0 and image height.} - * {@return a {@link JmeCursor} object that contains the data for an animated cursor.} + * OpenGL. 0, 0 being lower left. Must be between 0 and image height. + * @return a {@link JmeCursor} object that contains the data for an animated cursor. */ public static JmeCursor fromTextureFrames( Texture2D[] cursorFrames, @@ -179,18 +180,18 @@ public static JmeCursor fromTextureFrames( * The coordinate system used is the same specified in {@link JmeCursor}. The start point is 0, 0 being * lower left. * - * {@param cursorFrames the frames that will make up the cursor animation. - * No modifications will be applied.} - * {@param frameDelays the time delay that will take each frame to change to the next frame. Because of it, + * @param cursorFrames the frames that will make up the cursor animation. + * No modifications will be applied. + * @param frameDelays the time delay that will take each frame to change to the next frame. Because of it, * it must contains as many delays as frames (lengths of cursorFrames and frameDelays - * arrays must be equal).} - * {@param xHotspot tame specification as {@link JmeCursor#setxHotSpot(int)}: the cursor's X axis + * arrays must be equal). + * @param xHotspot tame specification as {@link JmeCursor#setxHotSpot(int)}: the cursor's X axis * coordinate for its hotspot. Note that the coordinate system is the same as - * OpenGL. 0, 0 being lower left. Must be between 0 and image width.} - * {@param yHotspot same specification as {@link JmeCursor#setyHotSpot(int)}: the cursor's Y axis + * OpenGL. 0, 0 being lower left. Must be between 0 and image width. + * @param yHotspot same specification as {@link JmeCursor#setyHotSpot(int)}: the cursor's Y axis * coordinate for its hotspot. Note that the coordinate system is the same as - * OpenGL. 0, 0 being lower left. Must be between 0 and image height.} - * {@return a {@link JmeCursor} object that contains the data for an animated cursor.} + * OpenGL. 0, 0 being lower left. Must be between 0 and image height. + * @return a {@link JmeCursor} object that contains the data for an animated cursor. */ public static JmeCursor fromTextureFrames( Texture2D[] cursorFrames, From b74a950f8151ff2ec537095299c26c93d2a7896c Mon Sep 17 00:00:00 2001 From: mondogo24 <167080436+mondogo24@users.noreply.github.com> Date: Sun, 8 Nov 2026 16:20:48 +0100 Subject: [PATCH 18/21] Improve CursorConverter's reliability and documentation. - Update the current changes to adapt to all the changes done in the project. - Now CursorConverter is open to be used as a crossplatform tool, since now it is posible to configure the class to output an specific JmeCursor object that meets the cursor consumer's requirements. - Documented the exceptions that can be thrown in CursorConverter's methods. - Fixed a bug in Image.deepClone where the image did a shallow clone instaed of a deep clone. --- .../src/main/java/com/jme3/texture/Image.java | 50 ++++----- .../jme3/cursors/plugins/CursorConverter.java | 103 ++++++++++++++---- .../main/java/jme3test/gui/TestCursor.java | 91 +++++++++------- .../main/resources/Textures/Cursors/meme.png | Bin 561 -> 0 bytes .../Textures/Cursors/monkey/frame_0001.png | Bin 697 -> 0 bytes .../Textures/Cursors/monkey/frame_0002.png | Bin 707 -> 0 bytes .../Textures/Cursors/monkey/frame_0003.png | Bin 699 -> 0 bytes .../Textures/Cursors/monkey/frame_0004.png | Bin 695 -> 0 bytes .../Textures/Cursors/monkey/frame_0005.png | Bin 685 -> 0 bytes .../Textures/Cursors/monkey/frame_0006.png | Bin 656 -> 0 bytes .../Textures/Cursors/monkey/monkey.png | Bin 2379 -> 0 bytes .../resources/Textures/Cursors/nyancat.png | Bin 1969 -> 0 bytes 12 files changed, 162 insertions(+), 82 deletions(-) delete mode 100644 jme3-testdata/src/main/resources/Textures/Cursors/meme.png delete mode 100644 jme3-testdata/src/main/resources/Textures/Cursors/monkey/frame_0001.png delete mode 100644 jme3-testdata/src/main/resources/Textures/Cursors/monkey/frame_0002.png delete mode 100644 jme3-testdata/src/main/resources/Textures/Cursors/monkey/frame_0003.png delete mode 100644 jme3-testdata/src/main/resources/Textures/Cursors/monkey/frame_0004.png delete mode 100644 jme3-testdata/src/main/resources/Textures/Cursors/monkey/frame_0005.png delete mode 100644 jme3-testdata/src/main/resources/Textures/Cursors/monkey/frame_0006.png delete mode 100644 jme3-testdata/src/main/resources/Textures/Cursors/monkey/monkey.png delete mode 100644 jme3-testdata/src/main/resources/Textures/Cursors/nyancat.png diff --git a/jme3-core/src/main/java/com/jme3/texture/Image.java b/jme3-core/src/main/java/com/jme3/texture/Image.java index 82f94acb00..9410ca81e9 100644 --- a/jme3-core/src/main/java/com/jme3/texture/Image.java +++ b/jme3-core/src/main/java/com/jme3/texture/Image.java @@ -79,16 +79,16 @@ public enum Format { Reserved2(0), /** - * half-precision floating-point grayscale/luminance. - * - * Requires {@link Caps#HalfFloatTexture}. + * half-precision floating-point grayscale/luminance. + * + * Requires {@link Caps#HalfFloatTexture}. */ Luminance16F(16,true), /** * single-precision floating-point grayscale/luminance. * - * Requires {@link Caps#FloatTexture}. + * Requires {@link Caps#FloatTexture}. */ Luminance32F(32,true), @@ -101,9 +101,9 @@ public enum Format { Reserved3(0), /** - * half-precision floating-point grayscale/luminance and alpha. - * - * Requires {@link Caps#HalfFloatTexture}. + * half-precision floating-point grayscale/luminance and alpha. + * + * Requires {@link Caps#HalfFloatTexture}. */ Luminance16FAlpha16F(32,true), @@ -255,7 +255,7 @@ public enum Format { * but will be converted to {@link Format#RGB111110F} when sent * to the video hardware. * - * Requires {@link Caps#HalfFloatTexture} and {@link Caps#PackedFloatTexture}. + * Requires {@link Caps#HalfFloatTexture} and {@link Caps#PackedFloatTexture}. */ RGB16F_to_RGB111110F(48,true), @@ -271,7 +271,7 @@ public enum Format { * but will be converted to {@link Format#RGB9E5} when sent * to the video hardware. * - * Requires {@link Caps#HalfFloatTexture} and {@link Caps#SharedExponentTexture}. + * Requires {@link Caps#HalfFloatTexture} and {@link Caps#SharedExponentTexture}. */ RGB16F_to_RGB9E5(48,true), @@ -283,9 +283,9 @@ public enum Format { RGB9E5(32,true), /** - * half-precision floating point red, green, and blue. - * - * Requires {@link Caps#HalfFloatTexture}. + * half-precision floating point red, green, and blue. + * + * Requires {@link Caps#HalfFloatTexture}. * May be supported for renderbuffers, but the OpenGL specification does not require it. */ RGB16F(48,true), @@ -293,22 +293,22 @@ public enum Format { /** * half-precision floating point red, green, blue, and alpha. * - * Requires {@link Caps#HalfFloatTexture}. + * Requires {@link Caps#HalfFloatTexture}. */ RGBA16F(64,true), /** - * single-precision floating point red, green, and blue. - * - * Requires {@link Caps#FloatTexture}. + * single-precision floating point red, green, and blue. + * + * Requires {@link Caps#FloatTexture}. * May be supported for renderbuffers, but the OpenGL specification does not require it. */ RGB32F(96,true), /** - * single-precision floating point red, green, blue and alpha. - * - * Requires {@link Caps#FloatTexture}. + * single-precision floating point red, green, blue and alpha. + * + * Requires {@link Caps#FloatTexture}. */ RGBA32F(128,true), @@ -507,21 +507,21 @@ public enum Format { /** * half-precision floating point red. * - * Requires {@link Caps#HalfFloatTexture}. + * Requires {@link Caps#HalfFloatTexture}. */ R16F(16,true), /** - * single-precision floating point red. - * - * Requires {@link Caps#FloatTexture}. + * single-precision floating point red. + * + * Requires {@link Caps#FloatTexture}. */ R32F(32,true), /** * half-precision floating point red and green. * - * Requires {@link Caps#HalfFloatTexture}. + * Requires {@link Caps#HalfFloatTexture}. */ RG16F(32,true), @@ -748,7 +748,7 @@ public Image deepClone() { private ArrayList dataDeepClone() { ArrayList clone = new ArrayList(data.size()); for (ByteBuffer originalBuffer : data) { - clone.add(BufferUtils.createByteBuffer(originalBuffer, originalBuffer.capacity())); + clone.add(BufferUtils.clone(originalBuffer)); } return clone; } diff --git a/jme3-core/src/plugins/java/com/jme3/cursors/plugins/CursorConverter.java b/jme3-core/src/plugins/java/com/jme3/cursors/plugins/CursorConverter.java index e70f930215..a4946ec2e1 100644 --- a/jme3-core/src/plugins/java/com/jme3/cursors/plugins/CursorConverter.java +++ b/jme3-core/src/plugins/java/com/jme3/cursors/plugins/CursorConverter.java @@ -44,8 +44,32 @@ /** * Convert any image like object to a {@link JmeCursor}. + * + * This class is configurable by it's constructors, allowing to create + * {@link JmeCursor} instances that meet the cursor consumer's necessities. */ public class CursorConverter { + /** + * Supported formats for {@link JmeCursor}'s image data. + */ + public enum CursorFormat {ARGB, RGBA} + + private final CursorConverter.CursorFormat outputCursorFormat; + private final boolean outputVerticalFlip; + + /** + * Creates a {@link CursorConverter} instance. + * + * @param cursorFormat the {@link JmeCursor}'s image data will follow the + * specified image format. + * @param verticalFlip if true, the {@link JmeCursor}'s image will be flipped vertically, otherwise + * it won't be flipped. + */ + public CursorConverter(CursorConverter.CursorFormat cursorFormat, boolean verticalFlip) { + outputCursorFormat = cursorFormat; + outputVerticalFlip = verticalFlip; + } + /** * Convert a {@link Texture2D} to a {@link JmeCursor}. * @@ -57,7 +81,7 @@ public class CursorConverter { * @param cursorTexture the texture to convert. No modifications will be applied. * @return the {@link JmeCursor} object that contains the data for a cursor. */ - public static JmeCursor fromTexture(Texture2D cursorTexture) { + public JmeCursor fromTexture(Texture2D cursorTexture) { return fromTexture(cursorTexture, 0, cursorTexture.getImage().getHeight()); } @@ -75,8 +99,10 @@ public static JmeCursor fromTexture(Texture2D cursorTexture) { * coordinate for its hotspot. Note that the coordinate system is the same as * OpenGL. 0, 0 being lower left. Must be between 0 and image height. * @return the {@link JmeCursor} object that contains the data for a cursor. + * + * @throws IllegalArgumentException if the specified hotspots are outside the image's size. */ - public static JmeCursor fromTexture(Texture2D cursorTexture, int xHotspot, int yHotspot) { + public JmeCursor fromTexture(Texture2D cursorTexture, int xHotspot, int yHotspot) { Image image = cursorTexture.getImage(); int imageHeight = image.getHeight(); @@ -88,6 +114,10 @@ public static JmeCursor fromTexture(Texture2D cursorTexture, int xHotspot, int y ); } + if (outputVerticalFlip) { + image = image.deepClone(); //Avoid modifying the original. + verticalFlip(image); + } IntBuffer adaptedImageData = getDataAsIntBuffer(image); JmeCursor jmeCursor = new JmeCursor(); @@ -114,8 +144,10 @@ public static JmeCursor fromTexture(Texture2D cursorTexture, int xHotspot, int y * No modifications will be applied. * @param frameDelay the time delay that will take for a cursor to change from one frame to another. * @return a {@link JmeCursor} object that contains the data for an animated cursor. + * + * @throws IllegalArgumentException if images inside cursorFrames array have different sizes. */ - public static JmeCursor fromTextureFrames(Texture2D[] cursorFrames, int frameDelay) { + public JmeCursor fromTextureFrames(Texture2D[] cursorFrames, int frameDelay) { int yHotspot = 0; if (cursorFrames.length > 0) { yHotspot = cursorFrames[0].getImage().getHeight(); @@ -134,10 +166,13 @@ public static JmeCursor fromTextureFrames(Texture2D[] cursorFrames, int frameDel * * @param cursorFrames the frames that will make up the cursor animation. * No modifications will be applied. - * @param frameDelay the time delay that will take for a cursor to change from one frame to another. + * @param frameDelays the time delay that will take for a cursor to change from one frame to another. * @return a {@link JmeCursor} object that contains the data for an animated cursor. + * + * @throws IllegalArgumentException if the lenghts of cursorFrames and frameDelays arrays doesn't match. + * @throws IllegalArgumentException if images inside cursorFrames array have different sizes. */ - public static JmeCursor fromTextureFrames(Texture2D[] cursorFrames, int[] frameDelays) { + public JmeCursor fromTextureFrames(Texture2D[] cursorFrames, int[] frameDelays) { int yHotspot = 0; if (cursorFrames.length > 0) { yHotspot = cursorFrames[0].getImage().getHeight(); @@ -162,8 +197,11 @@ public static JmeCursor fromTextureFrames(Texture2D[] cursorFrames, int[] frame * coordinate for its hotspot. Note that the coordinate system is the same as * OpenGL. 0, 0 being lower left. Must be between 0 and image height. * @return a {@link JmeCursor} object that contains the data for an animated cursor. + * + * @throws IllegalArgumentException if images inside cursorFrames array have different sizes. + * @throws IllegalArgumentException if the specified hotspots are outside the images's sizes. */ - public static JmeCursor fromTextureFrames( + public JmeCursor fromTextureFrames( Texture2D[] cursorFrames, int frameDelay, int xHotspot, @@ -192,8 +230,12 @@ public static JmeCursor fromTextureFrames( * coordinate for its hotspot. Note that the coordinate system is the same as * OpenGL. 0, 0 being lower left. Must be between 0 and image height. * @return a {@link JmeCursor} object that contains the data for an animated cursor. + * + * @throws IllegalArgumentException if the lenghts of cursorFrames and frameDelays arrays doesn't match. + * @throws IllegalArgumentException if images inside cursorFrames array have different sizes. + * @throws IllegalArgumentException if the specified hotspots are outside the images's sizes. */ - public static JmeCursor fromTextureFrames( + public JmeCursor fromTextureFrames( Texture2D[] cursorFrames, int[] frameDelays, int xHotspot, @@ -205,7 +247,6 @@ public static JmeCursor fromTextureFrames( } List imageFrames = Arrays.stream(cursorFrames) - //Avoid working and accidentally modifying original values .map((frame) -> frame.getImage()) .collect(Collectors.toList()); @@ -236,6 +277,14 @@ public static JmeCursor fromTextureFrames( IntBuffer imagesData = BufferUtils.createIntBuffer(imageHeight * imageWidth * cursorFrames.length); + if (outputVerticalFlip) { + imageFrames = imageFrames + .stream() + .map((image) -> image.deepClone()) //Avoid modifying the original image. + .peek((image) -> verticalFlip(image)) + .collect(Collectors.toList()); + } + List framesData = imageFrames .stream() .map((image) -> getDataAsIntBuffer(image)) @@ -257,7 +306,22 @@ public static JmeCursor fromTextureFrames( return jmeCursor; } - private static IntBuffer getDataAsIntBuffer(Image image) { + private static void verticalFlip(Image image) { + int height = image.getHeight(); + int width = image.getWidth(); + + ImageRaster raster = ImageRaster.create(image); + + for (int i = 0; i < width; i++) { + for (int j = 0; j < height / 2; j++){ + ColorRGBA reserve = raster.getPixel(i, j); + raster.setPixel(i, j, raster.getPixel(i, height - j -1)); + raster.setPixel(i, height - j -1, reserve); + } + } + } + + private IntBuffer getDataAsIntBuffer(Image image) { int width = image.getWidth(); int height = image.getHeight(); @@ -265,19 +329,18 @@ private static IntBuffer getDataAsIntBuffer(Image image) { IntBuffer data = BufferUtils.createIntBuffer(width * height); - //ARGB color system is needed to show cursors correctly. for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { - ColorRGBA color = raster.getPixel(x, y); - - int a = (int) (color.a * 255) & 0xFF; - int r = (int) (color.r * 255) & 0xFF; - int g = (int) (color.g * 255) & 0xFF; - int b = (int) (color.b * 255) & 0xFF; + ColorRGBA imagePixel = raster.getPixel(x, y); + int cursorPixel; + + if (outputCursorFormat == CursorFormat.ARGB) { + cursorPixel = imagePixel.asIntARGB(); + } else { + cursorPixel = imagePixel.asIntRGBA(); + } - int argb = (a << 24) | (r << 16) | (g << 8) | b; - - data.put(argb); + data.put(cursorPixel); } } @@ -285,7 +348,7 @@ private static IntBuffer getDataAsIntBuffer(Image image) { return data; } - private static boolean hotspotInsideImageLimits( + private boolean hotspotInsideImageLimits( int xHotspot, int yHotspot, int imageWidth, diff --git a/jme3-examples/src/main/java/jme3test/gui/TestCursor.java b/jme3-examples/src/main/java/jme3test/gui/TestCursor.java index 84614be13f..d306b6fbff 100644 --- a/jme3-examples/src/main/java/jme3test/gui/TestCursor.java +++ b/jme3-examples/src/main/java/jme3test/gui/TestCursor.java @@ -1,26 +1,54 @@ +/* + * 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 jme3test.gui; +import java.util.ArrayList; +import java.util.Arrays; + import com.jme3.app.SimpleApplication; import com.jme3.cursors.plugins.CursorConverter; import com.jme3.cursors.plugins.JmeCursor; import com.jme3.texture.Image; -import com.jme3.texture.image.ImageRaster; import com.jme3.texture.Texture2D; -import com.jme3.math.ColorRGBA; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.stream.Collectors; /** * This test class demonstrate how to change cursor in jME3. * * The different methods of {@link com.jme3.cursors.plugins.CursorConverter} will be tested. - * - * NOTE: This will not work on Android as it does not support cursors. + * Also, note that {@link com.jme3.cursors.plugins.CursorConverter} will be configured + * to match LWJGL cursor requirements. * * Cursor test - * @author MadJack + * @author MadJack, mondogo24 */ public class TestCursor extends SimpleApplication { @@ -42,6 +70,13 @@ public void simpleInitApp() { // will still be "used" and loaded, you just won't see it on the screen. inputManager.setCursorVisible(true); + // Note that vertical flip might be needed depending of the original + // image's format and the consumer of the resulting {@link com.jme3.cursors.plugins.JmeCursor}. + CursorConverter cursorConverter = new CursorConverter( + CursorConverter.CursorFormat.ARGB, + true + ); + /* * To make jME3 use a custom cursor it is as simple as putting the * image file in an asset directory. Here we use @@ -66,19 +101,17 @@ public void simpleInitApp() { * Checking and following the license restrictions in the process. */ - Image memeImage = ((Image) assetManager.loadAsset("Textures/Cursors/meme.png")).deepClone(); - flipVertically(memeImage); + Image memeImage = ((Image) assetManager.loadAsset("Textures/Cursors/meme.png")); Texture2D memeTexture = new Texture2D(memeImage); - Image nyancatImage = ((Image) assetManager.loadAsset("Textures/Cursors/nyancat.png")).deepClone(); - flipVertically(nyancatImage); + Image nyancatImage = ((Image) assetManager.loadAsset("Textures/Cursors/nyancat.png")); Texture2D nyancatTexture = new Texture2D(nyancatImage); //Shows a cursor with default hot spot. - cursors.add(CursorConverter.fromTexture(memeTexture)); + cursors.add(cursorConverter.fromTexture(memeTexture)); //Shows a cursor with custom hot spot. - cursors.add(CursorConverter.fromTexture(nyancatTexture, 10, 10)); + cursors.add(cursorConverter.fromTexture(nyancatTexture, 10, 10)); /* * For animated cursors. Each frame must be loaded. @@ -96,42 +129,26 @@ public void simpleInitApp() { }; Texture2D[] monkeyFrames = Arrays.stream(monkeyFramePaths) - .map(framePath -> ((Image) assetManager.loadAsset(framePath)).deepClone()) - .peek(frameImage -> flipVertically(frameImage)) + .map(framePath -> ((Image) assetManager.loadAsset(framePath))) .map(frameImage -> new Texture2D(frameImage)) .toArray(Texture2D[]::new); //Shows monkey cursor with same frame delay for each frame and default hot spot. - cursors.add(CursorConverter.fromTextureFrames(monkeyFrames, 60)); + cursors.add(cursorConverter.fromTextureFrames(monkeyFrames, 60)); //Shows monkey cursor with custom frame delay for each frame and default hot spot. - cursors.add(CursorConverter.fromTextureFrames(monkeyFrames, new int[]{40, 40, 40, 200, 200, 200})); + cursors.add(cursorConverter.fromTextureFrames(monkeyFrames, new int[]{40, 40, 40, 200, 200, 200})); //Shows monkey cursor with same frame delay for each frame and custom hot spot. - cursors.add(CursorConverter.fromTextureFrames(monkeyFrames, 150, 5, 7)); + cursors.add(cursorConverter.fromTextureFrames(monkeyFrames, 150, 5, 7)); //Shows monkey cursor with custom frame delay for each frame and hot spot. - cursors.add(CursorConverter.fromTextureFrames(monkeyFrames, new int[]{40, 200, 40, 200, 40, 200}, 10, 20)); + cursors.add(cursorConverter.fromTextureFrames(monkeyFrames, new int[]{40, 200, 40, 200, 40, 200}, 10, 20)); sysTime = System.currentTimeMillis(); inputManager.setMouseCursor(cursors.get(count)); } - private void flipVertically(Image image) { - int height = image.getHeight(); - int width = image.getWidth(); - - ImageRaster raster = ImageRaster.create(image); - - for (int i = 0; i < width; i++) { - for (int j = 0; j < height / 2; j++){ - ColorRGBA reserve = raster.getPixel(i, j); - raster.setPixel(i, j, raster.getPixel(i, height - j -1)); - raster.setPixel(i, height - j -1, reserve); - } - } - } - @Override public void simpleUpdate(float tpf) { long currentTime = System.currentTimeMillis(); @@ -142,7 +159,7 @@ public void simpleUpdate(float tpf) { count = 0; } sysTime = currentTime; - // 8 seconds have passed, + // Enough time have passed, // tell jME3 to switch to a different cursor. inputManager.setMouseCursor(cursors.get(count)); } diff --git a/jme3-testdata/src/main/resources/Textures/Cursors/meme.png b/jme3-testdata/src/main/resources/Textures/Cursors/meme.png deleted file mode 100644 index 4be43cc96bde30dc6a13225a52e6f2513c0cb041..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 561 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnF3?v&v(vO2U$sR$z3=CCj3=9n|3=F@3LJcn% z7)lKo7+xhXFj&oCU=S~uvn$XBD8V1#6XFV_f#CoD|J$~0GcP<6WWm-53FX?u!JLt8D*V((bvH#oo8( zJ19Bx-mIO>6^cbmTOJfC6s1+_eQRUkoH4b_t$NP`J3ggj66s9b25z6+5-jYQBN*6z zC&)4FuVG4kUeA@<)BfngF01*xtNPDxWB1-ZBf^RO>ateL2`jWB8)g~i>br~Y2p2rs zmh;YS)8BpOdK+7EFK0*ETksoBpPKS(I?!LLC9V-ADTyViR>?)FK#IZ0z{pJ3z*N`3 zGQ`lp%Fxux)KuHRz{?NMQuI#T^rTG-~ zC6!M|0fkgcTq875Jpiad18ze}W^QV6Nn&mRZas=m)cS#rPV{tf4AD5hchcUxW&@Gd zkJ1I*y^HeB-Z;CdlA*;jz5CY7ELRKv2^Z8?7^R;+5%Fzb{Q84WmY=+{S)tahVEuVT zMvuwXwtH3hA225~U2bNGIcQ>Z?tk5!0M(^h`I@1NPf0Hp(pYjmEQnR($yQMZpIw`p zPWl)}p8lMabGRm(M@}QnC+4Eov*xIU*XCXqzH|Ccx$LE7_cU+!g>BMYdzp7iw8M-ogzxg#E(C}z1L8ly=L#d-!FV5_}<$I zUXxBvpK&wOKl0YPtHpAyoI56}8vlx65M}KX;^GLXvI9q@O0xV+ z9=WKGot6qR&C3i98P-0U&~&<@(u>(A`RO`-_VdRXmODOi-yO#-v2VI&m_+ue-q(&F z9`0Z{k)mVrUCL;2`(y33PwuE2+nnDk?PvRsui)mKi1qD?aSz_P{a+K`emPYqt3K%g QFoGC7UHx3vIVCg!01dPSYybcN diff --git a/jme3-testdata/src/main/resources/Textures/Cursors/monkey/frame_0002.png b/jme3-testdata/src/main/resources/Textures/Cursors/monkey/frame_0002.png deleted file mode 100644 index 790e325001c19172b3bc947a666452ec6240eb9c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 707 zcmeAS@N?(olHy`uVBq!ia0vp^8X(NV3?%C=ER6$F3dtTpz6=aiY77hwEes65fIcptHiCL>?3;sP=f~ChLX(O)Z&uF+ydNs6rZT|109|1>EaloaenJ0+q`B6f!3SS z28?SLFX2kvb#+33a+}58o6HK<-n|=UFsn?w^PEj@?y0r4JKrQFZ)m!dIzhv3@6F23 zj~Rd5Tfd|7gW7|Z3(cI*d^!Jfq&qUi*s3PYZF+L8Cv(PxNoS`0Qck?4p=Fd7%x7@i zh~sn94%ve~cbA>E&=->X#L6~X+=b_P@`@M1!mFA6vv>o~veE-?sge z+S9@txm5rQZSZ-x${<{fBo`n8S b|HE*6adMxZQhq%!su(<7{an^LB{Ts5gaZ=Q diff --git a/jme3-testdata/src/main/resources/Textures/Cursors/monkey/frame_0003.png b/jme3-testdata/src/main/resources/Textures/Cursors/monkey/frame_0003.png deleted file mode 100644 index 8bf8c1b8a4006d250a8729f53ed4462bb91ab668..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 699 zcmeAS@N?(olHy`uVBq!ia0vp^8X(NV3?%C=ER6$F3dtTpz6=aiY77hwEes65fI;FR6S& z3Miyn;u=wsl30>zm0Xkxq!^40jLdWmOm&USLkvx+tPBj;*X)0Tq9Hdw zB{QuOw}!Hh>;XUx8gLs*GILXlOA>PnaO+WgqSg;|bh4+5V~EE2t&{fV9ySnYJ<9t< z>5$75t*l^96^ZRk{^uCX*g0~7w+ckaDCq5Ji>hp{*ctF|s<2}0x<4QKYQDcyQs}F@ zdbrqx$-PC;Ey6_Y&teI;IL}kAG7s;`8f!CGJb%sT>yaHdiSG;B&pl4(bkAzlPcG_R zIU(h2z^o@(*-Os+lzoyS?#5O0?^(j3l{_W8)@FJBl;rYDd7Jb-a^7(kX2pIO!Zps~$l z&T1RY)xP|e6?fRR_Ou&S)oy9w?3d&ce3a6uV*qlRdwL#NLzj>+%(T#k_p1&b$*C OMGT&sCwCg$1(237_J>}&SFLD7(#pOTqY ziCaV2NA>`q1`W6kC7HRY#U+Wk1-SJnK2hrjIoi|3F+}71)=3-v4jTwKytO}($h|tJ zBxSSAnVhFVrTID=-B!-A%<4=(a>+F3A^)uPM>0JpJ6-9Oo_}xd`*ZFLN}tr<$(`hx zBglEiz)1aG*G{>N9NvWt;rs=?LN2jYGrBw{CQsXOsXsJhl{Q;u+qrOMpQ3O3x-a|q zhpf6GH8p6%EGNn3VpH#|KlSPo<5|Jn1?^MQ3qL)+#AUd(toDNSmPk7uk?&u=9?pEf zR^}7;8)tFXmG|_1ZspRsc>8(I_f4ICrxmX3zA&-uwuyELzovN$M}gugTdga#R_1MP zQ@U?i?)SfRHthSdDK<=M2dd8p?DUzJHihkjT+fF9%`MN5?=<3mCKGb7)oJeSCmPWg zS{bCcc>W&>xyQmR_Q2IPNAr(wvcSUT2h*ls)s<>Hd3Y1&-(7;+cn_pEgg@JuyZdRd zi;L|W-H^Ypm~5D?EWYu)Job7;xZnClr`y{;ymtEa_w2nV_Q%b0UM9M`*HnU2m8Ywp J%Q~loCIIqx48H&X diff --git a/jme3-testdata/src/main/resources/Textures/Cursors/monkey/frame_0005.png b/jme3-testdata/src/main/resources/Textures/Cursors/monkey/frame_0005.png deleted file mode 100644 index f39c59cde533a1351c4c8c3cfe0017ab46192cce..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 685 zcmeAS@N?(olHy`uVBq!ia0vp^8X(NV3?%C=ER6$F3dtTpz6=aiY77hwEes65fIWo&F^Vy9{IBw4d5^7A#4n5Qx8}TNNtpKM z;$?3mM)e*}pA9LeB<>3DZWK_e&Tw|Quy?xG(yc-_=LTkJZr!(CNK>XbZ2Mi`t?5yo zX}d4_l$MCCzBg@p)+-;mSzF^`*7Uqm2o#T~wLj$dBCn+(v@xPnb=%j7dAH9+WMAEN z@5)ulPkT0QtK9nbRc*4~2h03>Ro<_!Z8`eYV?%1k)92d^9)2)%*>+#)QC+xU$$rlg zxreLXzrDVWYvQhoJyUP4=ob(c)Y4Hi5HVC-@p9$}KB&X2&WPFuaoEvfnq8 zZ>?nk*PQ7lwr6T~C59QqUkIqT+BrRh@tmpIo-0ocWNNw!>l$vzF#1}_{QdD*(xd(C zfu2%RdP`(kYX@0Ff!9MFx53O4>2^cGB&m{G1oRQure@UU$g%WiiX_$ zl+3hB+#1S0vIhV)XuxeK$;?eHE=kNSz^zB|iCRC<(bk?Wjv*T7*G}@yJ7gf>W^11C z>WI*3xr3W589rTDr(2L9y>(t(gIajPX`#LYn%UM4KTl4Iv^e#7^5mL$mPal&XK(M8 zXjEQwKv3=2k#HUbJ6rxGhR;4q8yUOZ?{3qXXjr~V<$1@Qy;^e@nz-FhVZIjF_?h*0 zy~oilzW0xIcWvufQm+#m@`1VJt69r=_Z8E=CD*oIWsttDvx__Csy3J6jIV3@_kTA2 z6DBj`r+4meiSq#=T8bxQj7_hHGcEnKZf>dF&!-+9)*B|iJW(ZX_`{PkDE)=-oi~px zgFJT~x%XZ6{USX>#+@auq1t(Z)2+gpM4u*vE;_hfXZdo^v`CQ%O`T#JCABcA6&hQ9 zN9ZnVpL}NRlsLtfZw;kU-*)a>nBDQyt|00|6o@{e^AG?4 diff --git a/jme3-testdata/src/main/resources/Textures/Cursors/monkey/monkey.png b/jme3-testdata/src/main/resources/Textures/Cursors/monkey/monkey.png deleted file mode 100644 index fdb5a0e9942a02c1b08eb7a4dd5a92af19e2417c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2379 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 KG?H}LG5SB9J`6hm diff --git a/jme3-testdata/src/main/resources/Textures/Cursors/nyancat.png b/jme3-testdata/src/main/resources/Textures/Cursors/nyancat.png deleted file mode 100644 index 36e9b339389bec31f9ecfd660bf23e24ec1a9526..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1969 zcmZ`&dpy$%AO1xswU^WTDbd?W9n7UN)rh3&7?#^ILJr#uOKyv~Y;xX>8Dh%a#%gvs z=;&C>IFv#zn^ewHDdb)|sW+E$V&?a*f8NjM{p0-c{XU=P`8?0(`RmC?`+7k1P4oc( zfS^3x{51&sc3ZSHA2+sDssU4ir@s#XP%Qv}#sGjdjYLxdK#Ior9tQwURRCZ_s%by# z3;@SZN@oVm9QfBub03D92 zoKCE|Ak;IXpcDk#L2z`oIU)9ijnSm--e<4_)Am*hrk}d&%SMkH90E5UZ>+PY)Uq}8 zy=73o(NhvWHJ)*p5^O+6@i%x>YrxvqMe9Ceem5j1E?7`cEU98}c;By%n z)Y8(@nwn}b9{NmdG}XR)o$k1thnP!op6>WT!SRz+O1NCENF=IfH+<>+(w^-dhQ&8D zG-Oc7O}Ao{lSWT^cYqr<8yj{@D>mc9Ce63Qi#dV{b`4Ju4uWp4n=tDkT{5AJ&8m_~ znS~jNjg5`9wY70^u^fJT zaGnP)h+g+FYS^VD?;`9e3f#sSj}48%2PdDsYQEQMwl;j48>$rH<4DQ&hs+SC5RBX_ zW~O_>0=x-ZHJL8Nep8zb^(ryd8=DMk2gC+%^h{GM>!FaqBH;;fgQ0w1cM*4g9X?*IN8CSU`K6W zFeezyE~)h1{{`X`BV(c|e;+{D+GzZM<^LMUG4WB!WPCj7?=wbD`V#=4O+&f426*-p zaJzK`xCOx7B6@pgPZpn1&f;=e+C9^eGt+OUdbQH43TKSuqr+h2qp|H8|pm#?N%^BU1MdS0Us`xuRvn5xoP0(jcE9fWAgdxIz#9%N#8rGjb>lGU6 zaXQEkJJE^2Vkgkp3E6JhG1wpF_BpXQhoi0HW{ITbw)Bqlb{dIPMmloI-X7Rwl$l8g z%GAyj>pJL$q=|J6w)t(dwY5#$hdreS0%F~hVPdMbx|4_FQHcZ|^V*PjVGF4<-R(6J z>EYo)>HXT@hwSZ#0&qmx9wpSmyiS-kLRG~4Ib|$on?gcfIkvS?D62X-$X=wmNEC&J zQ0LZk^QVlBQ9G^5%dG;RPs!oZ6ddFWqz$o`Me%tommkc*K`acR#sSU4FNRx~xq3P~ zb90Hqo27JkGcgTtB?cv6&cvtrCGL~y8U=}&og1dS_rlkaruC4UT-bL)ykgc0Q|D9d zLRcDP--(72k3FT2;Iy=o=Peje^C&YS4Uv@e$oU#`6+=7>ybnH}@&_MTP-$ix)-om^ z+TA!dLP2_^D+U5n;Kpz`9QCUz_F<>VuHQoLCXBaBx7GP_+ezwyX!e}%VMB<;9(&&{ zSA#?JhQ%?-Uk481H+BDuBsKcoMi zG#zq;qvY`1f9!LlZWrQJi{nt=xQUj`cTc!b-w$Jy9rdmT7joWMX?Hw3gn;y-t3 zx9pHGaX)!xkp=z!oP)s)aMZ*Z3i|7Yx0!(E#{hxvv2D;N_z=T+!2ORII&+j0~tC`5dd zh+7SP`LtVXcCc{*GWcohwr!6-eEMV%+Z8C2CDE34dKnBNU0z) UxV4U?^=-f?q_5j=r*T>T1w&!Ewg3PC From c75f6da00ad5ea8caca008eb1e1d11c298cf6762 Mon Sep 17 00:00:00 2001 From: mondogo24 <167080436+mondogo24@users.noreply.github.com> Date: Wed, 11 Nov 2026 13:44:57 +0100 Subject: [PATCH 19/21] Fixed invalid yhotspot positions --- .../java/com/jme3/cursors/plugins/CursorConverter.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/jme3-core/src/plugins/java/com/jme3/cursors/plugins/CursorConverter.java b/jme3-core/src/plugins/java/com/jme3/cursors/plugins/CursorConverter.java index a4946ec2e1..832e63b00b 100644 --- a/jme3-core/src/plugins/java/com/jme3/cursors/plugins/CursorConverter.java +++ b/jme3-core/src/plugins/java/com/jme3/cursors/plugins/CursorConverter.java @@ -82,7 +82,7 @@ public CursorConverter(CursorConverter.CursorFormat cursorFormat, boolean vertic * @return the {@link JmeCursor} object that contains the data for a cursor. */ public JmeCursor fromTexture(Texture2D cursorTexture) { - return fromTexture(cursorTexture, 0, cursorTexture.getImage().getHeight()); + return fromTexture(cursorTexture, 0, cursorTexture.getImage().getHeight() - 1); } /** @@ -150,7 +150,7 @@ public JmeCursor fromTexture(Texture2D cursorTexture, int xHotspot, int yHotspot public JmeCursor fromTextureFrames(Texture2D[] cursorFrames, int frameDelay) { int yHotspot = 0; if (cursorFrames.length > 0) { - yHotspot = cursorFrames[0].getImage().getHeight(); + yHotspot = cursorFrames[0].getImage().getHeight() - 1; } return fromTextureFrames(cursorFrames, frameDelay, 0, yHotspot); } @@ -175,7 +175,7 @@ public JmeCursor fromTextureFrames(Texture2D[] cursorFrames, int frameDelay) { public JmeCursor fromTextureFrames(Texture2D[] cursorFrames, int[] frameDelays) { int yHotspot = 0; if (cursorFrames.length > 0) { - yHotspot = cursorFrames[0].getImage().getHeight(); + yHotspot = cursorFrames[0].getImage().getHeight() - 1; } return fromTextureFrames(cursorFrames, frameDelays, 0, yHotspot); } @@ -353,6 +353,6 @@ private boolean hotspotInsideImageLimits( int yHotspot, int imageWidth, int imageHeight) { - return xHotspot >= 0 && xHotspot <= imageWidth && yHotspot >= 0 && yHotspot <= imageHeight; + return xHotspot >= 0 && xHotspot < imageWidth && yHotspot >= 0 && yHotspot < imageHeight; } } From 82ad7d99541f3d94de4e11faecc6af482471ea92 Mon Sep 17 00:00:00 2001 From: mondogo24 <167080436+mondogo24@users.noreply.github.com> Date: Wed, 11 Nov 2026 15:42:28 +0100 Subject: [PATCH 20/21] Fixed CursorConverter incompatibility with AndroidMouseInput24 - Now CursorConverter can output a JmeCursor compatible object with AndroidMouseInput24 - Changed some variable names in ColorRGBA for more clarity. - Changed commentary in AndroidMouseInput24.setNativeCursor for more clarity. --- .../input/android/AndroidMouseInput24.java | 2 +- .../main/java/com/jme3/math/ColorRGBA.java | 29 +++++++++--- .../jme3/cursors/plugins/CursorConverter.java | 44 +++++++++++++------ 3 files changed, 55 insertions(+), 20 deletions(-) diff --git a/jme3-android/src/main/java/com/jme3/input/android/AndroidMouseInput24.java b/jme3-android/src/main/java/com/jme3/input/android/AndroidMouseInput24.java index bdc1c5fef7..3dd56d7295 100644 --- a/jme3-android/src/main/java/com/jme3/input/android/AndroidMouseInput24.java +++ b/jme3-android/src/main/java/com/jme3/input/android/AndroidMouseInput24.java @@ -114,7 +114,7 @@ public void setCursorVisible(boolean visible) { public void setNativeCursor(JmeCursor cursor) { if(inputHandler.getView()!=null) { if(cursor!=null) { - // Translate into Android Bitmap format ARGB888. Assuming input image as RGBA + // Translate into Android Bitmap format ARGB888. Assuming input image as RGBA where each channel is an integer. int bufferSize = cursor.getHeight()*cursor.getWidth(); int[] outputBitmap=new int[bufferSize]; IntBuffer inputImage = cursor.getImagesData().asReadOnlyBuffer(); diff --git a/jme3-core/src/main/java/com/jme3/math/ColorRGBA.java b/jme3-core/src/main/java/com/jme3/math/ColorRGBA.java index a70a01bd21..61f57d9dc4 100644 --- a/jme3-core/src/main/java/com/jme3/math/ColorRGBA.java +++ b/jme3-core/src/main/java/com/jme3/math/ColorRGBA.java @@ -673,6 +673,25 @@ public int asIntABGR() { return toInt(a, b, g, r); } + /** + * Retrieves the component values of this ColorRGBA as an + * int[] of lenght four in r,g,b,a order. + * Where: + * int[0] = red value in range of 0-255. + * int[1] = green value in range of 0-255. + * int[2] = blue value in range of 0-255. + * int[3] = alpha value in range of 0-255. + * + * @return The integer array representation of this ColorRGBA in r,g,b,a order. + */ + public int[] asIntsRGBA() { + int intR = ((int) (r * 255) & 0xFF); + int intG = ((int) (g * 255) & 0xFF); + int intB = ((int) (b * 255) & 0xFF); + int intA = ((int) (a * 255) & 0xFF); + return new int[] {intR, intG, intB, intA}; + } + /** * Sets the component values of this ColorRGBA with the given * combined ARGB int. @@ -824,10 +843,10 @@ private float fromByte(int channelByte) { * Helper method to combine four float channels into an int. */ private int toInt(float c1, float c2, float c3, float c4) { - int r = ((int) (c1 * 255) & 0xFF); - int g = ((int) (c2 * 255) & 0xFF); - int b = ((int) (c3 * 255) & 0xFF); - int a = ((int) (c4 * 255) & 0xFF); - return (r << 24) | (g << 16) | (b << 8) | a; + int intC1 = ((int) (c1 * 255) & 0xFF); + int intC2 = ((int) (c2 * 255) & 0xFF); + int intC3 = ((int) (c3 * 255) & 0xFF); + int intC4 = ((int) (c4 * 255) & 0xFF); + return (intC1 << 24) | (intC2 << 16) | (intC3 << 8) | intC4; } } diff --git a/jme3-core/src/plugins/java/com/jme3/cursors/plugins/CursorConverter.java b/jme3-core/src/plugins/java/com/jme3/cursors/plugins/CursorConverter.java index 832e63b00b..805ca3cec3 100644 --- a/jme3-core/src/plugins/java/com/jme3/cursors/plugins/CursorConverter.java +++ b/jme3-core/src/plugins/java/com/jme3/cursors/plugins/CursorConverter.java @@ -52,7 +52,17 @@ public class CursorConverter { /** * Supported formats for {@link JmeCursor}'s image data. */ - public enum CursorFormat {ARGB, RGBA} + public enum CursorFormat { + /** + * One byte (8 bits) for each channel (Alpha, Red, Green, Blue). + */ + ARGB, + + /** + * One interger (32 bits) for each channel (Red, Green, Blue, Alpha). + */ + RGBAI + } private final CursorConverter.CursorFormat outputCursorFormat; private final boolean outputVerticalFlip; @@ -326,21 +336,27 @@ private IntBuffer getDataAsIntBuffer(Image image) { int height = image.getHeight(); ImageRaster raster = ImageRaster.create(image); - - IntBuffer data = BufferUtils.createIntBuffer(width * height); - - for (int y = 0; y < height; y++) { - for (int x = 0; x < width; x++) { - ColorRGBA imagePixel = raster.getPixel(x, y); - int cursorPixel; - - if (outputCursorFormat == CursorFormat.ARGB) { - cursorPixel = imagePixel.asIntARGB(); - } else { - cursorPixel = imagePixel.asIntRGBA(); + IntBuffer data; + + if (outputCursorFormat == CursorFormat.ARGB) { + data = BufferUtils.createIntBuffer(width * height); + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + ColorRGBA imagePixel = raster.getPixel(x, y); + int cursorPixel = imagePixel.asIntARGB(); + data.put(cursorPixel); } + } + } else { + data = BufferUtils.createIntBuffer(width * height * 4); - data.put(cursorPixel); + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + ColorRGBA imagePixel = raster.getPixel(x, y); + int[] cursorPixel = imagePixel.asIntsRGBA(); + data.put(cursorPixel); + } } } From 111851a5aaec2fcd07b031be8f09601d7f4035d9 Mon Sep 17 00:00:00 2001 From: mondogo24 <167080436+mondogo24@users.noreply.github.com> Date: Wed, 11 Nov 2026 19:25:50 +0100 Subject: [PATCH 21/21] Fixed wrong buffer size for Android and documented if hotspots flip --- .../jme3/cursors/plugins/CursorConverter.java | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/jme3-core/src/plugins/java/com/jme3/cursors/plugins/CursorConverter.java b/jme3-core/src/plugins/java/com/jme3/cursors/plugins/CursorConverter.java index 805ca3cec3..7830f97d19 100644 --- a/jme3-core/src/plugins/java/com/jme3/cursors/plugins/CursorConverter.java +++ b/jme3-core/src/plugins/java/com/jme3/cursors/plugins/CursorConverter.java @@ -73,7 +73,7 @@ public enum CursorFormat { * @param cursorFormat the {@link JmeCursor}'s image data will follow the * specified image format. * @param verticalFlip if true, the {@link JmeCursor}'s image will be flipped vertically, otherwise - * it won't be flipped. + * it won't be flipped. It only affect the cursor's image, It doesn't flip hotspots as well. */ public CursorConverter(CursorConverter.CursorFormat cursorFormat, boolean verticalFlip) { outputCursorFormat = cursorFormat; @@ -128,7 +128,7 @@ public JmeCursor fromTexture(Texture2D cursorTexture, int xHotspot, int yHotspot image = image.deepClone(); //Avoid modifying the original. verticalFlip(image); } - IntBuffer adaptedImageData = getDataAsIntBuffer(image); + IntBuffer imageData = getDataAsIntBuffer(image); JmeCursor jmeCursor = new JmeCursor(); jmeCursor.setWidth(imageWidth); @@ -137,7 +137,7 @@ public JmeCursor fromTexture(Texture2D cursorTexture, int xHotspot, int yHotspot jmeCursor.setyHotSpot(yHotspot); jmeCursor.setNumImages(1); jmeCursor.setImagesDelay(null); - jmeCursor.setImagesData(adaptedImageData); + jmeCursor.setImagesData(imageData); return jmeCursor; } @@ -285,7 +285,7 @@ public JmeCursor fromTextureFrames( ); } - IntBuffer imagesData = BufferUtils.createIntBuffer(imageHeight * imageWidth * cursorFrames.length); + IntBuffer imagesData = createImageDataBuffer(imageHeight * imageWidth * cursorFrames.length); if (outputVerticalFlip) { imageFrames = imageFrames @@ -336,11 +336,9 @@ private IntBuffer getDataAsIntBuffer(Image image) { int height = image.getHeight(); ImageRaster raster = ImageRaster.create(image); - IntBuffer data; + IntBuffer data = createImageDataBuffer(width * height); if (outputCursorFormat == CursorFormat.ARGB) { - data = BufferUtils.createIntBuffer(width * height); - for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { ColorRGBA imagePixel = raster.getPixel(x, y); @@ -349,8 +347,6 @@ private IntBuffer getDataAsIntBuffer(Image image) { } } } else { - data = BufferUtils.createIntBuffer(width * height * 4); - for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { ColorRGBA imagePixel = raster.getPixel(x, y); @@ -371,4 +367,12 @@ private boolean hotspotInsideImageLimits( int imageHeight) { return xHotspot >= 0 && xHotspot < imageWidth && yHotspot >= 0 && yHotspot < imageHeight; } + + private IntBuffer createImageDataBuffer(int baseSize) { + if (outputCursorFormat == CursorFormat.ARGB) { + return BufferUtils.createIntBuffer(baseSize); + } else { + return BufferUtils.createIntBuffer(baseSize * 4); + } + } }