www.xbdev.net
xbdev - software development
Friday April 19, 2024
home | contact | Support | 3D File Formats The bits and bytes... | 3D File Formats The bits and bytes...
>>
     
 

3D File Formats

The bits and bytes...

 

MD3 Format - What is it, yikes!...
Author bkenwright@xbdev.net

The new stuff here, is we get the tags!

 

Download Source Code
/***************************************************************************/
/*                                                                         */
/* File:   main.cpp                                                        */
/* Author: bkenwright@xbdev.net                                            */
/* URL:    www.xbdev.net                                                   */
/* Date:   19-03-2006 (easter)                                             */
/*                                                                         */
/***************************************************************************/
/*
   Understanding the Quake3 MD3 File Format
*/

//---------------------------------------------------------------------------



#define SZ_MD3_FILE "media\\model\\sarge\\upper.md3"
#define SZ_MD3_ANIM_FILE "media\\model\\sarge\\animation.cfg"
#define SZ_MD3_SKIN_FILE "media\\model\\sarge\\upper_default.skin"

#define MAX_FILENAME_LENGTH 256


//---------------------------------------------------------------------------

#include <windows.h>
#include <stdio.h>                      //sprintf(...)
#include <string.h>

//---------------------------------------------------------------------------

//Saving debug information to a log file
void abc(char *str)
{
      FILE *fp = fopen("output.txt", "a+");
      fprintf(fp, "%s\n", str);
      fclose(fp);
}


//---------------------------------------------------------------------------


typedef unsigned int        uint32;
typedef int                 int32;
typedef unsigned short int  uint16;
typedef short int           int16;
typedef float               float32;



struct stMD3Header
{
    char        ID[4];          //  ID of the file is always "IDP3"
    int32       Version;        //  Version number, usually 15
    char        Filename[68];   //  Filename, sometimes left blank
    int32       numBoneFrames;  //  Number of BoneFrames
    int32       numTags;        //  Number of 'tags' per BoneFrame
    int32       numMeshes;      //  Number of Meshes/Skins in MaxSkin
    int32       numMaxSkins;    //  Maximum number of unique skins
    int32       ofsFrames;      //  Always equal to the length this header
    int32       ofsTagStart;    //  Starting position of tag structures
    int32       ofMeshSurfaces; //  Ending position of tag structure
    int32       ofEndOfFile;    //  Size of file
};



struct stBoneFrame
{
    float32 mins[3];
    float32 maxs[3];
    float32 Position[3];
    float32 Scale;
    char    Creator[16];
};

struct stAnim
{
    int32 FirstFrame; 
    int32 numFrames;
    int32 LoopingFrames;
    int32 FPS; 
};

struct stSkin
{
    char Name[64];
    int32 index;
};

struct stTag
{
    char            Name[64];
    float32         Position[3];
    float32         Rotation[3][3];
};

struct stTriangle
{
    int32 Vertex[3];
};

struct stTexCoord
{
    float32 Coord[2];
};

struct stVertex // = Record
{
    int16           Vertex[3];
    unsigned char   Normal[2];
}; 

struct stMeshHeader
{
    char    ID[4];
    char    Name[64];
    int32   flags;
    int32   numMeshFrames;
    int32   numSkins; 
    int32   numVertexes;
    int32   numTriangles;
    int32   ofsTriangles;
    int32   ofsSkins;
    int32   ofsTexVector;
    int32   ofsVertex;
    int32   ofsEndMeshSize;
};

struct stMesh
{
    stMeshHeader    MeshHeader;
    stSkin*         pSkins;
    stTriangle*     pTriangle;
    stTexCoord*     pTexCoord;
    stVertex*       pVertex;
};

//---------------------------------------------------------------------------

 long filesize(FILE *stream)
 {
    long curpos, length;

    curpos = ftell(stream);
    fseek(stream, 0L, SEEK_END);
    length = ftell(stream);
    fseek(stream, curpos, SEEK_SET);
    return length;
 }

//---------------------------------------------------------------------------

 enum
{
    BOTH_DEATH1 = 0,
    BOTH_DEAD1  = 1,
    BOTH_DEATH2 = 2,
    BOTH_DEAD2  = 3,
    BOTH_DEATH3 = 4,
    BOTH_DEAD3  = 5,

