From f6139c3890f1a23fc6abf71d62e8def0e17bb72e Mon Sep 17 00:00:00 2001 From: Harry Huang Date: Sun, 17 Dec 2023 16:07:49 +0800 Subject: [PATCH] Asset Manage Reconstruction --- assets/UI/Homepage.fxml | 2 +- core/src/cn/harryh/arkpets/ArkChar.java | 3 +- core/src/cn/harryh/arkpets/ArkPets.java | 3 +- .../cn/harryh/arkpets/assets/AssetItem.java | 215 +++++++++++ .../harryh/arkpets/assets/AssetItemGroup.java | 197 ++++++++++ .../harryh/arkpets/assets/ModelsDataset.java | 113 ++++++ .../cn/harryh/arkpets/utils/AssetCtrl.java | 339 ------------------ .../harryh/arkpets/controllers/Homepage.java | 178 +++++---- .../arkpets/guitasks/VerifyModelsTask.java | 66 ++-- 9 files changed, 646 insertions(+), 470 deletions(-) create mode 100644 core/src/cn/harryh/arkpets/assets/AssetItem.java create mode 100644 core/src/cn/harryh/arkpets/assets/AssetItemGroup.java create mode 100644 core/src/cn/harryh/arkpets/assets/ModelsDataset.java delete mode 100644 core/src/cn/harryh/arkpets/utils/AssetCtrl.java diff --git a/assets/UI/Homepage.fxml b/assets/UI/Homepage.fxml index cceea7ec..c2e787fc 100644 --- a/assets/UI/Homepage.fxml +++ b/assets/UI/Homepage.fxml @@ -188,7 +188,7 @@ - diff --git a/core/src/cn/harryh/arkpets/ArkChar.java b/core/src/cn/harryh/arkpets/ArkChar.java index 0905e292..74573feb 100644 --- a/core/src/cn/harryh/arkpets/ArkChar.java +++ b/core/src/cn/harryh/arkpets/ArkChar.java @@ -6,6 +6,7 @@ import cn.harryh.arkpets.animations.AnimClip; import cn.harryh.arkpets.animations.AnimClipGroup; import cn.harryh.arkpets.animations.AnimData; +import cn.harryh.arkpets.assets.AssetItem; import cn.harryh.arkpets.utils.*; import cn.harryh.arkpets.transitions.*; import com.badlogic.gdx.Gdx; @@ -48,7 +49,7 @@ public class ArkChar { * @param assetAccessor The Asset Accessor of the model. * @param scale The scale of the character. */ - public ArkChar(String assetLocation, AssetCtrl.AssetAccessor assetAccessor, float scale) { + public ArkChar(String assetLocation, AssetItem.AssetAccessor assetAccessor, float scale) { // 1.Graphics setup camera = new OrthographicCamera(); batch = new TwoColorPolygonBatch(); diff --git a/core/src/cn/harryh/arkpets/ArkPets.java b/core/src/cn/harryh/arkpets/ArkPets.java index efdaafef..3d828a7d 100644 --- a/core/src/cn/harryh/arkpets/ArkPets.java +++ b/core/src/cn/harryh/arkpets/ArkPets.java @@ -4,6 +4,7 @@ package cn.harryh.arkpets; import cn.harryh.arkpets.animations.*; +import cn.harryh.arkpets.assets.AssetItem; import cn.harryh.arkpets.utils.*; import cn.harryh.arkpets.transitions.*; import com.badlogic.gdx.ApplicationAdapter; @@ -56,7 +57,7 @@ public void create() { getHWndLoopCtrl = new LoopCtrl(1.0f / APP_FPS * 4); // 2.Character setup Logger.info("App", "Using model asset \"" + config.character_asset + "\""); - cha = new ArkChar(config.character_asset, new AssetCtrl.AssetAccessor(config.character_files), skelBaseScale); + cha = new ArkChar(config.character_asset, new AssetItem.AssetAccessor(config.character_files), skelBaseScale); cha.setCanvas(); behavior = new GeneralBehavior(config, cha.animList); cha.setAnimation(behavior.defaultAnim()); diff --git a/core/src/cn/harryh/arkpets/assets/AssetItem.java b/core/src/cn/harryh/arkpets/assets/AssetItem.java new file mode 100644 index 00000000..22a210cf --- /dev/null +++ b/core/src/cn/harryh/arkpets/assets/AssetItem.java @@ -0,0 +1,215 @@ +/** Copyright (c) 2022-2023, Harry Huang + * At GPL-3.0 License + */ +package cn.harryh.arkpets.assets; + +import cn.harryh.arkpets.utils.Logger; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import com.alibaba.fastjson.annotation.JSONField; + +import java.io.File; +import java.io.Serializable; +import java.util.*; +import java.util.function.Function; + + +/** One Asset Item is corresponding to one certain local Spine asset. + */ +public class AssetItem implements Serializable { + @JSONField(serialize = false) + public File assetDir; + @JSONField + public String assetId; + @JSONField + public String type; + @JSONField + public String style; + @JSONField + public String name; + @JSONField + public String appellation; + @JSONField + public String skinGroupId; + @JSONField + public String skinGroupName; + @JSONField + public JSONArray sortTags; + @JSONField + public JSONObject assetList; + /** @deprecated Legacy field in old version dataset */ @JSONField @Deprecated + public JSONObject checksum; + + private AssetAccessor accessor; + + protected static final String[] extensions = {".atlas", ".png", ".skel"}; + + private AssetItem() { + } + + /** Gets the directory where the asset files located in. + * @return A relative path string. + */ + @JSONField(serialize = false) + public String getLocation() { + return assetDir.toString(); + } + + /** Gets the Asset Accessor of this asset. + * @return An Asset Accessor instance. + */ + @JSONField(serialize = false) + public AssetAccessor getAccessor() { + if (accessor == null) + accessor = new AssetAccessor(assetList); + return accessor; + } + + /** Verifies the integrity of the necessary fields of this {@code AssetItem}. + * @return {@code true} if all the following conditions are satisfied, otherwise {@code false}: + * 1. Both {@code assetDir} and {@code type} are not {@code null}. + * 2. The {@code AssetAccessor} is available. + */ + @JSONField(serialize = false) + public boolean isValid() { + return assetDir != null && type != null && getAccessor().isAvailable(); + } + + /** Verifies the existence of the target local directory. + * @return {@code true} if all the following conditions are satisfied, otherwise {@code false}: + * 1. The method {@code isValid()} returns {@code true}. + * 2. The local {@code assetDir} exists. + */ + @JSONField(serialize = false) + public boolean isExisted() { + return isValid() && assetDir.isDirectory(); + } + + /** Verifies the integrity of the related local files. + * @return {@code true} if all the following conditions are satisfied, otherwise {@code false}: + * 1. The method {@code isExisted()} returns {@code true}. + * 2. All the local files exist. + */ + @JSONField(serialize = false) + public boolean isChecked() { + if (isExisted()) { + try { + ArrayList existed = new ArrayList<>(List.of(Objects.requireNonNull(assetDir.list()))); + existed.replaceAll(String::toLowerCase); + for (String fileName : getAccessor().getAllFiles()) { + fileName = fileName.toLowerCase(); + if (!existed.contains(fileName)) { + Logger.warn("Asset", "The asset file " + fileName + " (" + assetDir.getName() + ") is missing."); + return false; + } + } + return true; + } catch (Exception e) { + Logger.warn("Asset", "Failed to check the asset " + assetDir.getName()); + return false; + } + } else { + return false; + } + } + + @Override + public String toString() { + return name; + } + + @Override + public int hashCode() { + return assetDir.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof AssetItem) { + return ((AssetItem)obj).assetDir.equals(assetDir); + } + return false; + } + + + /** The Asset Accessor providing methods to get the resource files of the asset. + * @since ArkPets 2.2 + */ + public static class AssetAccessor { + private final ArrayList list; + private final HashMap> map; + + public AssetAccessor(JSONObject fileMap) { + ArrayList list = new ArrayList<>(); + HashMap> map = new HashMap<>(); + try { + if (fileMap != null && !fileMap.isEmpty()) { + for (String fileType : fileMap.keySet()) { + try { + JSONArray someFiles; // Try to get as array + if ((someFiles = fileMap.getJSONArray(fileType)) != null) { + var temp = someFiles.toJavaList(String.class); + list.addAll(temp); + map.put(fileType, new ArrayList<>(temp)); + } + } catch (com.alibaba.fastjson.JSONException | com.alibaba.fastjson2.JSONException ex) { + String oneFile; // Try to get as string + if ((oneFile = fileMap.getString(fileType)) != null) { + list.add(oneFile); + map.put(fileType, new ArrayList<>(List.of(oneFile))); + } + } + } + } + } catch (Exception e) { + Logger.error("Asset", "Failed to establish an asset accessor, details see below.", e); + list = new ArrayList<>(); + map = new HashMap<>(); + } + this.list = list; + this.map = map; + } + + public String[] getAllFiles() { + return list.toArray(new String[0]); + } + + public String[] getAllFilesOf(String fileType) { + if (map.containsKey(fileType)) + return map.get(fileType).toArray(new String[0]); + Logger.warn("Asset", "getAllFilesOf() Method has returned an empty list."); + return new String[0]; + } + + public String getFirstFileOf(String fileType) { + String[] all = getAllFilesOf(fileType); + if (all != null && all.length > 0) + return all[0]; + Logger.warn("Asset", "getFirstFileOf() Method has returned null."); + return null; + } + + public boolean isAvailable() { + return !map.isEmpty() && !list.isEmpty(); + } + } + + + /** The Asset Property Extractor specializing in extracting a specified property. + * @param The type of the specified property, typically {@code String}. + * @since ArkPets 2.2 + */ + public interface PropertyExtractor extends Function> { + /** Extracts the specified property of the given Asset Item. + * @param assetItem The given Asset Item. + * @return A value {@link Set} of the extracted property. + */ + @Override + Set apply(AssetItem assetItem); + + PropertyExtractor ASSET_ITEM_TYPE = item -> Set.of(item.type); + PropertyExtractor ASSET_ITEM_STYLE = item -> Set.of(item.style); + PropertyExtractor ASSET_ITEM_SKIN_GROUP_NAME = item -> Set.of(item.skinGroupName); + PropertyExtractor ASSET_ITEM_SORT_TAGS = item -> new HashSet<>(item.sortTags.toJavaList(String.class)); + } +} diff --git a/core/src/cn/harryh/arkpets/assets/AssetItemGroup.java b/core/src/cn/harryh/arkpets/assets/AssetItemGroup.java new file mode 100644 index 00000000..b1dc8c49 --- /dev/null +++ b/core/src/cn/harryh/arkpets/assets/AssetItemGroup.java @@ -0,0 +1,197 @@ +/** Copyright (c) 2022-2023, Harry Huang + * At GPL-3.0 License + */ +package cn.harryh.arkpets.assets; + +import cn.harryh.arkpets.assets.AssetItem.PropertyExtractor; + +import java.io.File; +import java.util.*; +import java.util.function.Predicate; + + +/** The class implements the Collection of {@link AssetItem}. + *
+ * The structure of the root directory may be like what is shown below. + * Each {@code SubDir} represents an {@code AssetItem}. + *
+ * +-RootDir
+ * |-+-SubDir1 (whose name is the model name of xxx)
+ * | |---xxx.atlas
+ * | |---xxx.png
+ * | |---xxx.skel
+ * |---SubDir2
+ * |---SubDir3
+ * |---...
+ *
+ * @since ArkPets 2.4 + */ +public class AssetItemGroup implements Collection { + protected final ArrayList assetItemList; + + public AssetItemGroup(Collection assetItemList) { + this.assetItemList = new ArrayList<>(assetItemList); + } + + public AssetItemGroup() { + this(new ArrayList<>()); + } + + public AssetItemGroup searchByKeyWords(String keyWords) { + if (keyWords.isEmpty()) + return this; + String[] wordList = keyWords.split(" "); + AssetItemGroup result = new AssetItemGroup(); + for (AssetItem asset : this) { + for (String word : wordList) { + if (asset.name != null && + asset.name.toLowerCase().contains(word.toLowerCase())) { + result.add(asset); + } + } + for (String word : wordList) { + if (asset.appellation != null && + asset.appellation.toLowerCase().contains(word.toLowerCase())) { + if (!result.contains(asset)) + result.add(asset); + } + } + } + return result; + } + + public AssetItem searchByRelPath(String relPath) { + if (relPath.isEmpty()) + return null; + String assetId = new File(relPath).getName(); + for (AssetItem asset : this) + if (asset.assetDir.getName().equalsIgnoreCase(assetId)) + return asset; + return null; + } + + public Set extract(PropertyExtractor property) { + HashSet result = new HashSet<>(); + for (AssetItem item : this) + result.addAll(property.apply(item)); + return result; + } + + public AssetItemGroup filter(Predicate predicate) { + return new AssetItemGroup(assetItemList.stream().filter(predicate).toList()); + } + + public AssetItemGroup filter(PropertyExtractor property, Set filterValues, int mode) { + final boolean TRUE = (mode & FilterMode.MATCH_REVERSE) == 0; + return filter(assetItem -> { + Set itemValues = property.apply(assetItem); + if ((mode & FilterMode.MATCH_ANY) != 0) { + for (T value : itemValues) + if (filterValues.contains(value)) + return TRUE; + } else { + if (itemValues.containsAll(filterValues)) + return TRUE; + } + return !TRUE; + }); + } + + public AssetItemGroup filter(PropertyExtractor property, Set filterValues) { + return filter(property, filterValues, 0); + } + + public void sort() { + assetItemList.sort(Comparator.comparing(asset -> asset.assetDir, Comparator.naturalOrder())); + } + + @Deprecated + public void removeDuplicated() { + ArrayList newList = new ArrayList<>(); + for (AssetItem i : assetItemList) { + boolean flag = true; + for (AssetItem j : newList) { + if (j.equals(i)) { + flag = false; + break; + } + } + if (flag) + newList.add(i); + } + assetItemList.clear(); + assetItemList.addAll(newList); + } + + public static class FilterMode { + public static final int MATCH_ANY = 0b1; + public static final int MATCH_REVERSE = 0b10; + public static final int STRING_IGNORE_CASE = 0b100; + public static final int STRING_PARTIAL = 0b1000; + } + + @Override + public Iterator iterator() { + return assetItemList.iterator(); + } + + @Override + public boolean add(AssetItem assetItem) { + return assetItemList.add(assetItem); + } + + @Override + public boolean addAll(Collection c) { + return assetItemList.addAll(c); + } + + @Override + public boolean contains(Object o) { + return assetItemList.contains(o); + } + + @Override + public boolean containsAll(Collection c) { + return assetItemList.containsAll(c); + } + + @Override + public boolean remove(Object o) { + return assetItemList.remove(o); + } + + @Override + public boolean removeAll(Collection c) { + return assetItemList.removeAll(c); + } + + @Override + public boolean retainAll(Collection c) { + return assetItemList.retainAll(c); + } + + @Override + public void clear() { + assetItemList.clear(); + } + + @Override + public boolean isEmpty() { + return assetItemList.isEmpty(); + } + + @Override + public int size() { + return assetItemList.size(); + } + + @Override + public Object[] toArray() { + return assetItemList.toArray(); + } + + @Override + public T[] toArray(T[] a) { + return assetItemList.toArray(a); + } +} diff --git a/core/src/cn/harryh/arkpets/assets/ModelsDataset.java b/core/src/cn/harryh/arkpets/assets/ModelsDataset.java new file mode 100644 index 00000000..d3d25424 --- /dev/null +++ b/core/src/cn/harryh/arkpets/assets/ModelsDataset.java @@ -0,0 +1,113 @@ +/** Copyright (c) 2022-2023, Harry Huang + * At GPL-3.0 License + */ +package cn.harryh.arkpets.assets; + +import cn.harryh.arkpets.utils.Version; +import com.alibaba.fastjson.JSONObject; +import com.alibaba.fastjson.annotation.JSONField; + +import java.io.File; +import java.io.Serializable; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Objects; + + +public class ModelsDataset { + public final HashMap storageDirectory; + public final HashMap sortTags; + public final String gameDataVersionDescription; + public final String gameDataServerRegion; + public final AssetItemGroup data; + public final Version arkPetsCompatibility; + + public ModelsDataset(JSONObject jsonObject) { + this(jsonObject.toJavaObject(ModelsDatasetBean.class)); + } + + protected ModelsDataset(ModelsDatasetBean bean) { + Objects.requireNonNull(bean); + + storageDirectory = new HashMap<>(); + if (bean.storageDirectory == null || bean.storageDirectory.isEmpty()) + throw new DatasetKeyException("storageDirectory"); + for (String key : bean.storageDirectory.keySet()) + storageDirectory.put(key, Path.of(bean.storageDirectory.get(key)).toFile()); + + sortTags = bean.sortTags; + gameDataVersionDescription = bean.gameDataVersionDescription; + gameDataServerRegion = bean.gameDataServerRegion; + + if (bean.data == null || bean.data.isEmpty()) + throw new DatasetKeyException("data"); + data = new AssetItemGroup(); + for (String key : bean.data.keySet()) { + // Pre deserialization + AssetItem assetItem = bean.data.get(key).toJavaObject(AssetItem.class); + // Make up for `assetDir` field + if (assetItem == null || !storageDirectory.containsKey(assetItem.type)) + throw new DatasetKeyException("type"); + assetItem.assetDir = Path.of(storageDirectory.get(assetItem.type).toString(), key).toFile(); + // Compatible to lower version dataset + if (assetItem.assetList == null && assetItem.assetId != null && assetItem.checksum != null) { + HashMap defaultFileMap = new HashMap<>(); + for (String fileType : AssetItem.extensions) + defaultFileMap.put(fileType, assetItem.assetId + fileType); + assetItem.assetList = new JSONObject(defaultFileMap); + } + data.add(assetItem); + } + data.removeDuplicated(); + data.sort(); + + arkPetsCompatibility = new Version(bean.arkPetsCompatibility); + } + + + public static class DatasetKeyException extends IllegalArgumentException { + public DatasetKeyException(String keyName) { + super("The key \"" + keyName + "\" not found or invalid."); + } + } + + + protected static class ModelsDatasetBean implements Serializable { + private HashMap storageDirectory; + private HashMap sortTags; + private String gameDataVersionDescription; + private String gameDataServerRegion; + private HashMap data; + private int[] arkPetsCompatibility; + + @JSONField + public void setStorageDirectory(HashMap storageDirectory) { + this.storageDirectory = storageDirectory; + } + + @JSONField + public void setSortTags(HashMap sortTags) { + this.sortTags = sortTags; + } + + @JSONField + public void setGameDataVersionDescription(String gameDataVersionDescription) { + this.gameDataVersionDescription = gameDataVersionDescription; + } + + @JSONField + public void setGameDataServerRegion(String gameDataServerRegion) { + this.gameDataServerRegion = gameDataServerRegion; + } + + @JSONField + public void setData(HashMap data) { + this.data = data; + } + + @JSONField + public void setArkPetsCompatibility(int[] arkPetsCompatibility) { + this.arkPetsCompatibility = arkPetsCompatibility; + } + } +} diff --git a/core/src/cn/harryh/arkpets/utils/AssetCtrl.java b/core/src/cn/harryh/arkpets/utils/AssetCtrl.java deleted file mode 100644 index c658a4db..00000000 --- a/core/src/cn/harryh/arkpets/utils/AssetCtrl.java +++ /dev/null @@ -1,339 +0,0 @@ -/** Copyright (c) 2022-2023, Harry Huang - * At GPL-3.0 License - */ -package cn.harryh.arkpets.utils; - -import com.alibaba.fastjson.JSONArray; -import com.alibaba.fastjson.JSONObject; - -import java.io.File; -import java.util.*; - - -/** One Asset Controller is corresponding to one certain asset. - */ -public class AssetCtrl { - public File assetDir; - @Deprecated public String assetId; - public String type; - public String style; - public String name; - public String appellation; - public String skinGroupId; - public String skinGroupName; - public JSONArray sortTags; - public JSONObject assetList; - - private AssetAccessor accessor; - - protected static final String separator = File.separator; - protected static final String[] extensions = {".atlas", ".png", ".skel"}; - - private AssetCtrl() { - } - - /** Gets the directory where the asset files located in. - * @return A relative path string. - */ - public String getLocation() { - return assetDir.toString(); - } - - /** Gets the Asset Accessor of this asset. - * @return An Asset Accessor instance. - */ - public AssetAccessor getAccessor() { - if (accessor == null) - accessor = new AssetAccessor(assetList); - return accessor; - } - - /** Gets a single asset controller instance using the given asset directory. - * @param assetDir File instance of the specified asset directory. - * @param modelsDataset JSONObject of the models' dataset. - * @return The asset controller instance. - */ - public static AssetCtrl getAssetCtrl(File assetDir, JSONObject modelsDataset) { - if (AssetVerifier.isValidAsset(assetDir, modelsDataset)) { - AssetCtrl assetCtrl = modelsDataset.getObject(assetDir.getName(), AssetCtrl.class); - assetCtrl.assetDir = assetDir; - if (assetCtrl.assetList == null && assetCtrl.assetId != null) { - // Compatible to lower version - HashMap defaultFileMap = new HashMap<>(); - for (String fileType : extensions) - defaultFileMap.put(fileType, assetCtrl.assetId + fileType); - assetCtrl.assetList = new JSONObject(defaultFileMap); - } - return assetCtrl; - } - return new AssetCtrl(); - } - - /** Gets asset controller instances from the given root directory. - * @param rootDir File instance of the specified root directory. - * @param modelsDataset JSONObject of the models' dataset. - * @return The list containing asset controller instances. - */ - public static ArrayList getAllAssetCtrls(File rootDir, JSONObject modelsDataset) { - ArrayList list = new ArrayList<>(); - /* WorkDir - * +-rootDir - * |-+-subDir1(as key) - * | |---xxx.atlas - * | |---xxx.png - * | |---xxx.skel - * |---subDir2 - * |---... - */ - if (rootDir.isDirectory()) { - File[] subDirs = rootDir.listFiles(fp -> fp.getParent().equals(rootDir.getPath())); - if (subDirs == null) - return list; - for (File subDir : subDirs) { - if (modelsDataset.containsKey(subDir.getName())) { - if (!AssetVerifier.isExistedAsset(subDir, modelsDataset)) { - //System.out.println("Not Integral: " + subDir.getName()); - continue; - } - list.add(getAssetCtrl(subDir, modelsDataset)); - } - } - } - return list; - } - - /** Sorts the asset controllers. - * @param assetList The specified asset controllers. - * @return The sorted list. - */ - public static ArrayList sortAssetCtrls(ArrayList assetList) { - ArrayList newList = new ArrayList<>(); - for (AssetCtrl i : assetList) { - boolean flag = true; - for (AssetCtrl j : newList) { - if (j.equals(i)) { - flag = false; - break; - } - } - if (flag) - newList.add(i); - } - return newList; - } - - /** Gets all assets' locations of the given asset controllers. - * @param assetList The specified asset controllers. - * @return The list containing asset locations. - */ - public static ArrayList getAssetLocations(ArrayList assetList) { - ArrayList result = new ArrayList<>(); - for (AssetCtrl asset : assetList) - result.add(asset.getLocation()); - return result; - } - - /** Searches assets by keywords in the given asset controller list. - * @param keywords The keywords. - * @param assetList The specified asset controller list. - * @return The list containing asset controller instances that matches the keywords. - */ - public static ArrayList searchByKeyWords(String keywords, ArrayList assetList) { - if (keywords.isEmpty()) - return assetList; - String[] wordList = keywords.split(" "); - ArrayList result = new ArrayList<>(); - for (AssetCtrl asset : assetList) { - for (String word : wordList) { - if (asset.name != null && - asset.name.toLowerCase().contains(word.toLowerCase())) { - result.add(asset); - } - } - for (String word : wordList) { - if (asset.appellation != null && - asset.appellation.toLowerCase().contains(word.toLowerCase())) { - if (!result.contains(asset)) - result.add(asset); - } - } - } - return result; - } - - /** Searches index by relative asset path in the given asset controller list. - * @param assetRelPath The relative asset path (without ext), like {@code "models\xxx_xxx"}. - * @param assetList The specified asset controller list. - * @return The index of the 1st matched asset, otherwise {@code 0} will be return by default. - */ - public static int searchByAssetRelPath(String assetRelPath, ArrayList assetList) { - if (assetRelPath.isEmpty()) - return 0; - String assetId = new File(assetRelPath).getName(); - for (int i = 0; i < assetList.size(); i++) { - if (assetList.get(i).assetDir.getName().equalsIgnoreCase(assetId)) - return i; - } - return 0; - } - - @Override - public String toString() { - return name; - } - - @Override - public int hashCode() { - return assetDir.hashCode(); - } - - @Override - public boolean equals(Object obj) { - if (obj instanceof AssetCtrl) { - return ((AssetCtrl)obj).assetDir.equals(assetDir); - } - return false; - } - - /** The Asset Accessor providing methods to get the resource files of the asset - * @since ArkPets 2.2 - */ - public static class AssetAccessor { - private final ArrayList list; - private final HashMap> map; - - public AssetAccessor(JSONObject fileMap) { - ArrayList list = new ArrayList<>(); - HashMap> map = new HashMap<>(); - try { - if (fileMap != null && !fileMap.isEmpty()) { - for (String fileType : fileMap.keySet()) { - try { - JSONArray someFiles; // Try to get as array - if ((someFiles = fileMap.getJSONArray(fileType)) != null) { - var temp = someFiles.toJavaList(String.class); - list.addAll(temp); - map.put(fileType, new ArrayList<>(temp)); - } - } catch (com.alibaba.fastjson.JSONException | com.alibaba.fastjson2.JSONException ex) { - String oneFile; // Try to get as string - if ((oneFile = fileMap.getString(fileType)) != null) { - list.add(oneFile); - map.put(fileType, new ArrayList<>(List.of(oneFile))); - } - } - } - } - } catch (Exception e) { - Logger.error("AssetCtrl", "Failed to establish an asset accessor, details see below.", e); - list = new ArrayList<>(); - map = new HashMap<>(); - } - this.list = list; - this.map = map; - } - - public String[] getAllFiles() { - return list.toArray(new String[0]); - } - - public String[] getAllFilesOf(String fileType) { - if (map.containsKey(fileType)) - return map.get(fileType).toArray(new String[0]); - Logger.warn("AssetCtrl", "getAllFilesOf() Method has returned an empty list."); - return new String[0]; - } - - public String getFirstFileOf(String fileType) { - String[] all = getAllFilesOf(fileType); - if (all != null && all.length > 0) - return all[0]; - Logger.warn("AssetCtrl", "getFirstFileOf() Method has returned null."); - return null; - } - - public boolean isAvailable() { - return !map.isEmpty() && !list.isEmpty(); - } - } - - public enum AssetStatus { - NONE, VALID, EXISTED, CHECKED - } - - /** The Asset Verifier providing methods to verify the integrity of the asset - * @since ArkPets 2.2 - */ - public static class AssetVerifier { - private final JSONObject data; - - /** Initializes an Asset Verifier using the given dataset. - * @param modelsDataset The specified dataset. - */ - public AssetVerifier(JSONObject modelsDataset) { - data = modelsDataset; - } - - /** Verifies the integrity of the given asset. - * @param assetDir The asset directory. - * @return Asset Status enumeration. - */ - public AssetStatus verify(File assetDir) { - AssetStatus result = AssetStatus.NONE; - if (isValidAsset(assetDir, data)) { - result = AssetStatus.VALID; - if (isExistedAsset(assetDir, data)) { - result = AssetStatus.EXISTED; - if (isCheckedAsset(assetDir, data)) { - result = AssetStatus.CHECKED; - } - } - } - return result; - } - - private static boolean isValidAsset(File assetDir, JSONObject modelsDataset) { - if (assetDir == null || modelsDataset == null) - return false; - if (!modelsDataset.containsKey(assetDir.getName())) { - Logger.warn("Verifier", "The asset directory " + assetDir.getPath() + " is undefined."); - return false; - } - return true; - } - - private static boolean isExistedAsset(File assetDir, JSONObject modelsDataset) { - try { - if (!assetDir.isDirectory()) { - Logger.warn("Verifier", "The asset directory " + assetDir.getPath() + " is missing."); - return false; - } - return true; - } catch (Exception e) { - Logger.warn("Verifier", "Failed to handle the asset " + assetDir.getName()); - return false; - } - } - - private static boolean isCheckedAsset(File assetDir, JSONObject modelsDataset) { - try { - AssetCtrl assetCtrl = getAssetCtrl(assetDir, modelsDataset); - if (!assetCtrl.getAccessor().isAvailable()) - return true; // Skip data-emptied asset (marked as verified) - ArrayList existed = new ArrayList<>(List.of(Objects.requireNonNull(assetDir.list()))); - existed.replaceAll(String::toLowerCase); - for (String fileName : assetCtrl.getAccessor().getAllFiles()) { - fileName = fileName.toLowerCase(); - if (!existed.contains(fileName)) { - Logger.warn("Verifier", "The asset file " + fileName + " (" + assetDir + ") is missing."); - return false; - } - } - return true; - } catch (Exception e) { - Logger.warn("Verifier", "Failed to handle the asset " + assetDir.getName()); - return false; - } - } - } -} diff --git a/desktop/src/cn/harryh/arkpets/controllers/Homepage.java b/desktop/src/cn/harryh/arkpets/controllers/Homepage.java index 96e89d40..88d6fee0 100644 --- a/desktop/src/cn/harryh/arkpets/controllers/Homepage.java +++ b/desktop/src/cn/harryh/arkpets/controllers/Homepage.java @@ -4,6 +4,9 @@ package cn.harryh.arkpets.controllers; import cn.harryh.arkpets.ArkConfig; +import cn.harryh.arkpets.assets.AssetItem; +import cn.harryh.arkpets.assets.AssetItemGroup; +import cn.harryh.arkpets.assets.ModelsDataset; import cn.harryh.arkpets.guitasks.*; import cn.harryh.arkpets.utils.*; import com.alibaba.fastjson.JSONObject; @@ -28,11 +31,9 @@ import javafx.util.Duration; import java.io.*; -import java.net.*; import java.util.*; import java.util.function.Consumer; -import java.util.zip.ZipException; -import javax.net.ssl.SSLException; +import java.util.function.Predicate; import static cn.harryh.arkpets.Const.*; import static cn.harryh.arkpets.utils.ControlUtils.*; @@ -80,7 +81,7 @@ public final class Homepage { @FXML private JFXTextField searchModelInput; @FXML - private JFXListView> searchModelList; + private JFXListView> searchModelView; @FXML private JFXComboBox searchModelFilter; @FXML @@ -164,12 +165,12 @@ public final class Homepage { @FXML private Label aboutGitHub; - private ListCell selectedModelItem; - private ArrayList foundModelAssets = new ArrayList<>(); - private ArrayList> foundModelItems = new ArrayList<>(); + private AssetItemGroup assetItemList; + private JFXListCell selectedModelCell; + private ArrayList> modelCellList = new ArrayList<>(); public ArkConfig config; - public JSONObject modelsDatasetFull; + public ModelsDataset modelsDataset; private NoticeBar appVersionNotice; private NoticeBar diskFreeSpaceNotice; private NoticeBar datasetIncompatibleNotice; @@ -203,6 +204,7 @@ public void initialize() { config.saveConfig(); menuBtn1.getStyleClass().add("menu-btn-active"); new CheckAppUpdateTask(root, GuiTask.GuiTaskStyle.HIDDEN, "auto").start(); + }); } @@ -263,9 +265,9 @@ private void initModelSearch() { searchModelFilter.getItems().setAll("全部"); searchModelFilter.getSelectionModel().select(0); if (assertModelLoaded(false)) { - Set filterTags = modelsDatasetFull.getJSONObject("sortTags").keySet(); + Set filterTags = modelsDataset.sortTags.keySet(); for (String s : filterTags) - searchModelFilter.getItems().add(modelsDatasetFull.getJSONObject("sortTags").getString(s)); + searchModelFilter.getItems().add(modelsDataset.sortTags.get(s)); } searchModelFilter.valueProperty().addListener(filterListener); } @@ -273,21 +275,16 @@ private void initModelSearch() { private boolean initModelDataset(boolean doPopNotice) { try { try { - // Read dataset file - modelsDatasetFull = Objects.requireNonNull(JSONObject.parseObject(IOUtils.FileUtil.readString(new File(PathConfig.fileModelsDataPath), charsetDefault))); - // Assert keys existed - if (!modelsDatasetFull.containsKey("data")) - throw new DatasetException("The key 'data' may not in the dataset."); - if (!modelsDatasetFull.containsKey("storageDirectory")) - throw new DatasetException("The key 'storageDirectory' may not in the dataset."); - if (!modelsDatasetFull.containsKey("sortTags")) - throw new DatasetException("The key 'sortTags' may not in the dataset."); + // Read and initialize the dataset + modelsDataset = new ModelsDataset( + JSONObject.parseObject( + IOUtils.FileUtil.readString(new File(PathConfig.fileModelsDataPath), charsetDefault) + ) + ); + modelsDataset.data.removeIf(Predicate.not(AssetItem::isValid)); try { - // Check dataset compatibility - if (!modelsDatasetFull.containsKey("arkPetsCompatibility")) - throw new DatasetException("The key 'arkPetsCompatibility' may not in the dataset."); - int[] acVersionResult = modelsDatasetFull.getObject("arkPetsCompatibility", int[].class); - Version acVersion = new Version(acVersionResult); + // Check the dataset compatibility + Version acVersion = modelsDataset.arkPetsCompatibility; if (appVersion.lessThan(acVersion)) { isDatasetIncompatible = true; Logger.warn("ModelManager", "The model dataset may be incompatible (required " + acVersion + " or newer)"); @@ -300,15 +297,18 @@ private boolean initModelDataset(boolean doPopNotice) { Logger.debug("ModelManager", "Initialized model dataset successfully."); return true; } catch (Exception e) { - modelsDatasetFull = null; + // Explicitly set models dataset to empty. + modelsDataset = null; throw e; } + + // If any exception occurred during the progress above: } catch (FileNotFoundException e) { Logger.warn("ModelManager", "Failed to initialize model dataset due to file not found. (" + e.getMessage() + ")"); if (doPopNotice) DialogUtil.createCommonDialog(root, IconUtil.getIcon(IconUtil.ICON_WARNING_ALT, COLOR_WARNING), "模型载入失败", "模型未成功载入:未找到数据集。", "模型数据集文件 " + PathConfig.fileModelsDataPath + " 可能不在工作目录下。\n请先前往 [选项] 进行模型下载。", null).show(); - } catch (DatasetException e) { + } catch (ModelsDataset.DatasetKeyException e) { Logger.warn("ModelManager", "Failed to initialize model dataset due to dataset parsing error. (" + e.getMessage() + ")"); if (doPopNotice) DialogUtil.createCommonDialog(root, IconUtil.getIcon(IconUtil.ICON_WARNING_ALT, COLOR_WARNING), "模型载入失败", "模型未成功载入:数据集解析失败。", @@ -327,27 +327,25 @@ private boolean initModelAssets(boolean doPopNotice) { return false; try { // Find every model assets. - ArrayList foundModelAssets = new ArrayList<>(); - JSONObject modelsDatasetStorageDirectory = modelsDatasetFull.getJSONObject("storageDirectory"); - JSONObject modelsDatasetData = modelsDatasetFull.getJSONObject("data"); - for (String key : modelsDatasetStorageDirectory.keySet()) - foundModelAssets.addAll(AssetCtrl.getAllAssetCtrls(new File(modelsDatasetStorageDirectory.getString(key)), modelsDatasetData)); - this.foundModelAssets = AssetCtrl.sortAssetCtrls(foundModelAssets); - if (this.foundModelAssets.isEmpty()) + assetItemList = modelsDataset.data.filter(AssetItem::isExisted); + if (assetItemList.isEmpty()) throw new IOException("Found no assets in the target directories."); + // Initialize list view: + searchModelView.getSelectionModel().getSelectedItems().addListener( + (ListChangeListener>) (observable -> observable.getList().forEach( + (Consumer>) cell -> selectModel(cell.getItem(), cell)) + ) + ); + searchModelView.setFixedCellSize(30); // Write models to menu items. - ArrayList> foundModelItems = new ArrayList<>(); - searchModelList.getSelectionModel().getSelectedItems().addListener((ListChangeListener>) - (observable -> observable.getList().forEach((Consumer>)cell -> selectModel(cell.getItem(), cell)))); - searchModelList.setFixedCellSize(30); - for (AssetCtrl asset : this.foundModelAssets) - foundModelItems.add(getMenuItem(asset, searchModelList)); - this.foundModelItems = foundModelItems; + modelCellList = new ArrayList<>(); + assetItemList.forEach(assetItem -> modelCellList.add(getMenuItem(assetItem, searchModelView))); Logger.debug("ModelManager", "Initialized model assets successfully."); return true; } catch (IOException e) { - foundModelAssets = new ArrayList<>(); - foundModelItems = new ArrayList<>(); + // Explicitly set all lists to empty. + assetItemList = new AssetItemGroup(); + modelCellList = new ArrayList<>(); Logger.error("ModelManager", "Failed to initialize model assets due to unknown reasons, details see below.", e); if (doPopNotice) DialogUtil.createCommonDialog(root, IconUtil.getIcon(IconUtil.ICON_WARNING_ALT, COLOR_WARNING), "模型载入失败", "模型未成功载入:读取模型列表失败。", @@ -524,7 +522,7 @@ protected void onSucceeded(boolean result) { /* Foreground verify models */ if (!initModelDataset(true)) return; - new VerifyModelsTask(root, GuiTask.GuiTaskStyle.COMMON, modelsDatasetFull, foundModelAssets).start(); + new VerifyModelsTask(root, GuiTask.GuiTaskStyle.COMMON, modelsDataset).start(); }); } @@ -729,35 +727,33 @@ public void popLoading(EventHandler onLoading) { } private void dealModelSearch(String keyWords) { - searchModelList.getItems().clear(); - ArrayList result = AssetCtrl.searchByKeyWords(keyWords, foundModelAssets); - ArrayList assetIdList = AssetCtrl.getAssetLocations(result); - String tag = ""; - if (assertModelLoaded(false)) - for (String s : modelsDatasetFull.getJSONObject("sortTags").keySet()) - if (searchModelFilter.getValue().equals(modelsDatasetFull.getJSONObject("sortTags").getString(s))) - tag = s; - for (JFXListCell item : foundModelItems) { - // Search by keywords and tags - for (String assetId : assetIdList) { - if (item.getId().equals(assetId) && - (isNoFilter || tag.isEmpty() || item.getItem().sortTags == null || item.getItem().sortTags.contains(tag))) { - searchModelList.getItems().add(item); - break; - } - } + searchModelView.getItems().clear(); + if (assertModelLoaded(false)) { + // Handle tag + String filterTag = ""; + for (String s : modelsDataset.sortTags.keySet()) + if (searchModelFilter.getValue().equals(modelsDataset.sortTags.get(s))) + filterTag = s; + // Filter and search assets + AssetItemGroup filtered = filterTag.isEmpty() ? assetItemList : + assetItemList.filter(AssetItem.PropertyExtractor.ASSET_ITEM_SORT_TAGS, Set.of(filterTag)); + AssetItemGroup searched = filtered.searchByKeyWords(keyWords); + // Add cells + for (JFXListCell cell : modelCellList) + if (searched.contains(cell.getItem())) + searchModelView.getItems().add(cell); } - Logger.info("ModelManager", "Search \"" + keyWords + "\" (" + searchModelList.getItems().size() + ")"); - searchModelList.refresh(); + Logger.info("ModelManager", "Search \"" + keyWords + "\" (" + searchModelView.getItems().size() + ")"); + searchModelView.refresh(); } private void dealModelRandom() { if (!assertModelLoaded(true)) return; - int idx = (int)(Math.random() * (searchModelList.getItems().size() - 1)); - searchModelList.scrollTo(idx); - searchModelList.getSelectionModel().select(idx); - searchModelList.requestFocus(); + int idx = (int)(Math.random() * (searchModelView.getItems().size() - 1)); + searchModelView.scrollTo(idx); + searchModelView.getSelectionModel().select(idx); + searchModelView.requestFocus(); } private void dealModelReload(boolean doPopNotice) { @@ -765,49 +761,53 @@ private void dealModelReload(boolean doPopNotice) { initModelAssets(doPopNotice); initModelSearch(); dealModelSearch(""); - if (!foundModelItems.isEmpty() && config.character_asset != null && !config.character_asset.isEmpty()) { + if (!modelCellList.isEmpty() && config.character_asset != null && !config.character_asset.isEmpty()) { // Scroll to recent selected model - int character_asset_idx = AssetCtrl.searchByAssetRelPath(config.character_asset, foundModelAssets); - searchModelList.scrollTo(character_asset_idx); - searchModelList.getSelectionModel().select(character_asset_idx); + AssetItem recentSelected = assetItemList.searchByRelPath(config.character_asset); + if (recentSelected != null) + for (JFXListCell cell : searchModelView.getItems()) + if (recentSelected.equals(cell.getItem())) { + searchModelView.scrollTo(cell); + searchModelView.getSelectionModel().select(cell); + } } - loadFailureTip.setVisible(foundModelItems.isEmpty()); - startBtn.setDisable(foundModelItems.isEmpty()); + loadFailureTip.setVisible(modelCellList.isEmpty()); + startBtn.setDisable(modelCellList.isEmpty()); System.gc(); Logger.info("ModelManager", "Reloaded"); }); } - private JFXListCell getMenuItem(AssetCtrl assetCtrl, JFXListView> container) { + private JFXListCell getMenuItem(AssetItem assetItem, JFXListView> container) { double width = container.getPrefWidth(); width -= container.getPadding().getLeft() + container.getPadding().getRight(); width *= 0.75; double height = 30; double divide = 0.618; - JFXListCell item = new JFXListCell<>(); + JFXListCell item = new JFXListCell<>(); item.getStyleClass().addAll("Search-models-item"); - Label name = new Label(assetCtrl.toString()); + Label name = new Label(assetItem.toString()); name.getStyleClass().addAll("Search-models-label", "Search-models-label-primary"); - name.setPrefSize(assetCtrl.skinGroupName == null ? width : width * divide, height); + name.setPrefSize(assetItem.skinGroupName == null ? width : width * divide, height); name.setLayoutX(0); - Label alias1 = new Label(assetCtrl.skinGroupName); + Label alias1 = new Label(assetItem.skinGroupName); alias1.getStyleClass().addAll("Search-models-label", "Search-models-label-secondary"); alias1.setPrefSize(width * (1 - divide), height); - alias1.setLayoutX(assetCtrl.skinGroupName == null ? 0 : width * divide); + alias1.setLayoutX(assetItem.skinGroupName == null ? 0 : width * divide); item.setPrefSize(width, height); item.setGraphic(new Group(name, alias1)); - item.setItem(assetCtrl); - item.setId(assetCtrl.getLocation()); + item.setItem(assetItem); + item.setId(assetItem.getLocation()); return item; } - private void selectModel(AssetCtrl asset, ListCell item) { + private void selectModel(AssetItem asset, JFXListCell item) { // Reset - if (selectedModelItem != null) - selectedModelItem.getStyleClass().setAll("Search-models-item"); - selectedModelItem = item; - selectedModelItem.getStyleClass().add("Search-models-item-active"); + if (selectedModelCell != null) + selectedModelCell.getStyleClass().setAll("Search-models-item"); + selectedModelCell = item; + selectedModelCell.getStyleClass().add("Search-models-item-active"); // Display details selectedModelName.setText(asset.name); selectedModelAppellation.setText(asset.appellation); @@ -832,7 +832,7 @@ private void selectModel(AssetCtrl asset, ListCell item) { } private boolean assertModelLoaded(boolean doPopNotice) { - if (modelsDatasetFull == null) { + if (modelsDataset == null) { // Not loaded: if (doPopNotice) DialogUtil.createCommonDialog(root, IconUtil.getIcon(IconUtil.ICON_WARNING_ALT, COLOR_WARNING), "未能加载模型", "请确保模型加载成功后再进行此操作。", @@ -866,12 +866,6 @@ private static void fadeOutNode(Node node, Duration duration, EventHandler foundModelAssets; + protected final ModelsDataset modelsDataset; private final Node[] dialogGraphic = new Node[1]; private final String[] dialogHeader = new String[1]; private final String[] dialogContent = new String[1]; - public VerifyModelsTask(StackPane root, GuiTaskStyle style, JSONObject modelsDatasetFull, Iterable foundModelAssets) { + public VerifyModelsTask(StackPane root, GuiTaskStyle style, ModelsDataset modelsDataset) { super(root, style); - this.modelsDatasetFull = modelsDatasetFull; - this.foundModelAssets = foundModelAssets; + this.modelsDataset = modelsDataset; } @Override @@ -45,36 +41,34 @@ protected String getInitialContent() { protected Task getTask() { return new Task<>() { @Override - protected Boolean call() throws Exception { - ArrayList pendingDirs = new ArrayList<>(); - JSONObject modelsDatasetData = modelsDatasetFull.getJSONObject("data"); - for (AssetCtrl assetCtrl : foundModelAssets) - pendingDirs.add(new File(assetCtrl.getLocation())); + protected Boolean call() { + AssetItemGroup validModelAssets = modelsDataset.data.filter(AssetItem::isValid); + int currentProgress = 0; + int totalProgress = validModelAssets.size(); - Thread.sleep(100); boolean flag = false; - AssetCtrl.AssetVerifier assetVerifier = new AssetCtrl.AssetVerifier(modelsDatasetData); - for (int i = 0; i < pendingDirs.size(); i++) { - this.updateProgress(i, pendingDirs.size()); - File file = pendingDirs.get(i); - AssetCtrl.AssetStatus result = assetVerifier.verify(file); - if (result == AssetCtrl.AssetStatus.VALID) { - Logger.info("Checker", "Model repo check finished (not integral)"); - dialogGraphic[0] = PopupUtils.IconUtil.getIcon(PopupUtils.IconUtil.ICON_WARNING_ALT, COLOR_WARNING); - dialogHeader[0] = "已发现问题,模型资源可能不完整"; - dialogContent[0] = "资源 " + file.getPath() + " 可能不存在,重新下载模型文件可能解决此问题。"; - flag = true; - break; - } else if (result == AssetCtrl.AssetStatus.EXISTED) { - Logger.info("Checker", "Model repo check finished (checksum mismatch)"); - dialogGraphic[0] = PopupUtils.IconUtil.getIcon(PopupUtils.IconUtil.ICON_WARNING_ALT, COLOR_WARNING); - dialogHeader[0] = "已发现问题,模型资源可能不完整"; - dialogContent[0] = "资源 " + file.getPath() + " 可能缺少部分文件,重新下载模型文件可能解决此问题。"; - flag = true; - break; - } else if (this.isCancelled()) { + for (AssetItem item : validModelAssets) { + this.updateProgress(currentProgress++, totalProgress); + if (this.isCancelled()) { + // Cancelled: Logger.info("Checker", "Model repo check was cancelled in verification stage."); return false; + } else if (!item.isChecked()) { + if (!item.isExisted()) { + // Dir missing: + Logger.info("Checker", "Model repo check finished (dir not integral)"); + dialogGraphic[0] = PopupUtils.IconUtil.getIcon(PopupUtils.IconUtil.ICON_WARNING_ALT, COLOR_WARNING); + dialogHeader[0] = "已发现问题,模型资源可能不完整"; + dialogContent[0] = "资源 " + item.assetDir + " 不存在。重新下载模型文件可能解决此问题。"; + } else { + // Dir existing but file missing + Logger.info("Checker", "Model repo check finished (file not integral)"); + dialogGraphic[0] = PopupUtils.IconUtil.getIcon(PopupUtils.IconUtil.ICON_WARNING_ALT, COLOR_WARNING); + dialogHeader[0] = "已发现问题,模型资源可能不完整"; + dialogContent[0] = "资源 " + item.assetDir + " 缺少部分文件。重新下载模型文件可能解决此问题。"; + } + flag = true; + break; } }