java.lang.ClassCastException: class sun.net.www.protocol.file.FileURLConnection cannot be cast to class java.net.JarURLConnection

I’m finishing my 3d dice extension and so I began restructuring my project. At the beginning, just to test if what I pretended was possible, I used what was the easier way for me to load images into the project, after some trials and I set up the following folder structure (notice I’m using the Maven set tutorial module as a template):

Project
|-src 
|-target    
    |-classes    
        |-my_custom_component
            |-myclass        
            |-images        
            |-sounds

So I have set the resources manually into the target directory. It worked as pretended, but was badly structured.

I then decided to place a resources folder in the root folder, configured to “Resource Root”. I then began getting the error reproduced below. Without the “Resource Root” configuration I don’t get the error, but even if I get the path right, the compiler doesn’t seem to recognize it and I get a null getResourceAsStream returns null. I tried removing the “Resource Root” and placing the resources folder within the src folder as follows:

Project
|-src    
    |-main       
        |-java      
        |-resources          
            |-images          
            |-sounds 
|-target    
    |-classes        
        |-my_custom_components

which seems to be Maven standard. It seems to automatically recognize the folder as a resource folder and copy the resources to the target folder when we build, but I still get the error described below:

java.lang.ClassCastException: class sun.net.www.protocol.file.FileURLConnection cannot be cast to class java.net.JarURLConnection (sun.net.www.protocol.file.FileURLConnection and java.net.JarURLConnection are in module java.base of loader 'bootstrap')
    at VASSAL.tools.icon.IconFactory.findJarIcons(IconFactory.java:376)
    at VASSAL.tools.icon.IconFactory.initVassalIconFamilys(IconFactory.java:290)
    at VASSAL.tools.icon.IconFactory.lambda$new$0(IconFactory.java:97)
    at java.base/java.lang.Thread.run(Thread.java:833)

The project builds without error, but when I try to debug, by running vassal, it pops up. It seems to be related to how the folders are set up, since it doesn’t even invoke my class. Can someone point me to what may be causing this error. I’m really stuck here.

As an additional related question, less important for this moment: the compiler copies what is in the resources folder to the target/classes folder, but when I use the getResourceAsStream(“sounds/nameOfSound.wav”) command, it goes search the resource inside the my_custom_component folder instead. How can I configure the project so that the resources are copied to that last folder?

It would help if we could see the code involved.

I’ll reproduce my class here, but it is important to notice that there may be wrong paths on it, since I wasn’t able to alter them yet due to this error, which doesn’t allow me to test them. There is no single point in the class causing this error, since it isn’t even invoked before the error occurs. I’ll display the folder structure too, which is probably causing this problem, I don’t know why.

image

Notice that the images and sounds folders are doubled in the target, since the compiler places them under target>classes and the getResourceAsStream go search them inside the my_custom_components folder, so I kept a copy inside this folder to force the program to work.

package my_custom_component;

import VASSAL.build.AbstractConfigurable;
import VASSAL.build.Buildable;
import VASSAL.build.GameModule;
import VASSAL.build.module.Chatter;
import VASSAL.build.module.Map;
import VASSAL.build.module.documentation.HelpFile;
import VASSAL.command.Command;
import VASSAL.command.CommandEncoder;
import VASSAL.command.NullCommand;
import VASSAL.command.RemovePiece;
import VASSAL.configure.IntConfigurer;
import VASSAL.counters.BasicPiece;

import javax.imageio.ImageIO;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.Clip;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.*;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;


public final class AnimatedDice extends AbstractConfigurable implements CommandEncoder, Buildable{
    private GameModule gameModule;
    //private static final String IMAGES_FOLDER = System.getProperty("user.dir") + "/resources/images/";
    //private static final String SOUNDS_FOLDER = System.getProperty("user.dir") + "/resources/sounds/";
    private String RED_DIE_FOLDER_PATH = "images/DiceImages/RED DIE/";
    private String WHITE_DIE_FOLDER_PATH = "images/DiceImages/WHITE DIE/";
    private final String ANIMATED_DICE_PREFERENCES = "Animated 3D Dice";
    private final String FRAME_RATE_SETTINGS = "frameRateSettings";
    private final String DICE_POSITION_SETTINGS = "dicePositionSettings";
    private int dicePositionSettings;
    private int MAX_HORIZONTAL_OFFSET = 0;
    private int IMAGE_SIZE = 250;
    private boolean isImageVisible; // establish if last dice frame is still visible in the screen so that the button is in Hide mode.
    private boolean isAnimationInProgress = false; // prevents mousePress action before animation finishes.
    private JButton customButton;
    private int[] mouseBiasFactor; // Set of ints generated by mouse movement which will influence dice results
    private int nDice; // number of dices rolled
    private int nSides; // number of sides of a die
    private long imageDelay; // The number of MILLISECONDS between the display actions.
    private int currentFrame;
    private int frameRate;
    private final int MAX_FRAME_RATE = 60;
    private final int MIN_FRAME_RATE = 35;
    private ScheduledExecutorService scheduler; // Controls the frame rate of the displayed images
    private Image[] images; // to be fed with the dice images that will be drawn on the pieces
    private java.util.Map<String, Image[]> diceImages;
    private int[] lastDiceImageFolderIndexes = new int[]{0,0}; // Keeps the last two dice image folders indexes stored to prevent immediate repetition of same animation
    private java.util.Map<Integer, java.util.Map<Integer, ArrayList<String>>> hesitantDiceFolderIndexes; // keeps a list of folders which bring animation of paired hesitant dice, for which the player can set the rarity of occurrence.
    private boolean feedingImages; // Checks if the thread for loading images is still running
    private byte[] dieAudioData;
    private byte[] diceAudioData;
    private byte[] shakingDiceAudioData;
    private boolean shouldStopFlag;
    private Cursor dieCursor;
    private boolean actionInProgress = false;
    private BasicPiece[] pieces; // pieces are added to this array to be displayed in order
    private final Map currentMap;