    TORSO_GESTURE = 6,
    TORSO_ATTACK  = 7,
    TORSO_ATTACK2 = 8,
    TORSO_DROP    = 9,
    TORSO_RAISE   = 10,
    TORSO_STAND   = 11,
    TORSO_STAND2  = 12,

    LEGS_WALKCR   = 13,
    LEGS_WALK     = 14,
    LEGS_RUN      = 15,
    LEGS_BACK     = 16,
    LEGS_SWIM     = 17,
    LEGS_JUMP     = 18,
    LEGS_LAND     = 19,
    LEGS_JUMPB    = 20,
    LEGS_LANDB    = 21,
    LEGS_IDLE     = 22,
    LEGS_IDLECR   = 23,
    LEGS_TURN     = 24,

    MAX_ANIMATIONS
};

//---------------------------------------------------------------------------


class CMD3
{
public:
    char        m_md3FileName[MAX_FILENAME_LENGTH];
    stMD3Header m_md3Header;


    stBoneFrame* m_pBoneFrame;
    stTag*       m_pTags;
    stMesh*      m_pMeshes;

    stAnim       m_Anim[26];

    int m_FPS;
    int m_startFrame;
    int m_endFrame;
    int m_nextFrame;
    int m_animLower;
    int m_animUpper;
    float poll;
    float m_lastUpdate;
    int m_frame;

    CMD3()
    {
        m_startFrame = 0;   
        m_endFrame = 0;
        m_frame = 0;
        poll = 0;
        m_lastUpdate = 0;
    }


    void Create(/* file names */)
    {
        LoadModel(SZ_MD3_FILE);
        LoadAnim(SZ_MD3_ANIM_FILE);
        LoadSkin(SZ_MD3_SKIN_FILE);
        SetAnim(TORSO_STAND);

    }
    void Release()
    {
    }
    void Update(float time)
    {
        UpdateFrame(time);
    }
    void Render()
    {
        DrawSkeleton();
    }

protected:

    void UpdateFrame(float time)
    {
        poll = (time - m_lastUpdate);
        if (poll > 1/m_FPS)
        {
            m_frame = m_nextFrame;
            m_nextFrame++;
            if (m_nextFrame > m_endFrame)
            {
                m_nextFrame = m_startFrame;
                m_lastUpdate = time;
            }

        }
    }

    void DrawSkeleton()
    {
        DrawModel();
    }

    void DrawModel()
    {
        DrawModelInt(m_frame, m_nextFrame, poll);
    }

    void DrawModelInt(const int currentFrame, const int nexFrame, float pol)
    {
        for (int k=0; k<m_md3Header.numMeshes; k++)
        {
            stMesh* currentMesh = &m_pMeshes[k];
            int currentOffsetVertex = currentFrame * currentMesh->MeshHeader.numVertexes;
            //interpolation
            int nextCurrentOffsetVertex = nexFrame * currentMesh->MeshHeader.numVertexes;

            int TriangleNum = currentMesh->MeshHeader.numTriangles;

            // get/set texture
            // TO-DO

            for (int i=0; i<TriangleNum; i++)
            {
                for (int j=0; j<2; j++)
                {
                    int currentVertex = currentMesh->pTriangle[i].Vertex[j];

                    float v[3];
                    v[0] = currentMesh->pVertex[currentOffsetVertex + currentVertex].Vertex[0] / 64.0f;
                    v[1] = currentMesh->pVertex[currentOffsetVertex + currentVertex].Vertex[1] / 64.0f;
                    v[2] = currentMesh->pVertex[currentOffsetVertex + currentVertex].Vertex[2] / 64.0f;

                    float nextV[3];
                    nextV[0] = currentMesh->pVertex[nextCurrentOffsetVertex + currentVertex].Vertex[0] / 64.0f;
                    nextV[1] = currentMesh->pVertex[nextCurrentOffsetVertex + currentVertex].Vertex[1] / 64.0f;
                    nextV[2] = currentMesh->pVertex[nextCurrentOffsetVertex + currentVertex].Vertex[2] / 64.0f;

                    float normU, normV;
                    normU = currentMesh->pVertex[currentOffsetVertex + currentVertex].Normal[0];
                    normV = currentMesh->pVertex[currentOffsetVertex + currentVertex].Normal[1];

                    // Interplated value
                    float pA = v[0] + pol * (nextV[0] - v[0]);
                    float pB = v[1] + pol * (nextV[1] - v[1]);
                    float pC = v[2] + pol * (nextV[2] - v[2]);
                    
                }
            }

        }

    }


