-
-
Notifications
You must be signed in to change notification settings - Fork 55
/
Copy pathTutorial.cs
788 lines (617 loc) · 27.7 KB
/
Tutorial.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
using Ceras;
namespace Tutorial
{
using Ceras.Formatters;
using Ceras.Resolvers;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Diagnostics;
class Person
{
public string Name;
public int Health { get; set; } // This is a property just to show that Ceras handles properties as well.
public Person BestFriend;
}
class Tutorial
{
public void Step1_SimpleUsage()
{
//
// 1.) Simple usage
// aka. "I'm here for the cool features! I want to optimize for max-performance later"
var person = new Person { Name = "riki", Health = 100 };
var ceras = new CerasSerializer();
var data = ceras.Serialize(person);
data.VisualizePrint("Simple Person");
var clone1 = ceras.Deserialize<Person>(data);
Console.WriteLine($"Clone: Name={clone1.Name}, Health={clone1.Health}");
// 2.) Types
// You can also serialize as <object>.
// In that case the type information will be included.
// If a type is written it will only be written ONCE, so a List<Person> will not suddenly waste a
// ton of space by continously writing the type-names
var objectData = ceras.Serialize<object>(person);
objectData.VisualizePrint("Person as <object>");
var objectClone = ceras.Deserialize<object>(objectData);
//
// 3.) Improvement:
// Recycle the serialization buffer by keeping the reference to it around.
// Optionally we can even let Ceras create (or resize) the buffer for us.
byte[] buffer = null;
int writtenBytes = ceras.Serialize(person, ref buffer);
// Now we could send this over the network, for example:
// socket.Send(buffer, writtenBytes, SocketFlags.None);
var clone2 = ceras.Deserialize<Person>(buffer);
//
// 4.)
// Deciding what gets serialized
// There are multiple ways to configure what members to serialize
// Ceras determines member inclusion in this order:
//
// - a. Any custom configuration using ConfigType<T> or ConfigType(type)
//
// - b. [Include] and [Exclude] attributes on individual members
//
// - c. [MemberConfig] attribute
//
// - d. "DefaultTargets" setting in the SerializerConfig
// which defaults to 'TargetMember.PublicFields'
//
SerializerConfig config = new SerializerConfig();
config.DefaultTargets = TargetMember.PublicProperties | TargetMember.PrivateFields;
config.ConfigType<Person>()
.ConfigMember(p => p.Name).Include()
.ConfigMember(p => p.BestFriend).Include();
//
// 5.) Circular references
// Serializers commonly have trouble serializing circular references.
// Ceras supports every possible object-graph, and there's literally
// nothing to do or configure, it just works out of the box.
// Lets make an example anyway...
var personA = new Person { Name = "alice" };
var personB = new Person { Name = "bob" };
personA.BestFriend = personB;
personB.BestFriend = personA;
var dataWithCircularReferences = ceras.Serialize(personA);
dataWithCircularReferences.VisualizePrint("Circular references data");
var cloneA = ceras.Deserialize<Person>(dataWithCircularReferences);
if (cloneA.BestFriend.BestFriend.BestFriend.BestFriend.BestFriend.BestFriend == cloneA)
Console.WriteLine("Circular reference serialization working as intended!");
else
throw new Exception("There was some problem!");
// Works with self-references!
// Ceras maintains object references while deserializing, even if the object a field/prop points to doesn't exist yet
var personC = new Person { Name = "abc" };
personC.BestFriend = personC;
var cloneC = ceras.Deserialize<Person>(ceras.Serialize(personC));
Debug.Assert(cloneC.BestFriend.BestFriend.BestFriend.BestFriend == cloneC);
// Fully maintains identity of objects!
// There is only one actual object instance here (personC). The array refers to the same object two times.
// While Ceras deserializes the array it only creates a single instance of 'Person', exactly as intended.
Person[] personArray = new Person[2];
personArray[0] = personC;
personArray[1] = personC;
var personArrayClone = ceras.Deserialize<Person[]>(ceras.Serialize(personArray));
Debug.Assert(personArray[0] == personArray[1]);
Debug.Assert(ReferenceEquals(personArray[0], personArray[1]));
Debug.Assert(personArray[0].BestFriend == personArray[1].BestFriend);
Debug.Assert(personArray[0].BestFriend.BestFriend == personArray[1]);
}
public void Step2_Attributes()
{
/*
* Attributes are completely optional.
*
* Ceras has Attributes to include or skip fields. (todo: and later to set optional names/keys...)
*
*/
CerasSerializer ceras = new CerasSerializer();
var obj = new SomeAttributeExample();
var data = ceras.Serialize(obj);
data.VisualizePrint("Attribute Example");
data = ceras.Serialize(new SomeAttributeExample2());
data.VisualizePrint("Attribute Example 2");
}
public void Step3_Recycling()
{
/*
* Scenario:
* - You might be developing a game or other timing-sensitive application
* where performance is extremely important and where small stutters by the GC have to be avoided
* - You have your own object-pool and want Ceras to obtain instances from it and return them to it.
*
* What you want:
* - You want more performance
* - You want to decrease GC pauses and GC pressure
*
* What can we do:
* - Recycle objects, reducing GC pressure
* - Take objects from a pool instead of doing "new MyThing()" to improve cache-coherence.
*/
// Assuming we're receiving "network packets" over the internet, we'd instantiate a new object every time we call Deserialize
// That is pretty wasteful. We receive one object, deserialize it, use it, then discard it, ...
//
// What we can do is have one instance that we can just overwrite all the time!
//
// Ceras will use the provided object and overwrite the fields; instead of creating a new object instance.
// If no instance is provided, Ceras will just instantiate one.
//
// Hint: This can also be used to quickly set or reset an object to some predefined values in other scenarios.
var serializer = new CerasSerializer();
Person recycledPerson = new Person { Health = 35, Name = "test" };
byte[] buffer = serializer.Serialize(recycledPerson); // Lets assume we already got a network buffer and now we just have to read it.
for (int i = 0; i < 100; i++)
{
// No person object will be allocated, the fields
// of 'recycledPerson' will just be overwritten
serializer.Deserialize<Person>(ref recycledPerson, buffer);
}
//
// Now we'll use some extremely simple object-pooling solution to reduce GC pressure.
// The 'MyVerySimplePool' is obviously just for illustration, in something like Unity3D you would
// of course make something much more elaborate...
//
// If the data in the buffer tells us "it's a 'null' object" then Ceras will of course set 'recycledPerson' to null.
// But now you might wonder what happens to the instance that was previously inside 'recycledPerson'.
// Normally the answer would be that the object would be simply lost (and eventually its space would be reclaimed by the .NET "garbage-collector").
//
// In some scenarios (games) we don't want this because garbage-collections often cause stutters.
// A common solution to this is object-pooling.
// Ceras supports that by allowing you to "catch" unused objects, so you can return them to your object-pool.
MyVerySimplePool<Person> pool = new MyVerySimplePool<Person>();
SerializerConfig config = new SerializerConfig();
config.ConfigType<Person>()
.ConstructBy(() => new Person()) // select ctor
.ConstructBy(pool, () => pool.GetFromPool()); // or create from a pool
config.Advanced.DiscardObjectMethod = obj =>
{
pool.ReturnToPool((Person)obj);
};
serializer = new CerasSerializer(config);
// todo: the example is not fully done yet
/*
var personA = new Person { Name = "a", Health = 1 };
var personB = new Person { Name = "b", Health = 2 };
var personC = new Person { Name = "c", Health = 3 };
personA.BestFriend = personB;
personB.BestFriend = personC;
personC.BestFriend = personA;
serializer.Serialize();
*/
}
public void Step4_KnownTypes()
{
/*
* Assuming we want to reduce the used space to an absolute minimum, we can tell Ceras what types will be present throughout a serialization.
* Ceras will (while creating the serializer) assign unique IDs to each type and use that instead of writing the long type names.
*/
var person = new Person { Name = "riki", Health = 100 };
SerializerConfig config = new SerializerConfig();
config.KnownTypes.Add(typeof(Person));
var serializer = new CerasSerializer(config);
var data = serializer.Serialize(person);
data.VisualizePrint("Data serialized using KnownTypes");
var clone1 = serializer.Deserialize<Person>(data);
Console.WriteLine($"Clone (using KnownTypes): Name={clone1.Name}, Health={clone1.Health}");
/*
* This is only really useful for small packages, for example when sending data over the net.
*
* If you serialize, lets say, a list of 20 persons,
* then Ceras will only write the type-information once anyway.
* So there's not much savings to be had relative to the size of the data.
*
* Howver, using KnownTypes will still save you some space (and thus also serialization time)
* so using it is never a bad idea!
*
* For the network-scenario you can even use a 3rd option where Ceras still only writes type-data once,
* and the receiving side will remember it... Giving you the best of both approaches!
* More on that in the network example where all sorts of space-saving mechanisms (including KnownTypes) will be explored!
*/
}
public void Step5_CustomFormatters()
{
// Read this guide as well, it explains some more things:
// https://www.rikidev.com/extending-ceras-with-a-custom-formatter/
/*
* Scenario:
* - An object is somehow special an needs to be serialized in a very special way
* - Simply writing out the fields is not enough, maybe some meta-data is also needed or whatever...
* - Ceras' dynamic serializer (code generator) is not enough to deal with the object
*
* Solution:
* - Provide a specialized formatter
*
* I hear you: "what the hell is a formatter??"
* It's actually just a simple class that inherits from IFormatter<YourTypeHere>
* and it simply takes over the serialization completely.
*/
/*
* "OK where do I start?"
* 1.) Just inherit from IFormatter<YourTypeHere>
* 2.) Give an instance of your formatter to Ceras when it asks for one through the callback in config
*
*/
SerializerConfig config = new SerializerConfig();
config.OnResolveFormatter.Add((s, t) =>
{
if (t == typeof(Person))
return new MyCustomPersonFormatter();
return null;
});
var serializer = new CerasSerializer(config);
var p = new Person { Name = "riki", Health = 100 };
p.BestFriend = p;
var customSerializedData = serializer.Serialize(p);
var clone = serializer.Deserialize<Person>(customSerializedData);
}
public void Step6_NetworkExample()
{
// todo: ...
/*
* If you cannot wait for the guide, then take a look at those properties
* and read the XML documentation for them (hover over their names or press F12 on them)
*/
/*
SerializerConfig config = new SerializerConfig();
config.GenerateChecksum = true;
config.KnownTypes.Add();
config.PersistTypeCache = true;
config.ObjectFactoryMethod = ...;
config.DiscardObjectMethod = ...;
*/
}
public void Step7_GameDatabase()
{
/*
* Scenario:
* We have "MyMonster" and "MyAbility" for a game.
* We want to be able to easily serialize the whole graph, but we also
* want MyMonster and MyAbility instances to be saved in their own files!
*
* Lets first take a look at the classes we're working with:
*/
MyMonster monster = new MyMonster();
monster.Name = "Skeleton Mage";
monster.Health = 250;
monster.Mana = 100;
monster.Abilities.Add(new MyAbility
{
Name = "Fireball",
ManaCost = 12,
Cooldown = 0.5f,
});
monster.Abilities.Add(new MyAbility
{
Name = "Ice Lance",
ManaCost = 14,
Cooldown = 6,
});
// We want to save monsters and abilities in their their own files.
//
// Using other serializers this would be a terribly time-consuming task.
// How would a classic solution for that look like? (without Ceras)
// We would have to add attributes or maybe even write custom serializers so the "root objects"
// can be when they are referenced in another object..
// Then we'd need a separate field maybe where we'd save a list of IDs or something....
// And then at load(deserialization)-time we would have to manually load that list, and resolve the
// objects they stand for...
// And all that for literally every "foreign key" (as it is called in database terms). :puke: !
//
// Ceras offers a much better approach.
// Just implement the 'IExternalRootObject' interface so Ceras can obtain an "ID" of your objects.
// So whenever Ceras sees one of your objects implementing that interface, it will just write the ID of the object instead.
// You can generate that ID however you want, most people would proably use some sort of counter.
//
// When loading/deserializing an object again Ceras will ask you for the external objects (giving you the ID).
//
SerializerConfig config = new SerializerConfig();
// 1. Create a config with a (pretty simple) custom external-object-resolver.
var myGameObjectsResolver = new MyGameObjectsResolver();
config.ExternalObjectResolver = myGameObjectsResolver;
// 2. Using KnownTypes is not neccesary at all, it just makes the serialized data a bit smaller.
config.KnownTypes.Add(typeof(MyAbility));
config.KnownTypes.Add(typeof(MyMonster));
config.KnownTypes.Add(typeof(List<>));
// Ceras will call "OnExternalObject" (if you provide a function) when it encounters one of your IExternalRootObjects.
//
// So what would you use that for?
// Pretty often when serializing one object, you probably also want to know about all the other IExternalRootObjects that
// part of the "object graph" in some way (referenced by the original object) so you can save them as well.
//
// Maybe it would be a good idea to also include the last time an object has changed.
// Like, you could have an OnPropertyChanged and whenever something changes you'd set something like a 'LastModified' date.
// Then later you have the list of all the IExternalRootObjects and you can check LastModified to see if you have
// to serialize and save it into a file again, or if the object is still up to date.
//
// In our example that means when serializing our Monster, the OnExternalObject function would
// get called for the two abilities; that way we can save them as well.
List<IExternalRootObject> externalObjects = new List<IExternalRootObject>();
config.OnExternalObject = obj => { externalObjects.Add(obj); };
var serializer = new CerasSerializer(config);
myGameObjectsResolver.Serializer = serializer;
var monsterData = serializer.Serialize(monster);
// we can write this monster to the "monsters" sql-table now
monsterData.VisualizePrint("Monster data");
MyGameDatabase.Monsters[monster.Id] = monsterData;
// While serializing the monster we found some other external objects as well (the abilities)
// Since we have collected them into a list we can serialize them as well.
// Note: while in this example the abilities themselves don't reference any other external objects,
// it is quite common in a real-world scenario that every object has tons of references, so keep in mind that
// the following serializations would keep adding objects to our 'externalObjects' list.
for (var i = 0; i < externalObjects.Count; i++)
{
var obj = externalObjects[i];
var abilityData = serializer.Serialize(obj);
var id = obj.GetReferenceId();
MyGameDatabase.Abilities[id] = abilityData;
abilityData.VisualizePrint($"Ability {id} data:");
}
/*
* Note:
*
* 1.)
* Keep in mind that we can not share a deserialization buffer!
* That means overwriting the buffer you passed to Deserialize while the deserialization is still in progress will cause problems.
* "But why, when would I even attempt that??"
* -> If you remember Step1 there's a part about re-using buffers. Well, in some cases you might be tempted to share a deserialization buffer as well.
* For example you might think "if I use File.ReadAllBytes() for every object, that'd be wasteful, better use one big buffer and populate it from the file!"
* The idea is nice and would work to avoid creating a large buffer each time you want to read an object; but when combining it with this IExternalObject idea,
* things begin to break down because:
*
* Lets say you have a Monster1.bin file, and load it into the shared buffer. Now while deserializing Ceras realizes that the monster also has a reference to Spell3.bin.
* It will send a request to your OnExternalObject function, asking for Type=Spell ID=3.
* That's when you'd load the Spell3.bin data into the shared buffer, OVERWRITING THE DATA of the monster that is still being deserialized.
*
* In other words: Just make sure to not overwrite a buffer before the library is done with it (which should be common sense for any programmer tbh :P)
*
* 2.)
* Consider a situation where we have 2 Person objects, both refering to each other (like the BestFriend example in Step1)
* And now we'd like to load one person again.
* Obviously Ceras has to also load the second person, so it will request it from you
* Of course you again load the file (this time the requested person2.bin) and deserialize it.
* Now! While deserializing person2 Ceras sees that it needs Person1!
* And it calls your OnExternalObject again...
*
* > "Oh no, its an infinite loop, how to deal with this?"
*
* No problem. What you do is:
* At the very start before deserializing, you first create an empty object:
* var p = new Person();
* and then you add it to a dictionary!
* myDictionary.Add(id, p);
*
* And then you call Ceras in "populate" mode, passing the object you created.
* ceras.Deserialize(ref p, data);
*
* And you do it that way evertime something gets deserialized.
* Now the problem is solved: While deserializing Person2 ceras calls your load function, and this time you already have an object!
* Yes, it is not yet fully populated, but that doesn't matter at all. What matters is that the reference matches.
*
*
* If this was confusing to you wait until I wrote another, even more detailed guide or something (or just open an issue on github!)
*
*
* (todo: write better guide; maybe even write some kind of "helper" class that deals with all of this maybe?)
*/
// Load the data again:
var loadedMonster = serializer.Deserialize<MyMonster>(MyGameDatabase.Monsters[1]);
var ability1 = serializer.Deserialize<MyAbility>(MyGameDatabase.Abilities[1]);
var ability2 = serializer.Deserialize<MyAbility>(MyGameDatabase.Abilities[2]);
}
public void Step8_DataUpgrade_OLD()
{
/*
* So you have a settings class or something, and now you have done 3 types of changes:
* - removed a field
* - added a new field
* - renamed a field
*
* For now Ceras trades off versioning support for speed and binary-size, so this is not directly supported.
* You can still load the old data using the old class (maybe move it into a separate namepace, or an extra .dll)
*
*
* Note:
* - In a future version I'll most likely add an option to serialize
* by name / integer-keys and support for automatic conversion from older formats.
*
*
*/
// In this example I'm just using JObject (from Newtonsoft.Json) for easy editting and transfer.
// However you can also do it fully manually, but personally I find working with JObject pretty easy.
var settings = new SettingsOld { Bool1 = true, Int1 = 5, String1 = "test" };
var serializer = new CerasSerializer();
var oldData = serializer.Serialize(settings);
// Now we have some data in the old format
var oldLoaded = serializer.Deserialize<SettingsOld>(oldData);
JObject jObj = JObject.FromObject(oldLoaded);
// Remove Bool1
jObj.Remove("Bool1");
// Rename Int1 -> Int2
var int1 = jObj["Int1"];
jObj.Remove("Int1");
jObj["Int2"] = int1;
// Add String2
jObj["String2"] = "string added during data-upgrade";
var newSettings = jObj.ToObject<SettingsNew>();
var newData = serializer.Serialize(newSettings);
}
public void Step9_VersionTolerance()
{
/*
* This is like V2 of the 'DataUpgrade' section.
* Since then Ceras got a VersionTolerance feature which can be enabled in the config.
*
*
*/
// todo 1: show how to use it
// todo 2: mention that any type-changes of existing fields are not supported (int X; becoming a float X; or so)
// todo 3: show how to deal with changing types (like in #8, just keep the old object around)
// todo 4: In the future: Embed type-data and maybe a version number, so Ceras can also deal with changing types!
// Advantage: Now we have perfect version tolerance, no need for any workarounds!
// Disadvantage: Uses more additional space, we can't magically save more data (the member types) without using more space.
// Problems: If we are asked to load an older type, and we see that a type has changed, we need the user to provide some sort of type-converter thingy.
// but we can probably do automatic conversion for all the simple stuff (int->float, ...), but maybe that's too dangerous.
// What if someone has a locale set that uses '.' and we use ',' and then "1.45" becomes 145 as int (which is ofc wrong)
}
public void Step10_ReadonlyHandling()
{
/*
* Situation:
*
* You have an object, which has a 'readonly Settings CurrentSettings;'.
* Of course that can't (normally) be serialized or deserialized.
* But Ceras can still deal with it.
*
* Default: readonly fields are completely ignored
*
* Members: save/restore the content of the variable itself
*
* Forced: also fix if there's a mismatch
*
*
*
*/
{
}
// todo: explain that its only for readonly fields.
// Readonly props can add a {private set;},
// and if for whatever reason simply adding a private set is not possible and you only have a {get;}=...; then things get extremely complicated
// with SkipCompilerGeneratedFields and all the tons of problems that comes with.
// todo: explain in detail what a mismatch is (null -> not null, not null -> null, polymorphic type mismatch)
}
}
static class MyGameDatabase
{
public static Dictionary<int, byte[]> Monsters = new Dictionary<int, byte[]>();
public static Dictionary<int, byte[]> Abilities = new Dictionary<int, byte[]>();
}
class MyGameObjectsResolver : IExternalObjectResolver
{
// todo: in the future ExternalObject resolvers will also support dependency-injection, so you won't have to set this yourself...
public CerasSerializer Serializer;
public void Resolve<T>(int id, out T value)
{
byte[] requestedData;
if (typeof(T) == typeof(MyMonster))
requestedData = MyGameDatabase.Monsters[id];
else if (typeof(T) == typeof(MyAbility))
requestedData = MyGameDatabase.Abilities[id];
else
throw new Exception("cannot resolve external object type: " + typeof(T).FullName);
value = Serializer.Deserialize<T>(requestedData);
}
}
class MyCustomPersonFormatter : IFormatter<Person>
{
// Public IFormatter<T> fields are auto-injected by Ceras's dependency injection system
public IFormatter<Person> PersonFormatter;
public void Serialize(ref byte[] buffer, ref int offset, Person value)
{
SerializerBinary.WriteString(ref buffer, ref offset, value.Name);
SerializerBinary.WriteInt32(ref buffer, ref offset, value.Health);
// !! Important - Read below!
PersonFormatter.Serialize(ref buffer, ref offset, value.BestFriend);
// You might be tempted to just recursively call your own Serialize method (this method) again for 'BestFriend', but that won't work!
// That won't work because Ceras does many things behind the scenes to make reference-loops work.
//
// Think about it like this:
// When we want to serialize '.BestFriend' and someone is their own best friend (silly, i know :P) then we'd want the serialized data to
// say "this object was already written, look it up here..."
// Otherwise we'd get into an infinite loop, which is exactly what is happening if we'd just write "this.Serialize(ref buffer, ref offset, value.BestFriend);" here.
//
// Now, as for the 'PersonFormatter' field in this class.
// Ceras can inject fields of type 'IFormatter' and 'CerasSerializer' into all its formatters.
// So, even though it may look like that the object injected into "PersonFormatter" is just 'MyCustomPersonFormatter' itself again, that's not the case!
//
// In case you are interested in what's going on behind the scenes:
// The actual object Ceras injects into our 'PersonFormatter' field is 'ReferenceFormatter<Person>', which does all the magic to make references and object identity (and many other things) work.
}
public void Deserialize(byte[] buffer, ref int offset, ref Person value)
{
// Nothing interesting here, all the important stuff is explained in 'Serialize()'
value.Name = SerializerBinary.ReadString(buffer, ref offset);
value.Health = SerializerBinary.ReadInt32(buffer, ref offset);
PersonFormatter.Deserialize(buffer, ref offset, ref value.BestFriend);
// You can try changing 'BestFriend' into a property.
// If you do, you have to modify this last line a bit:
/*
var f = value.BestFriend;
PersonFormatter.Deserialize(buffer, ref offset, ref f);
value.BestFriend = f;
*/
}
}
class SettingsOld
{
public bool Bool1;
public int Int1;
public string String1;
}
class SettingsNew
{
// Removed:
// public bool Bool1;
// Renamed: from Int1
public int Int2;
// This one stays as it is
public string String1;
// Newly added:
public string String2;
}
class MyVerySimplePool<T> where T : new()
{
Stack<T> _stack = new Stack<T>();
public int Count => _stack.Count;
public T GetFromPool()
{
if (_stack.Count == 0)
return new T();
return _stack.Pop();
}
public void ReturnToPool(T obj)
{
_stack.Push(obj);
}
}
class MyAbility : IExternalRootObject
{
static int _autoIncrementId;
public int Id = ++_autoIncrementId;
int IExternalRootObject.GetReferenceId() => Id;
public string Name;
public float Cooldown;
public int ManaCost;
}
class MyMonster : IExternalRootObject
{
static int _autoIncrementId;
public int Id = ++_autoIncrementId;
int IExternalRootObject.GetReferenceId() => Id;
public string Name;
public int Health;
public int Mana;
public List<MyAbility> Abilities = new List<MyAbility>();
}
[MemberConfig(TargetMember.All)]
class SomeAttributeExample
{
int _privateNumber = 5;
public int PublicNumber = 7;
[Exclude]
string _privateString = "this will not get serialized";
[Exclude]
public string PublicString = "and neither will this...";
}
[MemberConfig(TargetMember.None)]
class SomeAttributeExample2
{
[Include]
int _private1 = 5;
string _privateString = "this will not get serialized";
public int Public1 = 7;
[Include]
public string Public2 = "this will be serialized";
}
}