P3D File Format - ODOLV4x: Difference between revisions
| m (→structStageTransform:  typo) | Lou Montana (talk | contribs)  m (Text replacement - "\[\[Category:BIS( |_)File( |_)Formats\]\]" to "Category:Real Virtuality File Formats") | ||
| (187 intermediate revisions by 4 users not shown) | |||
| Line 1: | Line 1: | ||
| {{ | {{TOC|side}} | ||
| {{Feature|UnsupportedDoc}} | |||
| <!-- | |||
| 			Wiki Editors: DO NOT DOCUMENT ANYTHING AFTER ARMA 1 | |||
| --> | |||
| == Introduction == | == Introduction == | ||
| ===Acknowledgements=== | |||
| === Acknowledgements === | |||
| This body of work is due to Synide's sweat and tears. To whom, all honour and glory. | This body of work is due to Synide's sweat and tears. To whom, all honour and glory. | ||
| Ably assisted by T_D,  | Ably assisted by T_D and Mikero that further detailed the data and gave this article a more general and correct structure. | ||
| === General === | |||
| The general format of an ArmA ODOLV4x p3d model is similar to the ODOLV7 format. The '''major''' differences are that ArmA models have | |||
| *an optional model.cfg, and | |||
| *Lods occur in the file from highest to lowest LodType value. | |||
| ==== Legend ==== | |||
| see [[Generic FileFormat Data Types]] | |||
| ==== Relative Coordinates ==== | |||
| All coordinates are '''relative''' to [[P3D Model Info|ModelInfo]].CentreOfGravity | |||
| ==== File Paths ==== | |||
| All  | '''All''' file references are absolute to the Linux \ (root). | ||
| As a convenience A drive letter is used (such as P:\) for sanity when modelling. The engine knows nothing about C:\my documents or even P:  | |||
| The  | The leading \ is optional. | ||
| \my_project\data\some.paa AND | |||
| my_project\data\some.paa | |||
| point to the same file. | |||
| === Versions === | |||
| This Document covers ODOL versions: | |||
| ===  | ==== V40 ({{arma1}}) ==== | ||
| { | * Original {{arma1}} binarised p3d | ||
| == File Format == | == File Format == | ||
| <syntaxhighlight lang="cpp"> | |||
| ODOLv4x | |||
| { | |||
| 	StandardP3DHeader Header; | |||
|     struct ModelInfo; | |||
| 	Animations		Animations; | |||
| 	ulong			StartAddressOfLods[Header.NoOfLods]; // offset relative to start of file. | |||
| 	ulong			EndAddressOfLods  [Header.NoOfLods]; | |||
| 	LODFaceDefaults	LODFaceDefaults; | |||
| 	ODOLv40Lod		ODOLv40Lods[Header.NoOfLods]; | |||
| } // EndOfFile | |||
| </syntaxhighlight> | |||
| == Structures == | |||
| ===  | === StandardP3DHeader === | ||
| Common header structure for all P3D file formats. | |||
| = | <syntaxhighlight lang="cpp"> | ||
| struct | |||
| { | |||
| 	char[4]	Filetype;		// "ODOL" | |||
| 	ulong	Version; | |||
| 	ulong	NoOfLods;		// alias NoOfResolutions; | |||
| } | |||
| </syntaxhighlight> | |||
| [[P3D Model Info|ModelInfo]] | |||
| === Animations === | |||
| <syntaxhighlight lang="cpp"> | |||
| Animations | |||
| { | |||
| 	tbool	AnimsExist; | |||
| 	if (AnimsExist) | |||
| 	{ | |||
| 		ulong			nAnimationClasses; // eg NoOfAnimSelections; | |||
| 		AnimationClass	AnimationClasses[nAnimationClasses]; | |||
| 		long		NoOfResolutions; // is -1 if nAnimationClasses == 0 | |||
| 		Bones2Anims	Bones2Anims[NoOfResolutions]; | |||
| 		Anims2Bones	Anims2Bones[NoOfResolutions]; | |||
| 		// For every bone there is a list of Animations for each resolution | |||
| 		// And, a reversed table of every Animation gets a Bone. | |||
| 		// The reversed table optionally appends axis info dependent on the AnimTransformType | |||
| 	} | |||
| } | |||
| </syntaxhighlight> | |||
| === AnimationClass === | |||
| <syntaxhighlight lang="cpp"> | |||
| 	AnimationClass | |||
| 	{ | |||
| 		ulong	AnimTransformType; | |||
| 		asciiz	AnimClassName;	// "RightDoor" | |||
| 		asciiz	AnimSource;		// "rotor" | |||
| 		float	MinMaxValue[2]; | |||
| 		float	MinMaxPhase[2]; | |||
| 		ulong	junk; // used to be sourceAddress, no longer, always 953267991 | |||
| 		IF ARMA3 | |||
| 			ulong	Always0;		// no idea what this is used for | |||
| 			ulong	sourceAddress;	// this is the actual source address, 0 = clamp, 1 = mirror, 2 = loop | |||
| 		endif | |||
| 		switch(AnimTransformType) | |||
| 		case 0://rotaton | |||
| 		case 1://rotationX | |||
| 		case 2://rotationY | |||
| 		case 3://rotationZ | |||
| 			float angle0; | |||
| 			float angle1; | |||
| 			break; | |||
| 		case 4://translation | |||
| 		case 5://translationX | |||
| 		case 6://translationY | |||
| 		case 7://translationZ | |||
| 			float offset0; | |||
| 			float offset1; | |||
| 			break; | |||
| 		case 8: //"direct" | |||
| 			float axisPos[3]; | |||
| 			float axisDir[3]; | |||
| 			float angle; //in radians whereas the model.cfg entry is in degrees | |||
| 			float axisOffset; | |||
| 			break; | |||
| 		case 9: //"hide" | |||
| 			float hideValue; | |||
| 			break; | |||
| 	 } | |||
| // corresponds to model.cfg | |||
| class CfgModels | |||
| { | |||
| 	// ... | |||
| 	class whateverModel : Default | |||
| 	{ | |||
| 	 // ... | |||
| 	 class Animations | |||
| 	 { | |||
| 		class RightDoor // AnimClassName | |||
| 		{ | |||
| 			type = "translation";	// AnimTransformType | |||
| 			source = "rotor";		// AnimSource | |||
| 			// etc | |||
| 		}; | |||
| 	}; | |||
| }; | |||
| </syntaxhighlight> | |||
| === | ==== Bones2Anims ==== | ||
|   Bones2Anims | |||
|   { |   { | ||
|    ulong        NoOfBones; | |||
|    Bone2AnimClassList   Bone2AnimClassLists[NoOfBones]; | |||
|  } | |||
| ==== Bone2AnimClassList ==== | |||
|  Bone2AnimClassList | |||
|  { | |||
|    ulong NoOfAnimClasses; | |||
|    ulong AnimationClassIndex[NoOfAnimClasses]; // a (sometimes repeating) list of zero based indexes into above animation classes | |||
|    ulong  | |||
|   } |   } | ||
| === | ==== Anims2Bones ==== | ||
|   Anims2Bones | |||
|   { |   { | ||
|   AnimBones AnimBones[Animations.nAnimationClasses]; | |||
|   } | |||
| ==  | ==== AnimBones ==== | ||
| every lod contains an identical list of animation entries that declare the position and axis of the each animation classes | |||
|   AnimBones | |||
|   { |   { | ||
|   long SkeletonBoneNameIndex; // zero based index to the SkeletonBoneName name & parentname | |||
|   // equivalent to selection = "LeftDoor"; eg in the model.cfg | |||
|   /* | |||
|   ** SkeletonBoneNameIndex== -1 when no skeleton bone is for this Anim and (obviously?) no axis information follows. | |||
|   */ | |||
|   if (SkeletonBoneNameIndex!= -1) && (AnimationClass.AnimTransformType != 8 || 9) | |||
|   { | |||
|   /* | |||
|   ** AnimationClass.AnimTransformType 8 (direct) and 9 (hide) never require axis information.  | |||
|   ** This because the "direct" (type 8) already has axis info in it is AnimationClass structure,  | |||
|   ** and "hidden" (type 9) clearly doesn't need it. | |||
|   */ | |||
|      XYZTriplet axisPos; //describes the position of the axis used for this anim | |||
|      XYZTriplet axisDir; | |||
|   } | |||
|   } |   } | ||
| === LODFaceDefaults === | |||
|   tbool             UseDefault[Header.NoOfLods]; | |||
|    FaceData | |||
|    { |    { | ||
|    ulong   HeaderFaceCount; | |||
|    ulong   aDefaultLong;     //ffffffff or 6f 7a 80 fa eg | |||
|    } |    byte    UnknownByte;      //generally zero | ||
|    byte    aFlag;            // zero or one | |||
|    bytes   Zeroes[7]; | |||
|    }[Number of false UseDefault's]; | |||
| A face data struct only exists for those lods who's UseDefault is zero | |||
| ===  | === ODOLv4xLod === | ||
| *Lod layout corresponds to Arma1 (type40). The differences in a2 are in the nitty gritty of the structures themselves. Arrowhead(v50) has some changes. | |||
|    ODOLv4xLod | |||
|    { |    { | ||
|      ulong                         nProxies; | |||
|      LodProxy                      LodProxies[nProxies];              // see [[P3D Lod Proxies]] | |||
|      ulong                         nLodItems; | |||
|      ulong                         LodItems[nLodItems];               // potentially compressed, except for v64 and later | |||
|     ulong                         nBoneLinks; | |||
|     LodBoneLink                   LodBoneLinks[nBoneLinks]; | |||
|     float                         UnknownFloat1; | |||
|     float                         UnknownFloat2; | |||
|     XYZTriplet                    MinPos; | |||
|     XYZTriplet                    MaxPos; | |||
|     XYZTriplet                    AutoCenterPos; | |||
|     float                         Sphere;                            // same as geo or mem values in modelinfo, if this lod is geo or memlod of course | |||
|     ulong                         NoOfTextures; | |||
|     asciiz                        LodPaaTextureNames[NoOfTextures];  //"ca\characters\hhl\hhl_01_co.paa" | |||
|     ulong                         NoOfMaterials; | |||
|     LodMaterial                   LodMaterials[NoOfMaterials]; | |||
|     LodEdges                      LodEdges;                          // compressed see [[P3D Lod Edges]] | |||
|     ulong                         NoOfFaces; | |||
|     ulong                         OffsetToSectionsStruct;            // see below | |||
|     ushort                        AlwaysZero; | |||
|     LodFace                       LodFace[NoOfFaces];                // see [[P3D Lod Faces]] | |||
|     ulong                         nSections; | |||
|     LodSection                    LodSections[nSections];            // see [[P3D Lod Sections]] | |||
|     ulong                         nNamedSelections; | |||
|     LodNamedSelection             LodNamedSelections[nNamedSelections]; //See [[P3D Named Selections]] potentially compressed | |||
|     ulong                         nTokens; | |||
|     NamedProperty                 NamedProperties[nTokens];          //See [[Named Properties]] | |||
|     ulong                         nFrames; | |||
|     LodFrame                      LodFrames[nFrames];                //see [[P3D Lod Frames]] | |||
|     ulong                         IconColor; | |||
|     ulong                         SelectedColor; | |||
|     ulong                         special; // IsAlpha|IsTransparent|IsAnimated|OnSurface | |||
|     byte                          vertexBoneRefIsSimple; | |||
|     ulong                         sizeOfVertexTable;                 //(including these 4 bytes) | |||
|     if (v5x) | |||
|     LodPointFlags                 LodPointFlags;                     // Potentially compressed | |||
|     endif | |||
|     VertexTable                   VertexTable; | |||
|    } |    } | ||
| ==== VertexTable ==== | |||
| all arrays are subject to compression | |||
|  struct | |||
|  { | |||
|     UvSet                         DefaultUVset; | |||
|     ulong                         nUVs; | |||
|     UvSet                         UVSets[nUVs-1]; | |||
|     ulong                         NoOfPoints; | |||
|     XYZTriplet                    LodPoints[NoOfPoints]; | |||
|     ulong                         nNormals; | |||
|     (A2)LodNormals                LodNormals[nNormals]; | |||
|     ulong                         nMinMax; | |||
|     (A2)LodMinMax                 MinMax[nMinMax];                   //optional | |||
|     ulong                         nProperties; | |||
|     VertProperty                  VertProperties[nProperties];       //optional related to skeleton | |||
|     ulong                         Count; | |||
|     VertexNeighborInfo            neighborBoneRef[Count];          //optional | |||
|  } | |||
| * All non zero counts counts are the same. | |||
| * Points,PointFlags, Normals and UV1 arrays are an integral group, they are either all there, or not specified (RacetK.p3d, a [[P3D Lod Frames|FrameTime]] lod has no counts at all) | |||
| * UV2,MinMax, VertProperties and Unknown are optional in the sense that their counts can individually be zero, else they are the same as the others | |||
| * In Odol7 PointFlags are part of this stucture, in Arma, they are a separated table. | |||
| ==== CompressedFill Arrays ==== | |||
| LodPointFlags, LodUV's and LodNormals arrays are not only subject to the standard 1024 rule compression, but also have a fill byte. | |||
|  struct | |||
|  { | |||
|   ulong                         Count; | |||
|   tbool                         DefaultFill; | |||
|   if (DefaultFill) | |||
|    type                         Array;          // default fill for all Counts | |||
|   else | |||
|    type                         Array[Count];   // potentially compressed | |||
|  } | |||
| The structure either contains a single set of type variables, or, an array of type variables. If a full array is declared (DefaultFill =false) then that array is subject to the 1024 rule as per normal. | |||
| ==== UVset ==== | |||
|  if TrueARMA2 | |||
|     float                         UVScale[4]; | |||
|  endif | |||
|     (A2)LodUV                     LodUV; | |||
| ==== LodUV ==== | |||
|  CompressedFill type = UVPair // eg float U,V; | |||
| ==== A2LodUV ==== | |||
|  CompressedFill type = float // eg float UV; | |||
| ==== LodNormals ==== | |||
|   CompressedFill type = XYZTriplet | |||
| ==== A2LodNormals ==== | |||
|   CompressedFill type = CompressedXYZTriplet | |||
| ===  | ===== CompressedXYZTriplet ===== | ||
| contains 3x 10 bit fields in a 32bit 'integer' | |||
| code for converting back to a standard XYZTriplet is: | |||
| ===  |  void DecodeXYZ(ulong CompressedXYZ, XYZTriplet *triplet) | ||
|  { | |||
|     double scaleFactor = -1.0 /511; | |||
|     trp->X=trp->Y=trp->Z=0.0; | |||
|     int x=   CompressedXYZ       & 0x3FF; | |||
|     int y = (CompressedXYZ>> 10) & 0x3FF; | |||
|     int z = (CompressedXYZ>> 20) & 0x3FF; | |||
|     if (x > 511) x -= 1024; | |||
|     if (y > 511) y -= 1024; | |||
|     if (z > 511) z -= 1024; | |||
|     if (x) trp->X = (float)(x * scaleFactor); | |||
|     if (y) trp->Y = (float)(y * scaleFactor); | |||
|     if (x) trp->Z = (float)(z * scaleFactor); | |||
|  } | |||
| ===  | ==== LodPointFlags ==== | ||
|  CompressedFill type = ulong bits | |||
| This table is the equivalent of Oxygen's points->properties dialog box. It specifically stores the user values and other flags for that point. | |||
| In ODOl7 it was part of the vertex table. In ArmA, it is separate. | |||
| See [[P3D Point and Face Flags]] | |||
| ==== LodMinMax ==== | |||
|  CompressedArray | |||
|  { | |||
|   XYZTriplet     MinMax[Count][2]; // 2 == min vs max | |||
|  } | |||
| ==== A2LodMinMax ==== | |||
|  CompressedArray | |||
|  { | |||
|   float         MinMax[Count][2]; // 2 == min vs max | |||
|  } | |||
| ==== VertProperty ==== | |||
|  CompressedArray | |||
|  { | |||
|   ulong  index;// seen range 0..4 | |||
|   ulong  a,b; // definite not floats. might be flags, or indices | |||
|  } | |||
| ==== VertexNeighborInfo ==== | |||
|  CompressedArray | |||
|  { | |||
|   ushort  vertexIndex _posA; | |||
|   AnimationRTWeight _rtwA; | |||
|   ushort  vertexIndex _posB; | |||
|   AnimationRTWeight _rtwB; | |||
|  } | |||
| ==== LodBoneLink ==== | |||
|  LodBoneLink | |||
|  { | |||
|    ulong NoOfLinks;         //range 0..3 | |||
|    ulong Value[NoOfLinks];  //the 'Value' seems to reference the 'LodItems' structure, resulting in a circular-reference. | |||
|  } | |||
| ==== LodMaterials ==== | |||
| Basically... A direct replication of the information in the given .rvmat file | |||
| The stages in the p3d include a default stage and a TI stage that are not normally listed in the rvmat. | |||
| :The first stage (in the p3d) is unconditionally the default stage. It is defaulted to empty (RvMatName=""), unless, specified in the rvmat | |||
| :The last stage is the TI stage, and is also defaulted empty, unless specified in the rvmat. | |||
| ::TI Stages were introduced for operation arrowhead. Lod Material Types 9 and 10 (Arma1 and Arma2) do not have a TI stage at all. | |||
| Neither of these two special, hidden, stage types use uvsets. The transform matrix for them is defaulted empty (so-called 'TexGen0'). | |||
| When specified in the rvmat (class Stage0 and StageTI respectively), no  class uvTransform is declared for them. It is assumed default empty. | |||
| In an rvmat, uvTransforms are ordinarily declared within each stage body. | |||
| In a P3D, identical UVTransforms are declared once, and multiple 'stages' refer to them. There is, always, a default UVSet0 Transform as the 1st entry. (IE some stages dont require uvsets) | |||
| This P3D style can, if preferred, be used in rvmat syntax as | |||
|   class TexGenX | |||
|   { |   { | ||
|    ....... | |||
|   }; |   }; | ||
|   class StageZ | |||
|   { |   { | ||
|     ......... | |||
|     Texgen=X; | |||
|  }; | |||
| where X and Z are numbers | |||
| === | ====LodMaterial==== | ||
|    LodMaterial | |||
|    { |    { | ||
|      asciiz  |      asciiz            RvMatName;     // "ca\characters\data\soldier_captive_hhl.rvmat" | ||
|      ulong  |      ulong             Type;          // 9 == ArmA | ||
|      D3DCOLORVALUE     Emissive; | |||
|      D3DCOLORVALUE     Ambient; | |||
|      D3DCOLORVALUE     Diffuse; | |||
|      D3DCOLORVALUE     forcedDiffuse; | |||
|      D3DCOLORVALUE     Specular; | |||
|      D3DCOLORVALUE     Specular2;       //Usually same as Specular | |||
|      float  |      float             SpecularPower;   //  | ||
|      ulong  |      ulong            PixelShaderId;   //See enumPixelShaderId | ||
|      ulong  |      ulong            VertexShaderId;  //See enumVertexShaderId | ||
|      ulong           |     LongBool          mainLight;     // 1 or zero | ||
|      ulong             ul_FogMode;    /// 0..4 | |||
|     Asciiz            BiSurfaceName;   // "ca\data\Penetration\plastic.bisurf" | |||
|     LongBool          Arma1Mostly1;    //rarely zero | |||
|     ulong             RenderFlags;     //Generally 0 | |||
|      ulong             nTextures; | |||
|      ulong             nTransforms;     // always same as nTextures | |||
|     LodStageTexture   StageTextures  [nTextures]; | |||
|      LodStageTransform StageTransforms[nTransforms]; | |||
|    }    |    }    | ||
| :Each lodmaterial entry contains a default StageTexture and StageTransform as the first entry. It is not shown in the rvmat file and has no PaaTexture | |||
| === | :It is the only entry if a SurfaceName exists. | ||
| === D3DCOLORVALUE === | |||
|   D3DCOLORVALUE | |||
|   { |   { | ||
|    float r,g,b,a; | |||
|   } |   } | ||
| ===== RenderFlags ===== | |||
| :*Bit0:AlwaysInShadow (A1 only) | |||
| :*Bit1:NoZWrite | |||
| :*Bit4:NoColorWrite | |||
| :*Bit5:NoAlphaWrite | |||
| :*Bit6:AddBlend | |||
| :*Bit7:AlphaTest (clutter) | |||
| :*Bit8:AlphaTest64 (clutter) | |||
| :*Bit19:Road      (a1only) | |||
| :*Bit11:NoTiWrite | |||
| ===== | ===== LodStageTexture ===== | ||
|   LodStageTexture | |||
|   { |   { | ||
|    ulong  TextureFilter; |    ulong  TextureFilter; // see below | ||
|    asciiz  |    asciiz PaaTexture;    // "ca\characters\data\civil_tvreport_body_as.paa | ||
|    ulong   |                         // alternatively "#(argb,8,8,3)color(0,0,0,1,CO)" (eg) | ||
|    ulong  TransformIndex;       // zero based, see below | |||
|   }; |   }; | ||
| ===== | |||
| :The first stageTexture is a dummy entry. For N humanly readable stage classes, there are 1+N LodStageTextures | |||
| :TextureFilter maybe 1 of the following values.  | |||
| :*0: Point // sometimes | |||
| :*1: Linear  // rarely | |||
| :*2: TriLinear // not seen | |||
| :*3: Anisotropic (default) | |||
| ===== LodStageTransform ===== | |||
|     LodStageTransform | |||
|     { |     { | ||
|      ulong UVSource; |      ulong UVSource; | ||
|      float Transform[4][3]; |      float Transform[4][3];//a DirectX texture space transform matrix | ||
|     }; |     }; | ||
| :UVSource corresponds to the 8 possible uvsets available  | |||
| :*0 "None" | |||
| :*1 "Tex" default | |||
| :*2: "Tex2" | |||
| :*........ | |||
| :*8:"Tex8" | |||
| :*Tex1..8 cannot be taken literally as uvsource 1..8. They can mean anything, according to the template and are scarcely encountered | |||
| ==== NamedProperty ==== | |||
|   struct | |||
|   { | |||
|      Asciiz Property;// "noshadow" = "1" eg | |||
|      Asciiz Value; | |||
|   } | |||
| *See [[Named Properties]] | |||
| == Decompression == | |||
| see [[Compressed LZSS File Format]] | |||
| see [[Compressed LZO File Format]] | |||
| In ODOL v40 format files, some of the data structures present in the file are compressed by using LZSS compression. | |||
| Unlike pbo compression, in ArmA model files, one only knows the number of items to decompress, the expected output size (in bytes) and the expected checksum. | |||
| With this information and the size of a given data item one has the necessary information to expand the data to it is original format and size. | |||
| {{Feature|informative|Data structures that are identified as being compressible will only be compressed if the 'expectedSize' is ≥ 1024 bytes.}} | |||
| == Reference Tables == | == Reference Tables == | ||
| === Material Stages === | === Material Stages === | ||
| Line 864: | Line 526: | ||
| A reference table is used when processing materials where depending on the shader specified the given number of stages should be processed. | A reference table is used when processing materials where depending on the shader specified the given number of stages should be processed. | ||
| <code><nowiki> | <code style="display: block"><nowiki> | ||
|   refShaderStages |   refShaderStages | ||
|   { |   { | ||
| Line 887: | Line 549: | ||
| |align="left"|0x03, 3||align="left"|NormalMapThrough||align="left"|normal map shader - through lighting||align="left"|3 | |align="left"|0x03, 3||align="left"|NormalMapThrough||align="left"|normal map shader - through lighting||align="left"|3 | ||
| |- | |- | ||
| |align="left"|0x04, 4||align="left"| | |align="left"|0x04, 4||align="left"|NormalMapSpecularDIMap||align="left"|VBS2 only||align="left"|2 | ||
| |- | |- | ||
| |align="left"|0x05, 5||align="left"|NormalMapDiffuse||align="left"|?||align="left"|2 | |align="left"|0x05, 5||align="left"|NormalMapDiffuse||align="left"|?||align="left"|2 | ||
| Line 895: | Line 557: | ||
| |align="left"|0x07, 7||align="left"|?||align="left"|?||align="left"|? | |align="left"|0x07, 7||align="left"|?||align="left"|?||align="left"|? | ||
| |- | |- | ||
| |align="left"|0x08, 8||align="left"|Water||align="left"|sea water||align="left"|2 | |align="left"|0x08, 8||align="left"|Water||align="left"|A1 only sea water||align="left"|2 | ||
| |- | |- | ||
| |align="left"|0x09, 9||align="left"|?||align="left"| | |align="left"|0x09, 9||align="left"|?||align="left"|vbs2||align="left"|? | ||
| |- | |- | ||
| |align="left"|0x0A, 10||align="left"|White||align="left"| | |align="left"|0x0A, 10||align="left"|White||align="left"|A1 only||align="left"|0 | ||
| |- | |- | ||
| |align="left"|0x0B, 11||align="left"|?||align="left"| | |align="left"|0x0B, 11||align="left"|?||align="left"|vbs2||align="left"|? | ||
| |- | |- | ||
| |align="left"|0x0C, 12||align="left"|AlphaShadow||align="left"|shadow alpha write||align="left"|0 | |align="left"|0x0C, 12||align="left"|AlphaShadow||align="left"|shadow alpha write||align="left"|0 | ||
| Line 907: | Line 569: | ||
| |align="left"|0x0D, 13||align="left"|AlphaNoShadow||align="left"|shadow alpha (no shadow) write||align="left"|0 | |align="left"|0x0D, 13||align="left"|AlphaNoShadow||align="left"|shadow alpha (no shadow) write||align="left"|0 | ||
| |- | |- | ||
| |align="left"|0x0E, 14||align="left"|?||align="left"| | |align="left"|0x0E, 14||align="left"|?||align="left"|vbs2||align="left"|? | ||
| |- | |- | ||
| |align="left"|0x0F, 15||align="left"|DetailMacroAS||align="left"|?||align="left"|3 | |align="left"|0x0F, 15||align="left"|DetailMacroAS||align="left"|?||align="left"|3 | ||
| |- | |- | ||
| |align="left"|0x10, 16||align="left"|?||align="left"| | |align="left"|0x10, 16||align="left"|?||align="left"|vbs2||align="left"|? | ||
| |- | |- | ||
| |align="left"|0x11, 17||align="left"|?||align="left"| | |align="left"|0x11, 17||align="left"|?||align="left"|vbs2||align="left"|? | ||
| |- | |- | ||
| |align="left"|0x12, 18||align="left"|NormalMapSpecularMap||align="left"|?||align="left"|2 | |align="left"|0x12, 18||align="left"|NormalMapSpecularMap||align="left"|?||align="left"|2 | ||
| Line 939: | Line 601: | ||
| |align="left"|0x3C, 60||align="left"|NormalMapThroughSimple||align="left"|?||align="left"|0 | |align="left"|0x3C, 60||align="left"|NormalMapThroughSimple||align="left"|?||align="left"|0 | ||
| |- | |- | ||
| |align="left"|0xxx, 102||align="left"|Super||align="left"|Arrowhead||align="left"|0 | |||
| |- | |||
| |align="left"|0xxx, 103||align="left"|Multi||align="left"|Arrowhead||align="left"|0 | |||
| |- | |||
| |align="left"|0xxx, 107||align="left"|Tree||align="left"|Arrowhead||align="left"|0 | |||
| |- | |||
| |align="left"|0xxx, 110||align="left"|Skin||align="left"|Arrowhead||align="left"|0 | |||
| |- | |||
| |align="left"|0x6F, 111||align="left"|CalmWater||align="left"|Arrowhead||align="left"|7 | |||
| |- | |- | ||
| |align="left"|0xxx, 114||align="left"|TreeAdv||align="left"|Arrowhead||align="left"|0 | |||
| |- | |||
| |align="left"|0xxx, 116||align="left"|TreeAdvTrunk||align="left"|Arrowhead||align="left"|0 | |||
| |} | |} | ||
| == Enums == | == Enums == | ||
| <code | <code style="display: block"><nowiki> | ||
|   int enum VertexShaderId |   int enum VertexShaderId | ||
|   { |   { | ||
| 	case 0: return "Basic"; | |||
| 	case 1: return "NormalMap"; | |||
| 	case 2: return "NormalMapDiffuse"; | |||
| 	case 3: return "Grass"; | |||
| 	case 8: return "Water"; | |||
| 	case 11: return  "NormalMapThrough"; | |||
| 	case 15: return "NormalMapAS"; | |||
| 	case 14: return "BasicAS"; | |||
| 	case 17: return "Glass"; | |||
| 	case 18: return "NormalMapSpecularThrough"; | |||
| 	case 19: return "NormalMapThroughNoFade"; | |||
| 	case 20: return "NormalMapSpecularThroughNoFade"; | |||
| 	case 23: return "Super"; | |||
| 	case 24: return "Multi"; | |||
| 	case 25: return "Tree"; | |||
| 	case 30: return "CalmWater"; | |||
| 	case 26: return "TreeNoFade"; | |||
| 	case 29: return "Skin"; | |||
| 	case 31: return "TreeAdv"; | |||
| 	case 32: return "TreeAdvTrunk"; | |||
|   } |   } | ||
| </nowiki></code> | </nowiki></code> | ||
| == Links == | == Links == | ||
| [[User:Sy|Article Author - Sy (Synide)]] -- [[User:Sy|Sy]] 17:16, 11 August 2007 (CEST) | [[User:Sy|Article Author - Sy (Synide)]] -- [[User:Sy|Sy]] 17:16, 11 August 2007 (CEST) | ||
| [[P3D File Format - ODOLV40|Original ODOLv40 Article detailed by Bxbx (Biki'd by Mikero)]] | [[P3D File Format - ODOLV40|Original ODOLv40 Article detailed by Bxbx (Biki'd by Mikero)]] | ||
| [[Category: | [[Category:Real Virtuality File Formats]] | ||
| {{GameCategory|arma1|File Formats}} | |||
Latest revision as of 12:27, 8 May 2025
Introduction
Acknowledgements
This body of work is due to Synide's sweat and tears. To whom, all honour and glory. Ably assisted by T_D and Mikero that further detailed the data and gave this article a more general and correct structure.
General
The general format of an ArmA ODOLV4x p3d model is similar to the ODOLV7 format. The major differences are that ArmA models have
- an optional model.cfg, and
- Lods occur in the file from highest to lowest LodType value.
Legend
see Generic FileFormat Data Types
Relative Coordinates
All coordinates are relative to ModelInfo.CentreOfGravity
File Paths
All file references are absolute to the Linux \ (root).
As a convenience A drive letter is used (such as P:\) for sanity when modelling. The engine knows nothing about C:\my documents or even P:
The leading \ is optional.
\my_project\data\some.paa AND my_project\data\some.paa
point to the same file.
Versions
This Document covers ODOL versions:
V40 (Armed Assault)
- Original Armed Assault binarised p3d
File Format
ODOLv4x
{
	StandardP3DHeader Header;
    struct ModelInfo;
	Animations		Animations;
	ulong			StartAddressOfLods[Header.NoOfLods]; // offset relative to start of file.
	ulong			EndAddressOfLods  [Header.NoOfLods];
	LODFaceDefaults	LODFaceDefaults;
	ODOLv40Lod		ODOLv40Lods[Header.NoOfLods];
} // EndOfFile
Structures
StandardP3DHeader
Common header structure for all P3D file formats.
struct
{
	char[4]	Filetype;		// "ODOL"
	ulong	Version;
	ulong	NoOfLods;		// alias NoOfResolutions;
}
Animations
Animations
{
	tbool	AnimsExist;
	if (AnimsExist)
	{
		ulong			nAnimationClasses; // eg NoOfAnimSelections;
		AnimationClass	AnimationClasses[nAnimationClasses];
		long		NoOfResolutions; // is -1 if nAnimationClasses == 0
		Bones2Anims	Bones2Anims[NoOfResolutions];
		Anims2Bones	Anims2Bones[NoOfResolutions];
		// For every bone there is a list of Animations for each resolution
		// And, a reversed table of every Animation gets a Bone.
		// The reversed table optionally appends axis info dependent on the AnimTransformType
	}
}
AnimationClass
	AnimationClass
	{
		ulong	AnimTransformType;
		asciiz	AnimClassName;	// "RightDoor"
		asciiz	AnimSource;		// "rotor"
		float	MinMaxValue[2];
		float	MinMaxPhase[2];
		ulong	junk; // used to be sourceAddress, no longer, always 953267991
		IF ARMA3
			ulong	Always0;		// no idea what this is used for
			ulong	sourceAddress;	// this is the actual source address, 0 = clamp, 1 = mirror, 2 = loop
		endif
		switch(AnimTransformType)
		case 0://rotaton
		case 1://rotationX
		case 2://rotationY
		case 3://rotationZ
			float angle0;
			float angle1;
			break;
		case 4://translation
		case 5://translationX
		case 6://translationY
		case 7://translationZ
			float offset0;
			float offset1;
			break;
		case 8: //"direct"
			float axisPos[3];
			float axisDir[3];
			float angle; //in radians whereas the model.cfg entry is in degrees
			float axisOffset;
			break;
		case 9: //"hide"
			float hideValue;
			break;
	 }
// corresponds to model.cfg
class CfgModels
{
	// ...
	class whateverModel : Default
	{
	 // ...
	 class Animations
	 {
		class RightDoor // AnimClassName
		{
			type = "translation";	// AnimTransformType
			source = "rotor";		// AnimSource
			// etc
		};
	};
};
Bones2Anims
Bones2Anims
{
 ulong        NoOfBones;
 Bone2AnimClassList   Bone2AnimClassLists[NoOfBones];
}
Bone2AnimClassList
Bone2AnimClassList
{
 ulong NoOfAnimClasses;
 ulong AnimationClassIndex[NoOfAnimClasses]; // a (sometimes repeating) list of zero based indexes into above animation classes
}
Anims2Bones
Anims2Bones
{
 AnimBones AnimBones[Animations.nAnimationClasses];
}
AnimBones
every lod contains an identical list of animation entries that declare the position and axis of the each animation classes
AnimBones
{
 long SkeletonBoneNameIndex; // zero based index to the SkeletonBoneName name & parentname
 // equivalent to selection = "LeftDoor"; eg in the model.cfg
 /*
 ** SkeletonBoneNameIndex== -1 when no skeleton bone is for this Anim and (obviously?) no axis information follows.
 */
 if (SkeletonBoneNameIndex!= -1) && (AnimationClass.AnimTransformType != 8 || 9)
 {
 /*
 ** AnimationClass.AnimTransformType 8 (direct) and 9 (hide) never require axis information. 
 ** This because the "direct" (type 8) already has axis info in it is AnimationClass structure, 
 ** and "hidden" (type 9) clearly doesn't need it.
 */
    XYZTriplet axisPos; //describes the position of the axis used for this anim
    XYZTriplet axisDir;
 }
}
LODFaceDefaults
 tbool             UseDefault[Header.NoOfLods];
 FaceData
 {
  ulong   HeaderFaceCount;
  ulong   aDefaultLong;     //ffffffff or 6f 7a 80 fa eg
  byte    UnknownByte;      //generally zero
  byte    aFlag;            // zero or one
  bytes   Zeroes[7];
 }[Number of false UseDefault's];
A face data struct only exists for those lods who's UseDefault is zero
ODOLv4xLod
- Lod layout corresponds to Arma1 (type40). The differences in a2 are in the nitty gritty of the structures themselves. Arrowhead(v50) has some changes.
 ODOLv4xLod
 {
   ulong                         nProxies;
   LodProxy                      LodProxies[nProxies];              // see P3D Lod Proxies
   ulong                         nLodItems;
   ulong                         LodItems[nLodItems];               // potentially compressed, except for v64 and later
   ulong                         nBoneLinks;
   LodBoneLink                   LodBoneLinks[nBoneLinks];
   float                         UnknownFloat1;
   float                         UnknownFloat2;
   XYZTriplet                    MinPos;
   XYZTriplet                    MaxPos;
   XYZTriplet                    AutoCenterPos;
   float                         Sphere;                            // same as geo or mem values in modelinfo, if this lod is geo or memlod of course
   ulong                         NoOfTextures;
   asciiz                        LodPaaTextureNames[NoOfTextures];  //"ca\characters\hhl\hhl_01_co.paa"
   ulong                         NoOfMaterials;
   LodMaterial                   LodMaterials[NoOfMaterials];
   LodEdges                      LodEdges;                          // compressed see P3D Lod Edges
   ulong                         NoOfFaces;
   ulong                         OffsetToSectionsStruct;            // see below
   ushort                        AlwaysZero;
   LodFace                       LodFace[NoOfFaces];                // see P3D Lod Faces
   ulong                         nSections;
   LodSection                    LodSections[nSections];            // see P3D Lod Sections
   ulong                         nNamedSelections;
   LodNamedSelection             LodNamedSelections[nNamedSelections]; //See P3D Named Selections potentially compressed
   ulong                         nTokens;
   NamedProperty                 NamedProperties[nTokens];          //See Named Properties
   ulong                         nFrames;
   LodFrame                      LodFrames[nFrames];                //see P3D Lod Frames
   ulong                         IconColor;
   ulong                         SelectedColor;
   ulong                         special; // IsAlpha|IsTransparent|IsAnimated|OnSurface
   byte                          vertexBoneRefIsSimple;
   ulong                         sizeOfVertexTable;                 //(including these 4 bytes)
   if (v5x)
   LodPointFlags                 LodPointFlags;                     // Potentially compressed
   endif
   VertexTable                   VertexTable;
 }
VertexTable
all arrays are subject to compression
struct
{
   UvSet                         DefaultUVset;
   ulong                         nUVs;
   UvSet                         UVSets[nUVs-1];
   ulong                         NoOfPoints;
   XYZTriplet                    LodPoints[NoOfPoints];
   ulong                         nNormals;
   (A2)LodNormals                LodNormals[nNormals];
   ulong                         nMinMax;
   (A2)LodMinMax                 MinMax[nMinMax];                   //optional
   ulong                         nProperties;
   VertProperty                  VertProperties[nProperties];       //optional related to skeleton
   ulong                         Count;
   VertexNeighborInfo            neighborBoneRef[Count];          //optional
}
- All non zero counts counts are the same.
- Points,PointFlags, Normals and UV1 arrays are an integral group, they are either all there, or not specified (RacetK.p3d, a FrameTime lod has no counts at all)
- UV2,MinMax, VertProperties and Unknown are optional in the sense that their counts can individually be zero, else they are the same as the others
- In Odol7 PointFlags are part of this stucture, in Arma, they are a separated table.
CompressedFill Arrays
LodPointFlags, LodUV's and LodNormals arrays are not only subject to the standard 1024 rule compression, but also have a fill byte.
struct
{
 ulong                         Count;
 tbool                         DefaultFill;
 if (DefaultFill)
  type                         Array;          // default fill for all Counts
 else
  type                         Array[Count];   // potentially compressed
}
The structure either contains a single set of type variables, or, an array of type variables. If a full array is declared (DefaultFill =false) then that array is subject to the 1024 rule as per normal.
UVset
if TrueARMA2 float UVScale[4]; endif (A2)LodUV LodUV;
LodUV
CompressedFill type = UVPair // eg float U,V;
A2LodUV
CompressedFill type = float // eg float UV;
LodNormals
CompressedFill type = XYZTriplet
A2LodNormals
CompressedFill type = CompressedXYZTriplet
CompressedXYZTriplet
contains 3x 10 bit fields in a 32bit 'integer'
code for converting back to a standard XYZTriplet is:
void DecodeXYZ(ulong CompressedXYZ, XYZTriplet *triplet)
{
   double scaleFactor = -1.0 /511;
   trp->X=trp->Y=trp->Z=0.0;
   int x=   CompressedXYZ       & 0x3FF;
   int y = (CompressedXYZ>> 10) & 0x3FF;
   int z = (CompressedXYZ>> 20) & 0x3FF;
   if (x > 511) x -= 1024;
   if (y > 511) y -= 1024;
   if (z > 511) z -= 1024;
   if (x) trp->X = (float)(x * scaleFactor);
   if (y) trp->Y = (float)(y * scaleFactor);
   if (x) trp->Z = (float)(z * scaleFactor);
}
LodPointFlags
CompressedFill type = ulong bits
This table is the equivalent of Oxygen's points->properties dialog box. It specifically stores the user values and other flags for that point.
In ODOl7 it was part of the vertex table. In ArmA, it is separate.
LodMinMax
CompressedArray
{
 XYZTriplet     MinMax[Count][2]; // 2 == min vs max
}
A2LodMinMax
CompressedArray
{
 float         MinMax[Count][2]; // 2 == min vs max
}
VertProperty
CompressedArray
{
 ulong  index;// seen range 0..4
 ulong  a,b; // definite not floats. might be flags, or indices
}
VertexNeighborInfo
CompressedArray
{
 ushort  vertexIndex _posA;
 AnimationRTWeight _rtwA;
 ushort  vertexIndex _posB;
 AnimationRTWeight _rtwB;
}
LodBoneLink
LodBoneLink
{
  ulong NoOfLinks;         //range 0..3
  ulong Value[NoOfLinks];  //the 'Value' seems to reference the 'LodItems' structure, resulting in a circular-reference.
}
LodMaterials
Basically... A direct replication of the information in the given .rvmat file
The stages in the p3d include a default stage and a TI stage that are not normally listed in the rvmat.
- The first stage (in the p3d) is unconditionally the default stage. It is defaulted to empty (RvMatName=""), unless, specified in the rvmat
- The last stage is the TI stage, and is also defaulted empty, unless specified in the rvmat.
- TI Stages were introduced for operation arrowhead. Lod Material Types 9 and 10 (Arma1 and Arma2) do not have a TI stage at all.
 
Neither of these two special, hidden, stage types use uvsets. The transform matrix for them is defaulted empty (so-called 'TexGen0').
When specified in the rvmat (class Stage0 and StageTI respectively), no class uvTransform is declared for them. It is assumed default empty.
In an rvmat, uvTransforms are ordinarily declared within each stage body.
In a P3D, identical UVTransforms are declared once, and multiple 'stages' refer to them. There is, always, a default UVSet0 Transform as the 1st entry. (IE some stages dont require uvsets)
This P3D style can, if preferred, be used in rvmat syntax as
class TexGenX
{
  .......
};
class StageZ
{
  .........
  Texgen=X;
};
where X and Z are numbers
LodMaterial
 LodMaterial
 {
   asciiz            RvMatName;     // "ca\characters\data\soldier_captive_hhl.rvmat"
   ulong             Type;          // 9 == ArmA
   D3DCOLORVALUE     Emissive;
   D3DCOLORVALUE     Ambient;
   D3DCOLORVALUE     Diffuse;
   D3DCOLORVALUE     forcedDiffuse;
   D3DCOLORVALUE     Specular;
   D3DCOLORVALUE     Specular2;       //Usually same as Specular
   float             SpecularPower;   // 
   ulong            PixelShaderId;   //See enumPixelShaderId
   ulong            VertexShaderId;  //See enumVertexShaderId
   LongBool          mainLight;     // 1 or zero
   ulong             ul_FogMode;    /// 0..4
   Asciiz            BiSurfaceName;   // "ca\data\Penetration\plastic.bisurf"
   LongBool          Arma1Mostly1;    //rarely zero
   ulong             RenderFlags;     //Generally 0
   ulong             nTextures;
   ulong             nTransforms;     // always same as nTextures
   LodStageTexture   StageTextures  [nTextures];
   LodStageTransform StageTransforms[nTransforms];
 }  
- Each lodmaterial entry contains a default StageTexture and StageTransform as the first entry. It is not shown in the rvmat file and has no PaaTexture
- It is the only entry if a SurfaceName exists.
D3DCOLORVALUE
D3DCOLORVALUE
{
  float r,g,b,a;
}
RenderFlags
- Bit0:AlwaysInShadow (A1 only)
- Bit1:NoZWrite
- Bit4:NoColorWrite
- Bit5:NoAlphaWrite
- Bit6:AddBlend
- Bit7:AlphaTest (clutter)
- Bit8:AlphaTest64 (clutter)
- Bit19:Road (a1only)
- Bit11:NoTiWrite
 
LodStageTexture
LodStageTexture
{
 ulong  TextureFilter; // see below
 asciiz PaaTexture;    // "ca\characters\data\civil_tvreport_body_as.paa
                       // alternatively "#(argb,8,8,3)color(0,0,0,1,CO)" (eg)
 ulong  TransformIndex;       // zero based, see below
};
- The first stageTexture is a dummy entry. For N humanly readable stage classes, there are 1+N LodStageTextures
- TextureFilter maybe 1 of the following values.
- 0: Point // sometimes
- 1: Linear // rarely
- 2: TriLinear // not seen
- 3: Anisotropic (default)
 
LodStageTransform
  LodStageTransform
  {
   ulong UVSource;
   float Transform[4][3];//a DirectX texture space transform matrix
  };
- UVSource corresponds to the 8 possible uvsets available
- 0 "None"
- 1 "Tex" default
- 2: "Tex2"
- ........
- 8:"Tex8"
 
- Tex1..8 cannot be taken literally as uvsource 1..8. They can mean anything, according to the template and are scarcely encountered
 
NamedProperty
 struct
 {
    Asciiz Property;// "noshadow" = "1" eg
    Asciiz Value;
 }
- See Named Properties
Decompression
see Compressed LZSS File Format
see Compressed LZO File Format
In ODOL v40 format files, some of the data structures present in the file are compressed by using LZSS compression.
Unlike pbo compression, in ArmA model files, one only knows the number of items to decompress, the expected output size (in bytes) and the expected checksum. With this information and the size of a given data item one has the necessary information to expand the data to it is original format and size.
Reference Tables
Material Stages
The number of material stages is dependant on the type of Shader that is used to process the material by the ArmA game engine. A reference table is used when processing materials where depending on the shader specified the given number of stages should be processed.
 refShaderStages
 {
   int PixelShaderId;
   int NoOfStages;
 };
| ID (Hex/Decimal) | Name | Description | NoOfStages | 
|---|---|---|---|
| 0x00, 0 | Normal | diffuse color modulate, alpha replicate | 0 | 
| 0x01, 1 | NormalDXTA | diffuse color modulate, alpha replicate, DXT alpha correction | 0 | 
| 0x02, 2 | NormalMap | normal map shader | 3 | 
| 0x03, 3 | NormalMapThrough | normal map shader - through lighting | 3 | 
| 0x04, 4 | NormalMapSpecularDIMap | VBS2 only | 2 | 
| 0x05, 5 | NormalMapDiffuse | ? | 2 | 
| 0x06, 6 | Detail | ? | 1 | 
| 0x07, 7 | ? | ? | ? | 
| 0x08, 8 | Water | A1 only sea water | 2 | 
| 0x09, 9 | ? | vbs2 | ? | 
| 0x0A, 10 | White | A1 only | 0 | 
| 0x0B, 11 | ? | vbs2 | ? | 
| 0x0C, 12 | AlphaShadow | shadow alpha write | 0 | 
| 0x0D, 13 | AlphaNoShadow | shadow alpha (no shadow) write | 0 | 
| 0x0E, 14 | ? | vbs2 | ? | 
| 0x0F, 15 | DetailMacroAS | ? | 3 | 
| 0x10, 16 | ? | vbs2 | ? | 
| 0x11, 17 | ? | vbs2 | ? | 
| 0x12, 18 | NormalMapSpecularMap | ? | 2 | 
| 0x13, 19 | NormalMapDetailSpecularMap | Similar to NormalMapDiffuse | 3 | 
| 0x14, 20 | NormalMapMacroASSpecularMap | ? | 4 | 
| 0x15, 21 | NormalMapDetailMacroASSpecularMap | ? | 5 | 
| 0x16, 22 | NormalMapSpecularDIMap | Same as NormalMapSpecularMap, but uses _SMDI texture | 2 | 
| 0x17, 23 | NormalMapDetailSpecularDIMap | ? | 3 | 
| 0x18, 24 | NormalMapMacroASSpecularDIMap | ? | 4 | 
| 0x19, 25 | NormalMapDetailMacroASSpecularDIMap | ? | 5 | 
| 0x38, 56 | Glass | ? | 2 | 
| 0x3A, 58 | NormalMapSpecularThrough | ? | 3 | 
| 0x3B, 59 | Grass | Special shader to allow volumetric shadows to be cast on grass clutter | 0 | 
| 0x3C, 60 | NormalMapThroughSimple | ? | 0 | 
| 0xxx, 102 | Super | Arrowhead | 0 | 
| 0xxx, 103 | Multi | Arrowhead | 0 | 
| 0xxx, 107 | Tree | Arrowhead | 0 | 
| 0xxx, 110 | Skin | Arrowhead | 0 | 
| 0x6F, 111 | CalmWater | Arrowhead | 7 | 
| 0xxx, 114 | TreeAdv | Arrowhead | 0 | 
| 0xxx, 116 | TreeAdvTrunk | Arrowhead | 0 | 
Enums
 int enum VertexShaderId
 {
	case 0: return "Basic";
	case 1: return "NormalMap";
	case 2: return "NormalMapDiffuse";
	case 3: return "Grass";
	case 8: return "Water";
	case 11: return  "NormalMapThrough";
	case 15: return "NormalMapAS";
	case 14: return "BasicAS";
	case 17: return "Glass";
	case 18: return "NormalMapSpecularThrough";
	case 19: return "NormalMapThroughNoFade";
	case 20: return "NormalMapSpecularThroughNoFade";
	case 23: return "Super";
	case 24: return "Multi";
	case 25: return "Tree";
	case 30: return "CalmWater";
	case 26: return "TreeNoFade";
	case 29: return "Skin";
	case 31: return "TreeAdv";
	case 32: return "TreeAdvTrunk";
 }
Links
Article Author - Sy (Synide) -- Sy 17:16, 11 August 2007 (CEST)
Original ODOLv40 Article detailed by Bxbx (Biki'd by Mikero)
 
	