commit a7d8c3cdc340396930b3a3a93d7968f62ed1b7e3
Author: Leszek Koltunski <leszek@koltunski.pl>
Date:   Sun Dec 5 22:19:21 2021 +0100

    Progress replacing the enum ObjetType with the class RubikObjectList.

diff --git a/src/main/java/org/distorted/config/ConfigScreen.java b/src/main/java/org/distorted/config/ConfigScreen.java
index 6363075d..bd55c401 100644
--- a/src/main/java/org/distorted/config/ConfigScreen.java
+++ b/src/main/java/org/distorted/config/ConfigScreen.java
@@ -33,14 +33,14 @@ import android.widget.ScrollView;
 import android.widget.TextView;
 
 import org.distorted.objectlib.main.ObjectControl;
-import org.distorted.objectlib.main.ObjectType;
 
 import org.distorted.helpers.TransparentImageButton;
 import org.distorted.main.R;
 import org.distorted.main.RubikActivity;
+import org.distorted.objects.RubikObject;
+import org.distorted.objects.RubikObjectList;
 
 import static android.view.View.inflate;
-import static org.distorted.objectlib.main.ObjectType.NUM_OBJECTS;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
@@ -97,12 +97,14 @@ public class ConfigScreen
       colSpecs[col] = GridLayout.spec(col);
       }
 
-    for(int object = 0; object< NUM_OBJECTS; object++)
+    int numObjects = RubikObjectList.getNumObjects();
+
+    for(int object=0; object<numObjects; object++)
       {
       final int ordinal = object;
-      ObjectType type = ObjectType.getObject(ordinal);
+      RubikObject rubikObject = RubikObjectList.getObject(ordinal);
       int iconSize = RubikActivity.getDrawableSize();
-      int icons = type.getIconID(iconSize);
+      int icons = rubikObject.getIconID(iconSize);
       int row = object/NUM_COLUMNS;
 
       ImageButton button = new ImageButton(act);
@@ -115,8 +117,8 @@ public class ConfigScreen
           if( act.getControl().isUINotBlocked() && mObjectOrdinal!=ordinal )
             {
             mObjectOrdinal = ordinal;
-            act.changeObject(type);
-            mMovesText.setText(act.getString(R.string.mo_placeholder,mObjectOrdinal+1,NUM_OBJECTS));
+            act.changeObject(rubikObject);
+            mMovesText.setText(act.getString(R.string.mo_placeholder,mObjectOrdinal+1,numObjects));
             mPane.updatePane(act,mObjectOrdinal);
             }
 
@@ -234,26 +236,26 @@ public class ConfigScreen
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  private void prevObject(ConfigActivity act)
+  private void prevObject(ConfigActivity act, int numObjects)
     {
     mObjectOrdinal--;
-    if( mObjectOrdinal<0 ) mObjectOrdinal=NUM_OBJECTS-1;
+    if( mObjectOrdinal<0 ) mObjectOrdinal=numObjects-1;
 
-    ObjectType type = ObjectType.getObject(mObjectOrdinal);
-    act.changeObject(type);
+    RubikObject object = RubikObjectList.getObject(mObjectOrdinal);
+    act.changeObject(object);
 
     mPane.updatePane(act,mObjectOrdinal);
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  private void nextObject(ConfigActivity act)
+  private void nextObject(ConfigActivity act, int numObjects)
     {
     mObjectOrdinal++;
-    if( mObjectOrdinal>=NUM_OBJECTS ) mObjectOrdinal=0;
+    if( mObjectOrdinal>=numObjects ) mObjectOrdinal=0;
 
-    ObjectType type = ObjectType.getObject(mObjectOrdinal);
-    act.changeObject(type);
+    RubikObject object = RubikObjectList.getObject(mObjectOrdinal);
+    act.changeObject(object);
 
     mPane.updatePane(act,mObjectOrdinal);
     }
@@ -271,8 +273,9 @@ public class ConfigScreen
       @Override
       public void onClick(View v)
         {
-        prevObject(act);
-        mMovesText.setText(act.getString(R.string.mo_placeholder,mObjectOrdinal+1,NUM_OBJECTS));
+        int numObjects = RubikObjectList.getNumObjects();
+        prevObject(act,numObjects);
+        mMovesText.setText(act.getString(R.string.mo_placeholder,mObjectOrdinal+1,numObjects));
         }
       });
     }
