Beispiel #1
0
        public void Load(System.IO.Stream stream)
        {
            CanSave = false;

            Renderer = new CMB_Renderer();
            DrawableContainer.Drawables.Add(Renderer);


            Skeleton = new STSkeleton();
            //These models/skeletons come out massive so scale them with an overridden scale
            Skeleton.PreviewScale   = Renderer.PreviewScale;
            Skeleton.BonePointScale = 40;
            Renderer.Skeleton       = Skeleton;

            DrawableContainer.Drawables.Add(Skeleton);


            cmb.ReadCMB(stream);

            Text = cmb.Header.Name;

            DrawableContainer.Name = Text;

            //Load textures
            if (cmb.TexturesChunk != null)
            {
                texFolder = new TextureFolder("Texture");
                TreeNode meshFolder     = new TreeNode("Meshes");
                TreeNode materialFolder = new TreeNode("Materials");
                TreeNode skeletonFolder = new TreeNode("Skeleton");

                bool HasTextures = cmb.TexturesChunk.Textures != null &&
                                   cmb.TexturesChunk.Textures.Count != 0;

                bool HasMeshes = cmb.MeshesChunk.SHP.SEPDs != null &&
                                 cmb.MeshesChunk.SHP.SEPDs.Count != 0;

                bool HasSkeleton = cmb.SkeletonChunk != null &&
                                   cmb.SkeletonChunk.Bones.Count != 0;

                bool HasMaterials = cmb.MaterialsChunk != null &&
                                    cmb.MaterialsChunk.Materials.Count != 0;

                if (HasSkeleton)
                {
                    var bonesOrdered = cmb.SkeletonChunk.Bones.OrderBy(x => x.ID).ToList();
                    foreach (var bone in bonesOrdered)
                    {
                        STBone genericBone = new STBone(Skeleton);
                        genericBone.parentIndex = bone.ParentID;
                        genericBone.Checked     = true;

                        genericBone.Text         = $"Bone {bone.ID}";
                        genericBone.RotationType = STBone.BoneRotationType.Euler;

                        genericBone.Position = new OpenTK.Vector3(
                            bone.Translation.X,
                            bone.Translation.Y,
                            bone.Translation.Z
                            );
                        genericBone.EulerRotation = new OpenTK.Vector3(
                            bone.Rotation.X,
                            bone.Rotation.Y,
                            bone.Rotation.Z
                            );
                        genericBone.Scale = new OpenTK.Vector3(
                            bone.Scale.X,
                            bone.Scale.Y,
                            bone.Scale.Z
                            );
                        Skeleton.bones.Add(genericBone);
                    }

                    foreach (var bone in Skeleton.bones)
                    {
                        if (bone.Parent == null)
                        {
                            skeletonFolder.Nodes.Add(bone);
                        }
                    }

                    Skeleton.reset();
                    Skeleton.update();
                }

                if (HasTextures)
                {
                    int texIndex = 0;
                    foreach (var tex in cmb.TexturesChunk.Textures)
                    {
                        var texWrapper = new CTXB.TextureWrapper(new CTXB.Texture());
                        texWrapper.Text             = $"Texture {texIndex++}";
                        texWrapper.ImageKey         = "texture";
                        texWrapper.SelectedImageKey = texWrapper.ImageKey;

                        if (tex.Name != string.Empty)
                        {
                            texWrapper.Text = tex.Name;
                        }

                        texWrapper.Width  = tex.Width;
                        texWrapper.Height = tex.Height;
                        CTXB.Texture.TextureFormat Format = (CTXB.Texture.TextureFormat)((tex.DataType << 16) | tex.ImageFormat);

                        texWrapper.Format    = CTR_3DS.ConvertPICAToGenericFormat(CTXB.Texture.FormatList[Format]);
                        texWrapper.ImageData = tex.ImageData;
                        texFolder.Nodes.Add(texWrapper);

                        Renderer.TextureList.Add(texWrapper);
                    }
                }

                if (HasMaterials)
                {
                    int materialIndex = 0;
                    foreach (var mat in cmb.MaterialsChunk.Materials)
                    {
                        H3DMaterial H3D = ToH3DMaterial(mat);

                        CMBMaterialWrapper material = new CMBMaterialWrapper(mat, this, H3D);
                        material.Text = $"Material {materialIndex++}";
                        materialFolder.Nodes.Add(material);
                        Materials.Add(material);

                        bool HasDiffuse = false;
                        foreach (var tex in mat.TextureMappers)
                        {
                            if (tex.TextureID != -1)
                            {
                                CMBTextureMapWrapper matTexture = new CMBTextureMapWrapper(tex, this);
                                matTexture.TextureIndex = tex.TextureID;
                                material.TextureMaps.Add(matTexture);

                                if (tex.TextureID < Renderer.TextureList.Count && tex.TextureID >= 0)
                                {
                                    matTexture.Name = Renderer.TextureList[tex.TextureID].Text;
                                    material.Nodes.Add(matTexture.Name);
                                }

                                if (!HasDiffuse && matTexture.Name != "bg_syadowmap") //Quick hack till i do texture env stuff
                                {
                                    matTexture.Type = STGenericMatTexture.TextureType.Diffuse;
                                    HasDiffuse      = true;
                                }
                            }
                        }
                    }
                }

                if (HasMeshes)
                {
                    int MeshIndex = 0;
                    foreach (var mesh in cmb.MeshesChunk.MSHS.Meshes)
                    {
                        STGenericMaterial mat = Materials[mesh.MaterialIndex];

                        CmbMeshWrapper genericMesh = new CmbMeshWrapper(mat);
                        genericMesh.Text          = $"Mesh_{MeshIndex++}_ID_{mesh.VisIndex}";
                        genericMesh.MaterialIndex = mesh.MaterialIndex;

                        var shape = cmb.MeshesChunk.SHP.SEPDs[mesh.SEPDIndex];
                        genericMesh.Mesh = shape;

                        List <ushort> SkinnedBoneTable = new List <ushort>();
                        foreach (var prim in shape.PRMS)
                        {
                            if (prim.BoneIndices != null)
                            {
                                SkinnedBoneTable.AddRange(prim.BoneIndices);
                            }
                        }

                        //Now load the vertex and face data

                        foreach (var prm in shape.PRMS)
                        {
                            if (shape.HasPosition)
                            {
                                int VertexCount = prm.VertexCount;
                                for (int v = 0; v < VertexCount; v++)
                                {
                                    Vertex vert = new Vertex();
                                    vert.pos = new OpenTK.Vector3(
                                        prm.Vertices.Position[v].X,
                                        prm.Vertices.Position[v].Y,
                                        prm.Vertices.Position[v].Z);
                                    if (shape.HasNormal)
                                    {
                                        vert.nrm = new OpenTK.Vector3(
                                            prm.Vertices.Normal[v].X,
                                            prm.Vertices.Normal[v].Y,
                                            prm.Vertices.Normal[v].Z).Normalized();
                                    }

                                    if (shape.HasColor)
                                    {
                                        vert.col = new OpenTK.Vector4(
                                            prm.Vertices.Color[v].X,
                                            prm.Vertices.Color[v].Y,
                                            prm.Vertices.Color[v].Z,
                                            prm.Vertices.Color[v].W).Normalized();
                                    }

                                    if (shape.HasUV0)
                                    {
                                        vert.uv0 = new OpenTK.Vector2(prm.Vertices.UV0[v].X, -prm.Vertices.UV0[v].Y + 1);
                                    }

                                    if (shape.HasUV1)
                                    {
                                        vert.uv1 = new OpenTK.Vector2(prm.Vertices.UV1[v].X, -prm.Vertices.UV1[v].Y + 1);
                                    }

                                    if (shape.HasUV2)
                                    {
                                        vert.uv2 = new OpenTK.Vector2(prm.Vertices.UV2[v].X, -prm.Vertices.UV2[v].Y + 1);
                                    }

                                    if (prm.SkinningMode == SkinningMode.Smooth)
                                    {
                                        //Indices
                                        if (shape.HasIndices)
                                        {
                                            if (shape.BoneDimensionCount >= 1)
                                            {
                                                vert.boneIds.Add((int)prm.Vertices.BoneIndices[v].X);
                                            }
                                            if (shape.BoneDimensionCount >= 2)
                                            {
                                                vert.boneIds.Add((int)prm.Vertices.BoneIndices[v].Y);
                                            }
                                            if (shape.BoneDimensionCount >= 3)
                                            {
                                                vert.boneIds.Add((int)prm.Vertices.BoneIndices[v].Z);
                                            }
                                            if (shape.BoneDimensionCount >= 4)
                                            {
                                                vert.boneIds.Add((int)prm.Vertices.BoneIndices[v].W);
                                            }
                                        }

                                        //Weights
                                        if (shape.HasWeights)
                                        {
                                            if (shape.BoneDimensionCount >= 1)
                                            {
                                                vert.boneWeights.Add(prm.Vertices.BoneWeights[v].X);
                                            }
                                            if (shape.BoneDimensionCount >= 2)
                                            {
                                                vert.boneWeights.Add(prm.Vertices.BoneWeights[v].Y);
                                            }
                                            if (shape.BoneDimensionCount >= 3)
                                            {
                                                vert.boneWeights.Add(prm.Vertices.BoneWeights[v].Z);
                                            }
                                            if (shape.BoneDimensionCount >= 4)
                                            {
                                                vert.boneWeights.Add(prm.Vertices.BoneWeights[v].W);
                                            }
                                        }
                                    }

                                    if (prm.SkinningMode == SkinningMode.Rigid)
                                    {
                                        int boneId = (int)prm.Vertices.BoneIndices[v].X;
                                        vert.boneIds.Add(boneId);
                                        vert.boneWeights.Add(1);

                                        vert.pos = OpenTK.Vector3.TransformPosition(vert.pos, Skeleton.bones[boneId].Transform);
                                        if (shape.HasNormal)
                                        {
                                            vert.nrm = OpenTK.Vector3.TransformNormal(vert.nrm, Skeleton.bones[boneId].Transform);
                                        }
                                    }

                                    if (prm.SkinningMode == SkinningMode.Mixed)
                                    {
                                        int boneId = prm.BoneIndices[0];
                                        vert.boneIds.Add(boneId);
                                        vert.boneWeights.Add(1);

                                        vert.pos = OpenTK.Vector3.TransformPosition(vert.pos, Skeleton.bones[boneId].Transform);
                                        if (shape.HasNormal)
                                        {
                                            vert.nrm = OpenTK.Vector3.TransformNormal(vert.nrm, Skeleton.bones[boneId].Transform);
                                        }
                                    }

                                    genericMesh.vertices.Add(vert);
                                }
                            }

                            STGenericPolygonGroup group = new STGenericPolygonGroup();
                            genericMesh.PolygonGroups.Add(group);

                            for (int i = 0; i < prm.FaceIndices.Count; i++)
                            {
                                group.faces.Add((int)prm.FaceIndices[i].X);
                                group.faces.Add((int)prm.FaceIndices[i].Y);
                                group.faces.Add((int)prm.FaceIndices[i].Z);
                            }
                        }

                        Renderer.Meshes.Add(genericMesh);
                        meshFolder.Nodes.Add(genericMesh);
                    }
                }

                if (meshFolder.Nodes.Count > 0)
                {
                    Nodes.Add(meshFolder);
                }

                if (skeletonFolder.Nodes.Count > 0)
                {
                    Nodes.Add(skeletonFolder);
                }

                if (materialFolder.Nodes.Count > 0)
                {
                    Nodes.Add(materialFolder);
                }

                if (texFolder.Nodes.Count > 0)
                {
                    Nodes.Add(texFolder);
                }
            }
        }