    bool SetAnim(int ani)
    {
        if ( ani>=0 && ani<=5)
        {
        }
        else if ( ani>=6 && ani<=12)
        {
        }
        else if ( ani>=13 && ani<=24)
        {
        }
        else
        {
            _asm
            {
                int 13;
            }
        }

        m_FPS = m_Anim[ani].FPS;
        m_startFrame = m_Anim[ani].FirstFrame;
        m_nextFrame = m_Anim[ani].FirstFrame;
        m_endFrame = m_Anim[ani].FirstFrame + m_Anim[ani].numFrames;
        m_animLower = ani;
        m_animUpper = ani;

        return true;
    }


    bool LoadSkin(const char* filename)
    {
        char buf[256];

        char  MeshName[256] = {0};
        char  ImageName[256] = {0};

        FILE* fp = fopen(filename, "r");

        if (fp==NULL)
        {
            abc("unable to open file");
            return false;
        }

        //char firstWord[256];

        char * val = NULL;
        int i = 0;

        do
        {
            val = fgets(buf,256,fp);
            if (val==NULL)
            {
                break;
            }
            //sscanf(buf, "%s", firstWord);

            if (buf[ strlen(buf)-1 ] == '\n')
            {
                buf[ strlen(buf)-1 ] = NULL;
            }

            if (buf[ strlen(buf)-1 ] == ',')
            {
                strcpy(MeshName, buf);
                MeshName[ strlen(MeshName)-1 ] = NULL;
            }

            if ( strncmp(buf, "tag_", 4)==0 )  // tags dont have skins
            {
                continue;
            }

            //char* pImageName = strstr(MeshName, ","); // get the full image and path name
            //strcpy(ImageName, pImageName);              // get name from last / (i.e only filename)
            char* pName = strrchr(buf, '/');
            strcpy(ImageName, pName+1); 

            // lose the starting /
        } while (val);


        return true;
    }



    bool LoadAnim(const char* filename)
    {
        char buf[256];

        FILE* fp = fopen(filename, "r");

        if (fp==NULL)
        {
            abc("unable to open file");
            return false;
        }

        char firstWord[256];

        char * val = NULL;
        int i = 0;

        do
        {

            val = fgets(buf,256,fp);
            if (val==NULL)
            {
                break;
            }
            sscanf(buf, "%s", firstWord);


            if (strcmp("sex", firstWord)==NULL)
            {
            }
            else if (strcmp("headoffset", firstWord)==NULL)
            {
            }
            else if (strcmp("footsteps", firstWord)==NULL)
            {
            }
            else if (strcmp("", firstWord)==NULL)
            {
            }
            else if (strcmp("//", firstWord)==NULL)
            {
            }
            else
            {
                char words[4][256];

                sscanf(buf, "%s %s %s %s", &words[0], &words[1], &words[2], &words[3]);
                // Extract the values of FirstFrame, numFrames, LoopingFrames, FPS from the String
                int FirstFrame      = atoi(words[0]);
                int numFrames       = atoi(words[1]);
                int loopingFrames   = atoi(words[2]);
                int FPS             = atoi(words[3]);

                m_Anim[i].FirstFrame = FirstFrame;
                m_Anim[i].numFrames  = numFrames;
                m_Anim[i].LoopingFrames = loopingFrames;
                m_Anim[i].FPS = FPS;

                i++;
            }

        } while (val);

        int skip = m_Anim[LEGS_WALKCR].FirstFrame - m_Anim[TORSO_GESTURE].FirstFrame;
  
        for (int i=LEGS_WALKCR; i<MAX_ANIMATIONS; i++)
        {
            m_Anim[i].FirstFrame = m_Anim[i].FirstFrame - skip;
        }

        for (int i=0; i<MAX_ANIMATIONS; i++)
        {
            if (m_Anim[i].numFrames > 0)
            {
                m_Anim[i].numFrames = m_Anim[i].numFrames - 1;
            }
        }

        fclose(fp);

        return true;
    }

