diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 085dc3f13..5f6106d50 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -15,16 +15,18 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Set up JDK - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: - java-version: '21' + java-version: '25' distribution: 'temurin' - - uses: Trass3r/setup-cpp@master + - uses: gradle/actions/setup-gradle@v6 + with: + add-job-summary: 'on-failure' - name: Build with Gradle - run: ./gradlew build - - uses: actions/upload-artifact@v4 + run: ./gradlew build --no-daemon + - uses: actions/upload-artifact@v7 with: name: distribution path: build/distributions/OpenKeeper.zip diff --git a/src/main/java/toniarts/openkeeper/Main.java b/src/main/java/toniarts/openkeeper/Main.java index 11c7f4ca1..cddffa1ef 100644 --- a/src/main/java/toniarts/openkeeper/Main.java +++ b/src/main/java/toniarts/openkeeper/Main.java @@ -96,8 +96,8 @@ public final class Main extends SimpleApplication { private static boolean conversionOk = false; public static final String VERSION = "*ALPHA*"; public static final String TITLE = "OpenKeeper"; - private static final String USER_HOME_FOLDER = System.getProperty("user.home").concat(File.separator).concat(".").concat(TITLE).concat(File.separator); - private static final String SCREENSHOTS_FOLDER = USER_HOME_FOLDER.concat("SCRSHOTS").concat(File.separator); + private static final String USER_HOME_FOLDER = System.getProperty("user.home") + "/." + TITLE + '/'; + private static final String SCREENSHOTS_FOLDER = USER_HOME_FOLDER + "SCRSHOTS/"; private static final Object LOCK = new Object(); private static Map params; private static boolean debug; @@ -242,6 +242,16 @@ private static void initSettings(Main app) { // Init the user settings (which in JME are app settings) app.settings = Settings.getInstance().getAppSettings(); + if (isAudioDisabled()) + app.settings.setAudioRenderer(null); + } + + // returns true when all three audio categories (Music, Voice, SFX) are disabled + public static boolean isAudioDisabled() { + var s = getUserSettings(); + return !s.getBoolean(Settings.Setting.MUSIC_ENABLED) + && !s.getBoolean(Settings.Setting.VOICE_ENABLED) + && !s.getBoolean(Settings.Setting.SFX_ENABLED); } /** @@ -342,7 +352,12 @@ public void simpleInitApp() { ((GLRenderer)renderer).setDebugEnabled(true); // get debug names for GL objects if (GL.getCapabilities().OpenGL43) { GLUtil.setupDebugMessageCallback(); - GL43C.glDebugMessageControl(GL43.GL_DEBUG_SOURCE_APPLICATION, GL43.GL_DONT_CARE, GL43.GL_DONT_CARE, (int[]) null, false); + GL43C.glDebugMessageControl(GL43C.GL_DONT_CARE, GL43C.GL_DEBUG_TYPE_PUSH_GROUP, GL43C.GL_DONT_CARE, (int[]) null, false); + GL43C.glDebugMessageControl(GL43C.GL_DONT_CARE, GL43C.GL_DEBUG_TYPE_POP_GROUP, GL43C.GL_DONT_CARE, (int[]) null, false); + final int[] noisyIds = { + 0x20071, // Nvidia: BO resides in VIDEO memory + }; + GL43C.glDebugMessageControl(GL43C.GL_DEBUG_SOURCE_API, GL43C.GL_DEBUG_TYPE_OTHER, GL43C.GL_DONT_CARE, noisyIds, false); } } diff --git a/src/main/java/toniarts/openkeeper/game/sound/SoundCategory.java b/src/main/java/toniarts/openkeeper/game/sound/SoundCategory.java index ee5118a18..316fb1f8f 100644 --- a/src/main/java/toniarts/openkeeper/game/sound/SoundCategory.java +++ b/src/main/java/toniarts/openkeeper/game/sound/SoundCategory.java @@ -56,11 +56,10 @@ public SoundCategory(String name, boolean useGlobal) { throw new RuntimeException("Sound category is empty"); } - if (useGlobal) { + if (useGlobal) this.folder = PathUtils.DKII_SFX_GLOBAL_FOLDER; - } else { - this.folder = PathUtils.DKII_SFX_FOLDER + name.toLowerCase() + File.separator; - } + else + this.folder = PathUtils.DKII_SFX_FOLDER + name.toLowerCase() + '/'; this.name = name; parseGroups(); diff --git a/src/main/java/toniarts/openkeeper/game/sound/SoundGroup.java b/src/main/java/toniarts/openkeeper/game/sound/SoundGroup.java index 9f1b2aa22..b159cce9d 100644 --- a/src/main/java/toniarts/openkeeper/game/sound/SoundGroup.java +++ b/src/main/java/toniarts/openkeeper/game/sound/SoundGroup.java @@ -16,7 +16,6 @@ */ package toniarts.openkeeper.game.sound; -import java.io.File; import java.lang.System.Logger; import java.lang.System.Logger.Level; import java.nio.file.Paths; @@ -24,9 +23,7 @@ import java.util.List; import toniarts.openkeeper.tools.convert.sound.BankMapFile; import toniarts.openkeeper.tools.convert.sound.SdtFile; -import toniarts.openkeeper.tools.convert.sound.sfx.SfxEEEntry; -import toniarts.openkeeper.tools.convert.sound.sfx.SfxGroupEntry; -import toniarts.openkeeper.tools.convert.sound.sfx.SfxSoundEntry; +import toniarts.openkeeper.tools.convert.sound.sfx.*; import toniarts.openkeeper.utils.PathUtils; /** @@ -82,11 +79,12 @@ private void parseFiles() { .relativize(sdt.getFile()).toString(); try { - String soundFilename = relative.substring(0, relative.length() - 4) + File.separator - + SdtFile.fixFileExtension(sdt.getEntries()[soundId]); + var sdtFileEntry = sdt.getEntries()[soundId]; + if (sdtFileEntry == null) + continue; // some entries are just "Blank" - SoundFile sf = new SoundFile(this, soundId, soundFilename); - files.add(sf); + String soundFilename = relative.substring(0, relative.length() - 4) + '/' + SdtFile.fixFileExtension(sdtFileEntry); + files.add(new SoundFile(this, soundId, soundFilename)); } catch (Exception ex) { logger.log(Level.ERROR, () -> { return "Error in file " + sdt.getFile().toString() + " with id " + soundId; diff --git a/src/main/java/toniarts/openkeeper/game/state/MainMenuScreenController.java b/src/main/java/toniarts/openkeeper/game/state/MainMenuScreenController.java index b04ec5b1a..f59dd83df 100644 --- a/src/main/java/toniarts/openkeeper/game/state/MainMenuScreenController.java +++ b/src/main/java/toniarts/openkeeper/game/state/MainMenuScreenController.java @@ -135,9 +135,11 @@ public MainMenuScreenController(MainMenuState state, Nifty nifty) { this.state = state; this.nifty = nifty; + if (Main.isAudioDisabled()) + return; SoundCategory sc = SoundsLoader.load(GlobalCategory.FRONT_END); String filename = sc.getGroup(GlobalType.FRONT_END_CKICK).getFiles().get(0).getFilename(); - this.nifty.registerSound(SOUND_MENU_ID, AssetsConverter.SOUNDS_FOLDER + File.separator + filename); + this.nifty.registerSound(SOUND_MENU_ID, AssetsConverter.SOUNDS_FOLDER + filename); this.nifty.registerSound(SOUND_BUTTON_ID, "Sounds/Global/FrontEndHD/FE BUTTON BIG 5.mp2"); } diff --git a/src/main/java/toniarts/openkeeper/game/state/MainMenuState.java b/src/main/java/toniarts/openkeeper/game/state/MainMenuState.java index f15716ae3..2c4e57129 100644 --- a/src/main/java/toniarts/openkeeper/game/state/MainMenuState.java +++ b/src/main/java/toniarts/openkeeper/game/state/MainMenuState.java @@ -546,12 +546,12 @@ public void doDebriefing(GameResult result) { protected String getMapThumbnail(KwdFile map) { // See if the map thumbnail exist, otherwise create one - String asset = AssetsConverter.MAP_THUMBNAILS_FOLDER + File.separator + PathUtils.stripFileName(map.getGameLevel().getName()) + ".png"; + String asset = AssetsConverter.THUMBNAILS_FOLDER + PathUtils.stripFileName(map.getGameLevel().getName()) + ".png"; if (assetManager.locateAsset(new TextureKey(asset)) == null) { // Generate try { - AssetsConverter.genererateMapThumbnail(map, AssetsConverter.getAssetsFolder() + AssetsConverter.MAP_THUMBNAILS_FOLDER + File.separator); + AssetsConverter.genererateMapThumbnail(map, AssetsConverter.getAssetsFolder() + AssetsConverter.THUMBNAILS_FOLDER); } catch (Exception e) { logger.log(Level.WARNING, "Failed to generate map file out of {0}!", map); asset = "Textures/Unique_NoTextureName.png"; diff --git a/src/main/java/toniarts/openkeeper/game/state/PlayerScreenController.java b/src/main/java/toniarts/openkeeper/game/state/PlayerScreenController.java index 7f0ef3580..8f98fbcf5 100644 --- a/src/main/java/toniarts/openkeeper/game/state/PlayerScreenController.java +++ b/src/main/java/toniarts/openkeeper/game/state/PlayerScreenController.java @@ -805,7 +805,7 @@ public void populateSpellTab() { ResearchEffectControl researchControl = new ControlBuilder(ResearchEffectControl.CONTROL_NAME) { { parameter("color", spell.isDiscovered() ? "" : Integer.toString(RESEARCH_COLOR.getRGB())); - parameter("image", spell.isDiscovered() ? AssetUtils.getCanonicalAssetKey(AssetsConverter.TEXTURES_FOLDER + File.separator + "GUI/Icons/Gold_Frame.png") : ""); + parameter("image", spell.isDiscovered() ? AssetUtils.getCanonicalAssetKey(AssetsConverter.TEXTURES_FOLDER + "GUI/Icons/Gold_Frame.png") : ""); } }.build(element).getControl(ResearchEffectControl.class); researchControl.initJme(state.app); @@ -999,13 +999,11 @@ private ImageBuilder createCreatureAbilityIcon(final String name, final int inde marginRight("6px"); focusable(true); id("creature-ability_" + index); - filename(AssetUtils.getCanonicalAssetKey(AssetsConverter.TEXTURES_FOLDER - + File.separator + name + ".png")); + filename(AssetUtils.getCanonicalAssetKey(AssetsConverter.TEXTURES_FOLDER + name + ".png")); valignCenter(); onFocusEffect(new EffectBuilder("imageOverlay") { { - effectParameter("filename", AssetUtils.getCanonicalAssetKey(AssetsConverter.TEXTURES_FOLDER - + File.separator + "GUI/Icons/selected-creature.png")); + effectParameter("filename", AssetUtils.getCanonicalAssetKey(AssetsConverter.TEXTURES_FOLDER + "GUI/Icons/selected-creature.png")); post(true); } }); @@ -1016,8 +1014,7 @@ private ImageBuilder createCreatureAbilityIcon(final String name, final int inde private ImageBuilder createCreatureIcon(final String name) { return new ImageBuilder() { { - filename(AssetUtils.getCanonicalAssetKey(AssetsConverter.TEXTURES_FOLDER - + File.separator + name + ".png")); + filename(AssetUtils.getCanonicalAssetKey(AssetsConverter.TEXTURES_FOLDER + name + ".png")); valignCenter(); } }; @@ -1026,16 +1023,14 @@ private ImageBuilder createCreatureIcon(final String name) { private ImageBuilder createCreatureMeleeIcon(final String name) { return new ImageBuilder() { { - filename(AssetUtils.getCanonicalAssetKey(AssetsConverter.TEXTURES_FOLDER - + File.separator + name + ".png")); + filename(AssetUtils.getCanonicalAssetKey(AssetsConverter.TEXTURES_FOLDER + name + ".png")); valignCenter(); marginLeft("6px"); focusable(true); id("creature-melee"); onFocusEffect(new EffectBuilder("imageOverlay") { { - effectParameter("filename", AssetUtils.getCanonicalAssetKey(AssetsConverter.TEXTURES_FOLDER - + File.separator + "GUI/Icons/selected-creature.png")); + effectParameter("filename", AssetUtils.getCanonicalAssetKey(AssetsConverter.TEXTURES_FOLDER + "GUI/Icons/selected-creature.png")); post(true); } }); @@ -1046,16 +1041,14 @@ private ImageBuilder createCreatureMeleeIcon(final String name) { private ImageBuilder createCreatureSpellIcon(final CreatureSpell cs, final int index) { return new ImageBuilder() { { - filename(AssetUtils.getCanonicalAssetKey(AssetsConverter.TEXTURES_FOLDER - + File.separator + cs.getGuiIcon().getName() + ".png")); + filename(AssetUtils.getCanonicalAssetKey(AssetsConverter.TEXTURES_FOLDER + cs.getGuiIcon().getName() + ".png")); valignCenter(); marginLeft("6px"); focusable(true); id("creature-spell_" + index); onFocusEffect(new EffectBuilder("imageOverlay") { { - effectParameter("filename", AssetUtils.getCanonicalAssetKey(AssetsConverter.TEXTURES_FOLDER - + File.separator + "GUI/Icons/selected-spell.png")); + effectParameter("filename", AssetUtils.getCanonicalAssetKey(AssetsConverter.TEXTURES_FOLDER + "GUI/Icons/selected-spell.png")); post(true); } }); @@ -1074,7 +1067,7 @@ private ControlBuilder createRoomIcon(final ResearchableEntity roomInfo) { } return createIcon(room.getId(), - "room", "gui\\rooms\\tba", null, null, false, false); + "room", "GUI/Rooms/tba", null, null, false, false); } private ControlBuilder createSpellIcon(final ResearchableEntity spell) { @@ -1086,7 +1079,7 @@ private ControlBuilder createSpellIcon(final ResearchableEntity spell) { return createIcon(keeperSpell.getKeeperSpellId(), "spell", spell.isUpgraded() ? keeperSpell.getGuiIcon().getName() + "-2" : keeperSpell.getGuiIcon().getName(), tip, hint, true, spell.isUpgraded()); } return createIcon(keeperSpell.getKeeperSpellId(), - "spell", "gui\\spells\\s-tba", null, null, false, false); + "spell", "GUI/Spells/s-tba", null, null, false, false); } private ControlBuilder createDoorIcon(final ResearchableEntity doorInfo) { @@ -1100,7 +1093,7 @@ private ControlBuilder createDoorIcon(final ResearchableEntity doorInfo) { } return createIcon(door.getId(), - "door", "gui\\traps\\w-tba", null, null, false, false); + "door", "GUI/Traps/w-tba", null, null, false, false); } private ControlBuilder createTrapIcon(final ResearchableEntity trapInfo) { @@ -1113,7 +1106,7 @@ private ControlBuilder createTrapIcon(final ResearchableEntity trapInfo) { } return createIcon(trap.getId(), - "trap", "gui\\traps\\w-tba", null, null, false, false); + "trap", "GUI/Traps/w-tba", null, null, false, false); } public ControlBuilder createIcon(final int id, final String type, final ArtResource guiIcon, final String tooltip, final String hint, final boolean allowSelect, final boolean hilightGold) { @@ -1122,9 +1115,9 @@ public ControlBuilder createIcon(final int id, final String type, final ArtResou public ControlBuilder createIcon(final int id, final String type, final String guiIcon, final String tooltip, final String hint, final boolean allowSelect, final boolean hilightGold) { ControlBuilder cb = new GuiIconBuilder(type + "_" + id, - AssetUtils.getCanonicalAssetKey(AssetsConverter.TEXTURES_FOLDER + File.separator + guiIcon + ".png"), - AssetUtils.getCanonicalAssetKey(AssetsConverter.TEXTURES_FOLDER + File.separator + (hilightGold ? "GUI/Icons/Hilight-2.png" : "GUI/Icons/hilight.png")), - AssetUtils.getCanonicalAssetKey(AssetsConverter.TEXTURES_FOLDER + File.separator + "GUI/Icons/selected-" + type + ".png"), + AssetUtils.getCanonicalAssetKey(AssetsConverter.TEXTURES_FOLDER + guiIcon + ".png"), + AssetUtils.getCanonicalAssetKey(AssetsConverter.TEXTURES_FOLDER + (hilightGold ? "GUI/Icons/Hilight-2.png" : "GUI/Icons/hilight.png")), + AssetUtils.getCanonicalAssetKey(AssetsConverter.TEXTURES_FOLDER + "GUI/Icons/selected-" + type + ".png"), hint != null ? hint : "", tooltip != null ? tooltip : "", "select(" + type + ", " + id + ")"); @@ -1136,6 +1129,8 @@ public ControlBuilder createIcon(final int id, final String type, final String g @Override public void playSound(String category, String id) { + if (Main.isAudioDisabled()) + return; SoundHandle soundHandler = NiftyUtils.getSoundHandler(nifty, category, Integer.parseInt(id)); if (soundHandler != null) { soundHandler.play(); @@ -1331,8 +1326,7 @@ private boolean isValidEntity(Entity entity) { private CreatureCardControl createPlayerCreatureIcon(Creature creature, Screen hud, Element parent) { ControlBuilder cb = new ControlBuilder("creature") { { - filename(AssetUtils.getCanonicalAssetKey(AssetsConverter.TEXTURES_FOLDER - + File.separator + creature.getPortraitResource().getName() + ".png")); + filename(AssetUtils.getCanonicalAssetKey(AssetsConverter.TEXTURES_FOLDER + creature.getPortraitResource().getName() + ".png")); parameter("creatureId", Integer.toString(creature.getCreatureId())); id("creature_" + creature.getCreatureId()); } diff --git a/src/main/java/toniarts/openkeeper/game/state/SoundState.java b/src/main/java/toniarts/openkeeper/game/state/SoundState.java index b7b05960c..c9cfce746 100644 --- a/src/main/java/toniarts/openkeeper/game/state/SoundState.java +++ b/src/main/java/toniarts/openkeeper/game/state/SoundState.java @@ -163,8 +163,7 @@ private void attachSpeech(String soundCategory, int speechId, ISpeechListener li throw new RuntimeException("Sound category " + soundCategory + " not found"); } - String file = AssetsConverter.SOUNDS_FOLDER + File.separator - + sc.getGroup(speechId).getFiles().get(0).getFilename(); + String file = AssetsConverter.SOUNDS_FOLDER + sc.getGroup(speechId).getFiles().get(0).getFilename(); speechQueue.add(new Speech(speechId, file, listener)); } catch (RuntimeException e) { logger.log(Level.WARNING, "Failed to attach speech from category " + soundCategory + " with id " + speechId, e); @@ -203,7 +202,10 @@ private void playBackground() { return; } - String file = AssetsConverter.SOUNDS_FOLDER + File.separator + backgroundState.getNext(); + String file = backgroundState.getNext(); + if (file == null) + return; + file = AssetsConverter.SOUNDS_FOLDER + file; backgroundNode = new AudioNode(app.getAssetManager(), file, DataType.Buffer); if (backgroundNode == null) { @@ -273,25 +275,21 @@ public final synchronized void setCategory(String category) { } public synchronized String getNext() { - if (itGroup == null) { + if (itGroup == null) itGroup = sc.getGroups().values().iterator(); - } - if (itFile == null) { + while (itFile == null || !itFile.hasNext()) { if (itGroup.hasNext()) { itFile = itGroup.next().getFiles().iterator(); } else { - itGroup = null; - return this.getNext(); + // Exhausted all groups, start over if there are files + itGroup = sc.getGroups().values().iterator(); + if (!itGroup.hasNext()) + return null; } } - if (itFile.hasNext()) { - return itFile.next().getFilename(); - } else { - itFile = null; - return this.getNext(); - } + return itFile.next().getFilename(); } } diff --git a/src/main/java/toniarts/openkeeper/gui/Cursor.java b/src/main/java/toniarts/openkeeper/gui/Cursor.java index f1b266367..6ff5d85a1 100644 --- a/src/main/java/toniarts/openkeeper/gui/Cursor.java +++ b/src/main/java/toniarts/openkeeper/gui/Cursor.java @@ -58,7 +58,7 @@ public Cursor(AssetManager assetManager, String Filename, int hotspotx, int hots throw new IllegalArgumentException("The cursor needs at least a framecount of 1."); } - Texture tex = assetManager.loadTexture(PathUtils.convertFileSeparators(AssetsConverter.MOUSE_CURSORS_FOLDER.concat(File.separator).concat(Filename))); + Texture tex = assetManager.loadTexture(PathUtils.convertFileSeparators(AssetsConverter.MOUSE_CURSORS_FOLDER + Filename)); Image img = tex.getImage(); // width must be a multiple of 16, otherwise the cursor gets distorted int width = img.getWidth() % 16 == 0 ? img.getWidth() : (img.getWidth() - img.getWidth() % 16) + 16; diff --git a/src/main/java/toniarts/openkeeper/gui/nifty/NiftyUtils.java b/src/main/java/toniarts/openkeeper/gui/nifty/NiftyUtils.java index 4c00ea6ec..d1f629d0f 100644 --- a/src/main/java/toniarts/openkeeper/gui/nifty/NiftyUtils.java +++ b/src/main/java/toniarts/openkeeper/gui/nifty/NiftyUtils.java @@ -58,7 +58,7 @@ public static SoundHandle getSoundHandler(Nifty nifty, String category, int id) SoundHandle soundHandler = nifty.getSoundSystem().getSound(file.toString()); if (soundHandler == null) { - String filename = AssetsConverter.SOUNDS_FOLDER + File.separator + file.getFilename(); + String filename = AssetsConverter.SOUNDS_FOLDER + file.getFilename(); if (nifty.getSoundSystem().addSound(file.toString(), filename)) { soundHandler = nifty.getSoundSystem().getSound(file.toString()); } diff --git a/src/main/java/toniarts/openkeeper/tools/convert/AssetsConverter.java b/src/main/java/toniarts/openkeeper/tools/convert/AssetsConverter.java index bbe50349d..203da3a70 100644 --- a/src/main/java/toniarts/openkeeper/tools/convert/AssetsConverter.java +++ b/src/main/java/toniarts/openkeeper/tools/convert/AssetsConverter.java @@ -18,7 +18,6 @@ import com.jme3.asset.AssetManager; import com.jme3.system.AppSettings; -import java.io.File; import java.io.IOException; import java.lang.System.Logger; import java.lang.System.Logger.Level; @@ -28,17 +27,7 @@ import java.util.concurrent.TimeUnit; import toniarts.openkeeper.Main; import toniarts.openkeeper.tools.convert.conversion.ConversionTaskManager; -import toniarts.openkeeper.tools.convert.conversion.task.ConvertFonts; -import toniarts.openkeeper.tools.convert.conversion.task.ConvertHiScores; -import toniarts.openkeeper.tools.convert.conversion.task.ConvertMapThumbnails; -import toniarts.openkeeper.tools.convert.conversion.task.ConvertModels; -import toniarts.openkeeper.tools.convert.conversion.task.ConvertMouseCursors; -import toniarts.openkeeper.tools.convert.conversion.task.ConvertPaths; -import toniarts.openkeeper.tools.convert.conversion.task.ConvertSounds; -import toniarts.openkeeper.tools.convert.conversion.task.ConvertTexts; -import toniarts.openkeeper.tools.convert.conversion.task.ConvertTextures; -import toniarts.openkeeper.tools.convert.conversion.task.IConversionTask; -import toniarts.openkeeper.tools.convert.conversion.task.IConversionTaskUpdate; +import toniarts.openkeeper.tools.convert.conversion.task.*; import toniarts.openkeeper.tools.convert.map.KwdFile; import toniarts.openkeeper.utils.PathUtils; @@ -60,7 +49,7 @@ public abstract class AssetsConverter implements IConversionTaskUpdate { public enum ConvertProcess { TEXTURES(7, new ConvertProcess[]{}), - MODELS(11, new ConvertProcess[]{TEXTURES}), + MODELS(11, new ConvertProcess[]{TEXTURES}), MOUSE_CURSORS(4, new ConvertProcess[]{}), MUSIC_AND_SOUNDS(4, new ConvertProcess[]{}), INTERFACE_TEXTS(3, new ConvertProcess[]{}), @@ -103,21 +92,20 @@ public String toString() { private static final Logger logger = System.getLogger(AssetsConverter.class.getName()); private static final boolean OVERWRITE_DATA = true; // Not exhausting your SDD :) or our custom graphics - private static final String ASSETS_FOLDER = "assets" + File.separator + "Converted"; - private static final String ABSOLUTE_ASSETS_FOLDER = getCurrentFolder() + ASSETS_FOLDER + File.separator; - - public static final String SOUNDS_FOLDER = "Sounds"; - public static final String MATERIALS_FOLDER = "Materials"; - public static final String MODELS_FOLDER = "Models"; - public static final String TEXTURES_FOLDER = "Textures"; - public static final String SPRITES_FOLDER = "Sprites"; - public static final String MAP_THUMBNAILS_FOLDER = "Thumbnails"; - private static final String INTERFACE_FOLDER = "Interface" + File.separator; - public static final String MOUSE_CURSORS_FOLDER = INTERFACE_FOLDER + "Cursors"; - public static final String FONTS_FOLDER = INTERFACE_FOLDER + "Fonts"; - public static final String TEXTS_FOLDER = INTERFACE_FOLDER + "Texts"; - public static final String PATHS_FOLDER = INTERFACE_FOLDER + "Paths"; - + private static final String ASSETS_FOLDER = "assets/Converted/"; + private static final String ABSOLUTE_ASSETS_FOLDER = getCurrentFolder() + ASSETS_FOLDER; + + public static final String MATERIALS_FOLDER = "Materials/"; + public static final String MODELS_FOLDER = "Models/"; + public static final String SOUNDS_FOLDER = "Sounds/"; + public static final String SPRITES_FOLDER = "Sprites/"; + public static final String TEXTURES_FOLDER = "Textures/"; + public static final String THUMBNAILS_FOLDER = "Thumbnails/"; + public static final String MOUSE_CURSORS_FOLDER = "Interface/Cursors/"; + public static final String PATHS_FOLDER = "Interface/Paths/"; + private static final String FONTS_FOLDER = "Interface/Fonts/"; + private static final String TEXTS_FOLDER = "Interface/Texts/"; + private final String dungeonKeeperFolder; private final AssetManager assetManager; @@ -188,7 +176,7 @@ public boolean convertAssets() { logger.log(Level.INFO, "Current folder set to: {0}", currentFolder); // Create an assets folder - String assetFolder = currentFolder.concat(ASSETS_FOLDER).concat(File.separator); + String assetFolder = getAssetsFolder(); // Create task manager for taking care of the conversion workflow ConversionTaskManager conversionTaskManager = new ConversionTaskManager(); @@ -235,23 +223,23 @@ public void onError(Exception ex, ConvertProcess process) { private IConversionTask createTask(ConvertProcess conversion, String currentFolder) { switch (conversion) { case TEXTURES: - return new ConvertTextures(dungeonKeeperFolder, currentFolder.concat(TEXTURES_FOLDER).concat(File.separator), OVERWRITE_DATA); + return new ConvertTextures(dungeonKeeperFolder, currentFolder + TEXTURES_FOLDER, OVERWRITE_DATA); case MODELS: - return new ConvertModels(dungeonKeeperFolder, currentFolder.concat(MODELS_FOLDER).concat(File.separator), OVERWRITE_DATA, assetManager); + return new ConvertModels(dungeonKeeperFolder, currentFolder + MODELS_FOLDER, OVERWRITE_DATA, assetManager); case MOUSE_CURSORS: - return new ConvertMouseCursors(dungeonKeeperFolder, currentFolder.concat(MOUSE_CURSORS_FOLDER).concat(File.separator), OVERWRITE_DATA); + return new ConvertMouseCursors(dungeonKeeperFolder, currentFolder + MOUSE_CURSORS_FOLDER, OVERWRITE_DATA); case MUSIC_AND_SOUNDS: - return new ConvertSounds(dungeonKeeperFolder, currentFolder.concat(SOUNDS_FOLDER).concat(File.separator), OVERWRITE_DATA); + return new ConvertSounds(dungeonKeeperFolder, currentFolder + SOUNDS_FOLDER, OVERWRITE_DATA); case INTERFACE_TEXTS: - return new ConvertTexts(dungeonKeeperFolder, currentFolder.concat(TEXTS_FOLDER).concat(File.separator), OVERWRITE_DATA); + return new ConvertTexts(dungeonKeeperFolder, currentFolder + TEXTS_FOLDER, OVERWRITE_DATA); case PATHS: - return new ConvertPaths(dungeonKeeperFolder, currentFolder.concat(PATHS_FOLDER).concat(File.separator), OVERWRITE_DATA); + return new ConvertPaths(dungeonKeeperFolder, currentFolder + PATHS_FOLDER, OVERWRITE_DATA); case HI_SCORES: return new ConvertHiScores(dungeonKeeperFolder, OVERWRITE_DATA); case FONTS: - return new ConvertFonts(dungeonKeeperFolder, currentFolder.concat(FONTS_FOLDER).concat(File.separator), OVERWRITE_DATA); + return new ConvertFonts(dungeonKeeperFolder, currentFolder + FONTS_FOLDER, OVERWRITE_DATA); case MAP_THUMBNAILS: - return new ConvertMapThumbnails(dungeonKeeperFolder, currentFolder.concat(MAP_THUMBNAILS_FOLDER).concat(File.separator), OVERWRITE_DATA); + return new ConvertMapThumbnails(dungeonKeeperFolder, currentFolder + THUMBNAILS_FOLDER, OVERWRITE_DATA); } throw new IllegalArgumentException("Conversion " + conversion + " not implemented!"); diff --git a/src/main/java/toniarts/openkeeper/tools/convert/Bf4Extractor.java b/src/main/java/toniarts/openkeeper/tools/convert/Bf4Extractor.java index 51186d779..0aa99d7b7 100644 --- a/src/main/java/toniarts/openkeeper/tools/convert/Bf4Extractor.java +++ b/src/main/java/toniarts/openkeeper/tools/convert/Bf4Extractor.java @@ -80,7 +80,7 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IO for (Bf4Entry entry : bf4) { if (entry.getImage() != null) { - String baseDir = destination + PathUtils.stripFileName(file.toString()) + File.separator; + String baseDir = destination + PathUtils.stripFileName(file.toString()) + '/'; Files.createDirectories(Paths.get(baseDir)); ImageIO.write(entry.getImage(), "png", new File(baseDir + PathUtils.stripFileName(entry.toString()) + "_" diff --git a/src/main/java/toniarts/openkeeper/tools/convert/KmfModelLoader.java b/src/main/java/toniarts/openkeeper/tools/convert/KmfModelLoader.java index 8e652e01e..06d7611a4 100644 --- a/src/main/java/toniarts/openkeeper/tools/convert/KmfModelLoader.java +++ b/src/main/java/toniarts/openkeeper/tools/convert/KmfModelLoader.java @@ -162,7 +162,7 @@ private Node createGroup(KmfFile kmfFile) { //Go trough the models and add them for (Grop grop : kmfFile.getGrops()) { - String key = AssetsConverter.MODELS_FOLDER + File.separator + grop.getName() + ".j3o"; + String key = AssetsConverter.MODELS_FOLDER + grop.getName() + ".j3o"; AssetLinkNode modelLink = new AssetLinkNode(key, new ModelKey(key)); modelLink.setLocalTranslation(new Vector3f(grop.getPos().x, -grop.getPos().z, grop.getPos().y)); groupNode.attachChild(modelLink); @@ -649,8 +649,8 @@ private Map> getMaterials(KmfFile kmfFile, boolean gener fileName = fileName.concat(MATERIAL_ALTERNATIVE_TEXTURE_SUFFIX_SEPARATOR).concat("0"); } - materialKey = AssetsConverter.MATERIALS_FOLDER.concat("/").concat(fileName).concat(".j3m"); - materialLocation = AssetsConverter.getAssetsFolder().concat(AssetsConverter.MATERIALS_FOLDER.concat(File.separator).concat(fileName).concat(".j3m")); + materialKey = AssetsConverter.MATERIALS_FOLDER + fileName + ".j3m"; + materialLocation = AssetsConverter.getAssetsFolder() + materialKey; // See if it exists Path file = Paths.get(materialLocation); @@ -659,8 +659,8 @@ private Map> getMaterials(KmfFile kmfFile, boolean gener if (!file.getFileName().toString().equals(fileName.concat(".j3m"))) { // Case sensitivity issue - materialKey = AssetsConverter.MATERIALS_FOLDER.concat("/").concat(file.getFileName().toString()); - materialLocation = AssetsConverter.getAssetsFolder().concat(AssetsConverter.MATERIALS_FOLDER.concat(File.separator).concat(file.getFileName().toString())); + materialKey = AssetsConverter.MATERIALS_FOLDER + file.getFileName().toString(); + materialLocation = AssetsConverter.getAssetsFolder() + materialKey; } material = assetInfo.getManager().loadMaterial(materialKey); } @@ -752,7 +752,7 @@ private void addAlternativeTextures(toniarts.openkeeper.tools.convert.kmf.Materi private Texture loadTexture(String texture, AssetInfo assetInfo) { // Load the texture - TextureKey textureKey = new TextureKey(AssetUtils.getCanonicalAssetKey(AssetsConverter.TEXTURES_FOLDER.concat("/").concat(texture).concat(".png")), false); + var textureKey = new TextureKey(AssetUtils.getCanonicalAssetKey(AssetsConverter.TEXTURES_FOLDER + texture + ".png"), false); Texture tex = assetInfo.getManager().loadTexture(textureKey); return tex; } diff --git a/src/main/java/toniarts/openkeeper/tools/convert/map/KwdFile.java b/src/main/java/toniarts/openkeeper/tools/convert/map/KwdFile.java index 892350612..773ae210b 100644 --- a/src/main/java/toniarts/openkeeper/tools/convert/map/KwdFile.java +++ b/src/main/java/toniarts/openkeeper/tools/convert/map/KwdFile.java @@ -171,7 +171,7 @@ public KwdFile(String basePath, Path file, boolean load) { // Fug throw new RuntimeException("Failed to read the file " + file + "!", e); } - this.basePath = PathUtils.fixFilePath(basePath); + this.basePath = basePath; // See if we need to load the actual data if (load) { @@ -680,7 +680,7 @@ private ArtResource readArtResource(IResourceChunkReader reader) throws IOExcept ArtResource artResource = new ArtResource(); // Read the data - artResource.setName(reader.readString(64).trim()); + artResource.setName(reader.readString(64).trim().replace('\\', '/')); artResource.setFlags(reader.readIntegerAsFlag(ArtResource.ArtResourceFlag.class)); reader.mark(); diff --git a/src/main/java/toniarts/openkeeper/tools/convert/wad/WadFile.java b/src/main/java/toniarts/openkeeper/tools/convert/wad/WadFile.java index 93a8b91d3..33e30c5ce 100644 --- a/src/main/java/toniarts/openkeeper/tools/convert/wad/WadFile.java +++ b/src/main/java/toniarts/openkeeper/tools/convert/wad/WadFile.java @@ -122,7 +122,7 @@ public WadFile(Path file) { // The path name = PathUtils.convertFileSeparators(name); - int index = name.lastIndexOf(File.separator); + int index = name.lastIndexOf('/'); if (index > -1) { path = name.substring(0, index + 1); } else if (!path.isEmpty()) { diff --git a/src/main/java/toniarts/openkeeper/tools/modelviewer/ModelViewer.java b/src/main/java/toniarts/openkeeper/tools/modelviewer/ModelViewer.java index e2275e96e..8eaedf979 100644 --- a/src/main/java/toniarts/openkeeper/tools/modelviewer/ModelViewer.java +++ b/src/main/java/toniarts/openkeeper/tools/modelviewer/ModelViewer.java @@ -52,7 +52,6 @@ import de.lessvoid.nifty.controls.DropDown; import de.lessvoid.nifty.controls.ListBox; import java.io.ByteArrayInputStream; -import java.io.File; import java.io.IOException; import java.lang.System.Logger; import java.lang.System.Logger.Level; @@ -68,9 +67,8 @@ import toniarts.openkeeper.Main; import toniarts.openkeeper.audio.plugins.MP2Loader; import toniarts.openkeeper.game.data.ISoundable; -import toniarts.openkeeper.game.sound.SoundCategory; -import toniarts.openkeeper.game.sound.SoundFile; -import toniarts.openkeeper.game.sound.SoundGroup; +import toniarts.openkeeper.game.data.Settings; +import toniarts.openkeeper.game.sound.*; import toniarts.openkeeper.gui.CursorFactory; import toniarts.openkeeper.tools.convert.AssetsConverter; import toniarts.openkeeper.tools.convert.KmfAssetInfo; @@ -186,7 +184,10 @@ public static void main(String[] args) { dkIIFolder = PathUtils.fixFilePath(args[0]); } - ModelViewer app = new ModelViewer(); + var app = new ModelViewer(); + app.settings = Settings.getInstance().getAppSettings(); + if (Main.isAudioDisabled()) + app.settings.setAudioRenderer(null); app.start(); } @@ -652,8 +653,7 @@ protected void fillList(Types type) { screen.getItemsControl().clear(); switch (type) { case MODELS: { - fillWithFiles(models, AssetsConverter.getAssetsFolder() - + AssetsConverter.MODELS_FOLDER + File.separator, ".j3o"); + fillWithFiles(models, AssetsConverter.getAssetsFolder() + AssetsConverter.MODELS_FOLDER, ".j3o"); break; } case MAPS: { diff --git a/src/main/java/toniarts/openkeeper/tools/modelviewer/ModelViewerScreenController.java b/src/main/java/toniarts/openkeeper/tools/modelviewer/ModelViewerScreenController.java index 2fc0adb2a..7f45c5fb6 100644 --- a/src/main/java/toniarts/openkeeper/tools/modelviewer/ModelViewerScreenController.java +++ b/src/main/java/toniarts/openkeeper/tools/modelviewer/ModelViewerScreenController.java @@ -416,14 +416,12 @@ private ControlBuilder createEffectControl(Effect item) { private String getResourceImageName(ArtResource resource) { String result = (resource != null && resource.getName() != null) ? resource.getName() + ".png" : "&mask&transparent.png"; - String textureName = AssetUtils.getCanonicalAssetKey(AssetsConverter.TEXTURES_FOLDER - + File.separator + result); + String textureName = AssetUtils.getCanonicalAssetKey(AssetsConverter.TEXTURES_FOLDER + result); TextureKey textureKey = new TextureKey(textureName, false); AssetInfo assetInfo = app.getAssetManager().locateAsset(textureKey); - return (assetInfo != null ? textureName : AssetUtils.getCanonicalAssetKey(AssetsConverter.TEXTURES_FOLDER - + File.separator + "&mask&transparent.png")); + return (assetInfo != null ? textureName : AssetUtils.getCanonicalAssetKey(AssetsConverter.TEXTURES_FOLDER + "&mask&transparent.png")); } private String getResourceString(int id) { diff --git a/src/main/java/toniarts/openkeeper/tools/modelviewer/SoundsLoader.java b/src/main/java/toniarts/openkeeper/tools/modelviewer/SoundsLoader.java index 9b487b5d1..2e49f2d0b 100644 --- a/src/main/java/toniarts/openkeeper/tools/modelviewer/SoundsLoader.java +++ b/src/main/java/toniarts/openkeeper/tools/modelviewer/SoundsLoader.java @@ -110,7 +110,7 @@ public static AudioNode getAudioNode(final AssetManager assetManager, final Soun } return new AudioNode(assetManager, - AssetsConverter.SOUNDS_FOLDER + File.separator + file.getFilename(), + AssetsConverter.SOUNDS_FOLDER + file.getFilename(), AudioData.DataType.Buffer); } diff --git a/src/main/java/toniarts/openkeeper/utils/AssetUtils.java b/src/main/java/toniarts/openkeeper/utils/AssetUtils.java index 83176862c..89d4d42d9 100644 --- a/src/main/java/toniarts/openkeeper/utils/AssetUtils.java +++ b/src/main/java/toniarts/openkeeper/utils/AssetUtils.java @@ -101,7 +101,7 @@ private AssetUtils() { public static Spatial loadModel(final AssetManager assetManager, String modelName, ArtResource artResource, final boolean useCache, final boolean useWeakCache) { - String filename = AssetsConverter.MODELS_FOLDER + File.separator + modelName + ".j3o"; + String filename = AssetsConverter.MODELS_FOLDER + modelName + ".j3o"; ModelKey assetKey = new ModelKey(getCanonicalAssetKey(filename)); Spatial result; @@ -168,7 +168,7 @@ public void visit(Spatial spatial) { */ public static Spatial loadAsset(final AssetManager assetManager, String modelName, ArtResource artResource) { - String filename = AssetsConverter.MODELS_FOLDER + File.separator + modelName + ".j3o"; + String filename = AssetsConverter.MODELS_FOLDER + modelName + ".j3o"; ModelKey assetKey = new ModelKey(getCanonicalAssetKey(filename)); Spatial result = loadModel(assetManager, assetKey, artResource); @@ -187,8 +187,7 @@ public static Spatial loadModel(final AssetManager assetManager, String resource } public static CameraSweepData loadCameraSweep(final AssetManager assetManager, String resourceName) { - String filename = AssetsConverter.PATHS_FOLDER + File.separator + resourceName + "." - + CameraSweepDataLoader.FILE_EXTENSION; + String filename = AssetsConverter.PATHS_FOLDER + resourceName + '.' + CameraSweepDataLoader.FILE_EXTENSION; String assetKey = getCanonicalAssetKey(filename); Object asset = assetManager.loadAsset(assetKey); @@ -352,7 +351,7 @@ public static Material createParticleMaterial(ArtResource resource, AssetManager } private static Texture createArtResourceTexture(ArtResource resource, AssetManager assetManager) throws IOException { - String assetFolder = AssetsConverter.TEXTURES_FOLDER + File.separator; + String assetFolder = AssetsConverter.TEXTURES_FOLDER; if (resource.getFlags().contains(ArtResource.ArtResourceFlag.ANIMATING_TEXTURE)) { return createAnimatingTexture(resource.getName(), @@ -362,7 +361,7 @@ private static Texture createArtResourceTexture(ArtResource resource, AssetManag if (resource.getType().equals(ArtResource.ArtResourceType.SPRITE) && resource.getData(ArtResource.KEY_WIDTH).intValue() > 1) { // only the unused sprites have a size of bigger than one - assetFolder = AssetsConverter.SPRITES_FOLDER + File.separator; + assetFolder = AssetsConverter.SPRITES_FOLDER; } // A regular texture @@ -373,7 +372,7 @@ private static Texture createArtResourceTexture(ArtResource resource, AssetManag private static List getTextureFrames(ArtResource resource) { int frames = resource.getData(ArtResource.KEY_FRAMES); - String assetFolder = AssetsConverter.TEXTURES_FOLDER + File.separator; + String assetFolder = AssetsConverter.TEXTURES_FOLDER; List framesList = new ArrayList<>(frames); for (int x = 0; x < frames; x++) { framesList.add(assetFolder + resource.getName() + x + ".png"); @@ -697,7 +696,7 @@ public static Spatial createProceduralMesh(ArtResource resource) { * @return fully qualified and working asset key */ public static String getCanonicalAssetKey(String asset) { - return PathUtils.getCanonicalRelativePath(AssetsConverter.getAssetsFolder(), asset).replaceAll(PathUtils.QUOTED_FILE_SEPARATOR, "/"); + return PathUtils.getCanonicalRelativePath(AssetsConverter.getAssetsFolder(), asset).replace('\\', '/'); } } diff --git a/src/main/java/toniarts/openkeeper/utils/MapThumbnailGenerator.java b/src/main/java/toniarts/openkeeper/utils/MapThumbnailGenerator.java index 299d98f80..336176d28 100644 --- a/src/main/java/toniarts/openkeeper/utils/MapThumbnailGenerator.java +++ b/src/main/java/toniarts/openkeeper/utils/MapThumbnailGenerator.java @@ -57,7 +57,7 @@ public final class MapThumbnailGenerator { private static final Logger logger = System.getLogger(MapThumbnailGenerator.class.getName()); - private static final String PALETTE_IMAGE = "Textures".concat(File.separator).concat("Thumbnails").concat(File.separator).concat("MapColours.png"); + private static final String PALETTE_IMAGE = "Textures/Thumbnails/MapColours.png"; private static ColorModel cm; private static Map playerColors; diff --git a/src/main/java/toniarts/openkeeper/utils/PathUtils.java b/src/main/java/toniarts/openkeeper/utils/PathUtils.java index 79eaf7d3b..7c3da03cf 100644 --- a/src/main/java/toniarts/openkeeper/utils/PathUtils.java +++ b/src/main/java/toniarts/openkeeper/utils/PathUtils.java @@ -16,7 +16,6 @@ */ package toniarts.openkeeper.utils; -import java.io.File; import java.io.IOException; import java.io.InputStream; import java.lang.System.Logger; @@ -30,30 +29,33 @@ import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; import java.util.Arrays; -import java.util.HashMap; import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.regex.Matcher; +import java.util.concurrent.ConcurrentHashMap; public final class PathUtils { - + private static final Logger logger = System.getLogger(PathUtils.class.getName()); - private static final Map FILENAME_CACHE = new HashMap<>(); - private static final PathTree PATH_CACHE = new PathTree(); - private static final Object FILENAME_LOCK = new Object(); - protected static final String QUOTED_FILE_SEPARATOR = Matcher.quoteReplacement(File.separator); - - public static final String DKII_DATA_FOLDER = getRealDKIIRelativeFolder("Data" + File.separator); - public static final String DKII_SFX_FOLDER = getRealDKIIRelativeFolder(DKII_DATA_FOLDER + "Sound" + File.separator - + "sfx" + File.separator); - public static final String DKII_MOVIES_FOLDER = getRealDKIIRelativeFolder(DKII_DATA_FOLDER + "Movies" + File.separator); - public static final String DKII_TEXT_DEFAULT_FOLDER = getRealDKIIRelativeFolder(DKII_DATA_FOLDER + "Text" + File.separator - + "Default" + File.separator); - public static final String DKII_EDITOR_FOLDER = getRealDKIIRelativeFolder(DKII_DATA_FOLDER + "editor" + File.separator); - public static final String DKII_MAPS_FOLDER = getRealDKIIRelativeFolder(DKII_EDITOR_FOLDER + "maps" + File.separator); - public static final String DKII_SFX_GLOBAL_FOLDER = getRealDKIIRelativeFolder(DKII_SFX_FOLDER + "Global" + File.separator); + /** + * Cache for fully resolved file paths: lowercase key -> exact-case real path. + * ConcurrentHashMap eliminates the data race from the old double-checked + * locking on a plain HashMap. + */ + private static final ConcurrentHashMap FILENAME_CACHE = new ConcurrentHashMap<>(); + + /** + * Cache for known directory paths: lowercase dir path -> exact-case dir path + * (always ends with '/'). Replaces the old PathTree/PathNode custom trie. + */ + private static final ConcurrentHashMap PATH_CACHE = new ConcurrentHashMap<>(); + + public static final String DKII_DATA_FOLDER = "Data/"; + public static final String DKII_EDITOR_FOLDER = "Data/Editor/"; + public static final String DKII_MAPS_FOLDER = "Data/Editor/Maps/"; + public static final String DKII_MOVIES_FOLDER = "Data/Movies/"; + public static final String DKII_SFX_FOLDER = "Data/Sound/Sfx/"; + public static final String DKII_SFX_GLOBAL_FOLDER = "Data/Sound/Sfx/Global/"; + public static final String DKII_TEXT_DEFAULT_FOLDER = "Data/Text/Default/"; private static final String DKII_FOLDER_KEY = "DungeonKeeperIIFolder"; private static final String TEST_FILE = DKII_MAPS_FOLDER + "FrontEnd3DLevel.kwd"; @@ -100,26 +102,12 @@ public static boolean checkDkFolder(String folder) { * @return folder with file separator at the end */ public static String fixFilePath(final String folderPath) { - if (!folderPath.endsWith(File.separator)) { - return folderPath.concat(File.separator); + if (!folderPath.endsWith("/")) { + return folderPath + '/'; } return folderPath; } - /** - * Get the relative folder that has been fixed for case sensitivity - * - * @param folder the path to fix - * @return fixed path relative to the DKII folder - */ - public static String getRealDKIIRelativeFolder(final String folder) { - String rootFolder = getDKIIFolder(); - if (rootFolder != null && !rootFolder.isEmpty()) { - return fixFilePath(getCanonicalRelativePath(rootFolder, folder)); - } - return fixFilePath(folder); - } - /** * Creates a filter for getting files that end in the wanted suffix. This is * case insensitive comparison. @@ -144,13 +132,13 @@ public static byte[] readInputStream(InputStream inputStream) throws IOException } /** - * Converts all the file separators to current system separators + * Converts all the file separators to forward slashes * * @param fileName the file name to convert - * @return the file name with native file separators + * @return the file name with forward slashes */ public static String convertFileSeparators(String fileName) { - return fileName.replaceAll("[/\\\\]", QUOTED_FILE_SEPARATOR); + return fileName.replace('\\', '/'); } /** @@ -194,64 +182,166 @@ public static String getCanonicalRelativePath(String rootPath, String path) { */ public static String getRealFileName(final String realPath, String uncertainPath) throws IOException { - // Make sure that the uncertain path's separators are system separators + // Make sure that the uncertain path's separators are forward slashes uncertainPath = convertFileSeparators(uncertainPath); String fileName = realPath.concat(uncertainPath); String fileKey = fileName.toLowerCase(); - // See cache + // Fast path: ConcurrentHashMap is safe for unsynchronized reads String cachedName = FILENAME_CACHE.get(fileKey); if (cachedName != null) { return cachedName; } - - synchronized (FILENAME_LOCK) { - - cachedName = FILENAME_CACHE.get(fileKey); - if (cachedName != null) { - return cachedName; - } - // If it exists as such, that is super! - Path testFile = Paths.get(fileName); - if (Files.exists(testFile)) { - cachedName = testFile.toRealPath().toString(); - FILENAME_CACHE.put(fileKey, cachedName); + // Compute the real path (two concurrent threads may both compute, but + // putIfAbsent ensures only the first result is stored, and the computation + // is idempotent filesystem work). + cachedName = resolveFileName(fileName, realPath); + if (cachedName == null) + throw new IOException("File not found " + Paths.get(fileName) + "!"); - return cachedName; - } + // Store in caches, using the winner if another thread beat us + String existing = FILENAME_CACHE.putIfAbsent(fileKey, cachedName); + String resolved = (existing != null) ? existing : cachedName; - // Otherwise we need to do a recursive search - String certainPath = PATH_CACHE.getCertainPath(fileName, realPath); - final String[] path = fileName.substring(certainPath.length()).split(QUOTED_FILE_SEPARATOR); - - // If the path length is 1, lets try, maybe it was just the file name - if (path.length == 1 && !certainPath.equalsIgnoreCase(realPath)) { - Path p = Paths.get(certainPath, path[0]); - if (Files.exists(p)) { - cachedName = p.toRealPath().toString(); - FILENAME_CACHE.put(fileKey, cachedName); - - return cachedName; - } - } + // Cache all parent directories for future lookups + cacheDirectoryPaths(resolved); + + return resolved; + } + + /** + * Resolve a file name case-insensitively. Returns null if not found. + */ + private static String resolveFileName(String fileName, String realPath) throws IOException { + // Try exact match first + Path testFile = Paths.get(fileName); + if (Files.exists(testFile)) + return testFile.toRealPath().toString(); + + // Find the longest known directory path from the cache + String dirPart = getDirectoryPart(fileName); + String certainPath = getCertainPath(dirPart, realPath); + + // If only a single filename segment, try a direct lookup from certainPath + String uncertainSuffix = fileName.substring(certainPath.length()); + if (!uncertainSuffix.startsWith("/")) + uncertainSuffix = '/' + uncertainSuffix; + String[] segments = uncertainSuffix.split("/"); + List nonEmpty = new ArrayList<>(); + for (String s : segments) + if (!s.isEmpty()) + nonEmpty.add(s); + + if (nonEmpty.isEmpty()) + return null; + + // Try one-segment shortcut: look it up directly from certainPath + if (nonEmpty.size() == 1 && !certainPath.equalsIgnoreCase(realPath)) { + Path p = Paths.get(certainPath, nonEmpty.get(0)); + if (Files.exists(p)) + return p.toRealPath().toString(); + } + + // Walk the path segments, resolving each case-insensitively + return resolveCaseInsensitive(Paths.get(certainPath), nonEmpty.toArray(new String[0])); + } + + /** + * Given a full file path, extract the directory part (everything before the + * last '/'). If the path ends with '/', it's already a directory path. + */ + private static String getDirectoryPart(String fileName) { + if (fileName.endsWith("/")) + return fileName; + + int lastSlash = fileName.lastIndexOf('/'); + return lastSlash >= 0 ? fileName.substring(0, lastSlash + 1) : ""; + } + + /** + * Find the longest cached known directory path. If nothing cached, returns + * defaultPath. Walks up the directory tree checking PATH_CACHE at each level. + */ + private static String getCertainPath(String dirPart, String defaultPath) { + String current = dirPart; + while (current.length() > defaultPath.length()) { + String cached = PATH_CACHE.get(current.toLowerCase()); + if (cached != null) + return cached; + + // Strip the last segment and trailing slash + int lastSlash = current.lastIndexOf('/'); + if (lastSlash <= 0) + break; + + current = current.substring(0, lastSlash); // e.g., "a/b/c/" -> "a/b" + lastSlash = current.lastIndexOf('/'); + current = lastSlash >= 0 ? current.substring(0, lastSlash + 1) : current + '/'; + } + return defaultPath; + } - // Find the file - final Path realPathAsPath = Paths.get(certainPath); - FileFinder fileFinder = new FileFinder(realPathAsPath, path); - Files.walkFileTree(realPathAsPath, fileFinder); - FILENAME_CACHE.put(fileKey, fileFinder.file); - cachedName = fileFinder.file; - if (fileFinder.file == null) { - throw new IOException("File not found " + testFile + "!"); + /** + * Cache all parent directory paths from a resolved file/directory path. + * For "a/b/c/file.txt", caches "a/", "a/b/", "a/b/c/". + * For "a/b/c/", caches "a/", "a/b/", "a/b/c/". + */ + private static void cacheDirectoryPaths(String resolvedPath) { + String[] parts = resolvedPath.split("/"); + int end = resolvedPath.endsWith("/") ? parts.length : parts.length - 1; + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < end; i++) { + if (parts[i].isEmpty()) + continue; // skip leading empty segment from absolute paths + + sb.append(parts[i]).append('/'); + String dirPath = sb.toString(); + PATH_CACHE.putIfAbsent(dirPath.toLowerCase(), dirPath); + } + } + + /** + * Walk from basePath through each segment, resolving each case-insensitively + * via directory listing. Returns the toRealPath() result, or null if any + * segment cannot be found. + */ + private static String resolveCaseInsensitive(Path basePath, String[] segments) throws IOException { + Path current = basePath; + for (int i = 0; i < segments.length; i++) { + String segment = segments[i]; + boolean isLast = (i == segments.length - 1); + + Path found = null; + try (DirectoryStream stream = Files.newDirectoryStream(current)) { + for (Path entry : stream) { + if (!entry.getFileName().toString().equalsIgnoreCase(segment)) { + continue; + } + if (isLast) { + // Last segment: accept file or directory + found = entry; + break; + } else if (Files.isDirectory(entry)) { + // Intermediate segment: must be a directory + found = entry; + break; + } + } } - // Cache the known path - PATH_CACHE.setPathToCache(fileFinder.file); + if (found == null) + return null; + current = found; } - return cachedName; + // Preserve trailing '/' for directory results (matches original FileFinder behavior) + String result = current.toRealPath().toString(); + if (Files.isDirectory(current)) { + result = result + '/'; + } + return result; } /** @@ -294,207 +384,4 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IO return true; } - /** - * File finder, recursively tries to find a file ignoring case - */ - private static final class FileFinder extends SimpleFileVisitor { - - private int level = 0; - private String file; - private final Path startingPath; - private final String[] path; - - private FileFinder(Path startingPath, String[] path) { - this.startingPath = startingPath; - this.path = path; - } - - @Override - public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { - if (startingPath.equals(dir)) { - return FileVisitResult.CONTINUE; // Just the root - } else if (startingPath.relativize(dir).getName(level).toString().equalsIgnoreCase(path[level])) { - if (level < path.length - 1) { - level++; - return FileVisitResult.CONTINUE; // Go to dir - } else { - - // We are looking for a directory and we found it - this.file = dir.toRealPath().toString().concat(File.separator); - return FileVisitResult.TERMINATE; - } - } - return FileVisitResult.SKIP_SUBTREE; - } - - @Override - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { - - // See if this is the file we are looking for - if (level == path.length - 1 && file.getName(file.getNameCount() - 1).toString().equalsIgnoreCase(path[level])) { - this.file = file.toRealPath().toString(); - return FileVisitResult.TERMINATE; - } - - return FileVisitResult.CONTINUE; - } - - @Override - public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { - return FileVisitResult.TERMINATE; // We already missed our window here - } - } - - /** - * Represents a simple path tree cache, with unlimited number of roots. - * Offers some methods to manage the tree. - */ - private static final class PathTree extends HashMap { - - /** - * Add the path to cache from KNOWN file - * - * @param file the known and existing file - */ - public void setPathToCache(String file) { - List paths = new ArrayList<>(Arrays.asList(file.split(QUOTED_FILE_SEPARATOR))); - if (!paths.isEmpty()) { - if (!file.endsWith(File.separator)) { - paths.remove(paths.size() - 1); - } - PathNode node = null; - for (String folder : paths) { - node = getPath(folder, node, true); - } - } - } - - private PathNode getPath(String folder, PathNode node, boolean add) { - String key = folder.toLowerCase(); - Map leaf; - if (node != null) { - leaf = node.children; - } else { - leaf = this; - } - PathNode result = leaf.get(key); - if (result == null && add) { - result = new PathNode(folder, (node != null ? node.level + 1 : 0), node); - leaf.put(key, result); - } - return result; - } - - /** - * Get certain path from cache - * - * @param fileName the file name we aim to find, if folder, we expect - * path separator at the end - * @param defaultPath the default path we know that exists, we'll return - * it if no cached path found - * @return the cached known path, quaranteed to be exactly the default - * path or deeper - */ - public String getCertainPath(String fileName, String defaultPath) { - List paths = new ArrayList<>(Arrays.asList(fileName.split(QUOTED_FILE_SEPARATOR))); - if (!paths.isEmpty()) { - if (!fileName.endsWith(File.separator)) { - paths.remove(paths.size() - 1); - } - PathNode node = null; - for (String folder : paths) { - PathNode nextNode = getPath(folder, node, false); - if (nextNode != null) { - node = nextNode; - } else { - break; - } - } - - // Return if we have longer path - if (node != null && node.path.length() > defaultPath.length()) { - return node.path; - } - } - return defaultPath; - } - - } - - /** - * Path node that represents a single folder - */ - private static final class PathNode { - - private final String path; - private final String name; - private final int level; - private final PathNode parent; - private final Map children = new HashMap<>(); - - public PathNode(String name, int level, PathNode parent) { - this.name = name; - this.level = level; - this.parent = parent; - - StringBuilder sb = new StringBuilder(); - if (parent != null) { - sb.append(parent.path); - } - sb.append(name); - sb.append(File.separator); - path = sb.toString(); - } - - public String getName() { - return name; - } - - public int getLevel() { - return level; - } - - public PathNode getParent() { - return parent; - } - - public Map getChildren() { - return children; - } - - public String getPath() { - return path; - } - - @Override - public int hashCode() { - int hash = 3; - hash = 67 * hash + Objects.hashCode(this.name); - hash = 67 * hash + this.level; - return hash; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - if (getClass() != obj.getClass()) { - return false; - } - final PathNode other = (PathNode) obj; - if (this.level != other.level) { - return false; - } - if (!Objects.equals(this.name, other.name)) { - return false; - } - return true; - } - - } - } diff --git a/src/main/java/toniarts/openkeeper/view/KeeperHandState.java b/src/main/java/toniarts/openkeeper/view/KeeperHandState.java index f4d2f1978..b2fc4f8bc 100644 --- a/src/main/java/toniarts/openkeeper/view/KeeperHandState.java +++ b/src/main/java/toniarts/openkeeper/view/KeeperHandState.java @@ -201,7 +201,7 @@ private Picture getIcon(final ArtResource image) { private Picture getIcon(String filename) { //FIXME if filename is null what todo ? - final String name = AssetUtils.getCanonicalAssetKey(TEXTURES_FOLDER + File.separator + filename + ".png"); + final String name = AssetUtils.getCanonicalAssetKey(TEXTURES_FOLDER + filename + ".png"); Texture tex = assetManager.loadTexture(name); diff --git a/src/main/java/toniarts/openkeeper/view/control/TrapFlowerControl.java b/src/main/java/toniarts/openkeeper/view/control/TrapFlowerControl.java index 33c341865..7e99cb4cc 100644 --- a/src/main/java/toniarts/openkeeper/view/control/TrapFlowerControl.java +++ b/src/main/java/toniarts/openkeeper/view/control/TrapFlowerControl.java @@ -47,8 +47,7 @@ public String getCenterIcon() { String result = null; if (getDataObject().getFlowerIcon() != null) { - result = AssetUtils.getCanonicalAssetKey(AssetsConverter.TEXTURES_FOLDER - + File.separator + getDataObject().getFlowerIcon().getName() + ".png"); + result = AssetUtils.getCanonicalAssetKey(AssetsConverter.TEXTURES_FOLDER + getDataObject().getFlowerIcon().getName() + ".png"); } return result; diff --git a/src/main/java/toniarts/openkeeper/view/map/Water.java b/src/main/java/toniarts/openkeeper/view/map/Water.java index 0b26fe944..0c12a58dc 100644 --- a/src/main/java/toniarts/openkeeper/view/map/Water.java +++ b/src/main/java/toniarts/openkeeper/view/map/Water.java @@ -109,7 +109,7 @@ public static Spatial construct(AssetManager assetManager, List