PDA

View Full Version : My mighty persistency system - for anyone to use!



Chebmaster
18-08-2006, 08:14 PM
* 23.11.2006: A small but significant step towards the true usability. :D
* 20.09.2006: Ported to Turbo Delphi.
* 13.09.2006: Resolved the conflict with Lazarus that prevented its code completion and Ctrl-click browsing from working.
* 01.09.2006: adressed one issue related to the usage convenience B).
* 31.08.2006: 4 bugs corrected, one of them severe :oops:

8) Project page on SourceForge.net >> (https://sourceforge.net/projects/chepersy/)

It always irked me that every time you add a new data structure, you need to spend a lot of effort on the boring, non-creative background work of creating the save/load mechanism for it - if you wish it stay persistent. And the published properties are no help. Mainly because there is no mechanism to check if you forgot to include something.

I spent two months resolving exactly the problem of storing complex data structures and backward compatibility of data files. The main idea that all used types are registered, and divided to "binary" (which are saved/loaded as is) and "complex" (each has a custom loading/saving procedure). Then all fields are registered. While saving, all the adjacent "binary" fields are saved as a solid block of bytes, and for all the "complex" fields a corresponding procedure is called. For the string, for example, it first stores the length, then, if the string isn't empty, its contents - and so on.

Cross-links and circular object referencesa are resolved automatically.

Well, aood test example can say more than a thousand docs: feel free to play with it ;)


Content of the Readme file:

1. This stuff was roughly cut from my game engine.
Hence many extraneous stub functions.

2. The documentation is built into the mo_classes unit
in the form of comments. See also the *.inc files
it uses.

3. I should add that average saving/loading speed
of 1.000.000 objects per second is quite real.
This stuf is *fast*.

4. The {$minenumsize 4} directive in your program
is ABSOLUTELY NECESSARY. Do not forget it,
or you'll get the corrupt data files that will give
you free AVs instead of backward compatibility.

5. The AddLog() and Die() procedure variations
in the mo_stubs_public.inc are a temporary stubs
using WriteLn(). You should replace these with
your own code for displaying log/ error messages.

The glUint and glFloat types are re defined in
mo_tmid_public.h - remove these if you plan
to use OpenGL.