    //-----------------------------------------------------------------------
    //
    //  Loads model from a .md3 file
    //
    //-----------------------------------------------------------------------
    bool LoadModel(const char* filename)
    {
        char buf[256];

        FILE* fp = fopen(filename, "rb");

        if (fp==NULL)
        {
            abc("unable to open file");
            return false;
        }

        // Lets get the size of this md3 file
        int md3filesize = filesize(fp);
        fseek(fp, 0L, SEEK_SET);

        if (strlen(filename)>255)
        {
            sprintf(buf, "filename is longer than %d",  MAX_FILENAME_LENGTH);
            abc(buf);
            return false;
        }
        // copy name
        strcpy(m_md3FileName, filename);
        sprintf(buf, "MD3 FileName: %s", m_md3FileName);
        abc(buf);

        sprintf(buf, "FileSize: %d", md3filesize);
        abc(buf);

        abc("\n~~MD3 Header~~\n");
    
        // read header
        fread(&m_md3Header, 1, sizeof(stMD3Header), fp);

        // log debug information to file
        sprintf(buf, "ID %c%c%c%c", m_md3Header.ID[0], m_md3Header.ID[1], m_md3Header.ID[2], m_md3Header.ID[3]);
        abc(buf);

        sprintf(buf, "Version: %d", m_md3Header.Version);
        abc(buf);

        sprintf(buf, "FileName: %s", m_md3Header.Filename);
        abc(buf);

        sprintf(buf, "numBoneFrames: %d", m_md3Header.numBoneFrames);
        abc(buf);

        sprintf(buf, "numTags: %d", m_md3Header.numTags);
        abc(buf);

        sprintf(buf, "numMeshes: %d", m_md3Header.numMeshes);
        abc(buf);

        sprintf(buf, "numMaxSkins: %d", m_md3Header.numMaxSkins);
        abc(buf);

        sprintf(buf, "ofsFrames: %d", m_md3Header.ofsFrames);
        abc(buf);

        sprintf(buf, "ofsTagStart: %d", m_md3Header.ofsTagStart);
        abc(buf);

        sprintf(buf, "ofMeshSurfaces: %d", m_md3Header.ofMeshSurfaces);
        abc(buf);

        sprintf(buf, "ofEndOfFile (Filesize): %d", m_md3Header.ofEndOfFile);
        abc(buf);


        if (strcmp("IDP3", m_md3Header.ID)==NULL)
        {
            sprintf(buf, "Incorrect File Format 'Incorrect ID' ie. ('IDP3')");
            abc(buf);
        }

        // Allocate memory for all or bones/tags/etc
        m_pBoneFrame = new stBoneFrame[m_md3Header.numBoneFrames];
        m_pTags      = new stTag[m_md3Header.numBoneFrames * m_md3Header.numTags];
        m_pMeshes    = new stMesh[m_md3Header.numMeshes];


        // Lets seek to the start of the bone frames & read boneframe
        fseek(fp, m_md3Header.ofsFrames, SEEK_SET);
        fread(m_pBoneFrame, 1, m_md3Header.numBoneFrames*sizeof(stBoneFrame), fp);

        sprintf(buf, "\n~~~~BoneFrames: %d~~~~~~", m_md3Header.numBoneFrames);
        abc(buf);
        for (int i=0; i<m_md3Header.numBoneFrames; i++)
        {
            abc("#");
            sprintf(buf, "mins[%.1f,%.1f,%.1f]", m_pBoneFrame[i].mins[0], m_pBoneFrame[i].mins[1], m_pBoneFrame[i].mins[2]);
            abc(buf);
            sprintf(buf, "maxs[%.1f,%.1f,%.1f]", m_pBoneFrame[i].maxs[0], m_pBoneFrame[i].maxs[1], m_pBoneFrame[i].maxs[2]);
            abc(buf);
            sprintf(buf, "Position[%.1f,%.1f,%.1f]", m_pBoneFrame[i].Position[0], m_pBoneFrame[i].Position[1], m_pBoneFrame[i].Position[2]);
            abc(buf);
            sprintf(buf, "Scale[%.1f]", m_pBoneFrame[i].Scale);
            abc(buf);
            sprintf(buf, "Creator[%s]", m_pBoneFrame[i].Creator);
            abc(buf);
        }


        // Seek to start of tags and read them all in
        fseek(fp, m_md3Header.ofsTagStart, SEEK_SET);
        fread(m_pTags, 1, m_md3Header.numBoneFrames*m_md3Header.numTags*sizeof(stTag), fp);

        sprintf(buf, "\n~~~~Tags: %d~~~~~~", m_md3Header.numTags);
        abc(buf);
        for (int i=0; i<m_md3Header.numTags; i++)
        {
            abc("#");
            sprintf(buf, "Name[%s]", m_pTags[i].Name);
            abc(buf);
            sprintf(buf, "Position[%.1f,%.1f,%.1f]", m_pTags[i].Position[0], m_pTags[i].Position[1], m_pTags[i].Position[2]);
            abc(buf);
            sprintf(buf, "Rotation[%.1f,%.1f,%.1f][...][...]", m_pTags[i].Rotation[0][0], m_pTags[i].Rotation[0][1], m_pTags[i].Rotation[0][2]);
            abc(buf);
        }


        int meshOFS = m_md3Header.ofMeshSurfaces;

        for (int j=0; j<m_md3Header.numMeshes; j++)
        {
            stMesh * pMesh = &m_pMeshes[j];
            stMeshHeader * pMeshHeader = &(pMesh->MeshHeader);

            fseek(fp, meshOFS, SEEK_SET);

            // Seek to the start of the mesh data and read it all in
            fread(pMeshHeader, 1, sizeof(stMeshHeader), fp);

            // Read in all the sub parts of the mesh data
            {
                fseek(fp, meshOFS + pMeshHeader->ofsTriangles, SEEK_SET);
                pMesh->pTriangle = new stTriangle [pMeshHeader->numTriangles];
                fread( pMesh->pTriangle, 1, pMeshHeader->numTriangles * sizeof(stTriangle), fp);

                fseek(fp, meshOFS + pMeshHeader->ofsSkins, SEEK_SET);
                pMesh->pSkins = new stSkin [pMeshHeader->numSkins];
                fread( pMesh->pSkins, 1, pMeshHeader->numSkins * sizeof(stSkin), fp);

                fseek(fp, meshOFS + pMeshHeader->ofsTexVector, SEEK_SET);
                pMesh->pTexCoord = new stTexCoord [pMeshHeader->numVertexes];
                fread( pMesh->pTexCoord, 1, pMeshHeader->numVertexes * sizeof(stTexCoord), fp);
            
                fseek(fp, meshOFS + pMeshHeader->ofsVertex, SEEK_SET);
                pMesh->pVertex = new stVertex [pMeshHeader->numVertexes];
                fread( pMesh->pVertex, 1, pMeshHeader->numVertexes * sizeof(stVertex), fp);
            }

            meshOFS += pMeshHeader->ofsEndMeshSize;

        }//End for meshes


        sprintf(buf, "\n~~~~Mesh Surfaces: %d~~~~~~", m_md3Header.numMeshes);
        abc(buf);
        for (int j=0; j<m_md3Header.numMeshes; j++)
        {
            abc("#");
            stMesh * pMesh = &m_pMeshes[j];
            stMeshHeader * pMeshHeader = &(pMesh->MeshHeader);

            sprintf(buf, "ID [%c%c%c%c]", pMeshHeader->ID[0], pMeshHeader->ID[1], pMeshHeader->ID[2], pMeshHeader->ID[3]);
            abc(buf);
            sprintf(buf, "Name [%s]", pMeshHeader->Name);
            abc(buf);
            sprintf(buf, "flags [0x%.2X]", pMeshHeader->flags);
            abc(buf);
            sprintf(buf, "numMeshFrames [%d]", pMeshHeader->numMeshFrames);
            abc(buf);
            sprintf(buf, "numSkins [%d]", pMeshHeader->numSkins);
            abc(buf);
            sprintf(buf, "numVertexes [%d]", pMeshHeader->numVertexes);
            abc(buf);
            sprintf(buf, "numVertexes [%d]", pMeshHeader->numVertexes);
            abc(buf);
            sprintf(buf, "ofsTriangles [%d]", pMeshHeader->ofsTriangles);
            abc(buf);
            sprintf(buf, "ofsSkins [%d]", pMeshHeader->ofsSkins);
            abc(buf);
            sprintf(buf, "ofsTexVector [%d]", pMeshHeader->ofsTexVector);
            abc(buf);
            sprintf(buf, "ofsVertex [%d]", pMeshHeader->ofsVertex);
            abc(buf);
            sprintf(buf, "ofsEndMeshSize [%d]", pMeshHeader->ofsEndMeshSize);
            abc(buf);


            // Mesh Triangles
            for (int i=0; i<pMeshHeader->numTriangles; i++)
            {
                stTriangle * pTri = &(pMesh->pTriangle[i]);
                sprintf(buf, "Triangle [%d,%d,%d]", pTri->Vertex[0], pTri->Vertex[1], pTri->Vertex[2]);
                abc(buf);
            }

            // Mesh Skins
            for (int i=0; i<pMeshHeader->numSkins; i++)
            {
                stSkin * pSkin = &(pMesh->pSkins[i]);
                sprintf(buf, "Skin:Name [%s]", pSkin->Name);
                abc(buf);
                sprintf(buf, "Skin:Index [%d]", pSkin->index);
                abc(buf);
            }

            for (int i=0; i<pMeshHeader->numVertexes; i++)
            {
                stTexCoord * pTex = &(pMesh->pTexCoord[i]);
                sprintf(buf, "TexCoord:Index [%.1f,%.1f]", pTex->Coord[0], pTex->Coord[1]);
                abc(buf);
            }


            for (int i=0; i<pMeshHeader->numVertexes; i++)
            {
                stVertex* pVert = &(pMesh->pVertex[i]);
                sprintf(buf, "Vertice:Vertex [%d,%d,%d]", pVert->Vertex[0], pVert->Vertex[1], pVert->Vertex[2]);
                abc(buf);

            }
        }


        fclose(fp);

        for (int j=0; j<m_md3Header.numMeshes; j++)
        {
            stMesh * pMesh = &m_pMeshes[j];

            delete[] pMesh->pSkins;
            delete[] pMesh->pTexCoord;
            delete[] pMesh->pTriangle;
            delete[] pMesh->pVertex;

            pMesh->pSkins       = NULL;
            pMesh->pTexCoord    = NULL;
            pMesh->pTriangle    = NULL;
            pMesh->pVertex      = NULL;
        }

        delete[] m_pBoneFrame;
        delete[] m_pTags;
        delete[] m_pMeshes;

        m_pBoneFrame = NULL;
        m_pTags      = NULL;
        m_pMeshes    = NULL;

        return true;
    }// End LoadModel(..)

};



