├── .github
└── FUNDING.yml
├── Assets
├── BinarySerializer.meta
├── BinarySerializer
│ ├── BinarySerializer.cs
│ └── BinarySerializer.cs.meta
├── Player.cs
├── Player.cs.meta
├── Sprites.meta
├── Sprites
│ ├── player_character.png
│ └── player_character.png.meta
├── TextMesh Pro.meta
├── scene.unity
└── scene.unity.meta
├── GenericBinarySerializer.unitypackage
├── LICENCE
└── README.md
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | custom: ["https://paypal.me/hamzaherbou"]
2 |
--------------------------------------------------------------------------------
/Assets/BinarySerializer.meta:
--------------------------------------------------------------------------------
1 | fileFormatVersion: 2
2 | guid: b753b841928557c4584ee8a090404809
3 | folderAsset: yes
4 | timeCreated: 1592826329
5 | licenseType: Pro
6 | DefaultImporter:
7 | userData:
8 | assetBundleName:
9 | assetBundleVariant:
10 |
--------------------------------------------------------------------------------
/Assets/BinarySerializer/BinarySerializer.cs:
--------------------------------------------------------------------------------
1 | using UnityEngine;
2 | using System.IO;
3 | using System.Runtime.Serialization;
4 | using System.Runtime.Serialization.Formatters.Binary;
5 |
6 | ///
7 | /// BinarySerializer is tool to save your game data in files as binary
8 | /// so that no body can read it or modify it,
9 | /// this tool is generic.
10 | ///
11 | /// Allowed types : int, float , strings, ...(all .Net types)
12 | /// But for unity types it accepts only 5 types: Vector2, Vector3, Vector4, Color, Quaternion.
13 | ///
14 | /// Developed by Hamza Herbou
15 | /// GITHUB : https://github.com/herbou
16 | ///
17 |
18 | public class BinarySerializer {
19 | static string folderName = "GameData";
20 |
21 | static string persistentDataPath = Application.persistentDataPath;
22 | static SurrogateSelector surrogateSelector = GetSurrogateSelector ( );
23 |
24 | ///
25 | /// Save data to disk.
26 | ///
27 | /// your dataClass instance.
28 | /// the file where you want to save data.
29 | ///
30 | public static void Save ( T data, string filename ) {
31 | if ( IsSerializable ( ) ) {
32 | if ( !Directory.Exists ( GetDirectoryPath ( ) ) )
33 | Directory.CreateDirectory ( GetDirectoryPath ( ) );
34 |
35 | BinaryFormatter formatter = new BinaryFormatter ( );
36 |
37 | formatter.SurrogateSelector = surrogateSelector;
38 |
39 | FileStream file = File.Create ( GetFilePath ( filename ) );
40 |
41 | formatter.Serialize ( file, data );
42 |
43 | file.Close ( );
44 | }
45 | }
46 |
47 | ///
48 | /// Save data to disk.
49 | ///
50 | /// the file where you saved data.
51 | ///
52 | public static T Load ( string filename ) {
53 | T data = System.Activator.CreateInstance ( );
54 |
55 | if ( IsSerializable ( ) ) {
56 | if ( HasSaved ( filename ) ) {
57 |
58 | BinaryFormatter formatter = new BinaryFormatter ( );
59 |
60 | formatter.SurrogateSelector = surrogateSelector;
61 |
62 | FileStream file = File.Open ( GetFilePath ( filename ), FileMode.Open );
63 |
64 | data = ( T )formatter.Deserialize ( file );
65 |
66 | file.Close ( );
67 | }
68 | }
69 |
70 | return data;
71 | }
72 |
73 | static bool IsSerializable ( ) {
74 | bool isSerializable = typeof( T ).IsSerializable;
75 | if ( !isSerializable ) {
76 | string type = typeof( T ).ToString ( );
77 | Debug.LogError (
78 | "Class " + type + " is not marked as Serializable, "
79 | + "make sure to add [System.Serializable] at the top of your " + type + " class."
80 | );
81 | }
82 |
83 | return isSerializable;
84 | }
85 |
86 |
87 | ///
88 | /// Check if data is saved.
89 | ///
90 | /// the file where you saved data
91 | ///
92 | public static bool HasSaved ( string filename ) {
93 | return File.Exists ( GetFilePath ( filename ) );
94 | }
95 |
96 | ///
97 | /// Delete a data file.
98 | ///
99 | /// the file where you saved data
100 | ///
101 | public static void DeleteDataFile ( string filename ) {
102 | if ( HasSaved ( filename ) )
103 | File.Delete ( GetFilePath ( filename ) );
104 | }
105 |
106 | ///
107 | /// Delete all data files.
108 | ///
109 | ///
110 | public static void DeleteAllDataFiles ( ) {
111 | if ( Directory.Exists ( GetDirectoryPath ( ) ) )
112 | Directory.Delete ( GetDirectoryPath ( ), true );
113 | }
114 |
115 | ///
116 | /// Get the path where data is saved.
117 | ///
118 | ///
119 | public static string GetDataPath ( ) {
120 | return GetDirectoryPath ( ) + "/";
121 | }
122 |
123 | static string GetDirectoryPath ( ) {
124 | return persistentDataPath + "/" + folderName;
125 | }
126 |
127 | static string GetFilePath ( string filename ) {
128 | return GetDirectoryPath ( ) + "/" + filename;
129 | }
130 |
131 |
132 | //Other non-serialized types /// SS: Serialization Surrogate
133 | //Vector2 , Vector3 , Vector4 , Color , Quaternion.
134 |
135 | static SurrogateSelector GetSurrogateSelector ( ) {
136 | SurrogateSelector surrogateSelector = new SurrogateSelector ( );
137 |
138 | Vector2_SS v2_ss = new Vector2_SS ( );
139 | Vector3_SS v3_ss = new Vector3_SS ( );
140 | Vector4_SS v4_ss = new Vector4_SS ( );
141 | Color_SS co_ss = new Color_SS ( );
142 | Quaternion_SS qu_ss = new Quaternion_SS ( );
143 |
144 | surrogateSelector.AddSurrogate ( typeof( Vector2 ), new StreamingContext ( StreamingContextStates.All ), v2_ss );
145 | surrogateSelector.AddSurrogate ( typeof( Vector3 ), new StreamingContext ( StreamingContextStates.All ), v3_ss );
146 | surrogateSelector.AddSurrogate ( typeof( Vector4 ), new StreamingContext ( StreamingContextStates.All ), v4_ss );
147 | surrogateSelector.AddSurrogate ( typeof( Color ), new StreamingContext ( StreamingContextStates.All ), co_ss );
148 | surrogateSelector.AddSurrogate ( typeof( Quaternion ), new StreamingContext ( StreamingContextStates.All ), qu_ss );
149 |
150 | return surrogateSelector;
151 | }
152 |
153 | class Vector2_SS: ISerializationSurrogate {
154 | //Serialize Vector2
155 | public void GetObjectData ( System.Object obj, SerializationInfo info, StreamingContext context ) {
156 | Vector2 v2 = ( Vector2 )obj;
157 | info.AddValue ( "x", v2.x );
158 | info.AddValue ( "y", v2.y );
159 | }
160 | //Deserialize Vector2
161 | public System.Object SetObjectData ( System.Object obj, SerializationInfo info, StreamingContext context, ISurrogateSelector selector ) {
162 | Vector2 v2 = ( Vector2 )obj;
163 |
164 | v2.x = ( float )info.GetValue ( "x", typeof( float ) );
165 | v2.y = ( float )info.GetValue ( "y", typeof( float ) );
166 |
167 | obj = v2;
168 | return obj;
169 | }
170 | }
171 |
172 | class Vector3_SS: ISerializationSurrogate {
173 | //Serialize Vector3
174 | public void GetObjectData ( System.Object obj, SerializationInfo info, StreamingContext context ) {
175 | Vector3 v3 = ( Vector3 )obj;
176 | info.AddValue ( "x", v3.x );
177 | info.AddValue ( "y", v3.y );
178 | info.AddValue ( "z", v3.z );
179 | }
180 | //Deserialize Vector3
181 | public System.Object SetObjectData ( System.Object obj, SerializationInfo info, StreamingContext context, ISurrogateSelector selector ) {
182 | Vector3 v3 = ( Vector3 )obj;
183 |
184 | v3.x = ( float )info.GetValue ( "x", typeof( float ) );
185 | v3.y = ( float )info.GetValue ( "y", typeof( float ) );
186 | v3.z = ( float )info.GetValue ( "z", typeof( float ) );
187 |
188 | obj = v3;
189 | return obj;
190 | }
191 | }
192 |
193 | class Vector4_SS: ISerializationSurrogate {
194 | //Serialize Vector4
195 | public void GetObjectData ( System.Object obj, SerializationInfo info, StreamingContext context ) {
196 | Vector4 v4 = ( Vector4 )obj;
197 | info.AddValue ( "x", v4.x );
198 | info.AddValue ( "y", v4.y );
199 | info.AddValue ( "z", v4.z );
200 | info.AddValue ( "w", v4.w );
201 | }
202 | //Deserialize Vector4
203 | public System.Object SetObjectData ( System.Object obj, SerializationInfo info, StreamingContext context, ISurrogateSelector selector ) {
204 | Vector4 v4 = ( Vector4 )obj;
205 |
206 | v4.x = ( float )info.GetValue ( "x", typeof( float ) );
207 | v4.y = ( float )info.GetValue ( "y", typeof( float ) );
208 | v4.z = ( float )info.GetValue ( "z", typeof( float ) );
209 | v4.w = ( float )info.GetValue ( "w", typeof( float ) );
210 |
211 | obj = v4;
212 | return obj;
213 | }
214 |
215 | }
216 |
217 | class Color_SS: ISerializationSurrogate {
218 | //Serialize Color
219 | public void GetObjectData ( System.Object obj, SerializationInfo info, StreamingContext context ) {
220 | Color color = ( Color )obj;
221 | info.AddValue ( "r", color.r );
222 | info.AddValue ( "g", color.g );
223 | info.AddValue ( "b", color.b );
224 | info.AddValue ( "a", color.a );
225 | }
226 | //Deserialize Color
227 | public System.Object SetObjectData ( System.Object obj, SerializationInfo info, StreamingContext context, ISurrogateSelector selector ) {
228 | Color color = ( Color )obj;
229 |
230 | color.r = ( float )info.GetValue ( "r", typeof( float ) );
231 | color.g = ( float )info.GetValue ( "g", typeof( float ) );
232 | color.b = ( float )info.GetValue ( "b", typeof( float ) );
233 | color.a = ( float )info.GetValue ( "a", typeof( float ) );
234 |
235 | obj = color;
236 | return obj;
237 | }
238 | }
239 |
240 | class Quaternion_SS: ISerializationSurrogate {
241 | //Serialize Quaternion
242 | public void GetObjectData ( System.Object obj, SerializationInfo info, StreamingContext context ) {
243 | Quaternion qua = ( Quaternion )obj;
244 | info.AddValue ( "x", qua.x );
245 | info.AddValue ( "y", qua.y );
246 | info.AddValue ( "z", qua.z );
247 | info.AddValue ( "w", qua.w );
248 | }
249 | //Deserialize Quaternion
250 | public System.Object SetObjectData ( System.Object obj, SerializationInfo info, StreamingContext context, ISurrogateSelector selector ) {
251 | Quaternion qua = ( Quaternion )obj;
252 |
253 | qua.x = ( float )info.GetValue ( "x", typeof( float ) );
254 | qua.y = ( float )info.GetValue ( "y", typeof( float ) );
255 | qua.z = ( float )info.GetValue ( "z", typeof( float ) );
256 | qua.w = ( float )info.GetValue ( "w", typeof( float ) );
257 |
258 | obj = qua;
259 | return obj;
260 | }
261 | }
262 | }
263 |
264 |
--------------------------------------------------------------------------------
/Assets/BinarySerializer/BinarySerializer.cs.meta:
--------------------------------------------------------------------------------
1 | fileFormatVersion: 2
2 | guid: dc06f171ef8f53845bfb396de512b401
3 | timeCreated: 1592755901
4 | licenseType: Pro
5 | MonoImporter:
6 | serializedVersion: 2
7 | defaultReferences: []
8 | executionOrder: 0
9 | icon: {instanceID: 0}
10 | userData:
11 | assetBundleName:
12 | assetBundleVariant:
13 |
--------------------------------------------------------------------------------
/Assets/Player.cs:
--------------------------------------------------------------------------------
1 | using UnityEngine ;
2 | using TMPro ;
3 |
4 | [System.Serializable]
5 | public class PlayerData {
6 | //Player Data
7 | public string nickName = "default" ;
8 | public Color color = Color.white ;
9 | public Vector3 pos = Vector3.zero ;
10 | public Quaternion rot = Quaternion.identity ;
11 | }
12 |
13 | public class Player : MonoBehaviour {
14 | //References
15 | [SerializeField] SpriteRenderer playerSprite ;
16 | [SerializeField] TMP_Text playerNickNameUIText ;
17 |
18 | public PlayerData playerData = new PlayerData () ;
19 |
20 |
21 |
22 | void Start () {
23 | //Load data (if it's already saved)
24 | if (BinarySerializer.HasSaved("playerdata")
25 | playerData = BinarySerializer.Load ("playerdata") ;
26 |
27 | Debug.Log (BinarySerializer.GetDataPath ()) ;
28 |
29 | UpdatePlayer () ; //Update player at the beginning.
30 | }
31 |
32 |
33 | void Update () {
34 | // Update player on mouse click.
35 | if (Input.GetMouseButtonUp (0))
36 | UpdatePlayer () ;
37 |
38 | // delete saved data "playerdata" when X key is pressed
39 | if (Input.GetKeyUp (KeyCode.X))
40 | BinarySerializer.DeleteDataFile ("playerdata") ;
41 | }
42 |
43 |
44 | void UpdatePlayer () {
45 | playerNickNameUIText.text = playerData.nickName ; //Update player nickname in UI.
46 | playerSprite.color = playerData.color ; //Update player color.
47 | transform.position = playerData.pos ; //Update player position.
48 | transform.rotation = playerData.rot ; //Update player rotation.
49 | }
50 |
51 |
52 | void OnApplicationQuit () {
53 | //save data before quit.
54 | //in your game you may want to save data in different ways.. maybe when player pos changed , ...
55 | BinarySerializer.Save (playerData, "playerdata") ;
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/Assets/Player.cs.meta:
--------------------------------------------------------------------------------
1 | fileFormatVersion: 2
2 | guid: d3e10eb61c41a28429dfc8bae4b779f1
3 | timeCreated: 1592756032
4 | licenseType: Pro
5 | MonoImporter:
6 | serializedVersion: 2
7 | defaultReferences: []
8 | executionOrder: 0
9 | icon: {instanceID: 0}
10 | userData:
11 | assetBundleName:
12 | assetBundleVariant:
13 |
--------------------------------------------------------------------------------
/Assets/Sprites.meta:
--------------------------------------------------------------------------------
1 | fileFormatVersion: 2
2 | guid: bf4a632a2334c5348a944e83c7970c69
3 | folderAsset: yes
4 | timeCreated: 1592830120
5 | licenseType: Pro
6 | DefaultImporter:
7 | userData:
8 | assetBundleName:
9 | assetBundleVariant:
10 |
--------------------------------------------------------------------------------
/Assets/Sprites/player_character.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/herbou/Unity_GenericBinarySerializer/03ef14289f13a5616dbc1ceff3c1b53c2cd8af48/Assets/Sprites/player_character.png
--------------------------------------------------------------------------------
/Assets/Sprites/player_character.png.meta:
--------------------------------------------------------------------------------
1 | fileFormatVersion: 2
2 | guid: 01200b6d3d5e4fd46a8bb2a5f96a487d
3 | timeCreated: 1592830111
4 | licenseType: Pro
5 | TextureImporter:
6 | fileIDToRecycleName: {}
7 | serializedVersion: 4
8 | mipmaps:
9 | mipMapMode: 0
10 | enableMipMap: 0
11 | sRGBTexture: 1
12 | linearTexture: 0
13 | fadeOut: 0
14 | borderMipMap: 0
15 | mipMapFadeDistanceStart: 1
16 | mipMapFadeDistanceEnd: 3
17 | bumpmap:
18 | convertToNormalMap: 0
19 | externalNormalMap: 0
20 | heightScale: 0.25
21 | normalMapFilter: 0
22 | isReadable: 0
23 | grayScaleToAlpha: 0
24 | generateCubemap: 6
25 | cubemapConvolution: 0
26 | seamlessCubemap: 0
27 | textureFormat: 1
28 | maxTextureSize: 2048
29 | textureSettings:
30 | filterMode: -1
31 | aniso: -1
32 | mipBias: -1
33 | wrapMode: 1
34 | nPOTScale: 0
35 | lightmap: 0
36 | compressionQuality: 50
37 | spriteMode: 1
38 | spriteExtrude: 1
39 | spriteMeshType: 1
40 | alignment: 0
41 | spritePivot: {x: 0.5, y: 0.5}
42 | spriteBorder: {x: 0, y: 0, z: 0, w: 0}
43 | spritePixelsToUnits: 100
44 | alphaUsage: 1
45 | alphaIsTransparency: 1
46 | spriteTessellationDetail: -1
47 | textureType: 8
48 | textureShape: 1
49 | maxTextureSizeSet: 0
50 | compressionQualitySet: 0
51 | textureFormatSet: 0
52 | platformSettings:
53 | - buildTarget: DefaultTexturePlatform
54 | maxTextureSize: 2048
55 | textureFormat: -1
56 | textureCompression: 1
57 | compressionQuality: 50
58 | crunchedCompression: 0
59 | allowsAlphaSplitting: 0
60 | overridden: 0
61 | spriteSheet:
62 | serializedVersion: 2
63 | sprites: []
64 | outline: []
65 | spritePackingTag:
66 | userData:
67 | assetBundleName:
68 | assetBundleVariant:
69 |
--------------------------------------------------------------------------------
/Assets/TextMesh Pro.meta:
--------------------------------------------------------------------------------
1 | fileFormatVersion: 2
2 | guid: a6568e96b208ba742a7b21a26e052052
3 | folderAsset: yes
4 | timeCreated: 1592828800
5 | licenseType: Pro
6 | DefaultImporter:
7 | userData:
8 | assetBundleName:
9 | assetBundleVariant:
10 |
--------------------------------------------------------------------------------
/Assets/scene.unity:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/herbou/Unity_GenericBinarySerializer/03ef14289f13a5616dbc1ceff3c1b53c2cd8af48/Assets/scene.unity
--------------------------------------------------------------------------------
/Assets/scene.unity.meta:
--------------------------------------------------------------------------------
1 | fileFormatVersion: 2
2 | guid: e50ce42ebd8eb0e4696d1b1aa60aa340
3 | timeCreated: 1592755880
4 | licenseType: Pro
5 | DefaultImporter:
6 | userData:
7 | assetBundleName:
8 | assetBundleVariant:
9 |
--------------------------------------------------------------------------------
/GenericBinarySerializer.unitypackage:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/herbou/Unity_GenericBinarySerializer/03ef14289f13a5616dbc1ceff3c1b53c2cd8af48/GenericBinarySerializer.unitypackage
--------------------------------------------------------------------------------
/LICENCE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Hamza Herbou
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Unity Generic Binary Serializer
2 | Generic BinarySerializer is a great and easy to use tool that helps you to save/load and secure your game data easily,
3 | Click on the image to watch the video tutorial :
4 |
5 | [](https://www.youtube.com/watch?v=PbPCW8vK3RQ)
6 |
7 |
8 |
9 | # ☴ How to use ?
10 | 1. Add ```GenericBinarySerializer``` package to your project.
11 | 2. Create a class (or struct) to hold your data and mark it as ```Serializable```:
12 |
13 | ```📄 PlayerData.cs```
14 | ```c#
15 | [System.Serializable]
16 | public class PlayerData
17 | {
18 | //This class contains only player data that you want to save.
19 | //don't add any logic code here (methods,..).
20 |
21 | public string nickName = "Default Name";
22 | public int score = 0;
23 | public Color color = Color.white;
24 | public Vector2 pos = Vector2.zero;
25 | public Quaternion rot = Quaternion.identity;
26 | }
27 | ```
28 |
29 |
30 | # ☴ Methods and properties :
31 | ## Load data :
32 | ```c#
33 | obj = BinarySerializer.Load ("filename");
34 | ```
35 | Example :
36 | ```📄 Player.cs```
37 | ```c#
38 | public class Player : MonoBehaviour
39 | {
40 | // References
41 | // ...
42 | // ...
43 |
44 | // Your data holder reference :
45 | public PlayerData playerDataInstance = new PlayerData ();
46 |
47 |
48 | void Start ()
49 | {
50 | //Load data from file.
51 | playerDataInstance = BinarySerializer.Load ("player.txt");
52 | }
53 | }
54 | ```
55 |
56 |
57 | ## Save data :
58 | ```c#
59 | BinarySerializer.Save (obj, "filename");
60 | ```
61 | Example :
62 | ```📄 Player.cs```
63 | ```c#
64 | public class Player : MonoBehaviour
65 | {
66 | // References
67 | // ...
68 | // ...
69 |
70 | // Your data holder reference :
71 | public PlayerData playerDataInstance = new PlayerData ();
72 |
73 |
74 | void Start ()
75 | {
76 | //Save new Data to file after change.
77 | BinarySerializer.Save (playerDataInstance, "player.txt");
78 | }
79 | }
80 | ```
81 |
82 |
83 | ## Check if data is already saved :
84 | ```c#
85 | BinarySerializer.HasSaved ("filename");
86 | ```
87 | Example :
88 | ```c#
89 | if (BinarySerializer.HasSaved("player.txt")){
90 | //do something.
91 | }
92 | ```
93 |
94 |
95 | ## Delete saved file :
96 | ```c#
97 | BinarySerializer.DeleteDataFile ("filename");
98 | ```
99 |
100 |
101 | ## Delete all saved files :
102 | ```c#
103 | BinarySerializer.DeleteAllDataFiles ( );
104 | ```
105 |
106 |
107 | ## Get the path where data is saved. :
108 | ```c#
109 | BinarySerializer.GetDataPath ( );
110 | ```
111 |
112 |
113 |
114 | ⚠Notes! :
115 |
116 | - The Load method already has a check for file existance, that's why you need to add default values to your Data Holder class fields, because the BinarySerializer's Load method returns a new instance of the Data if it's not saved before.
117 | - Not all data types are allowed inside Data holder class.
118 | ### Allowed types :
119 | - all variables that's not part of the Unity engine are allowed : int, float, bool, string, char, ....
120 | - Concerning UnityEngine types you can only use : Vector2, Vector3, Vector4, Color, and Quaternion
121 | - You can also save : Arrays, Lists, .... of thoes allowed types.
122 |
123 | ### Unallowed types :
124 | Except the 5 types mentioned above (Vector2, Vector3, Vector4, Color, and Quaternion),, all variables of UnityEngine are not allowed :
125 | - Transform, Gameobject, SpriteRenderer, BoxCollider, Mesh, .....
126 |
127 |
128 |
129 |
130 |
131 |
132 | ## ❤️ Donate
133 |
134 |
--------------------------------------------------------------------------------