    public AnimatedDice(){
        isImageVisible = false;
        gameModule = GameModule.getGameModule();
        currentMap = GameModule.getGameModule().getComponentsOf(Map.class).get(0);
        dicePositionSettings = 0;
        frameRate = 45;
        currentFrame = 0;
        nDice = 2;
        nSides = 6;

        // GET RESOURCES
        loadSounds(); // Preloads sounds for dices
        URL dieCursorImageURL = getClass().getResource("images/DieCursor.png");
        try {
            BufferedImage dieCursorImage = ImageIO.read(dieCursorImageURL);
            dieCursor = Toolkit.getDefaultToolkit().createCustomCursor(dieCursorImage, new Point(0,0), "Custom Die Cursor");
        } catch (IOException e) {
            e.printStackTrace();
        }
        // LOAD FIRST SET OF IMAGES
        getImages();
        // CREATE THE ARRAY INDICATING WHAT FOLDERS BRING HESITANT DIE ANIMATIONS
        CreateHesitantDieFolderMask();
    }

    public static void main(String[] args) {

    }


    @Override
    public void addTo(Buildable parent) {
        if (parent instanceof GameModule) {
            gameModule = (GameModule) parent;
            // Create your button instance
            customButton = new DelayedActionButton("Roll Dice", new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    toggleImagesVisibility();
                }
            });
            customButton.setToolTipText("Displays Image on map");

            // Add the button to the toolbar
            gameModule.getToolBar().add(customButton);

            // ADD SETTINGS TO PREFERENCE WINDOW

            // FRAME RATE
            final IntConfigurer frameRateSettings = new IntConfigurer(FRAME_RATE_SETTINGS, "Frame Rate (MAX: " + MAX_FRAME_RATE + " / MIN: " + MIN_FRAME_RATE + ")", frameRate);
            gameModule.getPrefs().addOption(ANIMATED_DICE_PREFERENCES, frameRateSettings);
            frameRate = Integer.parseInt(gameModule.getPrefs().getValue(FRAME_RATE_SETTINGS).toString());

            frameRateSettings.addFocusListener(new FocusListener() {
                Object initialValue;
                @Override
                public void focusGained(FocusEvent e) {
                    initialValue = gameModule.getPrefs().getValue(FRAME_RATE_SETTINGS);
                }
                @Override
                public void focusLost(FocusEvent e) {
                    Object presentValue = gameModule.getPrefs().getValue(FRAME_RATE_SETTINGS);
                    if (Integer.parseInt(presentValue.toString()) > MAX_FRAME_RATE || Integer.parseInt(presentValue.toString()) < MIN_FRAME_RATE) {
                        gameModule.getPrefs().setValue(FRAME_RATE_SETTINGS, initialValue);
                    } else {
                        gameModule.getPrefs().setValue(FRAME_RATE_SETTINGS, presentValue);
                        frameRate = Integer.parseInt(gameModule.getPrefs().getValue(FRAME_RATE_SETTINGS).toString());
                    }
                }
            });


            // DICE POSITION
            MAX_HORIZONTAL_OFFSET = currentMap.getView().getMaximumSize().width; // IMPLEMENT!!

            final IntConfigurer dicePositionSettings = new IntConfigurer(DICE_POSITION_SETTINGS, "Screen Position (MAX: " + MAX_HORIZONTAL_OFFSET + " / MIN: " + 0 + ")", this.dicePositionSettings);
            gameModule.getPrefs().addOption(ANIMATED_DICE_PREFERENCES, dicePositionSettings);
            this.dicePositionSettings = Integer.parseInt(gameModule.getPrefs().getValue(DICE_POSITION_SETTINGS).toString());