//---------------------------------------------------------------------------

INT WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
                   LPSTR lpCmdLine, int nCmdShow)
{
    
    static CMD3 md3;

    //md3.LoadModel(SZ_MD3_FILE);

    //md3.LoadAnim(SZ_MD3_ANIM_FILE);

    //md3.LoadSkin(SZ_MD3_SKIN_FILE);

    //md3.SetAnim(TORSO_STAND);

    // If something was not done, let it go
    return 0;
}

//---------------------------------------------------------------------------

    

 

Output.txt
d

MD3 FileName: media\model\sarge\upper.md3
FileSize: 468636

~~MD3 Header~~

ID IDP3
Version: 15
FileName: models/players/sarge/sarge.md3
numBoneFrames: 153
numTags: 3
numMeshes: 2
numMaxSkins: 0
ofsFrames: 108
ofsTagStart: 8676
ofMeshSurfaces: 60084
ofEndOfFile (Filesize): 468636

~~~~BoneFrames: 153~~~~~~
#
mins[-3.1,-20.1,-2.4]
maxs[19.7,8.1,22.7]
Position[0.0,0.0,0.0]
Scale[36.1]
Creator[(from ASE)]
#

.....

~~~~Tags: 3~~~~~~
#
Name[tag_head]
Position[10.8,-3.0,11.6]
Rotation[0.7,-0.1,-0.7][...][...]
#
Name[tag_weapon]
Position[5.9,-10.3,15.5]
Rotation[-0.9,-0.3,0.3][...][...]
#
Name[tag_torso]
Position[0.0,0.0,0.0]
Rotation[1.0,-0.0,-0.0][...][...]

~~~~Mesh Surfaces: 2~~~~~~
#
ID [IDP3]
Name [u_rshoulder]
flags [0x00]
numMeshFrames [153]
numSkins [1]

....

 

 

 
Advert (Support Website)

 
 Visitor:
Copyright (c) 2002-2024 xbdev.net - All rights reserved.
Designated articles, tutorials and software are the property of their respective owners.