Beispiel #2
0
        public void Load(System.IO.Stream stream)
        {
            DrawableContainer.Name = FileName;
            Renderer = new PunchOutWii_Renderer();
            DrawableContainer.Drawables.Add(Renderer);

            Text = FileName;

            HeaderFile = new DictionaryFile();
            HeaderFile.Read(new FileReader(stream), FilePath);

            var HashList = NLG_Common.HashNames;

            string DataFile = $"{FilePath.Replace(".dict", ".data")}";

            if (System.IO.File.Exists(DataFile))
            {
                using (var reader = new FileReader(DataFile, true))
                {
                    reader.SetByteOrder(true);

                    TreeNode blocks      = new TreeNode("Blocks");
                    TreeNode chunks      = new TreeNode("Chunks");
                    TreeNode modelFolder = new TreeNode("Models");

                    foreach (var blockInfo in HeaderFile.Blocks)
                    {
                        ChunkViewer chunkNode = new ChunkViewer("block");
                        if (blockInfo.Size > 0)
                        {
                            blocks.Nodes.Add(chunkNode);
                        }

                        chunkNode.FileData = new SubStream(reader.BaseStream, blockInfo.Offset, blockInfo.Size);
                    }

                    List <PO_Texture> currentTextures = new List <PO_Texture>();

                    List <ModelFileData> modelData = new List <ModelFileData>();

                    ModelFileData currentModel = null;

                    STTextureFolder textureFolder = new STTextureFolder("Textures");
                    Nodes.Add(blocks);
                    Nodes.Add(chunks);

                    Nodes.Add(textureFolder);
                    Nodes.Add(modelFolder);

                    foreach (var chunk in HeaderFile.DataChunks)
                    {
                        if (chunk.BlockIndex == -1)
                        {
                            continue;
                        }

                        ChunkViewer chunkNode = new ChunkViewer(chunk.Type.ToString("") + " " + chunk.Type.ToString("X"));
                        chunks.Nodes.Add(chunkNode);

                        var blockInfo = HeaderFile.Blocks[chunk.BlockIndex];
                        if (blockInfo.Offset + chunk.Offset + chunk.Size > reader.BaseStream.Length)
                        {
                            continue;
                        }

                        chunkNode.FileData = new SubStream(reader.BaseStream, blockInfo.Offset + chunk.Offset, chunk.Size);

                        uint chunkPos = blockInfo.Offset + chunk.Offset;

                        reader.SeekBegin(chunkPos);
                        switch (chunk.Type)
                        {
                        case SectionMagic.MaterialData:
                            currentModel = new ModelFileData();
                            currentModel.MaterialOffset = chunkPos;
                            modelData.Add(currentModel);
                            break;

                        case SectionMagic.TextureHeaders:
                            uint numTextures = chunk.Size / 96;
                            for (int i = 0; i < numTextures; i++)
                            {
                                var tex = new PO_Texture();
                                tex.ImageKey         = "texture";
                                tex.SelectedImageKey = "texture";

                                tex.Read(reader);
                                tex.Text = tex.HashID.ToString("X");
                                if (HashList.ContainsKey(tex.HashID))
                                {
                                    tex.Text = HashList[tex.HashID];
                                }

                                currentTextures.Add(tex);
                                Renderer.TextureList.Add(tex.Text, tex);

                                textureFolder.Nodes.Add(tex);
                            }
                            break;

                        case SectionMagic.TextureData:
                            for (int i = 0; i < currentTextures.Count; i++)
                            {
                                reader.SeekBegin(chunkPos + currentTextures[i].DataOffset);
                                currentTextures[i].ImageData = reader.ReadBytes((int)currentTextures[i].ImageSize);
                            }
                            break;

                        case SectionMagic.IndexData:
                            currentModel.indexBufferOffset = chunkPos;
                            break;

                        case SectionMagic.VertexData:
                            currentModel.vertexBufferOffset = chunkPos;
                            break;

                        case SectionMagic.MeshData:
                            uint numMeshes = chunk.Size / 52;
                            for (int i = 0; i < numMeshes; i++)
                            {
                                reader.SeekBegin(chunkPos + (i * 52));
                                PO_Mesh mesh = new PO_Mesh(reader);
                                currentModel.meshes.Add(mesh);
                            }
                            break;

                        case SectionMagic.VertexAttributePointerData:
                            uint numAttributes = chunk.Size / 8;
                            for (int i = 0; i < numAttributes; i++)
                            {
                                PO_VertexAttribute att = new PO_VertexAttribute();
                                att.Offset = reader.ReadUInt32();
                                att.Type   = reader.ReadByte();
                                att.Stride = reader.ReadByte();
                                reader.ReadUInt16();
                                currentModel.attributes.Add(att);
                            }
                            break;

                        case SectionMagic.ModelData:
                            uint numModels = chunk.Size / 12;
                            Console.WriteLine($"numModels {numModels}");
                            for (int i = 0; i < numModels; i++)
                            {
                                PO_Model mdl = new PO_Model();
                                mdl.ParentDictionary = this;
                                mdl.HashID           = reader.ReadUInt32();
                                mdl.NumMeshes        = reader.ReadUInt32();
                                reader.ReadUInt32();     //0
                                currentModel.models.Add(mdl);
                            }
                            break;

                        case SectionMagic.BoneData:
                            STSkeleton Skeleton = new STSkeleton();
                            DrawableContainer.Drawables.Add(Skeleton);

                            uint numBones = chunk.Size / 68;
                            for (int i = 0; i < numBones; i++)
                            {
                                reader.SeekBegin(chunkPos + (i * 68));

                                uint HashID = reader.ReadUInt32();
                                reader.ReadUInt32();     //unk
                                reader.ReadUInt32();     //unk
                                reader.ReadUInt32();     //unk
                                reader.ReadSingle();     //0

                                STBone bone  = new STBone(Skeleton);
                                var    Scale = new OpenTK.Vector3(
                                    reader.ReadSingle(),
                                    reader.ReadSingle(),
                                    reader.ReadSingle());
                                reader.ReadSingle();     //0
                                bone.EulerRotation = new OpenTK.Vector3(
                                    reader.ReadSingle(),
                                    reader.ReadSingle(),
                                    reader.ReadSingle());
                                reader.ReadSingle();     //0
                                bone.Position = new OpenTK.Vector3(
                                    reader.ReadSingle(),
                                    reader.ReadSingle(),
                                    reader.ReadSingle());
                                reader.ReadSingle();     //1

                                bone.Text = HashID.ToString("X");
                                if (NLG_Common.HashNames.ContainsKey(HashID))
                                {
                                    bone.Text = NLG_Common.HashNames[HashID];
                                }
                                else
                                {
                                    Console.WriteLine($"bone hash {HashID}");
                                }

                                bone.Scale = new Vector3(0.2f, 0.2f, 0.2f);

                                bone.RotationType = STBone.BoneRotationType.Euler;
                                Skeleton.bones.Add(bone);
                            }

                            Skeleton.reset();
                            Skeleton.update();
                            break;
                        }
                    }

                    foreach (var modelFile in modelData)
                    {
                        int pointerIndex = 0;
                        foreach (var model in modelFile.models)
                        {
                            model.Text = model.HashID.ToString("X");
                            if (HashList.ContainsKey(model.HashID))
                            {
                                model.Text = HashList[model.HashID];
                            }

                            modelFolder.Nodes.Add(model);
                            for (int i = 0; i < model.NumMeshes; i++)
                            {
                                var mesh = modelFile.meshes[i];

                                RenderableMeshWrapper genericMesh = new RenderableMeshWrapper();
                                model.Nodes.Add(genericMesh);
                                model.RenderedMeshes.Add(genericMesh);
                                Renderer.Meshes.Add(genericMesh);

                                genericMesh.Text = mesh.HashID.ToString("X");
                                if (HashList.ContainsKey(mesh.HashID))
                                {
                                    genericMesh.Text = HashList[mesh.HashID];
                                }

                                string material = mesh.MaterialHashID.ToString("X");
                                if (HashList.ContainsKey(mesh.MaterialHashID))
                                {
                                    material = HashList[mesh.MaterialHashID];
                                }
                                genericMesh.Nodes.Add(material);


                                genericMesh.Material = new STGenericMaterial();

                                reader.SeekBegin(modelFile.MaterialOffset + mesh.MaterialOffset);
                                switch (mesh.MaterailPreset)
                                {
                                case MaterailPresets.EnvDiffuseDamage:
                                {
                                    uint diffuseMapHashID    = reader.ReadUInt32();
                                    uint diffuseMapParam     = reader.ReadUInt32();
                                    uint envSpecMapHashID    = reader.ReadUInt32();
                                    uint envSpecMapParam     = reader.ReadUInt32();
                                    uint specMapHashID       = reader.ReadUInt32();
                                    uint specMapParam        = reader.ReadUInt32();
                                    uint megaStrikeMapHashID = reader.ReadUInt32();
                                    uint megaStrikeMapParam  = reader.ReadUInt32();
                                    uint dirtMapHashID       = reader.ReadUInt32();
                                    uint dirtMapParam        = reader.ReadUInt32();
                                    uint iceMapHashID        = reader.ReadUInt32();
                                    uint iceMapParam         = reader.ReadUInt32();

                                    string diffuseName = diffuseMapHashID.ToString("X");
                                    if (HashList.ContainsKey(diffuseMapHashID))
                                    {
                                        diffuseName = HashList[diffuseMapHashID];
                                    }

                                    var texUnit = 1;
                                    genericMesh.Material.TextureMaps.Add(new STGenericMatTexture()
                                        {
                                            textureUnit = texUnit++,
                                            Type        = STGenericMatTexture.TextureType.Diffuse,
                                            Name        = diffuseName,
                                        });
                                }
                                break;

                                default:
                                {
                                    uint   diffuseMapHashID = reader.ReadUInt32();
                                    string diffuseName      = diffuseMapHashID.ToString("X");
                                    if (HashList.ContainsKey(diffuseMapHashID))
                                    {
                                        diffuseName = HashList[diffuseMapHashID];
                                    }

                                    Console.WriteLine($"diffuseName {diffuseName}");

                                    var texUnit = 1;
                                    genericMesh.Material.TextureMaps.Add(new STGenericMatTexture()
                                        {
                                            textureUnit = texUnit++,
                                            Type        = STGenericMatTexture.TextureType.Diffuse,
                                            Name        = diffuseName,
                                        });
                                }
                                break;
                                }

                                Console.WriteLine($"mesh {i}");

                                STGenericPolygonGroup polyGroup = new STGenericPolygonGroup();
                                genericMesh.PolygonGroups.Add(polyGroup);
                                reader.SeekBegin(modelFile.indexBufferOffset + mesh.IndexStartOffset);

                                List <int> faces = new List <int>();
                                for (int f = 0; f < mesh.IndexCount; f++)
                                {
                                    if (mesh.IndexFormat == 0)
                                    {
                                        polyGroup.faces.Add(reader.ReadUInt16());
                                    }
                                    else
                                    {
                                        polyGroup.faces.Add(reader.ReadByte());
                                    }
                                }

                                if (mesh.FaceType == PO_Mesh.PolygonType.TriangleStrips)
                                {
                                    polyGroup.PrimativeType = STPrimitiveType.TrangleStrips;
                                }
                                else
                                {
                                    polyGroup.PrimativeType = STPrimitiveType.Triangles;
                                }


                                for (int a = 0; a < mesh.NumAttributePointers; a++)
                                {
                                    Console.WriteLine($"pointer {genericMesh.Text} { modelFile.vertexBufferOffset + modelFile.attributes[pointerIndex + a].Offset}");
                                }

                                for (int v = 0; v < mesh.VertexCount; v++)
                                {
                                    Vertex vert = new Vertex();
                                    genericMesh.vertices.Add(vert);

                                    int attributeIndex = 0;
                                    for (int a = 0; a < mesh.NumAttributePointers; a++)
                                    {
                                        var pointer = modelFile.attributes[pointerIndex + a];
                                        reader.SeekBegin(modelFile.vertexBufferOffset + pointer.Offset + (pointer.Stride * v));

                                        if (attributeIndex == 0)
                                        {
                                            if (pointer.Stride == 12)
                                            {
                                                vert.pos = new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle());
                                            }
                                        }
                                        if (attributeIndex == 1)
                                        {
                                            if (pointer.Stride == 12)
                                            {
                                                vert.nrm = new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle());
                                            }
                                        }
                                        if (attributeIndex == 2)
                                        {
                                            if (pointer.Stride == 4)
                                            {
                                                vert.uv0 = new Vector2(reader.ReadUInt16() / 1024f, reader.ReadUInt16() / 1024f);
                                            }
                                        }

                                        /*     if (pointer.Type == 0xD4)
                                         *   {
                                         *       vert.boneIds = new List<int>() { reader.ReadByte(), reader.ReadByte(), reader.ReadByte(), reader.ReadByte() };
                                         *   }
                                         *   if (pointer.Type == 0xB0)
                                         *   {
                                         *       vert.boneWeights = new List<float>() { reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle() };
                                         *   }*/

                                        attributeIndex++;
                                    }
                                }

                                genericMesh.TransformPosition(new Vector3(0), new Vector3(-90, 0, 0), new Vector3(1));

                                pointerIndex += mesh.NumAttributePointers;
                            }
                        }
                    }
                }
            }
        }