6. The following files are taken directly from my
engine (i.e. these are *shared* files) and thus
are subject to modification without notice.
Do not modify them if you want to keep your
version up to date (The updated versions may be
"borrowed" from my CGE distributions, starting
from the version included with the Test #13
(NOT Test #12 or below!). As I write these lines,
the Test #13 is not out yet.
For example, my future plans include adding
the compression of data files.

cl_cfile_func.inc
cl_keys.inc
mo_basket.inc
mo_classes.pp
mo_dyna.h
mo_dyna.inc
mo_dyna_1template.h
mo_dyna_1template.inc
mo_dyna_2template.h
mo_registrator.inc
mo_scenario.inc
mo_trulypersistent.inc
mo_typeprocs.inc

the home of CGE is
http://chebmaster.narod.ru/cge/cge_e.html

7. The correct one approach is to use classes.
The record types are not allowed (yet).

8. The mechanism of matching enumerated types by
their value names is quite powerful, as you
can see. You can swap the valuse by places,
add new ones at the beginning - the converter
will always assign the correct one. Note, that
if you *delete* the value, it will be replaced
at loading with the value which ord() is 0.

9.The object structure can be cross-linked,
can contain circular references - the
processing mechanism will resolve everything
automatically (although you may want to raise
the stack limit, because the alghoritm is
recursive).
And do NOT touch the _BasketIndex field, or
things will go kaboom.

10.The built-in validator checks if the registered
fields cover the entire instance, alsso if
there are any gaps between them, and gives
comprehensible error messages, refusing to run
if some clas is registered incorrectly.
[/b]

Traveler
19-08-2006, 10:48 AM
Thanks for sharing

Chebmaster
19-08-2006, 12:50 PM
It's my contribution, and my thanks to the people who created Free Pascal and The GIMP, libPNG and uncounted other GNU'd things...

But... Perharps, I should have published these from the start:

Strategy: Make your entire game world be contained in a single object, which contains other objects. The TArrayOfTrulyPersistent (a dynamic-array substitution class) should be of great help here. Definition may look a bit (or completely) unclear, since it is implemented via macros. ((look at TDyna and mo_Dyna_xxxxx files more closely - these are practically a substitutes for dynamic arrays, but have much more conveniencies built-in, like the "Last" property, reading the last element, "Add" and "Delete" methods, as well as some optimization towards the minimization of memory reallocation calls. )



text of the test program:
{$appmode console}
{$m 10000, 300000000} //Otherwise it crashes with stack overflow (error 202)
{$mode delphi}
{$minenumsize 4}
uses
SysUtils, typinfo, mo_classes;
type
TMyEnum = (Svobu, Dabu, Zigzag, ZugZug);
TTestData = class (TTrulyPersistent)
a: string;
c: TMyEnum;
b: integer;
d, e: TTestData;
procedure RegisterFields; override;
end;
var
TestData: TTestData;
FileVersion: AnsiChar;
procedure TTestData.RegisterFields;
begin
RegType(TypeInfo(TMyEnum));
RegField('a', @a, TypeInfo(string));
RegField('c', @c, TypeInfo(TMyEnum));
RegField('b', @b, TypeInfo(integer));
RegField('d', @d, TypeInfo(TTestData));
RegField('e', @e, TypeInfo(TTestData));
end;
begin
Try
AddLog('Started.');
ClassesRegistrationStart;
RegClass(TTestData);
if not FileExists(ExtractFilePath(ParamStr(0)) + 'testsave.cge')
then begin
TestData:=TTestData.Create;
TestData.a:='My Nice AnsiString';
TestData.b:=777;
Testdata.c:=ZugZug;
TestData.d:=TTestData.Create;

//A circular reference. Scary?.. Hell, no!
TestData.d.e:=TTestData.Create;
TestData.d.d:=Testdata.d;
TestData.d.e.e:=TestData;

WriteLn('Writing...');
//Format 0 (zero, not o) is the simple but fast.
//format s includes MD5 sum calculation and checking.
SaveGame(TestData, 's', ExtractFilePath(ParamStr(0)) + 'testsave.cge');
WriteLn('Success.');
WriteLn;
TestData.Free;
end;
WriteLn('Loading...');
TestData:=LoadGame(ExtractFilePath(ParamStr(0)) + 'testsave.cge', 'TTestData') as TTestData;
WriteLn('Success.');
WriteLn('a = ', TestData.d.e.e.a);
WriteLn('b = ', TestData.d.e.e.b);
WriteLn('c = ', GetEnumName(TypeInfo(TMyEnum), ord(TestData.d.e.e.c)));
Except
WriteLn ('Error! ', (ExceptObject as Exception).Message);
End;
ReadLn;
end.



TTrulyPersistent = class(TObject)
protected
...
public
...
procedure RegisterFields; virtual; abstract;
procedure AfterLoading(); virtual; //is called after the object is loaded.
procedure AfterConstruction(); override;
end;

...

procedure SaveGame(o: TObject; version: AnsiChar; TargetName: AnsiString);
function LoadGame(SourceName: AnsiString; ClassName: WideString): TObject;

...

// ****************** INIT PROCEDURES ********************************
procedure ClassesRegistrationStart;
//!!! procedure ClassesRegistratonEnd;

{ RegClass must be called for each descendant of TTrulyPersistent.
The best place to do it is the initialization procedure of the module.
Automatically calls the "Register" constructor of a class.
}
procedure RegClass(C: CTrulyPersistent);

// ****************** FIELD PROCEDURES *********************************

{
These intended to be called only and only from within the
"Register" constructor of TTrulyPersistent descendants.
(Why didn't I make them its methods...?)
Types should be registered first. )
}
procedure RegField(name: string; Pf: pointer; PI: PTypeInfo);
{ example: RegField('Merry rabbits', @bunnies, TypeInfo(TBunnies));
-- where "bunnies" is a field of type TBunnies}

//Skipped fields: for texture ids, and other stuff that
// cannot be safely restored from the save.
procedure RegSkip(name: string; Pf: pointer; PI: PTypeInfo);
//For pointers and non-registrable classes (technically, pointers too)
procedure RegSkipPtr(name: string; Pf: pointer);

// ****************** TYPE PROCEDURES ************************************

{ These could be called from anywhere after the classes registration started.
Even from the "Register" constructor.}

{ This bunny registers binary types (i.e. ones that could be
dumped into a stream directly, without caring about their structure).
...I wish TypeInfo or TypeData structures had a size field
so that pointing it manually would be unnecessary. Dreams, dreams... :( }
procedure RegType(Info: PTypeInfo; Size: Integer); overload;

{ This bunny acts the same as the one above, with two following exceptions:
A). It assumes type size is equal to pointer size (4 bytes), and
B). It could be used to register enumeration types which need
a special approach (saved as DWORDs, loaded either as
DWORDs or via special converting routine - depending on the
parsing results).
(Do NOT forget to use the $MINENUMSIZE 4 compiler directive!)}
procedure RegType(Info: PTypeInfo); overload;

{ This bunny allows to deal with complex types like strings,
dynamic arrays - even non-persistent objects! - by introducing
a custom processing procedure (see template in mo_typeprocs.inc).
Its only limitation is that it assumes base type to be a pointer,
so field size is always pointer-sized (4 bytes on 32-bit platform).}
procedure RegType(Info: PTypeInfo; Proc: TCustomTypeProcessingProc); overload;

{ These bunnies register types of corresponding classes.
Note, that persistent classes are registered automatically
when RegClass() is called. So you need to evoke
RegType(C: CTrulyPersistent) manually only in cases of
cross-reference (while class A has fields of type B,
and class B - fields of type A)}
procedure RegType(C: CTrulyPersistent); overload;

// *********** FIELD SAVING / LOADING ROUTINES **********************
{ These can be used in the custom processing procedures}
Function SizeToBufferIndex(s: integer): integer; inline;
procedure WriteInt(i: integer); inline;
function ReadInt: integer; inline;
function PeekInt: integer; inline; //read value but not remove it from buffer

//Don't forget! !!! len is not in bytes but in 32-bit words!
procedure WriteBin(p: pointer; len: integer); inline;
procedure ReadBin(p: pointer; len: integer); inline;
procedure SkipBin(len: integer); inline;
// These three work for anything 32-bit: glFloat, cardinal, integer...
procedure WriteDword(p: pointer); inline;
procedure ReadDWord(p: pointer); inline;
procedure SkipDword(); inline;

//resizes the string prior to writing, to align its size to 32-bit boundary.
// probably, can cause a memory re-allocation call.
procedure WriteAnsiString(v: AnsiString); inline;

//doesn't need the abovementioned trick, because of the additional
// trailing zero (PWideChar compatibility). This extra character
// (two bytes) isn't counted, and in the worst case scenario
// it'll be all that will be rewritten.
procedure WriteWideString(v: WideString); inline;

procedure SkipAnsiString; inline;
procedure SkipWideString; inline;
function ReadWideString(): WideString; inline;
function ReadAnsiString(): AnsiString; inline;
procedure WriteNil; inline;

procedure ReadPersistent(var o: TTrulyPersistent); register;
procedure WritePersistent(o: TTrulyPersistent);

...


If you want to add more complex types (although that wouldn' be highly advisable) - look the mo_typeprocs as an example.

{ ************** COMPLEX TYPE PROCESSING PROCEDURES ********}
(*
NOTE:
Since the reading and writing is always performed
in 32-bit words, AND it is assumed that *anything*
is aligned to the 32-bit boundary,
the data types whose size isn't a multiply of 4
are a big bad no-no.
Be especially watchful for the packed records.
************************************************** ************
*)
(*
//Procedure template:
procedure RP_ (PField: pointer; OP: TFieldOperation); typeprocscall;
begin
case op of
fio_Load:
fio_Save:
fio_Skip:
end;
end;
*)
...
procedure RP_Persistent (PField: pointer; OP: TFieldOperation); typeprocscall;
var
o: TTrulyPersistent;
begin
case op of
fio_Load: ReadPersistent(TTrulyPersistent(PField^));
fio_Save: WritePersistent(TTrulyPersistent(PField^));
fio_Skip: begin
ReadPersistent(o); //..load it..
MemoryLeakSuspected:=Yes;
//..And do NOTHING.
// All the persistent objects are stored in the special array
// and dealt with special alghorithm.
end;
end;
end;

In the end I want to add, that I plan to evolve this thing, together with my entire engine, and correct any bugs/shortcomings I might have missed. I dreamed of creating it for years.

dmantione
19-08-2006, 03:42 PM
Brilliant! This is really the kind of stuff you can be a language more powerfull.

I had a look, but unfortunately you sabotaged your code to be compiled anything besides Windows.

Some comments:
- Would it be possible to make the code do endian conversions? So for example the format on disk is always big endian?
- The registerfields doesn't look nice, it would be much nicer to do it with RTTI. I don't know if compiler can do it, but it can likely be made to output the right information to make this no longer necessary.

Chebmaster
19-08-2006, 05:11 PM
your code to be compiled anything besides Windows.
Untrue. It should go for anything intel-32 bit. For example, Linux or FreeBSD. I, myself, indend my engine to support both Windows and Linux, butt the latter shleduled for the later time, because I currently don't have the free hard disk to install the second OS.


- Would it be possible to make the code do endian conversions? So for example the format on disk is always big endian?
Theoretically - yes. Practically... I do not have that kind of hardware to test it, to begin with (note: for Russia the statement "There Is No Such Thing As Macintosh" is 99.9% true).

The cge-file header includes some reserved (zero) fields and contains platform info (sizes of pointer, integer, etc) but this information isn't currently used.

In short, the support of little-endian is theoretically possible but is above my abilities (at least, for now). The same could be said for the 64-bit platforms.


- The registerfields doesn't look nice, it would be much nicer to do it with RTTI.
I wish it was possible... But utfortunately, it is not :(
RTTI contains ONLY info on published fields.


This is really the kind of stuff you can be a language more powerfull.
The full RTTI containing info on all the fields, types and type sizes - *that* would truly make it more powerful. :roll:

dmantione
19-08-2006, 07:30 PM
your code to be compiled anything besides Windows.
Untrue. It should go for anything intel-32 bit. For example, Linux or FreeBSD. I, myself, indend my engine to support both Windows and Linux, butt the latter shleduled for the later time, because I currently don't have the free hard disk to install the second OS.

Perhaps, but if you code like:

{$ifndef win32}^
{$fatal AS OF JUNE 2006 LINUX SUPPORT IS TEMPORARILY DOWN}^
{$ifndef linux}^
{$fatal NOT PORTED TO THIS PLATFORM YET!}^
{$endif}^
{$endif}^


... it is not going to compile on any Intel 32-bit. :D




- Would it be possible to make the code do endian conversions? So for example the format on disk is always big endian?
Theoretically - yes. Practically... I do not have that kind of hardware to test it, to begin with (note: for Russia the statement "There Is No Such Thing As Macintosh" is 99.9% true).


Well, it is interresting because it would remove one of the most annoying porting tasks away from the programmer and would therefore make code automatically portable.




This is really the kind of stuff you can be a language more powerfull.
The full RTTI containing info on all the fields, types and type sizes - *that* would truly make it more powerful. :roll:

The compiler can be changed... It'll be quite some work though :?

Chebmaster
19-08-2006, 07:47 PM
Perhaps, but if you code like:
Oops... This was meant for the window manager of the CGE core - its Linux version is currently terribly out of date...

You can replace this code with

{$ifdef cge}
{$ifndef win32}
{$fatal AS OF JUNE 2006 LINUX SUPPORT IS TEMPORARILY DOWN}^
{$ifndef linux}
{$fatal NOT PORTED TO THIS PLATFORM YET!}
{$endif}
{$endif}
{$endif}

I updated the archive. Now it should kick and scream only if compiled as a part of my engine.

Chebmaster
31-08-2006, 03:35 PM
I have found and corrected 4 bugs.

The download address is the same (see the first post).

1: The boolean type wasn't registered by default.
2: It was impossible to refister any dynamic array fields
3: It was impossible to register any fields of class types that weren't descendants of TTrulyPersistent.
4: At registering a class, the temporary instance was being deleted via regular destructor. A very nasty bug, caused AV at registering any class that deallocated something in its destructor.

Chebmaster
01-09-2006, 03:51 PM
01.09.2006:
- added ability of nested registering
(i.e. you now can call RegClass()
from inside the REgisterFields() method.

Chebmaster
13-09-2006, 10:50 AM
September, 13 of 2006: A big, fat gingerbread fou you all, people!
I've resolved, at last, the conflict with Lazarus that prevented its code completion and Ctrl-click browsing from working at all. You need Lazarus 0.9.16 or newer for this to be true.

Download page (http://www.chebmaster.narod.ru/soft/libs_pers_e.html) (the old link is not valid anymore).

Chebmaster
15-09-2006, 06:01 PM
Now trying to adapt to Turbo Delphi (together with my entire engine). So far got a nice collection of AVs. :x

Chebmaster
18-09-2006, 04:14 PM
Delphi's a monster! :(
I battle it every day, but there seems no end to it... :cry:

:? // THIS FILE IS GENERATED AUTOMATICALLY,
// DO NOT EDIT IT!
// Make your modifications to "mo_ee_freepascal.inc" ,
// then run ConvertForDelphi.exe
procedure InitExportProcsDelphi;
begin


:x case Info^.Kind of
...
tkRecord:
DieTypeFailed(T, RuEn( ' ... '
,'Record types are prohibited.'#10#13
+'Reason #1: compiler compatibility. Delphi doesn''t support RTTI for them.'#10#13
+'Reason #2: better flexibility and possibility of support 64-bit/little-endian somewhere in the future. Records are a black boxes dependent on field sizes, alignments, and other fickle matter.'
))
else


:shock:
Class registration start: TTrulyPersistentTester
Class VMT is write-protected. Removing the protection at 02611748h...
Success. Gained write access for 81920 bytes at 02600000h.

Chebmaster
19-09-2006, 11:31 AM
:) I beat it. Almost. My entire engine now compiles in Delphi, and runs without a notch.
There is still a minor issue of some types named differently in Delphi and FPC RTTI, causing data files to be incompatible (longint/integer and longword/cardinal). Will add aliases mechanism to my TNameSpace class to resolve that.

I hope I will release the Delphi-compatible version tomorrow. Hmmm... how to name it?.. CPS, "Cheb's Persistency System"?.. Original enough, don't you think? :wink:

Chebmaster
21-09-2006, 12:29 PM
Long story short, I ported this miracle of engineering to Delphi with almost no problems. There are minor speed issues (since Boolean is every time converted from (0, $ffffffff) to (0, 1) and back) and lack of MD5 sum checking. Other than that... It works just finely, and the data files are perfectly compatible as far as I can see.

Chebmaster
23-11-2006, 11:32 PM
23 Nov. 2006: v 0.6.0 >> (https://sourceforge.net/projects/chepersy/)

Release notes:
A big step towards the real usability.
The arrays indexed by enums sport the same features as the single enum types: you can change the values order, add or remove them - the array will be loaded correctly regardless.
The nested registering of classes just makes chepersy usable for real.

Changes:
- Now supports types like
array [<Enum>] of <Something>
- Now you can safely call RegisterClass() for the same
class multiple times without raising an error.
- Now you can safely register other classes
from inside the RegisterFields() method.

Example:

type
TMyEnum = (Zigzag, Svobu, Dabu, ZugZug);
// TMyEnum = (Svobu, Dabu, Zigzag, ZugZug); //try this one
// TMyEnum = (Dabu, ZugZug, Svobu); //or this one

TEIArray = array [TMyEnum] of WideString;

...

TTestData = class (TTrulyPersistent)
public
a: Widestring;
c: TMyEnum;
x: TEIArray;
r: TMyArray;
b: integer;
d, e: TTestData;
procedure RegisterFields; override;
end;

...

procedure TTestData.RegisterFields;
begin
RegType(TypeInfo(TMyEnum));
RegType('TMyArray', TypeInfo(SmallInt), SizeOf(TMyArray));
RegType('TEIArray', TypeInfo(TEIArray), TypeInfo(WideString), TypeInfo(TMyEnum));
RegField('a', @a, TypeInfo(Widestring));
RegField('c', @c, TypeInfo(TMyEnum));
RegField('x', @x, TypeInfo(TEIArray));
RegField('r', @r, 'TMyArray');
RegField('b', @b, TypeInfo(integer));
RegField('d', @d, TypeInfo(TTestData));
RegField('e', @e, TypeInfo(TTestData));
end;

Chebmaster
25-11-2006, 07:02 PM
0.6.1. >> (https://sourceforge.net/projects/chepersy/)
One procedure declaration corrected:
procedure RegType(Info, BaseType, IndEnum: PTypeInfo); overload;
{deprecated} procedure RegType(StringID: ansistring; //must match Info^.Name
Info, BaseType, IndEnum: PTypeInfo); overload;

Chebmaster
09-01-2007, 02:26 AM
Currently working on the next version, 0.7.

Planned features:
- user's manual
- forward compatibility (automatic resolving of unknown types. For classes this means loading them as their ancestors if those are known)
- autoconversion of integer/float types (a step towards potential 64-bit platform support in the future)
- support for record types
- support for set types
- data stream format changed to accomodate these new features. The files created by the old versions of Chepersy (0.6.1 and below) are still 100% readable, though, with exception that most of the new features are disabled while reading such files.


What is not and will never be:
- Macintosh support. There are no Macs in Russia, period.

WARNING:
- the support for static array types will be no more (I mean those registered via procedure RegType(StringID: ansistring; Info: PTypeInfo; Size: Integer);) If someone used Chepersy in their project and used this feature, please TELL ME. Otherwise I'll just remove 'em. They are not flexible and not portable and just generally unsafe. Anyway, I already implemented the support for static arrays indexed by enumerated types - both portable and flexible like hell.

Chesso
09-01-2007, 12:41 PM
I have no idea what this is for what it's for, but it sounds pretty insane to make and maintain lol.

Chebmaster
12-01-2007, 11:56 AM
it sounds pretty insane to make and maintain
Well, I create it, first and foremost, for myself. For my own game engine. It just happened that it was very easy to cut out and present to the society.

But it may happen, if there will be no any useful comments or feedback, that I grow tired of mantaining the public version and focus solely on my engine.

...Well, any comments beside lol? :?


A piece of the manual being created:

WHY BOTHER?
(A bit of shameless ad)

A common approach today for creating a set of equippable jewelry slots on a creature includes creating a set of constants (for example, from ISL_LeftForefingerRing = 0 to ISL_RightEarring = 15) then creating a static array [0..15] of TfancyItem to contain them. Then you suddenly get a brilliant idea to add a new slot - for example, a pierced navel where you can equip earring-type items... Gotcha. You got a royal pain in the ass. The array was static, you need to rewrite the saving and loading procedures, to add, somehow, that 16-th element to your save games, to keep the old save games valid... Ouch.

Now let's look how this would look with Chepersy.
Stage One:
1. type
TFancySlotsInd = (ISL_LeftForefingerRing, <......> , ISL_RightEarring);
TFancySlots = array[TFancySlotsInd] of TfancyItem;
TfancySlotsSet = set of TfancySlotsInd;
2. class Tcreature(TTrulyPersistent)
<.....>
FancySlots: TfancySlots;
EquippedFancySlots: TfancySlotsSet;
<.....>
3. You use this scheme for some time, have a lot of saved games, then...

Stage Two:
1. You add a new constant, ISL_NavelPiercing, to TfancySlotsInd definition. You can even insert it in the middle of the list, or in the very beginning.
2. When you load an old saved game, Chepersy does all the dirty work for you. It will convert enumerated type ordinal values using their string names stored in the file header. It will move the array elements to their new places. It will shuffle the bits in the set field to match the new constants list. *All* you need is to lift your finger and add a new constant.
Of course, the conversion process will slow the loading down, about 3..4 times. But since it's *very* fast (1.000.000 objects/second fast) I doubt user will ever notice any delay given any sane estimates of the game world size.

Stage Three:
Here comes the real mega-bonus. As good as this system can convert forward, it can convert backward.
1. You load the new version of saved game in the old version of game executable. It knows nothing about the pierced navel.
2. But id *does* notice that there's one extra constant listed for the TfancySlotsInd type. Here comes the same process: the ordinal values are converted, the array elements put onto the new places, the bits in the set field shuffled to match the existing order.
3. For the earring that was equipped in the navel slot... The outcome depends. If your FancySlots field acts as a container, you can kiss the jewel goodbye -- it'll be permanently removed from the game world. If the slots just point to the item objects but the real container is creature's inventory (or some global list) -- it'll live.

The same goes for the new classes. You can develop TStinkyOgre, descendant of Togre, with a few additional fields for the fart strength, left ear color, etc., then load the saved game in an older version of the game executable. Chepersy will load TStinkyOgre as TOgre, omitting all the new fields.



WHAT'S THE CATCH?
(Your Mom did teach you everything about the free cheese and mouse traps, after all)

1. All your classes *should* be descendants of TTrulyPersistent.

2. You *must* register all the types and classes before you can use them. Don't be afraid, the process is simple and well described (see below).

3. You *must* register all the fields of your classes via special virtual method RegisterFields (see below). The registration process is strictly validated -- I made it as safe as the Pascal compiler's syntax check. You cannot miss any field or misplace their order -- the program just won't run, giving you a detailed, informative description of your blunder.

3. You *cannot* multi-thread it or perform more than one saving/loading simultaneously. Because it is based on the procedures and global variables, not on the classes. May seem strange, but I have a solid reason for this -- see the Cheb's game Engine and its structure.

4. You *must* organize your game world such way that it all would be contained in a single tree with one root object. No other structures are possible. Chepersy is intended for use with objects and objects only, and it takes a single root object as a parameter in its SaveGame() procedure.

5. There are some obstacles to using FCL/VCL -- I mean, TStringList, TCollection, et cetera. These classes are *not* descendants of TTrulyPersistent and thus need a bit of manual work to allow saving them via Chepersy. See the section «Writing a custom saving/loading procedure for a non-persistent class».

6. You *should not* use static arrays not indexed by an enum. They are *absolutely* inflexible, supported only for the sake of compatibility with the earlier betas. This feature is safety-checked. Add {$define chepersy_allowstaticarrays} to mo_globaldefs.h or use compiler directive -dchepersy_allowstaticarrays when compiling from the command line to allow it. See the RegTypeStaticArray() in the mo_registrator.inc for details, since this procedure is not described in this manual.

7 You *cannot* use dynamic arrays of records, enum-indexed arrays or static arrays. That is architecturally impossible.

8. You *cannot* use enumerated types with the base ordinal value other than 0. Shouldn't be a problem, though. Every enum you define as a set of consts does always have 0 as ordinal for its first const. Also, the maximum ordinal value of an enum is limited to 1000.

9. Chepersy requires the alignment to be set at 4. In Delphi the default is 8, so switching to 4 may cause performance degradation on 64-bit systems.

10. Currently no other form is supported other than direct writing/reading to/from a file on disk.

11. When compiled in Delphi, the MD5 sum checking is omitted. Only the programs compiled in FreePascal can use this feature.

Chebmaster
12-01-2007, 11:23 PM
Change in the plans from what was listed above:

-- autoconversion of integer/float types postponed for the later versions.
-- greatly simplified the registering of dynamic arrays (you won't need to write a custom procedure for them anymore)
-- maybe, just maybe, there is a faint hope of porting Chepersy to Macintosh. The architecture potentially allows it. But of course, that's for the much later stages.

Chebmaster
13-01-2007, 02:42 PM
A bug found in the previous version:
You cannot use enum-indexed arrays of anything that is not 4 bytes in size. :(
My fault. :oops: I didn't think such arrays are always packed. But they are. Both in Delphi and FreePascal.

Now trying to invent a workaround.

Chebmaster
25-02-2007, 08:49 PM
The undertested but (theoretically) ready candidate for v0.7 can be downloaded from here:

http://host-17-99.imsys.net/_share/_001/chepersy.0.6.99.tar.gz

Did not test it in Delphi yet, FreePascal 2.0.4 in Linux works Ok.

The new features
-- user's manual (included)
-- some forward compatibility and improver backward compatibility
-- support of record types (including packed ones)
-- support of set types
-- support of static array types
-- improved support and painless registration of dynamic array types
-- fix for the enum-indexed array types (in v0.62 they fail on anything that is not 4 nor 8 bytes in size).

The data stream format changed to accomodate these new features. The files created by the old versions of Chepersy (0.6.2 and below) are still 100% readable, though, with exception that most of the new features are disabled while reading such files.

Chebmaster
27-02-2007, 12:08 PM
8) I released the Delphi-compatible v 0.7.0 with manual included.

https://sourceforge.net/projects/chepersy/

Chebmaster
12-05-2007, 07:10 PM
0.7.1 >> (https://sourceforge.net/projects/chepersy/)
Minor bug fixes and reorganization.

Chebmaster
03-06-2007, 12:11 PM
0.8.0 is out 8)

-- A fatal dynarray-related bugs fixed, using the multi-dimensional
dynamic arrays in Delphi was causing a total collapse.

-- No more hacking VMT. A workaround found and applied.
To use the old, VMT-hacking code (in theory, it's a bit faster),
look for the commented out defines in mo_globaldefs.h

-- Now supports TStream (including TMemoryStream, TFileStream, etc.)
for saving and loading the data structure.
See the docs of the updated test.dpr.

-- Type conversion mechanism added,
has built-in support for most of standard Pascal types.
You can add your own converters for any registered classes,
but remember: it works only if *both* types are known.
The unknown types are dealt with as before.

-- Dynamic array registration improved, you don't need
to register the intermediary types anymore when
you register a multi-dimensional array type.
Added RegTypeDynArray2d and RegTypeDynArrayNd
(see the docs or the test example)

-- procedure RegTypeDynArray renamed to RegTypeDynArray1d

-- Added checking to avoid registering multiple fields
with the same name.

-- Documentation updated

-- Unreadable text in the Russian version of documentation corrected.

Chebmaster
27-02-2008, 09:59 PM
An unforgivable blunder: I didn't know that the dynamic arrays use different allocator from the one used in GetMem/FreeMem!

As a result, the first SetLength() call after loading the data structures trashes the heap...

Use the quick fix below or wait till I upload v0.8.3

Quick fix: replace the NewDynArray() procedure with this one:

type TArrayOfByte = array of byte;
Procedure NewDynArray(parray: pointer; Len, BaseTypeInd: integer);
var
p: pointer;
begin
pointer(parray^):= nil;
SetLength(TArrayOfByte(parray^), Len * Types[BaseTypeInd].Size);
if Len > 0 then //high value in FreePascal, length in Delphi
{$ifdef fpc}
dword((pointer(parray^) - 4)^):= Len - 1;
{$else}
dword(pointer(cardinal(parray^) - 4)^):= Len;
{$endif}
end;

arthurprs
27-02-2008, 11:44 PM
hi, what the unit do?

Chebmaster
28-02-2008, 12:19 AM
I beg your pardon?
Which unit do you refer to?

If you refet to Chepersy itself, the project page is http://www.chebmaster.narod.ru/soft/libs_pers_e.html
The full documentation is included with the sources on the sourceforge.net

arthurprs
28-02-2008, 05:17 AM
I beg your pardon?
Which unit do you refer to?

If you refet to Chepersy itself, the project page is http://www.chebmaster.narod.ru/soft/libs_pers_e.html
The full documentation is included with the sources on the sourceforge.net

sorry i was not clear, what this project do?

(my english is a trash sometimes)

Chebmaster
28-02-2008, 06:18 AM
It's a library for saving your game. The key feature is auto-conversion, so you can widely change your data structures and the old saves still remain loadable.
You base your project on it from the startto use it.

I'd better post the docs right here, for easier access.

WHAT'S SO GREAT?
(A bit of shameless ad)

A common approach today for creating a set of equippable jewelry slots on a creature includes creating a set of constants (for example, from ISL_LeftForefingerRing = 0 to ISL_RightEarring = 15) then creating a static array [0..15] of TfancyItem to contain them. Then you suddenly get a brilliant idea to add a new slot - for example, a pierced navel where you can equip the earring-type items... Gotcha. You got a royal pain in the ass. The array was static, you need to rewrite the saving and loading procedures, to add, somehow, that 16-th element to your save games, to keep the old save games valid... Ouch.

Now let's look how this would look with Chepersy.
Stage One:
1. type
TFancySlotsInd = (ISL_LeftForefingerRing, <......> , ISL_RightEarring);
TFancySlots = array[TFancySlotsInd] of TfancyItem;
TfancySlotsSet = set of TfancySlotsInd;
2. class Tcreature(TTrulyPersistent)
<.....>
FancySlots: TfancySlots;
EquippedFancySlots: TfancySlotsSet;
<.....>
3. You use this scheme for some time, have a lot of saved games, then...

Stage Two:
1. You add a new constant, ISL_NavelPiercing, to TfancySlotsInd definition. You can even insert it in the middle of the list, or in the very beginning -- the actual numeric values do not matter.
2. When you load an old saved game, Chepersy does all the dirty work for you. It will convert enumerated type ordinal values using their string names stored in the file header. It will move the array elements to their new places. It will shuffle the bits in the sets to match the new constants list. *All* you need is to lift your finger and add a new constant.
Of course, the conversion process will slow the loading down, about 3..4 times. But since it's *very* fast (up to 1.000.000 objects/second on an 1GHz CPU) I doubt the user will ever notice any delay given any sane estimations of the game world size (50.000 objects for a medium-sized RPG, I dare to say).

Stage Three:
Here comes the real mega-bonus. As good as this system can convert forward, it can convert backward.
1. You load the new version of saved game in the old version of game executable. It knows nothing about the pierced navel.
2. But id *does* notice that there's one extra constant listed for the TfancySlotsInd type. Here comes the same process: the ordinal values are converted, the array elements put onto the new places, the bits in the set field shuffled to match the existing order.
3. For the earring that was equipped in the navel slot... The outcome depends. If your FancySlots field acts as a container, you can kiss the jewel goodbye -- it'll be permanently removed from the game world. If the slots just point to the item objects but the real container is creature's inventory (or some global list) -- it'll live.

The same goes for the new classes. You can develop TStinkyOgre, the descendant of TOgre, with a few additional fields for, say, the fart strength, left ear color, and so on. Then someone loads the saved game in an older version of the game executable. Chepersy will resolve the problem by loading TStinkyOgre as TOgre, omitting all the new fields.

Starting from v0.8, it can convert all standard numerical types if you change field type (including the dynamic arrays of unlimited level of inclusion)



WHAT'S THE CATCH?
(Your Mom did teach you everything about the free cheese and mouse traps, after all)

1. All your classes *should* be descendants of TTrulyPersistent.

2. You *must* register all the types and classes before you can use them. Don't be afraid, the process is simple and well described (see below).

3. You *must* register all the fields of your classes via special virtual method RegisterFields (see below). The registration process is strictly validated -- I made it as safe as the Pascal compiler's syntax check. You cannot miss any field or misplace their order -- the program just won't run, giving you a detailed, informative description of your blunder.

3. You *cannot* multi-thread it or perform more than one saving/loading simultaneously, that's architecturally impossible. The system uses the index fields in class instances to keep track of the circular graphs, so trying to multi-instance it would be suicide.

4. You *must* organize your game world such way that it is contained in a single tree. No other structures are possible. Chepersy is intended for use with objects and objects only, it takes a single root object as a parameter in its SaveGame() procedure.

5. There are some obstacles to using FCL/VCL -- I mean, TStringList, TCollection, et cetera. These classes are *not* descendants of TTrulyPersistent and thus need a bit of manual work to allow saving them via Chepersy. See the section «Writing a custom saving/loading procedure for a non-persistent class».

6 You *cannot* use dynamic arrays of records or enum-indexed arrays. That is architecturally impossible.

7. You *cannot* use enumerated types with the base ordinal value other than 0. Shouldn't be a problem, though. Every enum you define as a set of consts does always have 0 as ordinal for its first const. Also, the maximum ordinal value of an enum is limited to 255. To be frank, I surmise the number of constants in most of the enums to be below 32. This is how I plan to use this system.

8. Chepersy requires the alignment to be set at 4. In Delphi the default is 8, so switching to 4 may cause performance degradation on the 64-bit systems.(Planned to be addressed in the later versions)

10. When compiled in Delphi, the MD5 sum checking is omitted. Only the programs compiled in FreePascal can use this feature. The FreePascal md5 unit is not usable in Delphi without the massive adaptation for which I don't have time.

11. My primary tool is FreePascal. I keep Chepersy compatible with Delphi, but the main brunt of testing is taken in FreePascal.

12. My primary goal is my game engine. Chepersy is nothing more than a by-product of it released for public convenience. So, do not expect the limitations to be lifted any time soon (if ever). You like it, it suits your needs -- use it. You don't like it or it doesn't have a feature you need -- tough luck.
The bugs, on the other hand, will be eventually weeded out, since I myself will rely on Chepersy a lot.




GETTING STARTED
(The typical daily operations of your average Chepersy-based program)

1. First and foremost, you need to open up mo_custom.inc and replace the five stub routines defined there with the ones that suit your program's needs. The AddLog(), as you can guess, is required to output Chepersy log messages, the Die() performs exception handling (it is used in Chepersy to build the error message chains), the RuEn() chooses either Russian or English message string, according to program settings.

2. Add {$include mo_globaldefs.h} at the very beginning of each your module, or just paste its content in. Otherwise your program will either refuse to run (the best-case scenario) or generate a corrupt cge-files that will crash the loader *much later* when you will change your types.

3. Add typinfo and chepersy units to your uses list.

4. Ad the call to ClassesRegistrationStart() ,at the very beginning, this function initializes Chepersy and should be called first.

5. Register your types and classes. The best place for this is in the initialization parts of your modules. When you register a class, its (and its ancestors') RegisterFields() method is called. You should define RegisterFields() method for all your classes (your classes must be descendants of TTrulyPersistent).

6. *After* all your types and classes are registered, you can use the new ones, added in v0.8
procedure SaveGame(o: TObject;
version: AnsiChar; Target: TStream;
Signature: WideString);
function LoadGame(
Source: TStream; Signature: WideString): TObject;
or the old ones
procedure SaveGame(o: TObject;
version: AnsiChar; TargetName: AnsiString;
Signature: WideString);
function LoadGame(
SourceName: AnsiString; Signature: WideString): TObject;
to save and load your object tree. TargetName/SourceName are plain file names. version is file format. Currently it can be either 0 (zero) or s. Format s features MD5 sum checking (causing a 50% slowdown at loading and saving) but its support is present only when you compile in FreePascal. Programs generated by Delphi silently treat s as 0.
Signature should be your root object's class name if you need to keep compatibility with the data files created by Chepersy v0.6 or below.
SourceNme/TargetName are plain file names.

The ones that work with streams work with TMemoryStream just fine. You must take care of the streams yourself, these procedures assume them created and ready. Also, you should free the streams bu yourself.

Note: I didn't test that, but it should be safe to save/load a data tree before the type registration is completed, *if* the data tree consists only of the types that are already registered.




RECOMMENDATIONS
(The intended way of using this system)

1. Use enumerated types wherever posssible
See the example from the «What's so great» section. The enumerated types are the key to flexibility. Basically, enums are the most suitable thing for: body parts, magic schools, spell effects, damage kinds, terrain types, creature kinds, movement modes, sfx kinds, game options, and multitude of other things where you have a small to medium set of variants that is likely to be enlarged during the development.

2. Performance tips
First, use fields of these types whenever possible: longbool, longint, dword, int64, qword, single, double, any enums. The standard streaming routine "glues together" the consecutive fields of these types, treating them as a solid «binary block», which greatly accelerates the saving/loading process. Warning: in Delphi the booleans behave differently, and are *not* streamlined.

Any other types (including Byte, ShortInt, Word, SmallInt and Extended) need at least one procedure call per field -- with the notable exception of records and enum-indexed arrays. Chepersy decomposes the fields of these types to their elements, registering them as series of standalone fields. These get streamlined if they meet the abovementioned requirements and they are aligned (i.e. the record containing them is not packed).

Of course, when class or its types declaration in the current version of your program and the file header differ, streamlining the loading is not possible. But that is decided on an per-class basis, so all the objects of the unchanged classes still load at a full speed.

3. Use dynamic array when you need one.
The static arrays cannot contain sets and their size is fixed in stone. Use only for things you are *sure* won't change their format (matrices, vectors, quaternions, et cetera).




REGISTERING YOUR TYPES
(If Pascal RTTI was more powerful, You'd not need this. If wishes were fishes...)

To register your types, you need the standard FPC/Delphi unit typinfo attached. The type registering procedures take the result of calling TypeInfo() as a parameter. So when registering TmyType you need to write «TypeInfo(TMyType)». Not overly convenient, but believe me, there is *no* better way, I tried.

Also, there is no possible way for the program to determine the type size based on TypeInfo only. So you either need to point the type size manually, or it is guessed based on the type kind. Thankfully, a lot of Pascal types are in fact pointers.

Note: there *are* functions that aren't described in this manual, but you shouldn't need them. Most of the standard Pascal types are registered automatically. See mo_registrator.inc if you feel curious.

Note #2: registering the same type twice doesn't hurt, you only get a hint via AddLog() that type xxx is "alraedy" registered.


procedure RegTypeEnum(Info: PtypeInfo);
This one registers enumerated types. The base ordinal should be zero, the maximum ordinal value should be no more than 255 (matches the limitation for sets), and, most importantly, the unit where the enum is defined should be compiled with {$MINENUMSIZE 4}. Unlike the previous versions of Chepersy, Enums can be subrange types (like 1..20) but the abovementioned 0/255 limit still applies.

procedure RegTypeArray(Info, BaseType, IndEnum: PtypeInfo);
Registers arrays indexed by an enumerated type. Info refers to the array itself, BaseType to its elements (that after «of») and IndEnum refers to the enumerated type used to index the array. Both must be registered prior.


procedure RegTypeDynArray1d(Info, BaseType: PtypeInfo);
(NOTE: formerly known as RegTypeDynArray(), renamed to avoid confusion)
procedure RegTypeDynArray2d(Info, BaseType: PtypeInfo);
procedure RegTypeDynArray3d(Info, BaseType: PtypeInfo);
procedure RegTypeDynArrayNd(Dimensions: integer; Info, BaseType: PtypeInfo);
Dynamic arrays *cannot* have the following things as their base types: a records, an enum-indexed arrays, a static arrays. I know, this sucks, but I'm not omnipotent.
Dynamic arrays *can* have as their base types: other dynamic arrays (the inclusion level is unlimited), classes, enums, sets, strings, and, of course, any integer and floating-point types.

The multi-dimensional procedures allow you to register these arrays without creating entire chain of intermediary types as v0.7 required.


procedure RegTypeStaticArray(
StringID: ansistring; BaseType: PTypeInfo; Size: Integer);
Please note that the format for storing static arrays had changed since v0.7 You should have no problems moving if your array's base type was 4 or 8, otherwise you'll get a crash -- stick with the function described below.
The new system supports all the types as the dynamic arrays minus sets (currently, at least).

"Size" should be the result of SizeOf() on your array - i.e. its size in *bytes*.

Changing the static array size is not supported (and, most likely, will never be). Use dynamic arrays if you want any degree of flexibility.


{DEPRECATED} procedure ObsoleteRegTypeStaticArray(
StringID: ansistring; BaseType: PTypeInfo; Size: Integer);
Use it *only* if you have older data files created by Chepersy v0.6 or older. The format of static arrays had changed and is not compatible unless your base type was an 4- or 8- byte integer, float or boolean.


procedure RegTypeSet(Info, BaseType: PtypeInfo);
Only the sets based on the enumerated types are allowed. The base enumerated type must be registered prior. The maximum limitation of 256 elements will likely remain even when it will be lifted in FreePascal, to keep the compatibility with Delphi.


procedure RegTypeRecord(
name: string; Size: integer; v: array of const);
procedure RegTypePackedRecord(
name: string; Size: integer; v: array of const);
There's *no* way to check fields order integrity, so I do not recommend to use record-type object fields -- use them only only if you absolutely have to. Only the total length inconsistencies would be detected.
You can notice that some procedures take a string instead of TypeInfo. That is because Delphi sucks, it doesn't generate RTTI for records and static arrays.
The name is your type name (call it as you want), the size must be a result of SizeOf() (an additional safety check), and the v parameter consists of the pairs string-TypeInfo (the former is the field name, the latter is its type).
Record cannot be packed.
Example:
RegTypeRecord('TMyRecord', sizeof(TMyRecord), [
'a', typeinfo(integer),
'b', typeinfo(byte),
'c', typeinfo(integer)]);


procedure RegTypeComplex(
Info: PTypeInfo; Proc: TCustomTypeProcessingProc);
See the "Custom type processing procedures" section for more details.
The type size is always assumed to be 4 bytes (pointer).
It should be noted that internally most of the types (those that aren't streamlined) are processed via such procedures.

procedure RegTypeClass(C: CTrulyPersistent);
Normally, you don't need to bother with it because it's called automatically when you call RegClass(). But you'll need it when your classes cross-reference each other.





TYPES AUTOCONVERSION
(You saw it too late that your field should be float instead of integer...)

Documentation not ready.

Currently supported all Pascal numeric types (integer and floating-point), including arrays of these types. Allows to register tour own type converters. More info, as well as more functionality, in the next version (0.8.1).





REGISTERING YOUR CLASSES
(A step I'd wish to avoid not less than you do. Dreams, dreams...)

First, use fields of these types whenever possible: longbool, longint, dword, int64, single, double, all enums, all sets with less than 33 elements. The standard streaming routine streamlines the consecutive fields of these types, treating them as a solid «binary block», which greatly accelerates the saving/loading process.

Any other types (including any types you can register) need at least one procedure call per field -- with the notable exception of records and enum-indexed arrays. Chepersy decomposes the fields of these types to their elements, registering them as a set of separate fields.

Of course, when class or its types declaration in the current version of program and the file header differs, streamlining the loading is not possible. But that is decided on an per-class basis, so all objects of unchanged classes still load at a full speed.

1. Call RegClass() with your class as a parameter.
A class instance is then created, its RegisterFields() method called, then instance is deleted.
You should not worry about memory leaks and other problems, since TTrulyPersistent has a special constructor/destructor pair for this case.
I tried hard to find a workaround and avoid creating the class instance, but there is none.
Attention: this procedure does recursively call RegClass() for all your class' ancestors down to TTrulyPersistent if they aren't registered yet!

2. Your RegisterFields() method is what does all the ground work.
It should call inherited before registering any fields (to register ancestor's fields). You can register any types and classes in your RegisterFields() method, but classes and class types should be registered prior to registering any fields (which may conflict with the inherited method - it is advisable to call it between registering types and registering fields.

The process of registering types is well described in the previous section.

3 Registering fields
Fields must be listed in the exact order they go in your class. You call an appropriate procedure feeding it with the field name (sorry, again no workaround to avoid this), field address (@MyField does the trick) and TypeInfo() for the type of your field. Why not type name?.. Well, Pascal does some interesting things when it works its magic. Would you be surprised if I tell you that TypeInfo(glUint) returns description of something called "LONGINT"?.. On top of this, some things are called differently in Delphi and FreePascal.

Now for the important detail. If you register the fields in the wrong order, Chepersy will give an error message and your program won't run. If you forget to register some field -- the same story. Just like the compiler that checks your program's syntax.

So this is one of the main reasons why I created Chepersy instead of settling with the standard Pascal's TPersistent (which lacks any checking mechanisms). I am too absent-minded to safely work with the latter.

procedure RegField(
name: string; Pf: pointer; PI: PTypeInfo); overload;
Just as I said above. Note also that the name you feed to this procedure may differ from the real field name. The spaces and other un-Pascal characters are acceptable.
RegField('Merry rabbits', @bunnies, TypeInfo(TBunnies));
Again, I'd like something more automatized, but the built-in Pascal mechanisms work *only* for published fields, and you are greatly limited in what you can make published.

procedure RegField(
name: string; Pf: pointer; TypeString: string); overload;
This version is used for the records (and static arrays) since Delphi doesn't generate RTTI for them and TypeInfo() cannot be used

procedure RegSkip(
name: string; Pf: pointer; PI: PTypeInfo); overload;
procedure RegSkip(
name: string; Pf: pointer; TypeString: string); overload;
"Skipped fields" are fields that are registered but excluded from saving and loading. After the loading they are filled with zeroes. Naturally, there are things that you don't want to save: OpenGL texture names, various system handles, et cetera. After all, there's the BeforeSaving/AfterLoading virtual methods, to take care of them manually (see below).

procedure RegSkipPtr(name: string; Pf: pointer);
This one deals with the pointers and similar things (like non-registered non-persistent classes). Since you *cannot* register a pointer type, this one takes no type info, just the field name and address.

procedure RegMultipleFields(v: array of const);
This one is a recent addition, an attempt to decrease the bulkiness of the registration code.
The parameters usually go by fours: field name (string), field address (pointer), TypeInfo, is the field skipped (boolean). The latter two can be omitted (in any combinations), meaning they will be borrowed from the previous field. (The defaults are TypeInfo(integer) and False).

Example:
RegMultipleFields([
'Merry rabbits', @bunnies, TypeInfo(TBunnies), No,
'Silly rabbits', @bunnies2, Yes,
'Gloomy rabbits', @bunnies3,
'Dirty rabbits', @bunnies4, No
]);
...As you can notice, I have a quirk for re-defining True and False as Yes and No. ;)
As you can also notice, the record and static array type fields cannot be registered via this procedure.

4. The BeforeSaving and AfterLoading virtual methods
Allow you to greatly customize the streaming process. For example, my BeforeSaving for a texture class first checks the particular program settings and if needed, creates a reduced thumbnail out of the texture image which it places into the (usually empty) dynamic array. Such way the reduced textures are stored in the saved game file. The corresponding AfterLoading first checks if the texture name became invalid (OpenGL restarted since the time the game was saved) and if yes then discards it, loads the reduced texture from the dynamic array and schedules the loading of the normal hi-res version.

In short, the applications are infinite.

Starting with v0.7.1, the call order is reversed, i.e. this method will be first called for the object that had been written to the stream last. Such way, when this method is called, all object fields are already initialized.

arthurprs
28-02-2008, 03:48 PM
o.o very good

Chebmaster
03-03-2008, 05:29 PM
ARRGH! :evil:
I found YET ANOTHER moronic blunder, making Chepersy 0.8.1 totally unusable! :oops:
Please wait for 0.8.2 that will DEFINITELY work (I already use it in one project at work, it now stopped crashing at last!) I just need a day or two to test it thoroughly, pack and upload.

But. Question is: WHY. Why didn'yt anybody notify me that this miracle of ingineering trashes memory instead of working as intended?
Sadly, the simple test I include with this library didn't reveal this fatal bug. Only the first attempt at a real-life use uncovered it.

Chebmaster
04-03-2008, 09:32 PM
Ok, here is it: the tested, really working 0.8.2:

http://sourceforge.net/projects/chepersy/