next up previous contents
Next: Compiling OPENCP Up: Device Development Previous: the Internals   Contents

Our first Cubic-DLL

To create a DLL, you first have to create an environment where you can do this best. The easiest method is to install the source of OPENCP and put one's DLL into the makefile. However, since the directory gets messy soon, I recommend developing one's DLLs elsewhere. At first you should get all .lib and .h of OPENCP by compiling the player once.8.4

Let's start it all over from the beginning step by step. As an example, we want to write an ACE-reader which displays the descriptions before the final decompressing.

First depack the environment, set the paths correctly and copy the TEMPLATE from the indev directory to indef\arcace.

There you have to change the makefile, namely in this way:

dest = arcACE.dll
 ...
arcace_desc = 'OpenCP .ACE Archive Reader (c) 1998 Felix Domke'
arcace_objs = arcace.obj
arcace_libs = cp.lib pfilesel.lib
arcace_ver = 0.0

arcace.dll: $(dlldeps) arcace.exp $(arcace_objs)
            $(libdir)$(arcace_libs)
            $(makedll)
            copy arcace.dll \opencp\bin

You have to enter all destinations for dest, ie all DLLs which shall be created in the end.

arcace_desc is the description which will be printed.

arcace_objs are the object-files which shall be linked to the DLL.

Since there is an implicit-rule which says that WPP386 gets executed for CPPs for which OBJs with the same name are demanded, there also has to be an arcace.cpp with our code.

arcace_libs are the (import) libraries from which the imports shall be searched. Standard functions like sprintf_ etc. are exported in cp.lib. PFILESEL.LIB contains some functions we will need later.

arcace_ver is the version which gets displayed in the linkview of OPENCP. You compute it in a rather strange way. How exactly, I myself haven't understood, but usually you should convert the version from hex to decimal (eg00x024000 for 2.5.0) and then divide by 100. But someone must have erred.

The copy at the end copies the DLL right into the CP directory.

In general in an archive-read, two functions are passed, one to read the file names from the archive and ``tell'' them to the file selector via callback and another to call the unpacker to unpack a file from the archive.

To be concrete, it looks like the following:

An adbregstruct gets exported which looks like this:

struct adbregstruct
{
  const char *ext;
  int (*Scan)(const char *path);
  int (*Call)(int act,
              const char *apath,
              const char *file,
              const char *dpath
             );
  adbregstruct *next;
};

ext is the extension, in our case .ace.

Scan is a function which gets called with the archive as parameter and tells it to the file selector by calling adbAdd().

Call will get executed then to unpack a file from the archive.

We leave out next because it will be set at runtime anyway.

Now let's export such a struct:

extern "C"
{
adbregstruct adbACEReg = {".ACE", adbACEScan, adbACECall};
char *dllinfo = "arcs _adbACEReg";
};

dllinfo makes an entry in cp.ini superfluous. It provides that CP takes notice of our archive-reader.

However, something like link=... arcace.dll has to be in cp.ini.

Our adbACEScan() looks like the following8.5:

static int adbACEScan(const char *path)
/*
  "path" is the filename of the archive.
  If everything works, 1 will be returned, otherwise 0.
*/
{
 [...]
 sbinfile archive;
/*
  "binfile" is the Library which is used in CP for (almost) all
  file accesses. I think it is EXTREMELY practical. It is
  actually quite self-explanatory. "sbinfile" is a normal file
  on disk.
*/
 if(!archive.open(path, sbinfile::openro))
  return(1);
/*
  When we can't open the archive, we can exit at once :)
*/

 unsigned short arcref;

 char arcname[12];
 char ext[_MAX_EXT];
 char name[_MAX_FNAME];
 _splitpath(path, 0, 0, name, ext);
 fsConvFileName12(arcname, name, ext);
 arcentry a;
 memcpy(a.name, arcname, 12);
 a.size=archive.length();
 a.flags=ADB_ARC;
 if (!adbAdd(a))
 {
  archive.close();
  return 0;
 }
/*
  Here, the archive itself gets added to the file listing,
  namely by adbAdd(arcentry &).
  The struct which adbAdd expects looks like:

  struct arcentry
  {
   unsigned short flags;
   unsigned short parent;
   char name[12];
   unsigned long size;
  };

  "flags" is e.g. ADB_ARC for archives.
  "parent" is not so important at the moment.
  "name" is the file name in 8.3-format (fsConvFileName12
         converts it pretty nicely :)
         (e.g. from "x.y" to "X       .Y  ")
  "size" is simply the length which will be displayed.
*/
  arcref=adbFind(arcname);
/*
  Aferwards we keep the "ref" in our mind so that we can enter
  it as "parent" afterwards. parent is the source-archive so
  that the files there will be unpacked from the right archive
  afterwards... (The file selector creates a list of all files
  in the directory and the files in the archives. For the last
  thing, it calls the archive-reader. If the user presses with
  "Enter" on a file afterwards, the file selector has to know
  what ARCer it has to call and most of all from which archive
  the files come. Therefore the parent has to be set.
*/
 [...]

/*
  Now let's deal with the ACE itself. The ACE format is very
  similar to the RAR one, a detailed
  @REF="format description":"coace.txt"

  In any case the function "ReadNextHeader" reads the header on
  the current position and skips additional bytes if they appear
  (i.e with files the packed data). In this way header comes
  after header, at least it appears. (In reality ReadNextHeader
  SEEKs at the beginning, not at the end, but that's not
  important now, don't get confused by that :)
*/

 arcentry a;
 while(ReadNextHeader(archive))
 {
/*
   Here, the header gets read.
*/
  char ext[_MAX_EXT];
  char name[_MAX_FNAME];
  [...]
  if(aheader->type==1)
  {
/*
  ...if the type is 1 (file), the file name will be read...
*/
   [...]
   char filename[_MAX_PATH];
   memcpy(filename, afheader->filename, afheader->filenamesize);
   filename[afheader->filenamesize]=0;
   strupr(filename);
   _splitpath(filename, 0, 0, name, ext);
/*
   ...splitted into "name" and "ext"...
*/
   if(fsIsModule(ext))
/*
   ...and if "ext" is a module-extension (see CP.INI), then...
*/
   {
    a.size=afheader->unpsize;
    a.parent=arcref;
    a.flags=0;
    fsConvFileName12(a.name, name, ext);
/*
   ... this file will be added to the file list by adbAdd.
*/
    if(!adbAdd(a))
    {
     archive.close();
/*
   If the whole thing doesn't work, well, then it doesn't work.
*/
     [...]
     return 0;
    } else
    {
/*
   Otherwise, if wished,
*/
     if(fsScanInArc&&[...])
     {
/*
some blocks get be depacked (by the UNACE-routines in UAC_DCPR.*)
*/
      dcpr_init_file();
      int rd=dcpr_adds_blk(buf_wr, size_wrb);
      unsigned short fileref;
      fileref=mdbGetModuleReference(a.name, a.size);
/*
   ...a reference for the just ADDed file will be retrieved...
*/
      if((fileref!=0xFFFF)&&(!mdbInfoRead(fileref)))
      {
       moduleinfostruct ms;
       if(mdbGetModuleInfo(ms, fileref))
       {
        mdbReadMemInfo(ms, (unsigned char*)buf_wr, rd);
/*
  ...and then we e.g. look for the song name etc. by
  "mdbReadMemInfo". Just for explanation, again:

  "buf_wr" contains about the first unpacked 2-4kb of the actual
  file. Now "mdbReadMemInfo" tries to get to the infos in it
  with various, format-specific routines.
*/
        mdbWriteModuleInfo(fileref, ms);
/*
  These will also be set then.

  Another thing about the "mdbInfoRead" and "mdbGetModuleInfo":
  At first you have to read what is already known about the
  file, then actualize it and write it back.
*/
       }
      }
     }
    }
   }
   [...]
  }
 }
 [...]
 archive.close();
 return(1);
}

Now let's come to adbACECall. This function is quite easy. Depending on act, it has to pack, unpack, move etc. the file file from the archive apath to dpath. To make it easier, only one act=="adbCallGet" is supported, ie unpacking.

static int adbACECall(int act,
                      const char *apath,
                      const char *file,
                      const char *dpath)
{
 switch (act)
 {
  case adbCallGet:
  {
   return !adbCallArc(cfGetProfileString("arcACE", "get",
                                         "ace e %a %n %d"),
                      apath, file, dpath);
/*
   "adbCallArc" is a nice function: it replaces something like
   %a, %n and %d with the passed arguments. The first argument
   has to be inserted for %a, the second for %n and the third
   for %d. Beforehand, "cfGetProfileString" gets from CP.INI
   whether the user wants to use another packer (XACE etc.) and
   that in the section "arcACE" and the key "get=".
   If the key hasn't been found, "ace e %a %n %d" will be taken
   by default, which, by the way, should be correct almost every
   time. :)
*/
  }
  case adbCallPut:
   // not implemented
   break;
  case adbCallDelete:
   // not implemented
   break;
  case adbCallMoveTo:
   // not implemented
   break;
  case adbCallMoveFrom:
   // not implemented
   break;
  }
  return 0;
}

Now a simple wmake compiles the whole thing presumed that a working makefile is available.



Footnotes

... once.8.4
Soon there is said to come an archive with all .lib and .h for those people who just want to develop something new.
... following8.5
The function has been heavily simplified and can't be run in this form, but all things concerning OPENCP archives are in it. Only the ACE-specifical things, most of all the thing with the SOLID-archives, are missing.

next up previous contents
Next: Compiling OPENCP Up: Device Development Previous: the Internals   Contents
documentation by doj / cubic