From 71b92fe9e151acbeb99f8d2c6959d7b18dfec733 Mon Sep 17 00:00:00 2001 From: Harry Huang Date: Mon, 20 Jan 2025 11:41:34 +0800 Subject: [PATCH 1/5] refactor: rename asset to model --- core/src/cn/harryh/arkpets/ArkChar.java | 8 +- .../assets/{AssetItem.java => ModelItem.java} | 43 ++++---- ...ssetItemGroup.java => ModelItemGroup.java} | 104 +++++++++--------- .../harryh/arkpets/assets/ModelsDataset.java | 22 ++-- .../arkpets/controllers/ModelsModule.java | 68 ++++++------ .../arkpets/guitasks/VerifyModelsTask.java | 8 +- 6 files changed, 127 insertions(+), 126 deletions(-) rename core/src/cn/harryh/arkpets/assets/{AssetItem.java => ModelItem.java} (87%) rename core/src/cn/harryh/arkpets/assets/{AssetItemGroup.java => ModelItemGroup.java} (59%) diff --git a/core/src/cn/harryh/arkpets/ArkChar.java b/core/src/cn/harryh/arkpets/ArkChar.java index 97919d37..3fad7a5a 100644 --- a/core/src/cn/harryh/arkpets/ArkChar.java +++ b/core/src/cn/harryh/arkpets/ArkChar.java @@ -8,7 +8,7 @@ import cn.harryh.arkpets.animations.AnimClipGroup; import cn.harryh.arkpets.animations.AnimComposer; import cn.harryh.arkpets.animations.AnimData; -import cn.harryh.arkpets.assets.AssetItem.AssetAccessor; +import cn.harryh.arkpets.assets.ModelItem.ModelAssetAccessor; import cn.harryh.arkpets.transitions.EasingFunction; import cn.harryh.arkpets.transitions.TransitionFloat; import cn.harryh.arkpets.transitions.TransitionVector3; @@ -87,9 +87,9 @@ public ArkChar(ArkConfig config, float scale) { SkeletonData skeletonData; try { String assetLocation = config.character_asset; - AssetAccessor assetAccessor = new AssetAccessor(config.character_files); - String path2atlas = assetLocation + separator + assetAccessor.getFirstFileOf(".atlas"); - String path2skel = assetLocation + separator + assetAccessor.getFirstFileOf(".skel"); + ModelAssetAccessor modelAssetAccessor = new ModelAssetAccessor(config.character_files); + String path2atlas = assetLocation + separator + modelAssetAccessor.getFirstFileOf(".atlas"); + String path2skel = assetLocation + separator + modelAssetAccessor.getFirstFileOf(".skel"); // Load atlas TextureAtlas atlas = new TextureAtlas(Gdx.files.internal(path2atlas)); // Load skel (use SkeletonJson instead of SkeletonBinary if the file type is JSON) diff --git a/core/src/cn/harryh/arkpets/assets/AssetItem.java b/core/src/cn/harryh/arkpets/assets/ModelItem.java similarity index 87% rename from core/src/cn/harryh/arkpets/assets/AssetItem.java rename to core/src/cn/harryh/arkpets/assets/ModelItem.java index 0c50a06e..a3fec754 100644 --- a/core/src/cn/harryh/arkpets/assets/AssetItem.java +++ b/core/src/cn/harryh/arkpets/assets/ModelItem.java @@ -14,9 +14,9 @@ import java.util.function.Function; -/** One Asset Item is corresponding to one certain local Spine asset. +/** One Model Item is corresponding to one certain local Spine model. */ -public class AssetItem implements Serializable { +public class ModelItem implements Serializable { @JSONField(serialize = false) public String key; @JSONField(serialize = false) @@ -42,11 +42,11 @@ public class AssetItem implements Serializable { /** @deprecated Legacy field in old version dataset */ @JSONField @Deprecated public JSONObject checksum; - private AssetAccessor accessor; + private ModelAssetAccessor accessor; protected static final String[] extensions = {".atlas", ".png", ".skel"}; - private AssetItem() { + private ModelItem() { } /** Gets the directory where the asset files located in. @@ -57,20 +57,20 @@ public String getLocation() { return assetDir.toString(); } - /** Gets the Asset Accessor of this asset. - * @return An Asset Accessor instance. + /** Gets the Model Asset Accessor of the model's asset files. + * @return A Model Asset Accessor instance. */ @JSONField(serialize = false) - public AssetAccessor getAccessor() { + public ModelAssetAccessor getAccessor() { if (accessor == null) - accessor = new AssetAccessor(assetList); + accessor = new ModelAssetAccessor(assetList); return accessor; } - /** Verifies the integrity of the necessary fields of this {@code AssetItem}. + /** Verifies the integrity of the necessary fields of this {@code ModelItem}. * @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. + * 2. The {@code ModelAssetAccessor} is available. */ @JSONField(serialize = false) public boolean isValid() { @@ -127,21 +127,21 @@ public int hashCode() { @Override public boolean equals(Object obj) { - if (obj instanceof AssetItem) { - return ((AssetItem)obj).assetDir.equals(assetDir); + if (obj instanceof ModelItem) { + return ((ModelItem)obj).assetDir.equals(assetDir); } return false; } - /** The Asset Accessor providing methods to get the resource files of the asset. + /** The Model Asset Accessor providing methods to get the resource files of the model's asset files. * @since ArkPets 2.2 */ - public static class AssetAccessor { + public static class ModelAssetAccessor { private final ArrayList list; private final HashMap> map; - public AssetAccessor(JSONObject fileMap) { + public ModelAssetAccessor(JSONObject fileMap) { ArrayList list = new ArrayList<>(); HashMap> map = new HashMap<>(); try { @@ -197,17 +197,17 @@ public boolean isAvailable() { } - /** The Asset Property Extractor specializing in extracting a specified property. + /** The Model 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. + public interface PropertyExtractor extends Function> { + /** Extracts the specified property of the given Model Item. + * @param modelItem The given Model Item. * @return A value {@link Set} of the extracted property. */ @Override - Set apply(AssetItem assetItem); + Set apply(ModelItem modelItem); PropertyExtractor ASSET_ITEM_KEY = item -> item.key == null ? Set.of() : Set.of(item.key); PropertyExtractor ASSET_ITEM_TYPE = item -> item.type == null ? Set.of() : Set.of(item.type); @@ -216,7 +216,8 @@ public interface PropertyExtractor extends Function> { PropertyExtractor ASSET_ITEM_SORT_TAGS = item -> new HashSet<>(item.sortTags.toJavaList(String.class)); } - /** The Asset Prefab storing the user prefab of the specific asset. + + /** The Model Prefab storing the user prefab of the specific model. * @since ArkPets 3.5 */ public static class AssetPrefab { diff --git a/core/src/cn/harryh/arkpets/assets/AssetItemGroup.java b/core/src/cn/harryh/arkpets/assets/ModelItemGroup.java similarity index 59% rename from core/src/cn/harryh/arkpets/assets/AssetItemGroup.java rename to core/src/cn/harryh/arkpets/assets/ModelItemGroup.java index 4fa07d5d..7e701496 100644 --- a/core/src/cn/harryh/arkpets/assets/AssetItemGroup.java +++ b/core/src/cn/harryh/arkpets/assets/ModelItemGroup.java @@ -3,16 +3,16 @@ */ package cn.harryh.arkpets.assets; -import cn.harryh.arkpets.assets.AssetItem.PropertyExtractor; +import cn.harryh.arkpets.assets.ModelItem.PropertyExtractor; import java.util.*; import java.util.function.Predicate; -/** The class implements the Collection of {@link AssetItem}. +/** The class implements the Collection of {@link ModelItem}. *
* The structure of the root directory may be like what is shown below. - * Each {@code SubDir} represents an {@code AssetItem}. + * Each {@code SubDir} represents an {@code ModelItem}. * *
  * +-RootDir
@@ -27,27 +27,27 @@
  *
  * @since ArkPets 2.4
  */
-public class AssetItemGroup implements Collection {
-    protected final ArrayList assetItemList;
+public class ModelItemGroup implements Collection {
+    protected final ArrayList modelItemList;
 
-    public AssetItemGroup(Collection assetItemList) {
-        this.assetItemList = new ArrayList<>(assetItemList);
+    public ModelItemGroup(Collection modelItemList) {
+        this.modelItemList = new ArrayList<>(modelItemList);
     }
 
-    public AssetItemGroup() {
+    public ModelItemGroup() {
         this(new ArrayList<>());
     }
 
-    /** Searches the Asset Items whose {@code name} and {@code appellation} match the given keywords.
+    /** Searches the Model Items whose {@code name} and {@code appellation} match the given keywords.
      * @param keyWords The given keywords. Each keyword should be separated by a blank.
-     * @return An Asset Item Group. Returns {@code this} if the parameter {@code keyWords} is {@code null} or empty.
+     * @return A Model Item Group. Returns {@code this} if the parameter {@code keyWords} is {@code null} or empty.
      */
-    public AssetItemGroup searchByKeyWords(String keyWords) {
+    public ModelItemGroup searchByKeyWords(String keyWords) {
         if (keyWords == null || keyWords.isEmpty())
             return this;
         String[] wordList = keyWords.split(" ");
-        AssetItemGroup result = new AssetItemGroup();
-        for (AssetItem asset : this) {
+        ModelItemGroup result = new ModelItemGroup();
+        for (ModelItem asset : this) {
             for (String word : wordList) {
                 if (asset.name != null &&
                         asset.name.toLowerCase().contains(word.toLowerCase())) {
@@ -65,50 +65,50 @@ public AssetItemGroup searchByKeyWords(String keyWords) {
         return result;
     }
 
-    /** Searches the Asset Item whose relative path provided by {@code getLocation} matches the given path string.
+    /** Searches the Model Item whose relative path provided by {@code getLocation} matches the given path string.
      * @param relPath The given path string.
-     * @return The first matched Asset Item. Returns {@code null} if no one matched.
+     * @return The first matched Model Item. Returns {@code null} if no one matched.
      */
-    public AssetItem searchByRelPath(String relPath) {
+    public ModelItem searchByRelPath(String relPath) {
         if (relPath == null || relPath.isEmpty())
             return null;
-        for (AssetItem asset : this)
-            if (asset.getLocation().equalsIgnoreCase(relPath))
-                return asset;
+        for (ModelItem model : this)
+            if (model.getLocation().equalsIgnoreCase(relPath))
+                return model;
         return null;
     }
 
-    /** Collects the values of a specified property of the Asset Items.
+    /** Collects the values of a specified property of the Model Items.
      * @param property A property extractor.
      * @return A Set that contains all the possible values of the property.
      * @param  The type of the property value.
      */
     public  Set extract(PropertyExtractor property) {
         HashSet result = new HashSet<>();
-        for (AssetItem item : this)
+        for (ModelItem item : this)
             result.addAll(property.apply(item));
         return result;
     }
 
-    /** Returns a new Asset Item Group consisting of the Asset Items that match the given predicate.
-     * @param predicate A predicate to apply to each Asset Item to determine if it should be included.
-     * @return An Asset Item Group.
+    /** Returns a new Model Item Group consisting of the Model Items that match the given predicate.
+     * @param predicate A predicate to apply to each Model Item to determine if it should be included.
+     * @return A Model Item Group.
      */
-    public AssetItemGroup filter(Predicate predicate) {
-        return new AssetItemGroup(assetItemList.stream().filter(predicate).toList());
+    public ModelItemGroup filter(Predicate predicate) {
+        return new ModelItemGroup(modelItemList.stream().filter(predicate).toList());
     }
 
-    /** Returns a new Asset Item Group consisting of the Asset Items whose property satisfied the requirements.
+    /** Returns a new Model Item Group consisting of the Model Items whose property satisfied the requirements.
      * @param property A property extractor.
      * @param filterValues The property values to be matched.
-     * @param mode The {@link AssetItemGroup.FilterMode}.
-     * @return An Asset Item Group.
+     * @param mode The {@link ModelItemGroup.FilterMode}.
+     * @return A Model Item Group.
      * @param  The type of the property value.
      */
-    public  AssetItemGroup filter(PropertyExtractor property, Set filterValues, int mode) {
+    public  ModelItemGroup filter(PropertyExtractor property, Set filterValues, int mode) {
         final boolean TRUE = (mode & FilterMode.MATCH_REVERSE) == 0;
-        return filter(assetItem -> {
-            Set itemValues = property.apply(assetItem);
+        return filter(item -> {
+            Set itemValues = property.apply(item);
             if ((mode & FilterMode.MATCH_ANY) != 0) {
                 for (T value : itemValues)
                     if (filterValues.contains(value))
@@ -121,20 +121,20 @@ public  AssetItemGroup filter(PropertyExtractor property, Set filterVal
         });
     }
 
-    /** Returns a new Asset Item Group consisting of the Asset Items whose property satisfied the requirements.
+    /** Returns a new Model Item Group consisting of the Model Items whose property satisfied the requirements.
      * @param property A property extractor.
      * @param filterValues The property values to be matched.
-     * @return An Asset Item Group.
+     * @return A Model Item Group.
      * @param  The type of the property value.
      */
-    public  AssetItemGroup filter(PropertyExtractor property, Set filterValues) {
+    public  ModelItemGroup filter(PropertyExtractor property, Set filterValues) {
         return filter(property, filterValues, 0);
     }
 
-    /** Sorts the Asset Items by their {@code assetDir} in natural order.
+    /** Sorts the Model Items by their {@code assetDir} in natural order.
      */
     public void sort() {
-        assetItemList.sort(Comparator.comparing(asset -> asset.assetDir, Comparator.naturalOrder()));
+        modelItemList.sort(Comparator.comparing(model -> model.assetDir, Comparator.naturalOrder()));
     }
 
 
@@ -144,17 +144,17 @@ public static class FilterMode {
     }
 
     @Override
-    public Iterator iterator() {
-        return assetItemList.iterator();
+    public Iterator iterator() {
+        return modelItemList.iterator();
     }
 
     @Override
-    public boolean add(AssetItem assetItem) {
-        return !assetItemList.contains(assetItem) && assetItemList.add(assetItem);
+    public boolean add(ModelItem modelItem) {
+        return !modelItemList.contains(modelItem) && modelItemList.add(modelItem);
     }
 
     @Override
-    public boolean addAll(Collection c) {
+    public boolean addAll(Collection c) {
         int size = size();
         c.forEach(this::add);
         return size != size();
@@ -162,51 +162,51 @@ public boolean addAll(Collection c) {
 
     @Override
     public boolean contains(Object o) {
-        return assetItemList.contains(o);
+        return modelItemList.contains(o);
     }
 
     @Override
     public boolean containsAll(Collection c) {
-        return assetItemList.containsAll(c);
+        return modelItemList.containsAll(c);
     }
 
     @Override
     public boolean remove(Object o) {
-        return assetItemList.remove(o);
+        return modelItemList.remove(o);
     }
 
     @Override
     public boolean removeAll(Collection c) {
-        return assetItemList.removeAll(c);
+        return modelItemList.removeAll(c);
     }
 
     @Override
     public boolean retainAll(Collection c) {
-        return assetItemList.retainAll(c);
+        return modelItemList.retainAll(c);
     }
 
     @Override
     public void clear() {
-        assetItemList.clear();
+        modelItemList.clear();
     }
 
     @Override
     public boolean isEmpty() {
-        return assetItemList.isEmpty();
+        return modelItemList.isEmpty();
     }
 
     @Override
     public int size() {
-        return assetItemList.size();
+        return modelItemList.size();
     }
 
     @Override
     public Object[] toArray() {
-        return assetItemList.toArray();
+        return modelItemList.toArray();
     }
 
     @Override
     public  T[] toArray(T[] a) {
-        return assetItemList.toArray(a);
+        return modelItemList.toArray(a);
     }
 }
diff --git a/core/src/cn/harryh/arkpets/assets/ModelsDataset.java b/core/src/cn/harryh/arkpets/assets/ModelsDataset.java
index 288fdd05..1840395c 100644
--- a/core/src/cn/harryh/arkpets/assets/ModelsDataset.java
+++ b/core/src/cn/harryh/arkpets/assets/ModelsDataset.java
@@ -19,7 +19,7 @@ public class ModelsDataset {
     public final HashMap sortTags;
     public final String gameDataVersionDescription;
     public final String gameDataServerRegion;
-    public final AssetItemGroup data;
+    public final ModelItemGroup data;
     public final Version arkPetsCompatibility;
 
     public ModelsDataset(JSONObject jsonObject) {
@@ -41,23 +41,23 @@ protected ModelsDataset(ModelsDatasetBean bean) {
 
         if (bean.data == null || bean.data.isEmpty())
             throw new DatasetKeyException("data");
-        data = new AssetItemGroup();
+        data = new ModelItemGroup();
         for (String key : bean.data.keySet()) {
             // Pre deserialization
-            AssetItem assetItem = bean.data.get(key).toJavaObject(AssetItem.class);
+            ModelItem modelItem = bean.data.get(key).toJavaObject(ModelItem.class);
             // Make up for `assetDir` field
-            if (assetItem == null || !storageDirectory.containsKey(assetItem.type))
+            if (modelItem == null || !storageDirectory.containsKey(modelItem.type))
                 throw new DatasetKeyException("type");
-            assetItem.key = key;
-            assetItem.assetDir = Path.of(storageDirectory.get(assetItem.type).toString(), key).toFile();
+            modelItem.key = key;
+            modelItem.assetDir = Path.of(storageDirectory.get(modelItem.type).toString(), key).toFile();
             // Compatible to lower version dataset
-            if (assetItem.assetList == null && assetItem.assetId != null && assetItem.checksum != null) {
+            if (modelItem.assetList == null && modelItem.assetId != null && modelItem.checksum != null) {
                 HashMap defaultFileMap = new HashMap<>();
-                for (String fileType : AssetItem.extensions)
-                    defaultFileMap.put(fileType, assetItem.assetId + fileType);
-                assetItem.assetList = new JSONObject(defaultFileMap);
+                for (String fileType : ModelItem.extensions)
+                    defaultFileMap.put(fileType, modelItem.assetId + fileType);
+                modelItem.assetList = new JSONObject(defaultFileMap);
             }
-            data.add(assetItem);
+            data.add(modelItem);
         }
         data.sort();
 
diff --git a/desktop/src/cn/harryh/arkpets/controllers/ModelsModule.java b/desktop/src/cn/harryh/arkpets/controllers/ModelsModule.java
index 21310f87..97fdca7a 100644
--- a/desktop/src/cn/harryh/arkpets/controllers/ModelsModule.java
+++ b/desktop/src/cn/harryh/arkpets/controllers/ModelsModule.java
@@ -4,8 +4,8 @@
 package cn.harryh.arkpets.controllers;
 
 import cn.harryh.arkpets.ArkHomeFX;
-import cn.harryh.arkpets.assets.AssetItem;
-import cn.harryh.arkpets.assets.AssetItemGroup;
+import cn.harryh.arkpets.assets.ModelItem;
+import cn.harryh.arkpets.assets.ModelItemGroup;
 import cn.harryh.arkpets.assets.ModelsDataset;
 import cn.harryh.arkpets.guitasks.*;
 import cn.harryh.arkpets.utils.*;
@@ -60,7 +60,7 @@ public final class ModelsModule implements Controller {
     @FXML
     private Label searchModelStatus;
     @FXML
-    private JFXListView> searchModelView;
+    private JFXListView> searchModelView;
     @FXML
     private Label selectedModelName;
     @FXML
@@ -108,9 +108,9 @@ public final class ModelsModule implements Controller {
     @FXML
     private Label modelHelp;
 
-    private AssetItemGroup assetItemList;
-    private JFXListCell selectedModelCell;
-    private ArrayList> modelCellList = new ArrayList<>();
+    private ModelItemGroup assetItemList;
+    private JFXListCell selectedModelCell;
+    private ArrayList> modelCellList = new ArrayList<>();
     private ObservableSet filterTagSet = FXCollections.observableSet();
 
     private GuiPrefabs.PeerNodeComposer infoPaneComposer;
@@ -158,7 +158,7 @@ public boolean initModelsDataset(boolean doPopNotice) {
                                 IOUtils.FileUtil.readString(new File(PathConfig.fileModelsDataPath), charsetDefault)
                         )
                 );
-                app.modelsDataset.data.removeIf(Predicate.not(AssetItem::isValid));
+                app.modelsDataset.data.removeIf(Predicate.not(ModelItem::isValid));
                 try {
                     // Check the dataset compatibility
                     Version compatibleVersion = app.modelsDataset.arkPetsCompatibility;
@@ -415,7 +415,7 @@ private void initModelFavorite() {
                 modelFavorite.setGraphic(favIcon);
                 Logger.debug("ModelManager", "Remove favorite model " + key);
             } else {
-                app.config.character_favorites.put(key, new AssetItem.AssetPrefab());
+                app.config.character_favorites.put(key, new ModelItem.AssetPrefab());
                 selectedModelCell.getStyleClass().add("Search-models-item-favorite");
                 modelFavorite.setGraphic(favFillIcon);
                 Logger.debug("ModelManager", "Add favorite model " + key);
@@ -433,9 +433,9 @@ private void initModelFavorite() {
             }
             filterFavorite = !filterFavorite;
             modelSearch(searchModelInput.getText());
-            AssetItem recentSelected = assetItemList.searchByRelPath(app.config.character_asset);
+            ModelItem recentSelected = assetItemList.searchByRelPath(app.config.character_asset);
             if (recentSelected != null)
-                for (JFXListCell cell : searchModelView.getItems())
+                for (JFXListCell cell : searchModelView.getItems())
                     if (recentSelected.equals(cell.getItem())) {
                         searchModelView.scrollTo(cell);
                         searchModelView.getSelectionModel().select(cell);
@@ -449,15 +449,15 @@ public void modelSearch(String keyWords) {
         if (assertModelLoaded(false)) {
             // Filter and search assets
             int rawSize = assetItemList.size();
-            AssetItemGroup favoured = !filterFavorite ? assetItemList :
-                    assetItemList.filter(AssetItem.PropertyExtractor.ASSET_ITEM_KEY, app.config.character_favorites.keySet(), AssetItemGroup.FilterMode.MATCH_ANY);
-            AssetItemGroup filtered = filterTagSet.isEmpty() ? favoured :
-                    favoured.filter(AssetItem.PropertyExtractor.ASSET_ITEM_SORT_TAGS, filterTagSet);
-            AssetItemGroup searched = filtered.searchByKeyWords(keyWords);
+            ModelItemGroup favoured = !filterFavorite ? assetItemList :
+                    assetItemList.filter(ModelItem.PropertyExtractor.ASSET_ITEM_KEY, app.config.character_favorites.keySet(), ModelItemGroup.FilterMode.MATCH_ANY);
+            ModelItemGroup filtered = filterTagSet.isEmpty() ? favoured :
+                    favoured.filter(ModelItem.PropertyExtractor.ASSET_ITEM_SORT_TAGS, filterTagSet);
+            ModelItemGroup searched = filtered.searchByKeyWords(keyWords);
             int curSize = searched.size();
             searchModelStatus.setText((rawSize == curSize ? rawSize : curSize + " / " + rawSize) + " 个模型");
             // Add cells
-            for (JFXListCell cell : modelCellList)
+            for (JFXListCell cell : modelCellList)
                 if (searched.contains(cell.getItem()))
                     searchModelView.getItems().add(cell);
         }
@@ -479,19 +479,19 @@ public void modelReload(boolean doPopNotice) {
             Logger.info("ModelManager", "Reloading");
             boolean willGc = modelCellList != null;
             modelCellList = new ArrayList<>();
-            assetItemList = new AssetItemGroup();
+            assetItemList = new ModelItemGroup();
 
             if (initModelsDataset(doPopNotice)) {
                 // 1. Update list cells and asset items:
                 try {
                     // Find every model assets.
-                    assetItemList.addAll(app.modelsDataset.data.filter(AssetItem::isExisted));
+                    assetItemList.addAll(app.modelsDataset.data.filter(ModelItem::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))
+                            (ListChangeListener>) (observable -> observable.getList().forEach(
+                                    (Consumer>) cell -> selectModel(cell.getItem(), cell))
                             )
                     );
                     searchModelView.setFixedCellSize(30);
@@ -502,7 +502,7 @@ public void modelReload(boolean doPopNotice) {
                     // Explicitly set all lists to empty.
                     Logger.error("ModelManager", "Failed to initialize model assets due to unknown reasons, details see below.", ex);
                     modelCellList = new ArrayList<>();
-                    assetItemList = new AssetItemGroup();
+                    assetItemList = new ModelItemGroup();
                     if (doPopNotice)
                         GuiPrefabs.Dialogs.createCommonDialog(app.body,
                                 GuiPrefabs.Icons.getIcon(GuiPrefabs.Icons.SVG_WARNING_ALT, GuiPrefabs.COLOR_WARNING),
@@ -529,7 +529,7 @@ public void modelReload(boolean doPopNotice) {
                 });
                 filterPaneTagFlow.getChildren().clear();
                 if (assetItemList != null && app.modelsDataset != null) {
-                    ArrayList sortTags = new ArrayList<>(assetItemList.extract(AssetItem.PropertyExtractor.ASSET_ITEM_SORT_TAGS));
+                    ArrayList sortTags = new ArrayList<>(assetItemList.extract(ModelItem.PropertyExtractor.ASSET_ITEM_SORT_TAGS));
                     sortTags.sort(Comparator.naturalOrder());
                     sortTags.forEach(s -> {
                         String t = app.modelsDataset.sortTags == null ? s : app.modelsDataset.sortTags.getOrDefault(s, s);
@@ -553,9 +553,9 @@ public void modelReload(boolean doPopNotice) {
                 if (assetItemList != null && !modelCellList.isEmpty() &&
                         app.config.character_asset != null && !app.config.character_asset.isEmpty()) {
                     // Scroll to recent selected model and then select it.
-                    AssetItem recentSelected = assetItemList.searchByRelPath(app.config.character_asset);
+                    ModelItem recentSelected = assetItemList.searchByRelPath(app.config.character_asset);
                     if (recentSelected != null) {
-                        for (JFXListCell cell : searchModelView.getItems())
+                        for (JFXListCell cell : searchModelView.getItems())
                             if (recentSelected.equals(cell.getItem())) {
                                 searchModelView.scrollTo(cell);
                                 searchModelView.getSelectionModel().select(cell);
@@ -577,20 +577,20 @@ public void modelReload(boolean doPopNotice) {
         });
     }
 
-    private JFXListCell getMenuItem(AssetItem assetItem, JFXListView> container) {
+    private JFXListCell getMenuItem(ModelItem modelItem, JFXListView> container) {
         double width = container.getPrefWidth() - 50;
         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(assetItem.toString());
+        Label name = new Label(modelItem.toString());
         name.getStyleClass().addAll("Search-models-label", "Search-models-label-primary");
-        name.setPrefSize(assetItem.skinGroupName == null ? width : width * divide, height);
+        name.setPrefSize(modelItem.skinGroupName == null ? width : width * divide, height);
         name.setLayoutX(15);
-        Label alias1 = new Label(assetItem.skinGroupName);
+        Label alias1 = new Label(modelItem.skinGroupName);
         alias1.getStyleClass().addAll("Search-models-label", "Search-models-label-secondary");
         alias1.setPrefSize(width * (1 - divide), height);
-        alias1.setLayoutX(assetItem.skinGroupName == null ? 0 : width * divide);
+        alias1.setLayoutX(modelItem.skinGroupName == null ? 0 : width * divide);
         SVGPath fav = GuiPrefabs.Icons.getIcon(GuiPrefabs.Icons.SVG_STAR_FILLED, GuiPrefabs.COLOR_WARNING);
         fav.getStyleClass().add("Search-models-star");
         fav.setLayoutX(0);
@@ -599,14 +599,14 @@ private JFXListCell getMenuItem(AssetItem assetItem, JFXListView item) {
+    private void selectModel(ModelItem asset, JFXListCell item) {
         // Reset
         if (selectedModelCell != null) {
             selectedModelCell.getStyleClass().setAll("Search-models-item");
diff --git a/desktop/src/cn/harryh/arkpets/guitasks/VerifyModelsTask.java b/desktop/src/cn/harryh/arkpets/guitasks/VerifyModelsTask.java
index 30ee36a3..a841a0b9 100644
--- a/desktop/src/cn/harryh/arkpets/guitasks/VerifyModelsTask.java
+++ b/desktop/src/cn/harryh/arkpets/guitasks/VerifyModelsTask.java
@@ -3,8 +3,8 @@
  */
 package cn.harryh.arkpets.guitasks;
 
-import cn.harryh.arkpets.assets.AssetItem;
-import cn.harryh.arkpets.assets.AssetItemGroup;
+import cn.harryh.arkpets.assets.ModelItem;
+import cn.harryh.arkpets.assets.ModelItemGroup;
 import cn.harryh.arkpets.assets.ModelsDataset;
 import cn.harryh.arkpets.utils.GuiPrefabs;
 import cn.harryh.arkpets.utils.Logger;
@@ -39,12 +39,12 @@ protected Task getTask() {
         return new Task<>() {
             @Override
             protected Boolean call() {
-                AssetItemGroup validModelAssets = modelsDataset.data.filter(AssetItem::isValid);
+                ModelItemGroup validModelAssets = modelsDataset.data.filter(ModelItem::isValid);
                 int currentProgress = 0;
                 int totalProgress = validModelAssets.size();
 
                 boolean flag = false;
-                for (AssetItem item : validModelAssets) {
+                for (ModelItem item : validModelAssets) {
                     this.updateProgress(currentProgress++, totalProgress);
                     if (this.isCancelled()) {
                         // Cancelled:

From a43d7956d2d4ec22418bf38639da38248bfe7d39 Mon Sep 17 00:00:00 2001
From: Harry Huang 
Date: Mon, 20 Jan 2025 15:02:11 +0800
Subject: [PATCH 2/5] feat: search with pinyin

add lib tinypinyin
---
 build.gradle                                  |  2 +
 .../cn/harryh/arkpets/assets/ModelItem.java   | 34 +++++++++++
 .../harryh/arkpets/assets/ModelItemGroup.java | 60 +++++++++++++++----
 .../arkpets/controllers/ModelsModule.java     |  8 ++-
 4 files changed, 91 insertions(+), 13 deletions(-)

diff --git a/build.gradle b/build.gradle
index 8695593d..dc9314c2 100644
--- a/build.gradle
+++ b/build.gradle
@@ -72,5 +72,7 @@ project(":core") {
         api "com.alibaba:fastjson:2.0.39"
         // Log4j
         api "apache-log4j:log4j:1.2.15"
+        // TiniPinyin
+        api 'com.github.promeg:tinypinyin:2.0.3'
     }
 }
diff --git a/core/src/cn/harryh/arkpets/assets/ModelItem.java b/core/src/cn/harryh/arkpets/assets/ModelItem.java
index a3fec754..a698aa53 100644
--- a/core/src/cn/harryh/arkpets/assets/ModelItem.java
+++ b/core/src/cn/harryh/arkpets/assets/ModelItem.java
@@ -7,6 +7,7 @@
 import com.alibaba.fastjson.JSONArray;
 import com.alibaba.fastjson.JSONObject;
 import com.alibaba.fastjson.annotation.JSONField;
+import com.github.promeg.pinyinhelper.Pinyin;
 
 import java.io.File;
 import java.io.Serializable;
@@ -42,7 +43,10 @@ public class ModelItem implements Serializable {
     /** @deprecated Legacy field in old version dataset */ @JSONField @Deprecated
     public JSONObject checksum;
 
+    // Lazy generated fields:
     private ModelAssetAccessor accessor;
+    private String pinyinQuanpin;
+    private String pinyinSuoxie;
 
     protected static final String[] extensions = {".atlas", ".png", ".skel"};
 
@@ -67,6 +71,36 @@ public ModelAssetAccessor getAccessor() {
         return accessor;
     }
 
+    /** Gets the 拼音全拼 (Pinyin Quanpin, full Pinyin transcription) of the model's name.
+     * @return A String.
+     */
+    @JSONField(serialize = false)
+    public String getPinyinQuanpin() {
+        if (pinyinQuanpin == null)
+            pinyinQuanpin = Pinyin.toPinyin(name, "");
+        return pinyinQuanpin;
+    }
+
+    /** Gets the 拼音缩写 (Pinyin Suoxie, abbreviate Pinyin transcription) of the model's name.
+     * @return A String.
+     */
+    @JSONField(serialize = false)
+    public String getPinyinSuoxie() {
+        if (pinyinSuoxie == null) {
+            String quanpin = Pinyin.toPinyin(name, " ");
+            if (!quanpin.trim().isEmpty()) {
+                StringBuilder builder = new StringBuilder();
+                for (String word : quanpin.split("\\s+")) {
+                    builder.append(word.charAt(0));
+                }
+                pinyinSuoxie = builder.toString();
+            } else {
+                pinyinSuoxie = "";
+            }
+        }
+        return pinyinSuoxie;
+    }
+
     /** Verifies the integrity of the necessary fields of this {@code ModelItem}.
      * @return {@code true} if all the following conditions are satisfied, otherwise {@code false}:
      *          1. Both {@code assetDir} and {@code type} are not {@code null}.
diff --git a/core/src/cn/harryh/arkpets/assets/ModelItemGroup.java b/core/src/cn/harryh/arkpets/assets/ModelItemGroup.java
index 7e701496..1da8661c 100644
--- a/core/src/cn/harryh/arkpets/assets/ModelItemGroup.java
+++ b/core/src/cn/harryh/arkpets/assets/ModelItemGroup.java
@@ -45,23 +45,61 @@ public ModelItemGroup() {
     public ModelItemGroup searchByKeyWords(String keyWords) {
         if (keyWords == null || keyWords.isEmpty())
             return this;
-        String[] wordList = keyWords.split(" ");
+        String[] wordList = keyWords.toUpperCase().split(" ");
         ModelItemGroup result = new ModelItemGroup();
-        for (ModelItem asset : this) {
-            for (String word : wordList) {
-                if (asset.name != null &&
-                        asset.name.toLowerCase().contains(word.toLowerCase())) {
-                    result.add(asset);
+
+        // Rule: match name
+        for (ModelItem model : this) {
+            if (!result.contains(model) && model.name != null) {
+                String nameLower = model.name.toUpperCase();
+                for (String word : wordList) {
+                    if (nameLower.contains(word)) {
+                        result.add(model);
+                        break;
+                    }
+                }
+            }
+        }
+
+        // Rule: match appellation
+        for (ModelItem model : this) {
+            if (!result.contains(model) && model.appellation != null) {
+                String appellLower = model.appellation.toUpperCase();
+                for (String word : wordList) {
+                    if (appellLower.contains(word)) {
+                        result.add(model);
+                        break;
+                    }
+                }
+            }
+        }
+
+        // Rule: match Pinyin suoxie
+        for (ModelItem model : this) {
+            if (!result.contains(model) && model.getPinyinSuoxie() != null) {
+                String appellLower = model.getPinyinSuoxie().toUpperCase();
+                for (String word : wordList) {
+                    if (appellLower.contains(word)) {
+                        result.add(model);
+                        break;
+                    }
                 }
             }
-            for (String word : wordList) {
-                if (asset.appellation != null &&
-                        asset.appellation.toLowerCase().contains(word.toLowerCase())) {
-                    if (!result.contains(asset))
-                        result.add(asset);
+        }
+
+        // Rule: match Pinyin quanpin
+        for (ModelItem model : this) {
+            if (!result.contains(model) && model.getPinyinQuanpin() != null) {
+                String appellLower = model.getPinyinQuanpin().toUpperCase();
+                for (String word : wordList) {
+                    if (appellLower.contains(word)) {
+                        result.add(model);
+                        break;
+                    }
                 }
             }
         }
+
         return result;
     }
 
diff --git a/desktop/src/cn/harryh/arkpets/controllers/ModelsModule.java b/desktop/src/cn/harryh/arkpets/controllers/ModelsModule.java
index 97fdca7a..793d7175 100644
--- a/desktop/src/cn/harryh/arkpets/controllers/ModelsModule.java
+++ b/desktop/src/cn/harryh/arkpets/controllers/ModelsModule.java
@@ -447,21 +447,25 @@ public void modelSearch(String keyWords) {
         searchModelView.getItems().clear();
         searchModelStatus.setText("");
         if (assertModelLoaded(false)) {
-            // Filter and search assets
+            // Filter assets
             int rawSize = assetItemList.size();
             ModelItemGroup favoured = !filterFavorite ? assetItemList :
                     assetItemList.filter(ModelItem.PropertyExtractor.ASSET_ITEM_KEY, app.config.character_favorites.keySet(), ModelItemGroup.FilterMode.MATCH_ANY);
             ModelItemGroup filtered = filterTagSet.isEmpty() ? favoured :
                     favoured.filter(ModelItem.PropertyExtractor.ASSET_ITEM_SORT_TAGS, filterTagSet);
+            // Search assets
+            long tStart = System.nanoTime();
             ModelItemGroup searched = filtered.searchByKeyWords(keyWords);
+            long tEnd = System.nanoTime();
             int curSize = searched.size();
             searchModelStatus.setText((rawSize == curSize ? rawSize : curSize + " / " + rawSize) + " 个模型");
             // Add cells
             for (JFXListCell cell : modelCellList)
                 if (searched.contains(cell.getItem()))
                     searchModelView.getItems().add(cell);
+            Logger.info("ModelManager", "Search \"%s\" (%d results, %.1f ms)"
+                    .formatted(keyWords, curSize, (tEnd - tStart) / 1000000f));
         }
-        Logger.info("ModelManager", "Search \"" + keyWords + "\" (" + searchModelView.getItems().size() + ")");
         searchModelView.refresh();
     }
 

From ce3051d78707293a9aad7d6b30ef9fcb68c66519 Mon Sep 17 00:00:00 2001
From: Harry Huang 
Date: Mon, 20 Jan 2025 15:10:01 +0800
Subject: [PATCH 3/5] feat: search with traditional chinese

add lib opencc4j
---
 build.gradle                                  |  2 ++
 .../cn/harryh/arkpets/assets/ModelItem.java   |  4 +--
 .../harryh/arkpets/assets/ModelItemGroup.java | 29 ++++++++++++++++---
 3 files changed, 29 insertions(+), 6 deletions(-)

diff --git a/build.gradle b/build.gradle
index dc9314c2..460f10ef 100644
--- a/build.gradle
+++ b/build.gradle
@@ -74,5 +74,7 @@ project(":core") {
         api "apache-log4j:log4j:1.2.15"
         // TiniPinyin
         api 'com.github.promeg:tinypinyin:2.0.3'
+        // OpenCC4j
+        api 'com.github.houbb:opencc4j:1.8.1'
     }
 }
diff --git a/core/src/cn/harryh/arkpets/assets/ModelItem.java b/core/src/cn/harryh/arkpets/assets/ModelItem.java
index a698aa53..9707a8d7 100644
--- a/core/src/cn/harryh/arkpets/assets/ModelItem.java
+++ b/core/src/cn/harryh/arkpets/assets/ModelItem.java
@@ -87,8 +87,8 @@ public String getPinyinQuanpin() {
     @JSONField(serialize = false)
     public String getPinyinSuoxie() {
         if (pinyinSuoxie == null) {
-            String quanpin = Pinyin.toPinyin(name, " ");
-            if (!quanpin.trim().isEmpty()) {
+            String quanpin = Pinyin.toPinyin(name, " ").trim();
+            if (!quanpin.isEmpty()) {
                 StringBuilder builder = new StringBuilder();
                 for (String word : quanpin.split("\\s+")) {
                     builder.append(word.charAt(0));
diff --git a/core/src/cn/harryh/arkpets/assets/ModelItemGroup.java b/core/src/cn/harryh/arkpets/assets/ModelItemGroup.java
index 1da8661c..ef6f9195 100644
--- a/core/src/cn/harryh/arkpets/assets/ModelItemGroup.java
+++ b/core/src/cn/harryh/arkpets/assets/ModelItemGroup.java
@@ -4,6 +4,7 @@
 package cn.harryh.arkpets.assets;
 
 import cn.harryh.arkpets.assets.ModelItem.PropertyExtractor;
+import com.github.houbb.opencc4j.util.ZhConverterUtil;
 
 import java.util.*;
 import java.util.function.Predicate;
@@ -45,14 +46,21 @@ public ModelItemGroup() {
     public ModelItemGroup searchByKeyWords(String keyWords) {
         if (keyWords == null || keyWords.isEmpty())
             return this;
-        String[] wordList = keyWords.toUpperCase().split(" ");
+        // Word list: uppercase and deduplicate
+        String[] wordList = deduplicateArray(keyWords.toUpperCase().split(" "));
+        // Word list: extend with zh-Hans and zh-Hant conversions
+        String[] wordListST = deduplicateArray(concatArrays(
+                wordList,
+                ZhConverterUtil.toSimple(keyWords).toUpperCase().split(" "),
+                ZhConverterUtil.toTraditional(keyWords).toUpperCase().split(" ")
+        ));
         ModelItemGroup result = new ModelItemGroup();
 
         // Rule: match name
         for (ModelItem model : this) {
             if (!result.contains(model) && model.name != null) {
                 String nameLower = model.name.toUpperCase();
-                for (String word : wordList) {
+                for (String word : wordListST) {
                     if (nameLower.contains(word)) {
                         result.add(model);
                         break;
@@ -175,12 +183,25 @@ public void sort() {
         modelItemList.sort(Comparator.comparing(model -> model.assetDir, Comparator.naturalOrder()));
     }
 
+    protected String[] concatArrays(String[] array1, String[] array2, String[] array3) {
+        String[] result = new String[array1.length + array2.length + array3.length];
+        System.arraycopy(array1, 0, result, 0, array1.length);
+        System.arraycopy(array2, 0, result, array1.length, array2.length);
+        System.arraycopy(array3, 0, result, array1.length + array2.length, array3.length);
+        return result;
+    }
+
+    protected String[] deduplicateArray(String[] array) {
+        return Arrays.stream(array).distinct().toArray(String[]::new);
+    }
+
 
     public static class FilterMode {
-        public static final int MATCH_ANY           = 0b1;
-        public static final int MATCH_REVERSE       = 0b10;
+        public static final int MATCH_ANY     = 0b1;
+        public static final int MATCH_REVERSE = 0b10;
     }
 
+
     @Override
     public Iterator iterator() {
         return modelItemList.iterator();

From b1ac6947b2352ea131d40b3bf0bcada4874fc52b Mon Sep 17 00:00:00 2001
From: Harry Huang 
Date: Mon, 20 Jan 2025 16:30:07 +0800
Subject: [PATCH 4/5] fix: typo

---
 .../harryh/arkpets/assets/ModelItemGroup.java   | 17 +++++++++--------
 1 file changed, 9 insertions(+), 8 deletions(-)

diff --git a/core/src/cn/harryh/arkpets/assets/ModelItemGroup.java b/core/src/cn/harryh/arkpets/assets/ModelItemGroup.java
index ef6f9195..f268039a 100644
--- a/core/src/cn/harryh/arkpets/assets/ModelItemGroup.java
+++ b/core/src/cn/harryh/arkpets/assets/ModelItemGroup.java
@@ -59,9 +59,9 @@ public ModelItemGroup searchByKeyWords(String keyWords) {
         // Rule: match name
         for (ModelItem model : this) {
             if (!result.contains(model) && model.name != null) {
-                String nameLower = model.name.toUpperCase();
+                String upper = model.name.toUpperCase();
                 for (String word : wordListST) {
-                    if (nameLower.contains(word)) {
+                    if (upper.contains(word)) {
                         result.add(model);
                         break;
                     }
@@ -72,9 +72,9 @@ public ModelItemGroup searchByKeyWords(String keyWords) {
         // Rule: match appellation
         for (ModelItem model : this) {
             if (!result.contains(model) && model.appellation != null) {
-                String appellLower = model.appellation.toUpperCase();
+                String upper = model.appellation.toUpperCase();
                 for (String word : wordList) {
-                    if (appellLower.contains(word)) {
+                    if (upper.contains(word)) {
                         result.add(model);
                         break;
                     }
@@ -85,9 +85,9 @@ public ModelItemGroup searchByKeyWords(String keyWords) {
         // Rule: match Pinyin suoxie
         for (ModelItem model : this) {
             if (!result.contains(model) && model.getPinyinSuoxie() != null) {
-                String appellLower = model.getPinyinSuoxie().toUpperCase();
+                String upper = model.getPinyinSuoxie().toUpperCase();
                 for (String word : wordList) {
-                    if (appellLower.contains(word)) {
+                    if (upper.contains(word)) {
                         result.add(model);
                         break;
                     }
@@ -98,9 +98,9 @@ public ModelItemGroup searchByKeyWords(String keyWords) {
         // Rule: match Pinyin quanpin
         for (ModelItem model : this) {
             if (!result.contains(model) && model.getPinyinQuanpin() != null) {
-                String appellLower = model.getPinyinQuanpin().toUpperCase();
+                String upper = model.getPinyinQuanpin().toUpperCase();
                 for (String word : wordList) {
-                    if (appellLower.contains(word)) {
+                    if (upper.contains(word)) {
                         result.add(model);
                         break;
                     }
@@ -108,6 +108,7 @@ public ModelItemGroup searchByKeyWords(String keyWords) {
             }
         }
 
+
         return result;
     }
 

From 90f066032122b39619466d8eb98233de44674d73 Mon Sep 17 00:00:00 2001
From: Harry Huang 
Date: Mon, 20 Jan 2025 17:54:10 +0800
Subject: [PATCH 5/5] feat: search with skin group name

---
 .../cn/harryh/arkpets/assets/ModelItemGroup.java | 16 ++++++++++++++--
 1 file changed, 14 insertions(+), 2 deletions(-)

diff --git a/core/src/cn/harryh/arkpets/assets/ModelItemGroup.java b/core/src/cn/harryh/arkpets/assets/ModelItemGroup.java
index f268039a..250e7eeb 100644
--- a/core/src/cn/harryh/arkpets/assets/ModelItemGroup.java
+++ b/core/src/cn/harryh/arkpets/assets/ModelItemGroup.java
@@ -108,6 +108,18 @@ public ModelItemGroup searchByKeyWords(String keyWords) {
             }
         }
 
+        // Rule: match skin group name
+        for (ModelItem model : this) {
+            if (!result.contains(model) && model.skinGroupName != null) {
+                String upper = model.skinGroupName.toUpperCase();
+                for (String word : wordListST) {
+                    if (upper.contains(word)) {
+                        result.add(model);
+                        break;
+                    }
+                }
+            }
+        }
 
         return result;
     }
@@ -184,7 +196,7 @@ public void sort() {
         modelItemList.sort(Comparator.comparing(model -> model.assetDir, Comparator.naturalOrder()));
     }
 
-    protected String[] concatArrays(String[] array1, String[] array2, String[] array3) {
+    protected static String[] concatArrays(String[] array1, String[] array2, String[] array3) {
         String[] result = new String[array1.length + array2.length + array3.length];
         System.arraycopy(array1, 0, result, 0, array1.length);
         System.arraycopy(array2, 0, result, array1.length, array2.length);
@@ -192,7 +204,7 @@ protected String[] concatArrays(String[] array1, String[] array2, String[] array
         return result;
     }
 
-    protected String[] deduplicateArray(String[] array) {
+    protected static String[] deduplicateArray(String[] array) {
         return Arrays.stream(array).distinct().toArray(String[]::new);
     }