            dicePositionSettings.addFocusListener(new FocusListener() {
                Object initialValue;

                @Override
                public void focusGained(FocusEvent e) {
                    initialValue = gameModule.getPrefs().getValue(DICE_POSITION_SETTINGS);
                }

                @Override
                public void focusLost(FocusEvent e) {
                    Object presentValue = gameModule.getPrefs().getValue(DICE_POSITION_SETTINGS);
                    if (Integer.parseInt(presentValue.toString()) > MAX_HORIZONTAL_OFFSET || Integer.parseInt(presentValue.toString()) < 0) {
                        gameModule.getPrefs().setValue(DICE_POSITION_SETTINGS, initialValue);
                    } else {
                        gameModule.getPrefs().setValue(DICE_POSITION_SETTINGS, presentValue);
                        AnimatedDice.this.dicePositionSettings = Integer.parseInt(gameModule.getPrefs().getValue(DICE_POSITION_SETTINGS).toString());
                    }
                }
            });
        }
    }

    private void loadSounds(){
        InputStream dieSoundStream = getClass().getResourceAsStream( "sounds/Selected1" + ".wav");
        InputStream diceSoundStream = getClass().getResourceAsStream("sounds/Selected2" + ".wav"); // For more than one die
        InputStream shakingDiceSoundStream = getClass().getResourceAsStream("sounds/Selected3" + ".wav"); // For shaking dice

        // Preload the audio data into memory
        try {
            dieAudioData = dieSoundStream.readAllBytes();
            diceAudioData = diceSoundStream.readAllBytes();
            shakingDiceAudioData = shakingDiceSoundStream.readAllBytes();
        } catch (IOException e){
            System.out.println("Exception reading sounds data");
            e.printStackTrace();
        }
        try {
            dieSoundStream.close(); // Close the stream
            diceSoundStream.close();
        } catch (IOException e){
            e.printStackTrace();
        }
    }

    private void playSounds(byte[] audioData){
        try{
            AudioInputStream audioInputStream = AudioSystem.getAudioInputStream(new ByteArrayInputStream(audioData));
            Clip clip = AudioSystem.getClip();
            clip.open(audioInputStream);
            new Thread(() -> {
                clip.start();
                while (clip.getFramePosition() < clip.getFrameLength()){
                    if (shouldStopFlag)
                        clip.close();
                    Thread.yield();
                }
                clip.close();
                Thread.currentThread().interrupt();
            }).start();
            try{  // Works without the try block, but seems to delay the first use of sounds
                audioInputStream.close();
            } catch(IOException e){
                e.printStackTrace();
            }
        } catch (Exception e){
            System.out.println("Exception thrown");
            e.printStackTrace();
        }
        shouldStopFlag = false;
    }


    private void toggleImagesVisibility(){
        // ENDS THE ANIMATION
        if (isImageVisible){
            customButton.setEnabled(false);
            hideImage(pieces[pieces.length - 1]); // Hide last frame, which remained visible
            isImageVisible = false;
            // We definitely remove each piece so that no artifacts are presented on screen and load new images for each result
            try{
                new Thread(() -> {
                    feedingImages = true;
                    for (BasicPiece piece: pieces){
                        Command remove = new RemovePiece(piece);
                        remove.execute();
                    }
                    getImages();
                    while (feedingImages){
                        Thread.yield();
                    }
                    customButton.setText("Roll Dice");
                    customButton.setEnabled(true);
                    currentMap.getView().repaint();
                    Thread.currentThread().interrupt();
                }).start();
            } catch (Exception e){
                System.out.println("Exception thrown");
                e.printStackTrace();
            }


        } else {
            // BEGINS THE ANIMATION
            isAnimationInProgress = true;
            customButton.setEnabled(false);
            imageDelay = (1000/frameRate); // transform frame rate into milliseconds delay
            RollDices (nDice, nSides);
            createPieces();
            scheduler = Executors.newSingleThreadScheduledExecutor();
            playSounds(dieAudioData);

            Rectangle rectangle = currentMap.getView().getVisibleRect();
            // If dicePosition (set up in preferences), which is the offset of the animation to the left,,
            // is larger than the width of the window minus the width of the images, we adjust it to the maximum place to which the animation may be offset without cropping the image.
            int adjustedDicePositionSettings = (dicePositionSettings > (rectangle.width - IMAGE_SIZE))? Math.max (rectangle.width - IMAGE_SIZE, 0): dicePositionSettings;
            int min_x = rectangle.x; // leftmost point of the current visible rectangle
            int x = (min_x + adjustedDicePositionSettings); // we add the adjusted offset to the leftmost point of the window.
            int y = rectangle.y;

            Runnable task = () -> displayImage(x,y);
            scheduler.scheduleAtFixedRate(task, 0, imageDelay, TimeUnit.MILLISECONDS);
        }
    }

    public void stopImageDisplay(){
        scheduler.shutdown();
        try{
            if (!scheduler.awaitTermination(1, TimeUnit.SECONDS)){
                scheduler.shutdownNow();
                customButton.setEnabled(true);
                isAnimationInProgress = false;
            }
        } catch (InterruptedException ex){
            scheduler.shutdownNow();
            Thread.currentThread().interrupt();
        }
    }

    private void displayImage(int x, int y){
        if (currentMap != null){
            if (currentFrame == pieces.length) {
                stopImageDisplay();
                currentFrame = 0;
                isImageVisible = true; // can only set this to true after last image is displayed, since when the button is pressed again, the behavior depends on that variable.
                customButton.setText("Hide Dice");
                return;
            }

            int xCoordinate = x;
            int yCoordinate = y;

            currentMap.placeAt(pieces[currentFrame], new Point(xCoordinate,yCoordinate));
            if (currentFrame > 1){
                currentMap.removePiece(pieces[currentFrame - 1]);
            }

            currentFrame = (currentFrame + 1);
        }
    }

    private void hideImage(BasicPiece piece) {
        if (currentMap != null) {
            currentMap.removePiece(piece);
            currentMap.repaint();
        }
    }

    private void createPieces(){
        int numberOfPieces = images.length;
        pieces = new BasicPiece[numberOfPieces];
        for (int i = 0; i < numberOfPieces; i++){
            final int index = i; // make index final, so it can be accessed from the inner class
            BasicPiece piece = new BasicPiece() {
                private final Image image = images[index];
                @Override
                public void draw(Graphics g, int x, int y, Component obs, double zoom) {
                    super.draw(g, x, y, obs, zoom);
                    // Draw the image at the specified (x, y) coordinates
                    g.drawImage(image, x, y, obs);
                }
            };
            pieces[i] = piece;
        }
    }

   /* // Retrieve dice images from the proper folder and places them into the images Array;
    public void getImages(){
        int filesInFolder = countFilesInFolder(System.getProperty("user.dir") + "/target/classes/" + IMAGES_FOLDER + "/DiceImages/RED DIE/R1_1/");
        images = new Image[filesInFolder];
        for (int i = 0; i < filesInFolder; i++) {
            try {
                URL imageURL = getClass().getResource("/" + IMAGES_FOLDER + "/DiceImages/RED DIE/R1_1/" + String.format("die%04d", i) + ".png");
                if (imageURL != null) {
                    images[i] = (ImageIO.read(imageURL));
                } else {
                    throw new IOException("Image file not found.");
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        feedingImages = false;
    }*/
    // Retrieve dice images from the proper folder and places them into the images Array;
    public void getImages(){
        String[] diceFolders = DrawDiceFolders(); // chooses the next animation folders to preload
        diceImages = new HashMap<>();
        int filesInFolder = countFilesInFolder( RED_DIE_FOLDER_PATH + "/R1_1/");
        images = new Image[filesInFolder];
        for (int i = 0; i < filesInFolder; i++) {
            try {
                URL imageURL = getClass().getResource(RED_DIE_FOLDER_PATH + "/R1_1/" + String.format("die%04d", i) + ".png");
                if (imageURL != null) {
                    images[i] = (ImageIO.read(imageURL));
                } else {
                    throw new IOException("Image file not found.");
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        feedingImages = false;
    }

    private String[] DrawDiceFolders(){
        return null;
    }

    private void RollDices (int numberOfDice, int numberOfSides){
        int[] unalteredRolls = DR(numberOfDice,numberOfSides);
        int[] alteredRolls = new int[unalteredRolls.length]; // Rolls after bias application

        for (int i = 0; i < unalteredRolls.length; i++){
            alteredRolls[i] = (unalteredRolls[i] + mouseBiasFactor[i] % 6);
            if (alteredRolls[i] > 6)
                alteredRolls[i] = alteredRolls[i] - 6;
        }

        StringBuilder report = new StringBuilder();
        report.append("Original Rolls: ");
        for (int j = 0; j < nDice; ++j){
            report.append(unalteredRolls[j]);
            if (j < nDice - 1)
                report.append(", ");
        }
        report.append("Altered Rolls: ");
        for (int k = 0; k < nDice; ++k){
            report.append(alteredRolls[k]);
            if (k < nDice - 1)
                report.append(", ");
        }

        Command c = report.length() == 0 ? new NullCommand() : new Chatter.DisplayText(GameModule.getGameModule().getChatter(), report.toString());
        ((Command)c).execute();
        GameModule.getGameModule().sendAndLog(c);
    }
    protected int[] DR(int nDice, int nSides) {
        int[] rawRolls = new int[nDice];

        for(int i = 0; i < nDice; ++i) {
            Random ran = new Random();
            int roll = ran.nextInt(nSides) + 1;
            rawRolls[i] = roll;
        }

        return rawRolls;
    }

    public void removeFrom(Buildable parent) {
        if (parent instanceof GameModule) {
            GameModule gameModule = (GameModule) parent;

            // Remove the button from the toolbar
            gameModule.getToolBar().remove(customButton);
        }
    }

    private static int countFilesInFolder(String folderPath) {
        File folder = new File(folderPath);
        if (!folder.exists() || !folder.isDirectory()) {
            java.lang.System.out.println(folderPath);
            java.lang.System.out.println("The specified folder does not exist or is not a directory.");
            return 0;
        }

        String[] files = folder.list();
        if (files == null) {
            java.lang.System.out.println("Error listing files in the folder.");
            return 0;
        }

        return files.length;
    }

    // Creates a mask that indicates what animation indexes (the number just to the right of the die color letter in the folder) bring hesitant die animations
    private void CreateHesitantDieFolderMask(){
        File directory = new File (RED_DIE_FOLDER_PATH);
        if (directory.exists() && directory.isDirectory()){
            File[] subdirectories = directory.listFiles(File::isDirectory);
            hesitantDiceFolderIndexes = new HashMap<>();
            int counter = 0;
            if (subdirectories != null){
                // Pattern for paired hesitant die animation: Ex.  "R2_6_3"
                Pattern hesitantDieFolderPattern = Pattern.compile("^[A-Za-z]\\d+_\\d_\\d$");
                for (File subdirectory : subdirectories){
                    String folderName = subdirectory.getName();

                    Matcher matcher1 = hesitantDieFolderPattern.matcher(folderName);
                    if (matcher1.matches()){
                        String[] parts = folderName.split("_");

                        int animationIndex = Integer.parseInt(parts[0].substring(1));
                        int result = Integer.parseInt(parts[2]);
                        if (!hesitantDiceFolderIndexes.containsKey(animationIndex)) {
                            hesitantDiceFolderIndexes.put(animationIndex, new HashMap<>()); // is a hesitant die animation
                            for (int i = 1; i <= 6; i++) {
                                hesitantDiceFolderIndexes.get(animationIndex).put(i, new ArrayList<String>());
                            }
                        }
                        hesitantDiceFolderIndexes.get(animationIndex).get(result).add("_" + parts[1]);

                        System.out.println("Number of animation: " + animationIndex + " brings the following patterns for result " + parts[2] + ": " + hesitantDiceFolderIndexes.get(animationIndex).get(result));
                    }
                }
            }
        }
    }

    @Override
    public Command decode(String s) {
        return null;
    }
    @Override
    public String encode(Command command){
        return null;
    }

    @Override
    public String[] getAttributeNames(){
        return new String[]{"AnimatedDice"};
    }
    @Override
    public void setAttribute(String attribute, Object object){
        attribute = "AnimatedDice";
        object = new AnimatedDice();
    }

    @Override
    public String getAttributeValueString(String value){
        return "AnimatedDice";
    }
    @Override
    public String[] getAttributeDescriptions() {
        return new String[]{"3D dices"};
    }

    @Override
    public Class<?>[] getAttributeTypes() {
        return new Class[]{AnimatedDice.class};
    }

    @Override
    public HelpFile getHelpFile(){
        HelpFile help = new HelpFile();
        return help;
    }
    @Override
    public Class<?>[] getAllowableConfigureComponents() {
        return new Class[]{AnimatedDice.class};
    }

    public class DelayedActionButton extends JButton {
        private boolean mouseButtonPressed = false;
        private ActionListener delayedActionListener;

        public DelayedActionButton(String buttonText, ActionListener delayedActionListener) {
            super();
            this.delayedActionListener = delayedActionListener;
            this.setText(buttonText);
            setupMouseListener();
        }

        private void setupMouseListener() {
            addMouseListener(new MouseAdapter() {
                java.util.Timer timer;
                @Override
                public void mousePressed(MouseEvent e) {
                    System.out.println("NEW ROLL");
                    super.mousePressed(e);
                    mouseButtonPressed = true;
                    if (!isImageVisible && !isAnimationInProgress) { // doesn't execute when pressed to hide the dices (dice images are still visible)
                        setCursor(dieCursor);
                        playSounds(shakingDiceAudioData);
                        mouseBiasFactor = new int[nDice]; // We'll the number of factors correspondent to the number of dice;
                        Arrays.fill(mouseBiasFactor, 1);
                        final int[] counter = new int[]{0}; // Use of single element array in order to be able to change it inside runnable.

                        timer = new java.util.Timer();
                        timer.schedule(new TimerTask() {
                            @Override
                            public void run() {
                                System.out.println("Mouse Position: " + MouseInfo.getPointerInfo().getLocation());

                                mouseBiasFactor[counter[0]] += MouseInfo.getPointerInfo().getLocation().x + MouseInfo.getPointerInfo().getLocation().y;
                                if (mouseBiasFactor[counter[0]] > 10000)
                                    mouseBiasFactor[counter[0]] = mouseBiasFactor[counter[0]] - 10000;

                                if (counter[0] == mouseBiasFactor.length - 1) {
                                    counter[0] = 0;
                                } else {
                                    counter[0] = counter[0] + 1;
                                }
                                System.out.println("Bias " + counter[0] + " = " + mouseBiasFactor[counter[0]]);
                            }
                        }, 0, 10);
                    }
                }

                @Override
                public void mouseReleased(MouseEvent e) {
                    super.mouseReleased(e);
                    setCursor(Cursor.getDefaultCursor());
                    if (mouseButtonPressed && isEnabled()) {
                        shouldStopFlag = true;
                        timer.cancel();
                        delayedActionListener.actionPerformed(new ActionEvent(this, ActionEvent.ACTION_PERFORMED, ""));
                    }
                    mouseButtonPressed = false;
                }
            });
        }
    }

}


Without the full error log it’s difficult to tell exactly where that error is coming from. The IconFactory is initialised when Vassal first starts up to find the sets of Icons stored in the Vengine,jar installed by Vassal. When you are running under a debugger, there is no Vengine.jar. the icons are sourced from the target folder.

In this case, if your run configuration is running in the custom component project, then the Vengine.jar should be provisioned from the Vassal maven dependency in your project.

One thing you should check is what version of Java do you have specified in your Run configuration? There seems to be some sort if incompatibility. I debug Vassal in Intellij all the time and don’t have this issue.

Some other things I noticed in the code.

getClass().getResource()

if for loading resources from the VASSAL Vengine.jar, which is not what I think you want, To load images from the module zip use

GameModule.getGameModule().getDataArchive().getURL(subPath);

Also,

    private static int countFilesInFolder(String folderPath) {

is not going to do anything useful from inside a module. It will work in Debug mode when everything is stored in filed in your target folder, but once zipped up in a module, this will be looking at the file system outside the module.

Cheers.

1 Like

Brent, I have reproduced the relevant part of the Vassal log. It is really short. Here is it as fully presented:

2023-09-12 19:23:07,986 [18376-main] INFO  VASSAL.launch.StartUp - Starting
2023-09-12 19:23:08,007 [18376-main] INFO  VASSAL.launch.StartUp - OS Windows 10 10.0 amd64
2023-09-12 19:23:08,007 [18376-main] INFO  VASSAL.launch.StartUp - Java version 17.0.2
2023-09-12 19:23:08,007 [18376-main] INFO  VASSAL.launch.StartUp - Java home C:\Program Files\Amazon Corretto\jdk17.0.2_8
2023-09-12 19:23:08,008 [18376-main] INFO  VASSAL.launch.StartUp - VASSAL version 3.6.17
2023-09-12 19:23:08,008 [18376-main] INFO  VASSAL.launch.Launcher - Player
2023-09-12 19:23:08,643 [18376-IconFactory-preload] ERROR VASSAL.tools.ErrorDialog - 
java.lang.ClassCastException: class sun.net.www.protocol.file.FileURLConnection cannot be cast to class java.net.JarURLConnection (sun.net.www.protocol.file.FileURLConnection and java.net.JarURLConnection are in module java.base of loader 'bootstrap')
    at VASSAL.tools.icon.IconFactory.findJarIcons(IconFactory.java:376)
    at VASSAL.tools.icon.IconFactory.initVassalIconFamilys(IconFactory.java:290)
    at VASSAL.tools.icon.IconFactory.lambda$new$0(IconFactory.java:97)
    at java.base/java.lang.Thread.run(Thread.java:833)

Here is the Intellij log for building (until 7:40:36) and running under the debugger the project:

2023-09-13 07:36:47,073 [ 614003]   INFO - #c.i.w.i.i.j.s.JpsGlobalModelSynchronizerImpl - Saving global entities to files
2023-09-13 07:36:52,283 [ 619213]   INFO - #c.i.w.i.i.j.s.JpsGlobalModelSynchronizerImpl - Saving global entities to files
2023-09-13 07:36:52,305 [ 619235]   INFO - #c.i.c.i.CompileDriver - COMPILATION STARTED (BUILD PROCESS)
2023-09-13 07:36:53,278 [ 620208]   INFO - #c.i.c.s.BuildManager - BUILDER_PROCESS [stderr]: Be careful, logger will be shut down earlier than application: Unable to make field private static java.util.IdentityHashMap java.lang.ApplicationShutdownHooks.hooks accessible: module java.base does not "opens java.lang" to unnamed module @1d81eb93
2023-09-13 07:36:53,303 [ 620233]   INFO - #c.i.c.s.BuildManager - BUILDER_PROCESS [stdout]: Build process started. Classpath: C:/Program Files/JetBrains/IntelliJ IDEA Community Edition 2023.1.3/plugins/java/lib/jps-launcher.jar
2023-09-13 07:36:54,155 [ 621085]   INFO - #o.j.k.i.s.r.KotlinCompilerReferenceIndexStorage - KCRI storage is closed (didn't exist)
2023-09-13 07:36:54,158 [ 621088]   INFO - #c.i.c.b.CompilerReferenceServiceBase - backward reference index reader is closed
2023-09-13 07:36:56,799 [ 623729]   INFO - #c.i.w.i.i.j.s.JpsGlobalModelSynchronizerImpl - Saving global entities to files
2023-09-13 07:39:33,158 [ 780088]   WARN - #o.j.k.i.s.r.KotlinCompilerReferenceIndexStorage - kotlin-data-container is not found
2023-09-13 07:39:33,178 [ 780108]   INFO - #c.i.c.b.CompilerReferenceServiceBase - backward reference index reader is opened
2023-09-13 07:39:33,228 [ 780158]   INFO - #c.i.c.i.CompilerUtil - 	COMPILATION FINISHED (BUILD PROCESS); Errors: 0; warnings: 0 took 161014 ms: 2 min 41sec
2023-09-13 07:39:34,381 [ 781311]   INFO - #c.i.c.s.BuildManager - BUILDER_PROCESS [stderr]: Be careful, logger will be shut down earlier than application: Unable to make field private static java.util.IdentityHashMap java.lang.ApplicationShutdownHooks.hooks accessible: module java.base does not "opens java.lang" to unnamed module @1d81eb93
2023-09-13 07:39:34,415 [ 781345]   INFO - #c.i.c.s.BuildManager - BUILDER_PROCESS [stdout]: Build process started. Classpath: C:/Program Files/JetBrains/IntelliJ IDEA Community Edition 2023.1.3/plugins/java/lib/jps-launcher.jar
2023-09-13 07:39:52,516 [ 799446]   INFO - #c.i.w.i.i.j.s.JpsGlobalModelSynchronizerImpl - Saving global entities to files
2023-09-13 07:39:53,612 [ 800542]   INFO - #c.i.c.ComponentStoreImpl - Saving Project(name=vassal-module-template, containerState=COMPONENT_CREATED, componentStore=C:\Users\netto\Desktop\VASSAL TUTORIAL\VASSAL DEVELOPMENT\MyCustomClasses\vassal-module-template)ChangeListManager took 332 ms
2023-09-13 07:39:53,612 [ 800542]   INFO - #c.i.o.c.i.s.StoreUtil - saveProjectsAndApp took 1096 ms
2023-09-13 07:40:36,984 [ 843914]   INFO - #c.i.w.i.i.j.s.JpsGlobalModelSynchronizerImpl - Saving global entities to files
2023-09-13 07:40:37,032 [ 843962]   INFO - #c.i.c.ComponentStoreImpl - Saving Project(name=vassal-module-template, containerState=COMPONENT_CREATED, componentStore=C:\Users\netto\Desktop\VASSAL TUTORIAL\VASSAL DEVELOPMENT\MyCustomClasses\vassal-module-template)RunManager took 30 ms
2023-09-13 07:41:02,820 [ 869750]   INFO - #c.i.u.WinFocusStealer - Foreground lock timeout set to 200000
2023-09-13 07:41:02,922 [ 869852]   INFO - #c.i.w.i.i.j.s.JpsGlobalModelSynchronizerImpl - Saving global entities to files
2023-09-13 07:41:02,930 [ 869860]   INFO - #c.i.c.i.CompileDriver - COMPILATION STARTED (BUILD PROCESS)
2023-09-13 07:41:02,932 [ 869862]   INFO - #c.i.c.s.BuildManager - Using preloaded build process to compile C:/Users/netto/Desktop/VASSAL TUTORIAL/VASSAL DEVELOPMENT/MyCustomClasses/vassal-module-template
2023-09-13 07:41:02,933 [ 869863]   INFO - #o.j.k.i.s.r.KotlinCompilerReferenceIndexStorage - KCRI storage is closed (didn't exist)
2023-09-13 07:41:02,935 [ 869865]   INFO - #c.i.c.b.CompilerReferenceServiceBase - backward reference index reader is closed
2023-09-13 07:41:03,661 [ 870591]   WARN - #o.j.k.i.s.r.KotlinCompilerReferenceIndexStorage - kotlin-data-container is not found
2023-09-13 07:41:03,675 [ 870605]   INFO - #c.i.c.b.CompilerReferenceServiceBase - backward reference index reader is opened
2023-09-13 07:41:03,699 [ 870629]   INFO - #c.i.c.i.CompilerUtil - 	COMPILATION FINISHED (BUILD PROCESS); Errors: 0; warnings: 0 took 844 ms: 0 min 0sec
2023-09-13 07:41:03,721 [ 870651]   INFO - #c.i.e.JavaExecutionUtil - Agent jars were copied to C:\Users\netto\AppData\Local\JetBrains\IdeaIC2023.1\captureAgent
2023-09-13 07:41:04,414 [ 871344]   INFO - #c.i.c.s.BuildManager - BUILDER_PROCESS [stderr]: Be careful, logger will be shut down earlier than application: Unable to make field private static java.util.IdentityHashMap java.lang.ApplicationShutdownHooks.hooks accessible: module java.base does not "opens java.lang" to unnamed module @1d81eb93
2023-09-13 07:41:04,454 [ 871384]   INFO - #c.i.u.WinFocusStealer - Foreground lock timeout set to 0
2023-09-13 07:41:04,702 [ 871632]   INFO - #c.i.c.s.BuildManager - BUILDER_PROCESS [stdout]: Build process started. Classpath: C:/Program Files/JetBrains/IntelliJ IDEA Community Edition 2023.1.3/plugins/java/lib/jps-launcher.jar
2023-09-13 07:41:12,165 [ 879095]   INFO - #c.i.w.i.i.j.s.JpsGlobalModelSynchronizerImpl - Saving global entities to files
2023-09-13 07:41:25,255 [ 892185]   INFO - #c.i.w.i.i.j.s.JpsGlobalModelSynchronizerImpl - Saving global entities to files

I have tried to remove all reading/writing methods from my class, just in case, but the error persists. It must have something to do with the resources folder and how Intellij is treating it. I was using Corretto-17.0.8.1. Was using 17.0.2 and upgraded, but without effect.

My resources folder is structured like this:

image

I don’t know if the way it is structured can cause any harm.

The fact is that I’m stuck.

How exactly are you launching the Player?

As oriented by Brent in another thread. Has been working until I created the resources folder inside src>main.
image

You probably shouldn’t include resources in your module that way.

Take a look at the pom.xml in the vassal-module-template:

The <resources> section lets you specify which files are packed into the module when mvnw package is run, and those files may be stored outside of the src tree. (The example puts them in dist.)

A couple of things to try

First, get rid of the getClass().getResource() code, You need to access all the in-module contents through the DataArchive (GameModule.getGameModule.getDataArchive(). I suspect the resources folder is confusing the resource loader.

Ultimately, you just want the images for your project to reside in the jar in an images folder.

You could also try a different JDK, that error is really weird.

Thanks for the tip uckelman. I’ll try that approach and see if it works.

Brent, I have basically removed all my class methods, leaving just an empty class and the problem persisted, so that a folder set to “Resource Root” or placed inside the src>main folder is definitely causing the problem. Will I be able to use getDataArchive() with an external folder not set as a resource folder? I’ll look at it tomorrow.

I have tried other JDK without success.

I’ll also try uckelman’s approach and see if it works.

Ok, sounds like that is the problem, the class/resource loader is getting confused by the extra resources folder.

getDataArchive() will go to the Jar archive you specify in your run config, never to external files, Your images should be loaded from the images file there, even when running under the debugger and your resources (classes) are being loaded from the target directory.

This is somewhat confused because you are building an extension, not custom code that will be loaded directly into the module.

So, create and install an extension jar file and manually add your images and the current version of your custom code. When you run the Player over the module that has the extension loaded, it should pull the images from that jar, but use the classes in the target folder.

I wouldn’t worry too much about building the entire Jar via Maven, but if you can succeed, it will be interesting to know how.

EDIT: Never mind. After removing all error causes, the addTo is running and the button displaying properly.

This seems to be the simpler way in fact. Since I had not yet structured my project as an extension, I have done it today. Have successfully attached it to my test module. Before dealing with the resources, I’ll have to change some functionalities though, since things like my Roll Die button isn’t displaying. What is the rule followed by Vassal when a button is added. Does it add it to the main toolbar in the order extensions are loaded?

Brent, I was able to do it how you recommended, by creating the extension and using getDataArchive(), so that now it is reading images and sounds from my extension zip. Yet, I have gone through the documentation trying to find some class that will allow me to read the name of folders and give me the count of files. I had structure the code so that my class would classify each folder according to its name and put it in a hashmap, since I’ll be randomizing which folder will be used each time a die is rolled and some folders should have a lesser probability of being choosed.

Is is possible to iterate through folder and read their name using DataArchive? If not, I’ll have to find another solution and rename all my image files, which will be painful, since doing the graphical part and organizing it was the most time consuming activity until now.

Have a look at IconFactory.findJarIcons(). This is code I built many years ago to find the files in a folder in the Vassal supplied Vengine.jar.

Something like this should really have been added to DataArchive, I’m not sure how easy that would be.

1 Like

I have tried to use the JarArchive as you used in the IconFactory.findJarIcons() method, but when using:

JarURLConnection j = (JarURLConnection)jar.getURL("images/").openConnection();
            JarFile file = j.getJarFile();
            Enumeration<JarEntry> e = file.entries();
            while(e.hasMoreElements()){
                    System.out.println(e.nextElement().getName());
            }

It is reading from the VASSAL jar file:
image

When using it with DataArchive:

            JarURLConnection j = (JarURLConnection)dataArchive.getURL("images/").openConnection();
            JarFile file = j.getJarFile();
            Enumeration<JarEntry> e = file.entries();

It is reading from my mod jar file, without reaching the extension added to it.

As far as I understood, JarArchive should read the jar in which the class that called it is, but since IntelliJ is running Vassal in the debug, I think it this may be causing it to read the VASSAL jar file (just speculation).

EDIT: I have been searching and I can’t seem to find a single extension with resources and code that reference them to see how it can be solved. Usually, we have either java classes or just images and sounds.

EDIT 2: Ok, now if I manually include the extension in the classPath on the debugger, I can access my files using getResource. The problem is that this is an absolute path and it won’t work when running the final build. I’m really having problem on having access to the extension jar to read resources from it. getDataArchive, as said, will reference the module jar; using JarArchive I get data from the Vassal jar. Any idea on how to get access to resources on the extension jar?

This problem got me returning to the error that originated this thread:

java.lang.ClassCastException: class sun.net.www.protocol.file.FileURLConnection cannot be cast to class java.net.JarURLConnection (sun.net.www.protocol.file.FileURLConnection and java.net.JarURLConnection are in module java.base of loader 'bootstrap')
    at VASSAL.tools.icon.IconFactory.findJarIcons(IconFactory.java:376)

Since it seemed to be originated on the folder structure, I tried different combinations and succeeded in eliminating the error. It seems that if we have a top folder with sounds and images in folders below and it is made a “Resource Root” folder, we get the error above. If I create separate folders for sounds and images, no matter how many other folders inside and make those, individually, “Resource Root” folders, I don’t get the error.
So, if I have:

|-resources          
            |-images     
                 |-folder1
                 |-folder 2
            |-sounds

And make the resources folder a “Resource Root” folder, I get the error.
If I make the images folder and the sounds folder “Resource Root” folders, the error is gone. I don’t know why exactly, but it seems to work.

JarURLConnection will never find files in extensions. You must read those via DataArchive.

If you want a list of all the files, use DataArchive.getArchive() to get the FileArchive and then call getFiles() on it.

Try

OpIcon icon = new OpIcon(Op.Load(imageName))

That should handle it for you if the image is in the images folder in the extension. OpIcon is a subclass of ImageIcon,

To get an Image

SourceOp imageOp = Op.load(imageName); BufferedImage image = imageOp.getImage();

I have succeeded in gaining access to the extension folders with DataArchive in the following way:

URL imagesFolder = dataArchive.getURL(HESITANT_RED_DIE_FOLDER_PATH);
            j = (JarURLConnection)imagesFolder.openConnection();
            Enumeration<JarEntry> e = j.getJarFile().entries();

If we point to the folder itself, it can find it inside the extension JAR. When using DataArchive.getArchive.getFiles() it just returns the files in the module JAR.

The problem below is solved
Well, anyway, I got the access I wished. But then I got a further problem which is kind of bizarre. I have the following code to select specific folders and put part of their name on a HashMap<Integer, HashMap<Integer, ArrayList>>:

Pattern hesitantDieFolderPattern = Pattern.compile("^[A-Za-z]\\d+_\\d_\\d$");
            hesitantDiceFolderIndexes = new HashMap<>();

            while (e.hasMoreElements()){
                JarEntry entry = e.nextElement();

                if (entry.getName().startsWith(HESITANT_RED_DIE_FOLDER_PATH) && !entry.getName().endsWith(HESITANT_RED_DIE_FOLDER_PATH) && entry.isDirectory()){
                    String folderName = entry.getName();
                    folderName = folderName.substring(0, folderName.length() - 1);
                    folderName = folderName.substring(HESITANT_RED_DIE_FOLDER_PATH.length()); // concatenation is causing memory problems
                    Matcher matcher1 = hesitantDieFolderPattern.matcher(folderName);
                    if (matcher1.matches()){
                        String[]parts = folderName.split("_");
                        int animationIndex = Integer.parseInt(parts[0].substring(1));
                        int result = Integer.parseInt(parts[2]);
                        hesitantDiceFolderIndexes
                                .computeIfAbsent(animationIndex, k -> new HashMap<>())
                                .computeIfAbsent(result, k -> new ArrayList<>())
                                .add("_" + parts[1]);
                    }
                }
            }
        }catch (IOException e){
            e.printStackTrace();
        }finally{
            if (j != null){
                try{
                    j.getJarFile().close();
                } catch (IOException e){
                    e.printStackTrace();
                }
            }
        }

        for (int i = 0; i < hesitantDiceFolderIndexes.size(); i++) {
            for (int k = 1; k <= 6; k++) {
                HashMap<Integer, ArrayList<String>> patterns = hesitantDiceFolderIndexes.get(i);
                List<String> p = patterns.get(k);
                String s = p.get(0);
                System.out.println("Number of animation: " + i +
                        " brings the following patterns for /'/" + s +
                        "/'/ result : " + k);
            }
        }

The code is working as pretended, but I get an Out Of Memory message from Vassal. Here, it is being caused by the loop for printing the hashmap (if I remove it, there is no message), but the simple concatenation of the following code into

folderName = entry.getName().substring(HESITANT_RED_DIE_FOLDER_PATH.length(), entry.getName().length() - 1)

will get me the message:

String folderName = entry.getName();
                    folderName = folderName.substring(0, folderName.length() - 1);
                    folderName = folderName.substring(HESITANT_RED_DIE_FOLDER_PATH.length());

Notice that there I’m treating a block of 50 folders which will be divided into a hashmap with 5 integer keys and under each we have a hashmap with 6 integer keys and, at most 3 strings as values. That isn’t a lot and doing that shouldn’t cause memory problems. Notice that this happens even if I place it on the top of the constructor, as the first thing to be done, before loading anything else.

May be it is the open connection to the JAR file that is causing it. I don’t know. I don’t even know if I closed it on a proper way, using getJarFile.close().

Maybe it is the sheer quantity of entries, since no matter if I use a specific folder path like:

URL imagesFolder = dataArchive.getURL(HESITANT_RED_DIE_FOLDER_PATH);
 j = (JarURLConnection)imagesFolder.openConnection();

the connection seems to bring all entries in the JAR file and there are plenty of them. HESITANT_RED_DIE_FOLDER_PATH corresponds to “DiceImages/HRED DIE/”, but the list of entries bring everything in the jar file, things like “DiceSounds/name.wav” for instance. I couldn’t find a way to prevent that. So my while loop goes through all entries filtering them in the if statement. Maybe the print statement is just the last drop.

Anyway, I’ll simplify it and encode all needed information into the folder structure. But it would really be nice to know what is going on here.

As a note, I found out what (not why) was causing the original problem of this thread. When I put an images and a sound folders inside a resource folder, the compiler copies the images folder into the root of the JAR file and the java.lang.ClassCastException error shows up. I then took those two folders out of the resource folder and the compiler copied my DiceImages and DiceSounds folder into the root of the JAR. It worked. Then I tried to place another images and sounds folder inside the original ones (images>images>DiceImages), so that the compiler would copy and images and a sounds folder into the root. It did and the error showed up again. So, it seems that naming those folders “images” and “sounds” may be conflicting somehow with Vassal code calling those (just a guess).

EDIT: Ok, I feel stupid now. For the printing part, it was an index error, since we don’t have a 0 index. Still don’t get why the substring concatenation would cause it. Also, why it shows a memory error and not an IndexOutOfBoundsException, but the error was mine.