@@ -290,15 +293,16 @@ public class ConfigScreen
       @Override
       public void onClick(View v)
         {
-        nextObject(act);
-        mMovesText.setText(act.getString(R.string.mo_placeholder,mObjectOrdinal+1,NUM_OBJECTS));
+        int numObjects = RubikObjectList.getNumObjects();
+        nextObject(act,numObjects);
+        mMovesText.setText(act.getString(R.string.mo_placeholder,mObjectOrdinal+1,numObjects));
         }
       });
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  private void setupTextView(final ConfigActivity act, final float width)
+  private void setupTextView(final ConfigActivity act, final float width, int numObjects)
     {
     int padding = (int)(width*RubikActivity.PADDING);
     int margin  = (int)(width*RubikActivity.MARGIN);
@@ -314,18 +318,19 @@ public class ConfigScreen
     mMovesText.setPadding(padding,0,padding,0);
     mMovesText.setGravity(Gravity.CENTER);
     mMovesText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mButtonSize);
-    mMovesText.setText(act.getString(R.string.mo_placeholder,mObjectOrdinal+1,NUM_OBJECTS));
+    mMovesText.setText(act.getString(R.string.mo_placeholder,mObjectOrdinal+1,numObjects));
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
   void onAttachedToWindow(final ConfigActivity act, final int objectOrdinal)
     {
+    int numObjects = RubikObjectList.getNumObjects();
     int width = act.getScreenWidthInPixels();
     mBarHeight = act.getHeightBar();
     mButtonSize = width*RubikActivity.BUTTON_TEXT_SIZE;
 
-    mRowCount = (NUM_OBJECTS + NUM_COLUMNS-1) / NUM_COLUMNS;
+    mRowCount = (numObjects + NUM_COLUMNS-1) / NUM_COLUMNS;
     mColCount = NUM_COLUMNS;
 
     mObjectOrdinal = objectOrdinal;
@@ -344,7 +349,7 @@ public class ConfigScreen
     setupObjectButton(act,width);
     setupPrevButton(act);
     setupNextButton(act);
-    setupTextView(act,width);
+    setupTextView(act,width,numObjects);
     setupBackButton(act);
 
     layoutLeft.addView(mObjectButton);
diff --git a/src/main/java/org/distorted/config/ConfigScreenPane.java b/src/main/java/org/distorted/config/ConfigScreenPane.java
index ca0641ca..90d74ef6 100644
--- a/src/main/java/org/distorted/config/ConfigScreenPane.java
+++ b/src/main/java/org/distorted/config/ConfigScreenPane.java
@@ -24,13 +24,13 @@ import android.widget.ImageView;
 import android.widget.LinearLayout;
 import android.widget.RadioButton;
 import android.widget.RadioGroup;
-import android.widget.RelativeLayout;
 import android.widget.TextView;
 
 import org.distorted.jsons.ObjectJson;
 import org.distorted.main.R;
 import org.distorted.objectlib.json.JsonReader;
-import org.distorted.objectlib.main.ObjectType;
+import org.distorted.objects.RubikObject;
+import org.distorted.objects.RubikObjectList;
 
 import java.io.InputStream;
 
@@ -90,8 +90,9 @@ public class ConfigScreenPane
 
     mObjectOrdinal = objectOrdinal;
 
-    ObjectType type = ObjectType.getObject(objectOrdinal);
-    InputStream stream = ObjectJson.getStream(type,act);
+    RubikObject object = RubikObjectList.getObject(objectOrdinal);
+    int jsonID = object.getJsonID();
+    InputStream stream = ObjectJson.getStream(jsonID,act);
     mReader.parseJsonFileMetadata(stream);
 
     String name = mReader.getObjectName();
diff --git a/src/main/java/org/distorted/network/RubikNetwork.java b/src/main/java/org/distorted/network/RubikNetwork.java
index b6a9e1b0..259949a6 100644
--- a/src/main/java/org/distorted/network/RubikNetwork.java
+++ b/src/main/java/org/distorted/network/RubikNetwork.java
@@ -34,10 +34,10 @@ import androidx.appcompat.app.AppCompatActivity;
 import androidx.fragment.app.FragmentActivity;
 
 import org.distorted.library.main.DistortedLibrary;
-import org.distorted.objectlib.main.ObjectType;
+import org.distorted.objects.RubikObject;
+import org.distorted.objects.RubikObjectList;
 
 import static org.distorted.screens.RubikScreenPlay.MAX_LEVEL;
-import static org.distorted.objectlib.main.ObjectType.NUM_OBJECTS;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
@@ -108,15 +108,20 @@ public class RubikNetwork implements Runnable
   private static int mMode = IDLE;
   private static Receiver mReceiver;
   private static String mVersion;
+  private static int mNumObjects;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
   private static void initializeStatics()
     {
-    if( mCountry==null ) mCountry = new String[NUM_OBJECTS][MAX_LEVEL][MAX_PLACES];
-    if( mName==null    ) mName    = new String[NUM_OBJECTS][MAX_LEVEL][MAX_PLACES];
-    if( mTime==null    ) mTime    = new  float[NUM_OBJECTS][MAX_LEVEL][MAX_PLACES];
-    if( mPlaces==null  ) mPlaces  = new int[NUM_OBJECTS][MAX_LEVEL];
+    int newNum = RubikObjectList.getNumObjects();
+
+    if( mCountry==null || newNum!=mNumObjects ) mCountry = new String[newNum][MAX_LEVEL][MAX_PLACES];
+    if( mName==null    || newNum!=mNumObjects ) mName    = new String[newNum][MAX_LEVEL][MAX_PLACES];
+    if( mTime==null    || newNum!=mNumObjects ) mTime    = new  float[newNum][MAX_LEVEL][MAX_PLACES];
+    if( mPlaces==null  || newNum!=mNumObjects ) mPlaces  = new int[newNum][MAX_LEVEL];
+
+    mNumObjects = newNum;
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
@@ -165,7 +170,7 @@ public class RubikNetwork implements Runnable
       return false;
       }
 
-    for(int i=0; i<NUM_OBJECTS; i++)
+    for(int i=0; i<mNumObjects; i++)
       for(int j=0; j<MAX_LEVEL; j++)
         {
         mPlaces[i][j] = 0;
@@ -204,9 +209,9 @@ public class RubikNetwork implements Runnable
 
     if( s5>s4 && s4>s3 && s3>s2 && s2>s1 && s1>0 )
       {
-      int object = ObjectType.getOrdinal( row.substring(0,s1) );
+      int object = RubikObjectList.getOrdinal( row.substring(0,s1) );
 
-      if( object>=0 && object<NUM_OBJECTS )
+      if( object>=0 && object<mNumObjects )
         {
         int level      = Integer.parseInt( row.substring(s1+1,s2) );
         String name    = row.substring(s2+1, s3);
@@ -463,12 +468,16 @@ public class RubikNetwork implements Runnable
   private static String getObjectList()
     {
     StringBuilder list = new StringBuilder();
-    ObjectType[] objects = ObjectType.values();
 
-    for(int i=0; i<NUM_OBJECTS; i++)
+    for(int i=0; i<mNumObjects; i++)
       {
-      if( i>0 ) list.append(',');
-      list.append(objects[i].name());
+      RubikObject object = RubikObjectList.getObject(i);
+
+      if( object!=null )
+        {
+        if( i>0 ) list.append(',');
+        list.append(object.getName());
+        }
       }
 
     return list.toString();
diff --git a/src/main/java/org/distorted/network/RubikScores.java b/src/main/java/org/distorted/network/RubikScores.java
index 4da06a63..e1847076 100644
--- a/src/main/java/org/distorted/network/RubikScores.java
+++ b/src/main/java/org/distorted/network/RubikScores.java
@@ -19,6 +19,7 @@
 
 package org.distorted.network;
 
+import java.util.HashMap;
 import java.util.UUID;
 
 import android.content.Context;
@@ -27,24 +28,20 @@ import android.telephony.TelephonyManager;
 
 import com.google.firebase.crashlytics.FirebaseCrashlytics;
 
-import static org.distorted.objectlib.main.ObjectType.NUM_OBJECTS;
 import static org.distorted.screens.RubikScreenPlay.MAX_LEVEL;
-
-import org.distorted.objectlib.main.ObjectType;
-
 import org.distorted.main.BuildConfig;
+import org.distorted.objects.RubikObject;
+import org.distorted.objects.RubikObjectList;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 // hold my own scores, and some other statistics.
 
 public class RubikScores
   {
+  public static final int MULT = 1000000;
   public static final long NO_RECORD = Long.MAX_VALUE;
   private static RubikScores mThis;
 
-  private final long[][] mRecords;
-  private final int [][] mSubmitted;
-
   private String mName, mCountry;
   private boolean mNameIsVerified;
   private int mNumRuns;
@@ -52,19 +49,25 @@ public class RubikScores
   private int mNumWins;
   private int mDeviceID;
 
+  private static class MapValue
+    {
+    long record;
+    boolean submitted;
+
+    MapValue(long rec,int sub)
+      {
+      record    = rec;
+      submitted = sub!=0;
+      }
+    }
+
+  private final HashMap<Integer,MapValue> mMap;
+
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
   private RubikScores()
     {
-    mRecords   = new long[NUM_OBJECTS][MAX_LEVEL];
-    mSubmitted = new int [NUM_OBJECTS][MAX_LEVEL];
-
-    for(int object=0; object<NUM_OBJECTS; object++)
-      for(int level=0; level<MAX_LEVEL; level++)
-        {
-        mRecords[object][level]   = NO_RECORD;
-        mSubmitted[object][level] = 0;
-        }
+    mMap = new HashMap<>();
 
     mName = "";
     mCountry = "un";
@@ -77,6 +80,13 @@ public class RubikScores
     mNumWins =  0;
     }
 
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private int mapKey(int object,int level)
+    {
+    return object*MULT + level;
+    }
+
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
   private int privateGetDeviceID()
@@ -103,11 +113,11 @@ public class RubikScores
     {
     mNameIsVerified = true;
 
-    for(int object=0; object<NUM_OBJECTS; object++)
-      for(int level=0; level<MAX_LEVEL; level++)
-        {
-        mSubmitted[object][level]=1;
-        }
+    for(int key: mMap.keySet())
+      {
+      MapValue value = mMap.get(key);
+      if( value!=null ) value.submitted = true;
+      }
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
@@ -121,11 +131,11 @@ public class RubikScores
 
   synchronized boolean thereAreUnsubmittedRecords()
     {
-    for(int object=0; object<NUM_OBJECTS; object++)
-      for(int level=0; level<MAX_LEVEL; level++)
-        {
-        if( mSubmitted[object][level]==0 && mRecords[object][level]<NO_RECORD ) return true;
-        }
+    for(int key: mMap.keySet())
+      {
+      MapValue value = mMap.get(key);
+      if( value!=null && !value.submitted && value.record<NO_RECORD) return true;
+      }
 
     return false;
     }
@@ -139,25 +149,27 @@ public class RubikScores
     StringBuilder builderTim = new StringBuilder();
     boolean first = true;
 
-    for(int object=0; object<NUM_OBJECTS; object++)
+    for(int key: mMap.keySet())
       {
-      String name = ObjectType.getObject(object).name();
+      MapValue value = mMap.get(key);
 
-      for(int level=0; level<MAX_LEVEL; level++)
+      if( value!=null && !value.submitted && value.record<NO_RECORD)
         {
-        if( mSubmitted[object][level]==0 && mRecords[object][level]<NO_RECORD )
+        if( !first )
           {
-          if( !first )
-            {
-            builderObj.append(',');
-            builderLvl.append(',');
-            builderTim.append(',');
-            }
-          first=false;
+          builderObj.append(',');
+          builderLvl.append(',');
+          builderTim.append(',');
+          }
+        first=false;
 
-          builderObj.append(name);
-          builderLvl.append(level);
-          builderTim.append(mRecords[object][level]);
+        RubikObject object = RubikObjectList.getObject(key/MULT);
+
+        if( object!=null )
+          {
+          builderObj.append(object.getName());
+          builderLvl.append(key%MULT);
+          builderTim.append(value.record);
           }
         }
       }
@@ -167,13 +179,6 @@ public class RubikScores
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 // Public API
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public synchronized long getRecord(int obj, int level)
-    {
-    return (obj>=0 && obj<NUM_OBJECTS && level>=0 && level<MAX_LEVEL) ? mRecords[obj][level] : -1;
-    }
-
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
   public boolean isVerified()
@@ -240,9 +245,45 @@ public class RubikScores
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  public synchronized boolean isSolved(int obj, int level)
+  public synchronized boolean setRecord(int object, int level, long record)
+    {
+    int key = mapKey(object,level)-1; // -1 - historical reasons; previous versions saved it like this.
+    MapValue oldValue = mMap.get(key);
+
+    if( oldValue==null )
+      {
+      MapValue value = new MapValue(record,0);
+      mMap.put(key,value);
+      return true;
+      }
+
+    long oldRecord = oldValue.record;
+
+    if( oldRecord>record)
+      {
+      MapValue value = new MapValue(record,0);
+      mMap.put(key,value);
+      return true;
+      }
+
+    return false;
+    }
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public synchronized long getRecord(int object, int level)
+    {
+    int key = mapKey(object,level);
+    MapValue value = mMap.get(key);
+    return value!=null ? value.record : NO_RECORD;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public synchronized boolean isSolved(int object, int level)
     {
-    return obj>=0 && obj<NUM_OBJECTS && level>=0 && level<MAX_LEVEL && mRecords[obj][level]<NO_RECORD;
+    int key = mapKey(object,level);
+    MapValue value = mMap.get(key);
+    return value!=null && value.record<NO_RECORD;
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
@@ -289,22 +330,28 @@ public class RubikScores
 
   public synchronized void savePreferences(SharedPreferences.Editor editor)
     {
+    int numObjects = RubikObjectList.getNumObjects();
     StringBuilder builder = new StringBuilder();
-    String name;
 
     for(int level=0; level<MAX_LEVEL; level++)
       {
       builder.setLength(0);
 
-      for(int object=0; object<NUM_OBJECTS; object++)
+      for(int object=0; object<numObjects; object++)
         {
-        name = ObjectType.getObject(object).name();
-        builder.append(name);
-        builder.append("=");
-        builder.append(mRecords[object][level]);
-        builder.append(",");
-        builder.append(mSubmitted[object][level]);
-        builder.append(" ");
+        int key = mapKey(object,level);
+        RubikObject obj = RubikObjectList.getObject(object);
+        MapValue value = mMap.get(key);
+
+        if( obj!=null && value!=null && value.record<NO_RECORD )
+          {
+          builder.append(obj.getName());
+          builder.append("=");
+          builder.append(value.record);
+          builder.append(",");
+          builder.append(value.submitted ? 1:0 );
+          builder.append(" ");
+          }
         }
 
       editor.putString("scores_record"+level, builder.toString());
@@ -326,6 +373,7 @@ public class RubikScores
     int start, end, equals, comma, object, subm;
     long time;
     boolean thereWasError = false;
+    int numObjects = RubikObjectList.getNumObjects();
 
     for(int level=0; level<MAX_LEVEL; level++)
       {
@@ -350,17 +398,18 @@ public class RubikScores
           timeStr = subStr.substring(equals+1,comma);
           submStr = subStr.substring(comma+1);
 
-          object = ObjectType.getOrdinal(nameStr);
+          object = RubikObjectList.getOrdinal(nameStr);
 
-          if( object>=0 && object< NUM_OBJECTS )
+          if( object>=0 && object<numObjects )
             {
             time = Long.parseLong(timeStr);
             subm = Integer.parseInt(submStr);
 
             if( subm>=0 && subm<=1 )
               {
-              mRecords  [object][level] = time;
-              mSubmitted[object][level] = subm;
+              MapValue value = new MapValue(time,subm);
+              int key = mapKey(object,level);
+              mMap.put(key,value);
               }
             else
               {
@@ -405,18 +454,4 @@ public class RubikScores
       crashlytics.recordException(ex);
       }
     }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public synchronized boolean setRecord(int object, int level, long timeTaken)
-    {
-    if( object>=0 && object<NUM_OBJECTS && level>=1 && level<=MAX_LEVEL && mRecords[object][level-1]>timeTaken )
-      {
-      mRecords  [object][level-1] = timeTaken;
-      mSubmitted[object][level-1] = 0;
-      return true;
-      }
-
-    return false;
-    }
   }
diff --git a/src/main/java/org/distorted/objects/RubikObject.java b/src/main/java/org/distorted/objects/RubikObject.java
new file mode 100644
index 00000000..e8e01092
--- /dev/null
+++ b/src/main/java/org/distorted/objects/RubikObject.java
@@ -0,0 +1,106 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2021 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Magic Cube.                                                              //
+//                                                                                               //
+// Magic Cube is free software: you can redistribute it and/or modify                            //
+// it under the terms of the GNU General Public License as published by                          //
+// the Free Software Foundation, either version 2 of the License, or                             //
+// (at your option) any later version.                                                           //
+//                                                                                               //
+// Magic Cube is distributed in the hope that it will be useful,                                 //
+// but WITHOUT ANY WARRANTY; without even the implied warranty of                                //
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the                                 //
+// GNU General Public License for more details.                                                  //
+//                                                                                               //
+// You should have received a copy of the GNU General Public License                             //
+// along with Magic Cube.  If not, see <http://www.gnu.org/licenses/>.                           //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.objects;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+import org.distorted.dmesh.ObjectMesh;
+import org.distorted.jsons.ObjectJson;
+import org.distorted.objectlib.main.ObjectType;
+import org.distorted.patterns.RubikPatternList;
+
+public class RubikObject
+{
+  private static final int NUM = 4;
+
+  private final String mName;
+  private final int mNumScramble;
+  private final int mOrdinal;
+  private final int mJsonID, mMeshID;
+  private final int[] mIconID;
+  private final String[][] mPatterns;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  RubikObject(int ordinal, ObjectType type)
+    {
+    mIconID = new int[NUM];
+    for(int i=0; i<NUM; i++) mIconID[i] = type.getIconID(i);
+
+    mName        = type.name();
+    mNumScramble = type.getNumScramble();
+    mOrdinal     = ordinal;
+    mJsonID      = ObjectJson.getJsonID(type);
+    mMeshID      = ObjectMesh.getMeshID(type);
+
+    int patternOrdinal  = RubikPatternList.getOrdinal(type);
+    mPatterns = RubikPatternList.getPatterns(patternOrdinal);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// PUBLIC API
+
+  public String getName()
+    {
+    return mName;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int getIconID(int size)
+    {
+    return size>=0 && size<NUM ? mIconID[size] : 0;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int getNumScramble()
+    {
+    return mNumScramble;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int getOrdinal()
+    {
+    return mOrdinal;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int getJsonID()
+    {
+    return mJsonID;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int getMeshID()
+    {
+    return mMeshID;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public String[][] getPatterns()
+    {
+    return mPatterns;
+    }
+}
diff --git a/src/main/java/org/distorted/objects/RubikObjectList.java b/src/main/java/org/distorted/objects/RubikObjectList.java
new file mode 100644
index 00000000..105a5bfb
--- /dev/null
+++ b/src/main/java/org/distorted/objects/RubikObjectList.java
@@ -0,0 +1,90 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2021 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Magic Cube.                                                              //
+//                                                                                               //
+// Magic Cube is free software: you can redistribute it and/or modify                            //
+// it under the terms of the GNU General Public License as published by                          //
+// the Free Software Foundation, either version 2 of the License, or                             //
+// (at your option) any later version.                                                           //
+//                                                                                               //
+// Magic Cube is distributed in the hope that it will be useful,                                 //
+// but WITHOUT ANY WARRANTY; without even the implied warranty of                                //
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the                                 //
+// GNU General Public License for more details.                                                  //
+//                                                                                               //
+// You should have received a copy of the GNU General Public License                             //
+// along with Magic Cube.  If not, see <http://www.gnu.org/licenses/>.                           //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.objects;
+
+import org.distorted.objectlib.main.ObjectType;
+
+import java.util.ArrayList;
+import static org.distorted.objectlib.main.ObjectType.NUM_OBJECTS;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public class RubikObjectList
+{
+  private static RubikObjectList mType;
+  private static int mNumObjects;
+  private static ArrayList<RubikObject> mObjects;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private RubikObjectList()
+    {
+    mNumObjects = 0;
+    mObjects = new ArrayList<>();
+
+    createBuiltinObjects();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void createBuiltinObjects()
+    {
+    for(int i=0; i<NUM_OBJECTS; i++)
+      {
+      ObjectType type = ObjectType.getObject(i);
+      RubikObject obj = new RubikObject(i,type);
+      mObjects.add(obj);
+      mNumObjects++;
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// PUBLIC API
+
+  public static RubikObject getObject(int ordinal)
+    {
+    if( mType==null ) mType = new RubikObjectList();
+    return ordinal>=0 && ordinal<mNumObjects ? mObjects.get(ordinal) : null;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static int getNumObjects()
+    {
+    if( mType==null ) mType = new RubikObjectList();
+    return mNumObjects;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static int getOrdinal(String name)
+    {
+    if( mType==null ) mType = new RubikObjectList();
+
+    for(int i=0; i<mNumObjects; i++)
+      {
+      RubikObject obj = mObjects.get(i);
+
+      if( obj.getName().equals(name) ) return i;
+      }
+
+    return -1;
+    }
+}
diff --git a/src/main/java/org/distorted/patterns/RubikPatternList.java b/src/main/java/org/distorted/patterns/RubikPatternList.java
index dae703d7..8fb8568f 100644
--- a/src/main/java/org/distorted/patterns/RubikPatternList.java
+++ b/src/main/java/org/distorted/patterns/RubikPatternList.java
@@ -55,9 +55,9 @@ public enum RubikPatternList
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  static String[][] getPatterns(int ordinal)
+  public static String[][] getPatterns(int ordinal)
     {
-    return objects[ordinal].mPatterns;
+    return ordinal>=0 && ordinal<NUM_OBJECTS ? objects[ordinal].mPatterns : null;
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
