public AssemblerData Assemble(UserAvatar avatar)
        {
            Contract.Requires(avatar != null);

            UserInfo userInfo = avatar.UserInfo;
            string   userName = FileUtility.MakeNameWindowsSafe(userInfo.Username);

            string appData = Environment.GetEnvironmentVariable("LocalAppData");
            string rbx2Src = Path.Combine(appData, "Rbx2Source");
            string avatars = Path.Combine(rbx2Src, "Avatars");
            string userBin = Path.Combine(avatars, userName);

            string modelDir     = Path.Combine(userBin, "Model");
            string anim8Dir     = Path.Combine(modelDir, "Animations");
            string texturesDir  = Path.Combine(userBin, "Textures");
            string materialsDir = Path.Combine(userBin, "Materials");

            FileUtility.InitiateEmptyDirectories(modelDir, anim8Dir, texturesDir, materialsDir);

            AvatarType          avatarType = avatar.PlayerAvatarType;
            ICharacterAssembler assembler;

            if (avatarType == AvatarType.R15)
            {
                assembler = new R15CharacterAssembler();
            }
            else
            {
                assembler = new R6CharacterAssembler();
            }

            string compileDir = "roblox_avatars/" + userName;

            string avatarTypeName  = Rbx2Source.GetEnumName(avatarType);
            Folder characterAssets = AppendCharacterAssets(avatar, avatarTypeName);

            Rbx2Source.ScheduleTasks
            (
                "BuildCharacter",
                "BuildCollisionModel",
                "BuildAnimations",
                "BuildTextures",
                "BuildMaterials",
                "BuildCompilerScript"
            );

            Rbx2Source.PrintHeader("BUILDING CHARACTER MODEL");
            #region Build Character Model
            ///////////////////////////////////////////////////////////////////////////////////////////////////////

            StudioMdlWriter writer = assembler.AssembleModel(characterAssets, avatar.Scales, DEBUG_RAPID_ASSEMBLY);

            string studioMdl = writer.BuildFile();
            string modelPath = Path.Combine(modelDir, "CharacterModel.smd");
            FileUtility.WriteFile(modelPath, studioMdl);

            string staticPose = writer.BuildFile(false);
            string refPath    = Path.Combine(modelDir, "ReferencePos.smd");
            FileUtility.WriteFile(refPath, staticPose);

            Rbx2Source.MarkTaskCompleted("BuildCharacter");

            ///////////////////////////////////////////////////////////////////////////////////////////////////////
            #endregion

            Rbx2Source.PrintHeader("BUILDING COLLISION MODEL");
            #region Build Character Collisions
            ///////////////////////////////////////////////////////////////////////////////////////////////////////

            Folder          collisionAssets = AppendCollisionAssets(avatar, avatarTypeName);
            StudioMdlWriter collisionWriter = assembler.AssembleModel(collisionAssets, avatar.Scales, true);

            string collisionModel = collisionWriter.BuildFile();
            string cmodelPath     = Path.Combine(modelDir, "CollisionModel.smd");
            FileUtility.WriteFile(cmodelPath, collisionModel);

            byte[] collisionJoints = assembler.CollisionModelScript;
            string cjointsPath     = Path.Combine(modelDir, "CollisionJoints.qc");

            FileUtility.WriteFile(cjointsPath, collisionJoints);
            Rbx2Source.MarkTaskCompleted("BuildCollisionModel");

            ///////////////////////////////////////////////////////////////////////////////////////////////////////
            #endregion

            Rbx2Source.PrintHeader("BUILDING CHARACTER ANIMATIONS");
            #region Build Character Animations
            ///////////////////////////////////////////////////////////////////////////////////////////////////////

            var animIds      = assembler.CollectAnimationIds(avatar);
            var compileAnims = new Dictionary <string, Asset>();

            if (animIds.Count > 0)
            {
                Rbx2Source.Print("Collecting Animations...");
                Rbx2Source.IncrementStack();

                Action <string, Asset> collectAnimation = (animName, animAsset) =>
                {
                    if (!compileAnims.ContainsKey(animName))
                    {
                        Rbx2Source.Print("Collected animation {0} with id {1}", animName, animAsset.Id);
                        compileAnims.Add(animName, animAsset);
                    }
                };

                foreach (string animName in animIds.Keys)
                {
                    var animId    = animIds[animName];
                    var animAsset = animId.GetAsset();
                    var import    = animAsset.OpenAsModel();

                    if (animId.AnimationType == AnimationType.R15AnimFolder)
                    {
                        Folder r15Anim = import.FindFirstChild <Folder>("R15Anim");

                        if (r15Anim != null)
                        {
                            foreach (Instance animDef in r15Anim.GetChildren())
                            {
                                if (animDef.Name == "idle")
                                {
                                    var anims = animDef.GetChildrenOfType <Animation>();

                                    if (anims.Length == 2)
                                    {
                                        var getLookAnim = anims.OrderBy((anim) =>
                                        {
                                            var weight = anim.FindFirstChild <NumberValue>("Weight");

                                            if (weight != null)
                                            {
                                                return(weight.Value);
                                            }

                                            return(0.0);
                                        });

                                        var lookAnim = getLookAnim.First();
                                        lookAnim.Destroy();

                                        Asset lookAsset = Asset.GetByAssetId(lookAnim.AnimationId);
                                        collectAnimation("Idle2", lookAsset);
                                    }
                                }

                                Animation compileAnim = animDef.FindFirstChildOfClass <Animation>();

                                if (compileAnim != null)
                                {
                                    Asset  compileAsset = Asset.GetByAssetId(compileAnim.AnimationId);
                                    string compileName  = animName;

                                    if (animDef.Name == "pose")
                                    {
                                        compileName = "Pose";
                                    }

                                    collectAnimation(compileName, compileAsset);
                                }
                            }
                        }
                    }
                    else
                    {
                        collectAnimation(animName, animAsset);
                    }
                }

                Rbx2Source.DecrementStack();
            }
            else
            {
                Rbx2Source.Print("No animations found :(");
            }

            if (compileAnims.Count > 0)
            {
                Rbx2Source.Print("Assembling Animations...");
                Rbx2Source.IncrementStack();

                foreach (string animName in compileAnims.Keys)
                {
                    Rbx2Source.Print("Building Animation {0}...", animName);

                    Asset animAsset = compileAnims[animName];
                    var   import    = animAsset.OpenAsModel();

                    var sequence = import.FindFirstChildOfClass <KeyframeSequence>();
                    sequence.Name = animName;

                    var avatarTypeRef = new StringValue()
                    {
                        Value  = $"{avatarType}",
                        Name   = "AvatarType",
                        Parent = sequence
                    };

                    string animation = AnimationBuilder.Assemble(sequence, writer.Skeleton[0].Bones);
                    string animPath  = Path.Combine(anim8Dir, animName + ".smd");

                    FileUtility.WriteFile(animPath, animation);
                }

                Rbx2Source.DecrementStack();
            }

            Rbx2Source.MarkTaskCompleted("BuildAnimations");

            ///////////////////////////////////////////////////////////////////////////////////////////////////////
            #endregion

            Rbx2Source.PrintHeader("BUILDING CHARACTER TEXTURES");
            #region Build Character Textures
            ///////////////////////////////////////////////////////////////////////////////////////////////////////

            var             materials = writer.Materials;
            TextureBindings textures;

            if (DEBUG_RAPID_ASSEMBLY)
            {
                textures = new TextureBindings();
                materials.Clear();
            }
            else
            {
                TextureCompositor texCompositor = assembler.ComposeTextureMap(characterAssets, avatar.BodyColors);
                textures = assembler.BindTextures(texCompositor, materials);
            }

            var images = textures.Images;
            textures.MaterialDirectory = compileDir;

            foreach (string imageName in images.Keys)
            {
                Rbx2Source.Print("Writing Image {0}.png", imageName);

                Image  image     = images[imageName];
                string imagePath = Path.Combine(texturesDir, imageName + ".png");

                try
                {
                    image.Save(imagePath, ImageFormat.Png);
                }
                catch
                {
                    Rbx2Source.Print("IMAGE {0}.png FAILED TO SAVE!", imageName);
                }

                FileUtility.LockFile(imagePath);
            }

            CompositData.FreeAllocatedTextures();
            Rbx2Source.MarkTaskCompleted("BuildTextures");

            ///////////////////////////////////////////////////////////////////////////////////////////////////////
            #endregion

            Rbx2Source.PrintHeader("WRITING MATERIAL FILES");
            #region Write Material Files
            ///////////////////////////////////////////////////////////////////////////////////////////////////////

            var matLinks = textures.MatLinks;

            foreach (string mtlName in matLinks.Keys)
            {
                Rbx2Source.Print("Building VMT {0}.vmt", mtlName);

                string targetVtf = matLinks[mtlName];
                string vmtPath   = Path.Combine(materialsDir, mtlName + ".vmt");

                ValveMaterial mtl = materials[mtlName];
                mtl.SetVmtField("basetexture", "models/" + compileDir + "/" + targetVtf);
                mtl.WriteVmtFile(vmtPath);
            }

            Rbx2Source.MarkTaskCompleted("BuildMaterials");

            ///////////////////////////////////////////////////////////////////////////////////////////////////////
            #endregion

            Rbx2Source.PrintHeader("WRITING COMPILER SCRIPT");
            #region Write Compiler Script
            ///////////////////////////////////////////////////////////////////////////////////////////////////////

            string       modelName = compileDir + ".mdl";
            QuakeCWriter qc        = new QuakeCWriter();

            qc.Add("body", userName, "CharacterModel.smd");
            qc.Add("modelname", modelName);
            qc.Add("upaxis", "y");

            // Compute the floor level of the avatar.
            Folder assembly = characterAssets.FindFirstChild <Folder>("ASSEMBLY");

            if (assembly != null)
            {
                float  floor  = ComputeFloorLevel(assembly);
                string origin = "0 " + floor.ToInvariantString() + " 0";
                qc.Add("origin", origin);
            }

            qc.Add("cdmaterials", "models/" + compileDir);
            qc.Add("surfaceprop", "flesh");
            qc.Add("include", "CollisionJoints.qc");

            QuakeCItem refAnim = qc.Add("sequence", "reference", "ReferencePos.smd");
            refAnim.AddSubItem("fps", 1);
            refAnim.AddSubItem("loop");

            foreach (string animName in compileAnims.Keys)
            {
                QuakeCItem sequence = qc.Add("sequence", animName.ToLowerInvariant(), "Animations/" + animName + ".smd");
                sequence.AddSubItem("fps", AnimationBuilder.FrameRate);

                if (avatarType == AvatarType.R6)
                {
                    sequence.AddSubItem("delta");
                }

                sequence.AddSubItem("loop");
            }

            string qcFile = qc.ToString();
            string qcPath = Path.Combine(modelDir, "Compile.qc");

            FileUtility.WriteFile(qcPath, qcFile);
            Rbx2Source.MarkTaskCompleted("BuildCompilerScript");

            ///////////////////////////////////////////////////////////////////////////////////////////////////////
            #endregion

            AssemblerData data = new AssemblerData()
            {
                ModelData      = writer,
                ModelName      = modelName,
                TextureData    = textures,
                CompilerScript = qcPath,

                RootDirectory     = userBin,
                CompileDirectory  = compileDir,
                TextureDirectory  = texturesDir,
                MaterialDirectory = materialsDir,
            };

            return(data);
        }
        public static string Assemble(KeyframeSequence sequence, List <Bone> rig)
        {
            StudioMdlWriter animWriter = new StudioMdlWriter();
            List <Keyframe> keyframes  = new List <Keyframe>();

            Dictionary <string, Bone> boneLookup = new Dictionary <string, Bone>();
            List <Node> nodes = animWriter.Nodes;

            foreach (Bone bone in rig)
            {
                Node node = bone.Node;
                if (node != null)
                {
                    nodes.Add(node);
                    string boneName = node.Name;
                    if (!boneLookup.ContainsKey(boneName))
                    {
                        boneLookup.Add(boneName, bone);
                    }
                }
            }

            foreach (Keyframe kf in sequence.GetChildrenOfClass <Keyframe>())
            {
                Pose rootPart = kf.FindFirstChild <Pose>("HumanoidRootPart");

                if (rootPart != null)
                {
                    // We don't need the rootpart for this.
                    foreach (Pose subPose in rootPart.GetChildrenOfClass <Pose>())
                    {
                        subPose.Parent = kf;
                    }

                    rootPart.Destroy();
                }

                kf.Time /= sequence.TimeScale;
                keyframes.Add(kf);
            }

            keyframes.Sort(0, keyframes.Count, sorter);

            Keyframe lastKeyframe = keyframes[keyframes.Count - 1];
            float    fLength      = lastKeyframe.Time;
            int      frameCount   = ToFrameRate(fLength);

            // Animations in source are kinda dumb, because theres no frame interpolation.
            // I have to account for every single CFrame for every single frame.

            Dictionary <int, Dictionary <string, Pose> > keyframeMap = new Dictionary <int, Dictionary <string, Pose> >();

            for (int i = 0; i <= frameCount; i++)
            {
                keyframeMap[i] = new Dictionary <string, Pose>();
            }

            foreach (Keyframe kf in keyframes)
            {
                int frame = ToFrameRate(kf.Time);
                Dictionary <string, Pose> poseMap = keyframeMap[frame];
                List <Pose> poses = GatherPoses(kf);
                foreach (Pose pose in poses)
                {
                    poseMap[pose.Name] = pose;
                }
            }

            List <BoneKeyframe> boneKeyframes = animWriter.Skeleton;

            Keyframe baseFrame = keyframes[0];

            for (int i = 0; i < frameCount; i++)
            {
                BoneKeyframe frame = new BoneKeyframe();
                frame.Time = i;
                if (sequence.AvatarType == AvatarType.R15)
                {
                    frame.DeltaSequence = true;
                    frame.BaseRig       = rig;
                }
                List <Bone> bones = frame.Bones;
                foreach (Node node in nodes)
                {
                    PosePair closestPoses = GetClosestPoses(keyframeMap, i, node.Name);
                    float    current      = i;
                    float    min          = closestPoses.Min.Frame;
                    float    max          = closestPoses.Max.Frame;
                    float    alpha        = (min == max ? 0 : (current - min) / (max - min));
                    Pose     pose0        = closestPoses.Min.Pose;
                    Pose     pose1        = closestPoses.Max.Pose;
                    float    weight       = EasingUtil.GetEasing(pose1.PoseEasingStyle, pose1.PoseEasingDirection, 1 - alpha);

                    CFrame lastCFrame = pose0.CFrame;
                    CFrame nextCFrame = pose1.CFrame;

                    Bone   baseBone = boneLookup[node.Name];
                    CFrame c0       = baseBone.C0;
                    CFrame c1       = baseBone.C1;

                    CFrame interp = lastCFrame.lerp(nextCFrame, weight);
                    // some ugly manual fixes.
                    // todo: make this unnecessary :(
                    if (sequence.AvatarType == AvatarType.R6)
                    {
                        Vector3 pos = interp.p;
                        CFrame  rot = interp - pos;
                        if (node.Name == "Torso")
                        {
                            float[] ang = interp.toEulerAnglesXYZ();
                            rot = CFrame.Angles(ang[0], ang[2], ang[1]);
                            pos = new Vector3(pos.x, pos.z, pos.y);
                        }
                        else if (node.Name.StartsWith("Right"))
                        {
                            pos *= new Vector3(-1, 1, 1);
                        }

                        if (node.Name.Contains("Arm") || node.Name.Contains("Leg"))
                        {
                            pos = new Vector3(pos.z, pos.y, pos.x);
                        }

                        if (sequence.Name == "Climb" && node.Name.Contains("Leg")) // https://www.youtube.com/watch?v=vfJ7DqyDl9w
                        {
                            pos += new Vector3(-.1f, 0, 0);
                        }

                        interp = new CFrame(pos) * rot;
                    }
                    else if (sequence.AvatarType == AvatarType.R15)
                    {
                        if (node.Name.Contains("UpperArm"))
                        {
                            Vector3 pos = interp.p;
                            CFrame  rot = interp - pos;
                            float[] ang = rot.toEulerAnglesXYZ();
                            if (sequence.Name == "Climb" || sequence.Name == "Swim")
                            {
                                rot = CFrame.Angles(ang[0], -ang[2], -ang[1]);
                            }

                            interp = new CFrame(pos) * rot;
                        }
                    }

                    Bone bone = new Bone(node.Name, i, interp);
                    bone.Node = node;
                    bones.Add(bone);
                }
                boneKeyframes.Add(frame);
            }

            return(animWriter.BuildFile());
        }
Esempio n. 3
0
        public static string Assemble(KeyframeSequence sequence, List <Bone> rig)
        {
            StudioMdlWriter animWriter = new StudioMdlWriter();
            List <Keyframe> keyframes  = new List <Keyframe>();

            var boneLookup = new Dictionary <string, Bone>();
            var nodes      = animWriter.Nodes;

            foreach (Bone bone in rig)
            {
                Node node = bone.Node;

                if (node != null)
                {
                    string boneName = node.Name;

                    if (!boneLookup.ContainsKey(boneName))
                    {
                        boneLookup.Add(boneName, bone);
                    }

                    nodes.Add(node);
                }
            }

            foreach (Keyframe kf in sequence.GetChildrenOfClass <Keyframe>())
            {
                Pose rootPart = kf.FindFirstChild <Pose>("HumanoidRootPart");

                if (rootPart != null)
                {
                    // We don't need the rootpart for this.
                    foreach (Pose subPose in rootPart.GetChildrenOfClass <Pose>())
                    {
                        subPose.Parent = kf;
                    }

                    rootPart.Destroy();
                }

                kf.Time /= sequence.TimeScale;
                keyframes.Add(kf);
            }

            keyframes.Sort(0, keyframes.Count, sorter);

            Keyframe lastKeyframe = keyframes[keyframes.Count - 1];

            float fLength    = lastKeyframe.Time;
            int   frameCount = ToFrameRate(fLength);

            // As far as I can tell, models in Source require you to store poses for every
            // single frame, so I need to fill in the gaps with interpolated pose CFrames.

            var keyframeMap = new Dictionary <int, Dictionary <string, Pose> >();

            foreach (Keyframe kf in keyframes)
            {
                int frame = ToFrameRate(kf.Time);
                var poses = GatherPoses(kf);

                var poseMap = poses.ToDictionary(pose => pose.Name);
                keyframeMap[frame] = poseMap;
            }

            // Make sure there are no holes in the data.
            for (int i = 0; i < frameCount; i++)
            {
                if (!keyframeMap.ContainsKey(i))
                {
                    var emptyState = new Dictionary <string, Pose>();
                    keyframeMap.Add(i, emptyState);
                }
            }

            List <BoneKeyframe> boneKeyframes = animWriter.Skeleton;

            for (int i = 0; i < frameCount; i++)
            {
                BoneKeyframe frame = new BoneKeyframe(i);
                List <Bone>  bones = frame.Bones;

                if (sequence.AvatarType == AvatarType.R15)
                {
                    frame.BaseRig       = rig;
                    frame.DeltaSequence = true;
                }

                foreach (Node node in nodes)
                {
                    PosePair closestPoses = GetClosestPoses(keyframeMap, i, node.Name);

                    float min = closestPoses.Min.Frame;
                    float max = closestPoses.Max.Frame;

                    float alpha = (min == max ? 0 : (i - min) / (max - min));

                    Pose pose0 = closestPoses.Min.Pose;
                    Pose pose1 = closestPoses.Max.Pose;

                    CFrame lastCFrame = pose0.CFrame;
                    CFrame nextCFrame = pose1.CFrame;

                    Bone   baseBone = boneLookup[node.Name];
                    CFrame interp   = lastCFrame.Lerp(nextCFrame, alpha);

                    // Make some patches to the interpolation offsets. Unfortunately I can't
                    // identify any single fix that I can apply to each joint, so I have to get crafty.
                    // At some point in the future, I want to find a more practical solution for this problem,
                    // but it is extremely difficult to isolate if any single solution exists.

                    if (sequence.AvatarType == AvatarType.R6)
                    {
                        Vector3 pos = interp.Position;
                        CFrame  rot = interp - pos;

                        if (node.Name == "Torso")
                        {
                            // Flip the YZ axis of the Torso.
                            float[] ang = interp.ToEulerAnglesXYZ();
                            rot = CFrame.Angles(ang[0], ang[2], ang[1]);
                            pos = new Vector3(pos.X, pos.Z, pos.Y);
                        }
                        else if (node.Name.StartsWith("Right"))
                        {
                            // X-axis is inverted for the right arm/leg.
                            pos *= new Vector3(-1, 1, 1);
                        }

                        if (node.Name.EndsWith("Arm") || node.Name.EndsWith("Leg"))
                        {
                            // Rotate position offset of the arms & legs 90* counter-clockwise.
                            pos = new Vector3(-pos.Z, pos.Y, pos.X);
                        }

                        if (node.Name != "Head")
                        {
                            rot = rot.Inverse();
                        }

                        interp = new CFrame(pos) * rot;
                    }
                    else if (sequence.AvatarType == AvatarType.R15)
                    {
                        float[] ang = interp.ToEulerAnglesXYZ();

                        // Cancel out the rotations
                        interp *= CFrame.Angles(-ang[0], -ang[1], -ang[2]);

                        // Patch the Y-axis
                        PatchAngles(ref interp, 1, ang);

                        // Patch the Z-axis
                        PatchAngles(ref interp, 2, ang);

                        // Patch the X-axis
                        PatchAngles(ref interp, 0, ang);
                    }

                    Bone bone = new Bone(node, interp);
                    bones.Add(bone);
                }

                boneKeyframes.Add(frame);
            }

            return(animWriter.BuildFile());
        }
Esempio n. 4
0
        public AssemblerData Assemble(long assetId)
        {
            Asset  asset     = Asset.Get(assetId);
            string assetName = asset.ProductInfo.WindowsSafeName.Trim();

            string appData    = Environment.GetEnvironmentVariable("LocalAppData");
            string rbx2Source = Path.Combine(appData, "Rbx2Source");
            string items      = Path.Combine(rbx2Source, "Items");
            string rootDir    = Path.Combine(items, assetName);

            string modelDir     = Path.Combine(rootDir, "Model");
            string texturesDir  = Path.Combine(rootDir, "Textures");
            string materialsDir = Path.Combine(rootDir, "Materials");

            FileUtility.InitiateEmptyDirectories(modelDir, texturesDir, materialsDir);
            Rbx2Source.ScheduleTasks("BuildModel", "BuildTextures", "BuildMaterials", "BuildCompilerScript");

            Rbx2Source.PrintHeader("BUILDING MODEL");
            #region Build Model
            ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////

            StudioMdlWriter writer = AssembleModel(asset);

            string studioMdl = writer.BuildFile();
            string modelPath = Path.Combine(modelDir, "Asset.smd");
            FileUtility.WriteFile(modelPath, studioMdl);

            string reference = writer.BuildFile(false);
            string refPath   = Path.Combine(modelDir, "Reference.smd");
            FileUtility.WriteFile(refPath, reference);

            Rbx2Source.MarkTaskCompleted("BuildModel");

            ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
            #endregion

            Rbx2Source.PrintHeader("BUILDING TEXTURES");
            #region Build Textures
            ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////

            var materials = writer.Materials;
            var textures  = BindTextures(materials);

            var images     = textures.Images;
            var compileDir = "roblox_assets/" + assetName;

            foreach (string imageName in images.Keys)
            {
                Rbx2Source.Print("Writing Image {0}", imageName);

                Image  image     = images[imageName];
                string imagePath = Path.Combine(texturesDir, imageName + ".png");

                try
                {
                    image.Save(imagePath, ImageFormat.Png);
                }
                catch
                {
                    Rbx2Source.Print("IMAGE {0}.png FAILED TO SAVE!", imageName);
                }

                FileUtility.LockFile(imagePath);
            }

            textures.MaterialDirectory = compileDir;
            Rbx2Source.MarkTaskCompleted("BuildTextures");

            ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
            #endregion

            Rbx2Source.PrintHeader("WRITING MATERIAL FILES");
            #region Write Materials
            ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////

            string mtlDir   = "models/" + compileDir;
            var    matLinks = textures.MatLinks;

            foreach (string matName in matLinks.Keys)
            {
                string vtfTarget = matLinks[matName];
                string vmtPath   = Path.Combine(materialsDir, matName + ".vmt");

                ValveMaterial mat = materials[matName];
                mat.SetVmtField("basetexture", mtlDir + '/' + vtfTarget);
                mat.WriteVmtFile(vmtPath);
            }

            Rbx2Source.MarkTaskCompleted("BuildMaterials");

            ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
            #endregion

            Rbx2Source.PrintHeader("WRITING COMPILER SCRIPT");
            #region Write Compiler Script
            ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////

            string       modelName = compileDir + ".mdl";
            QuakeCWriter qc        = new QuakeCWriter();

            qc.Add("body", assetName, "Asset.smd");
            qc.Add("modelname", modelName);
            qc.Add("upaxis", "y");
            qc.Add("cdmaterials", mtlDir);

            QuakeCItem phys = qc.Add("collisionjoints", "Asset.smd");
            phys.AddSubItem("$mass", 115.0);
            phys.AddSubItem("$inertia", 2.00);
            phys.AddSubItem("$damping", 0.01);
            phys.AddSubItem("$rotdamping", 0.40);

            QuakeCItem refAnim = qc.Add("sequence", "reference", "Reference.smd");
            refAnim.AddSubItem("fps", 1);
            refAnim.AddSubItem("loop");

            string qcFile = qc.ToString();
            string qcPath = Path.Combine(modelDir, "Compile.qc");

            FileUtility.WriteFile(qcPath, qcFile);
            Rbx2Source.MarkTaskCompleted("BuildCompilerScript");

            ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
            #endregion

            AssemblerData data = new AssemblerData()
            {
                ModelData      = writer,
                ModelName      = modelName,
                TextureData    = textures,
                CompilerScript = qcPath,

                RootDirectory     = rootDir,
                CompileDirectory  = compileDir,
                TextureDirectory  = texturesDir,
                MaterialDirectory = materialsDir
            };

            return(data);
        }
        public AssemblerData Assemble(object metadata)
        {
            UserAvatar avatar = metadata as UserAvatar;

            if (avatar == null)
            {
                throw new Exception("bad cast");
            }

            UserInfo userInfo = avatar.UserInfo;
            string   userName = FileUtility.MakeNameWindowsSafe(userInfo.Username);

            string appData    = Environment.GetEnvironmentVariable("AppData");
            string rbx2Source = Path.Combine(appData, "Rbx2Source");
            string avatars    = Path.Combine(rbx2Source, "Avatars");
            string userBin    = Path.Combine(avatars, userName);

            string modelDir     = Path.Combine(userBin, "Model");
            string animDir      = Path.Combine(modelDir, "Animations");
            string texturesDir  = Path.Combine(userBin, "Textures");
            string materialsDir = Path.Combine(userBin, "Materials");

            FileUtility.InitiateEmptyDirectories(modelDir, animDir, texturesDir, materialsDir);

            AvatarType          avatarType = avatar.ResolvedAvatarType;
            ICharacterAssembler assembler;

            if (avatarType == AvatarType.R15)
            {
                assembler = new R15CharacterAssembler();
            }
            else
            {
                assembler = new R6CharacterAssembler();
            }

            string avatarTypeName  = Rbx2Source.GetEnumName(avatar.ResolvedAvatarType);
            Folder characterAssets = AppendCharacterAssets(avatar, avatarTypeName);

            Rbx2Source.ScheduleTasks("BuildCharacter", "BuildCollisionModel", "BuildAnimations", "BuildTextures", "BuildMaterials", "BuildCompilerScript");
            Rbx2Source.PrintHeader("BUILDING CHARACTER MODEL");

            StudioMdlWriter writer = assembler.AssembleModel(characterAssets, avatar.Scales);

            string studioMdl = writer.BuildFile();
            string modelPath = Path.Combine(modelDir, "CharacterModel.smd");

            FileUtility.WriteFile(modelPath, studioMdl);

            // Clear the triangles so we can build a reference pose .smd file.
            writer.Triangles.Clear();
            string staticPose = writer.BuildFile();
            string refPath    = Path.Combine(modelDir, "ReferencePos.smd");

            FileUtility.WriteFile(refPath, staticPose);
            Rbx2Source.MarkTaskCompleted("BuildCharacter");

            Rbx2Source.PrintHeader("BUILDING COLLISION MODEL");

            Folder lowPoly = new Folder();

            SpecialMesh lowPolyHead = new SpecialMesh();

            lowPolyHead.MeshId   = "rbxassetid://582002794";
            lowPolyHead.MeshType = MeshType.FileMesh;
            lowPolyHead.Scale    = new Vector3(1, 1, 1);
            lowPolyHead.Offset   = new Vector3();
            lowPolyHead.Parent   = lowPoly;

            StudioMdlWriter collisionWriter = assembler.AssembleModel(lowPoly, avatar.Scales);

            string collisionModel = collisionWriter.BuildFile();
            string cmodelPath     = Path.Combine(modelDir, "CollisionModel.smd");

            FileUtility.WriteFile(cmodelPath, collisionModel);

            byte[] collisionJoints = assembler.CollisionModelScript;
            string cjointsPath     = Path.Combine(modelDir, "CollisionJoints.qc");

            FileUtility.WriteFile(cjointsPath, collisionJoints);
            Rbx2Source.MarkTaskCompleted("BuildCollisionModel");

            Rbx2Source.PrintHeader("BUILDING CHARACTER ANIMATIONS");
            Dictionary <string, string> animations = GatherAnimations(avatarType);

            if (animations.Count > 0)
            {
                foreach (string animName in animations.Keys)
                {
                    Rbx2Source.Print("Building Animation {0}", animName);
                    string           localAnimPath = animations[animName];
                    Asset            animAsset     = Asset.FromResource(localAnimPath);
                    Folder           import        = RBXM.LoadFromAsset(animAsset);
                    KeyframeSequence sequence      = import.FindFirstChildOfClass <KeyframeSequence>();
                    sequence.Name       = animName;
                    sequence.AvatarType = avatarType;
                    string animation = AnimationAssembler.Assemble(sequence, writer.Skeleton[0].Bones);
                    string animPath  = Path.Combine(animDir, animName + ".smd");
                    FileUtility.WriteFile(animPath, animation);
                }
            }
            else
            {
                Rbx2Source.Print("No animations found :(");
            }

            Rbx2Source.MarkTaskCompleted("BuildAnimations");
            Dictionary <string, Material> materials = writer.Materials;

            Rbx2Source.PrintHeader("BUILDING CHARACTER TEXTURES");

            string compileDirectory = "roblox_avatars/" + userName;

            TextureCompositor texCompositor = assembler.ComposeTextureMap(characterAssets, avatar.BodyColors);
            TextureAssembly   texAssembly   = assembler.AssembleTextures(texCompositor, materials);

            CompositData.FreeAllocatedTextures();
            texAssembly.MaterialDirectory = compileDirectory;

            Dictionary <string, Image> images = texAssembly.Images;

            foreach (string imageName in images.Keys)
            {
                Rbx2Source.Print("Writing Image {0}.png", imageName);
                Image  image     = images[imageName];
                string imagePath = Path.Combine(texturesDir, imageName + ".png");
                try
                {
                    image.Save(imagePath, ImageFormat.Png);
                }
                catch
                {
                    Rbx2Source.Print("IMAGE {0}.png FAILED TO SAVE!", imageName);
                }
                FileUtility.LockFile(imagePath);
            }

            Rbx2Source.MarkTaskCompleted("BuildTextures");
            Rbx2Source.PrintHeader("BUILDING MATERIAL FILES");

            Dictionary <string, string> matLinks = texAssembly.MatLinks;

            foreach (string mtlName in matLinks.Keys)
            {
                Rbx2Source.Print("Building VMT {0}.vmt", mtlName);
                string        targetVtf = matLinks[mtlName];
                Material      mtl       = materials[mtlName];
                ValveMaterial vmt       = new ValveMaterial(mtl);
                vmt.SetField("basetexture", "models/" + compileDirectory + "/" + targetVtf);
                string vmtPath    = Path.Combine(materialsDir, mtlName + ".vmt");
                string vmtContent = vmt.ToString();
                FileUtility.WriteFile(vmtPath, vmtContent);
            }

            Rbx2Source.MarkTaskCompleted("BuildMaterials");
            Rbx2Source.PrintHeader("WRITING COMPILER SCRIPT");

            QCWriter qc = new QCWriter();

            QCommand model = new QCommand("body", userName, "CharacterModel.smd");

            qc.AddCommand(model);

            string modelNameStr = compileDirectory + ".mdl";

            qc.WriteBasicCmd("modelname", modelNameStr);
            qc.WriteBasicCmd("upaxis", "y");

            string originStr = "";

            if (avatarType == AvatarType.R6)
            {
                originStr = "0 -30 0";
            }
            else
            {
                originStr = "0 " + (-23.5 * avatar.Scales.Height).ToString(Rbx2Source.NormalParse) + " 0";
            }

            qc.WriteBasicCmd("origin", originStr, false);
            qc.WriteBasicCmd("cdmaterials", "models/" + compileDirectory);
            qc.WriteBasicCmd("surfaceprop", "flesh");
            qc.WriteBasicCmd("include", "CollisionJoints.qc");

            QCommand reference = new QCommand("sequence", "reference", "ReferencePos.smd");

            reference.AddParameter("fps", "1");
            reference.AddParameter("loop");
            qc.AddCommand(reference);

            foreach (string animName in animations.Keys)
            {
                QCommand sequence = new QCommand("sequence", animName.ToLower(), "Animations/" + animName + ".smd");
                sequence.AddParameter("fps", AnimationAssembler.FrameRate.ToString());
                sequence.AddParameter("loop");
                if (avatarType == AvatarType.R6) // TODO: Find a work around so I can get rid of this.
                {
                    sequence.AddParameter("delta");
                }

                qc.AddCommand(sequence);
            }

            string qcFile = qc.BuildFile();
            string qcPath = Path.Combine(modelDir, "Compile.qc");

            FileUtility.WriteFile(qcPath, qcFile);
            Rbx2Source.MarkTaskCompleted("BuildCompilerScript");

            AssemblerData data = new AssemblerData();

            data.ModelData         = writer;
            data.TextureData       = texAssembly;
            data.CompilerScript    = qcPath;
            data.RootDirectory     = userBin;
            data.MaterialDirectory = materialsDir;
            data.TextureDirectory  = texturesDir;
            data.CompileDirectory  = compileDirectory;
            data.ModelName         = modelNameStr;

            return(data);
        }
        public AssemblerData Assemble(object metadata)
        {
            long   assetId   = (long)metadata;
            Asset  asset     = Asset.Get(assetId);
            string assetName = asset.ProductInfo.WindowsSafeName;

            string appData    = Environment.GetEnvironmentVariable("AppData");
            string rbx2Source = Path.Combine(appData, "Rbx2Source");
            string items      = Path.Combine(rbx2Source, "Items");
            string rootDir    = Path.Combine(items, assetName);

            string modelDir     = Path.Combine(rootDir, "Model");
            string texturesDir  = Path.Combine(rootDir, "Textures");
            string materialsDir = Path.Combine(rootDir, "Materials");

            FileUtility.InitiateEmptyDirectories(modelDir, texturesDir, materialsDir);
            Rbx2Source.ScheduleTasks("BuildModel", "BuildTextures", "BuildMaterials", "BuildCompilerScript");

            // Build Model

            StudioMdlWriter writer    = AssembleModel(asset);
            string          studioMdl = writer.BuildFile();
            string          modelPath = Path.Combine(modelDir, "Asset.smd");

            FileUtility.WriteFile(modelPath, studioMdl);

            // Build Reference Sequence

            Triangle[] triangles = writer.Triangles.ToArray();
            writer.Triangles.Clear();

            string reference = writer.BuildFile();
            string refPath   = Path.Combine(modelDir, "Reference.smd");

            FileUtility.WriteFile(refPath, reference);
            Rbx2Source.MarkTaskCompleted("BuildModel");

            // Build Textures

            Rbx2Source.PrintHeader("BUILDING TEXTURES");
            Dictionary <string, Material> materials = writer.Materials;
            string compileDirectory = "roblox_assets/" + assetName;

            TextureAssembly texAssembly = AssembleTextures(materials);

            texAssembly.MaterialDirectory = compileDirectory;

            Dictionary <string, Image> images = texAssembly.Images;

            foreach (string imageName in images.Keys)
            {
                Rbx2Source.Print("Writing Image {0}", imageName);
                Image  image     = images[imageName];
                string imagePath = Path.Combine(texturesDir, imageName + ".png");
                try
                {
                    image.Save(imagePath, ImageFormat.Png);
                }
                catch
                {
                    Rbx2Source.Print("IMAGE {0}.png FAILED TO SAVE!", imageName);
                }
                FileUtility.LockFile(imagePath);
            }

            Rbx2Source.MarkTaskCompleted("BuildTextures");

            // Build Materials

            Rbx2Source.PrintHeader("BUILDING MATERIAL FILES");
            string mtlDir = "models/" + compileDirectory;

            Dictionary <string, string>   matLinks  = texAssembly.MatLinks;
            Dictionary <string, Material> matLookup = new Dictionary <string, Material>();

            foreach (string mtlName in matLinks.Keys)
            {
                Material mtl       = materials[mtlName];
                string   vtfTarget = matLinks[mtlName];
                string   vmtPath   = Path.Combine(materialsDir, mtlName + ".vmt");
                if (!File.Exists(vmtPath))
                {
                    Rbx2Source.Print("Building VMT {0}.vmt", mtlName);
                    ValveMaterial vmt = new ValveMaterial(mtl);
                    vmt.SetField("basetexture", mtlDir + "/" + vtfTarget);
                    string vmtContent = vmt.ToString();
                    FileUtility.WriteFile(vmtPath, vmtContent);
                    matLookup[mtlName] = mtl;
                }
            }

            Rbx2Source.MarkTaskCompleted("BuildMaterials");

            // Build Compiler Script

            Rbx2Source.PrintHeader("WRITING COMPILER SCRIPT");
            QCWriter qc = new QCWriter();

            QCommand model = new QCommand("body", assetName, "Asset.smd");

            qc.AddCommand(model);

            string modelNameStr = compileDirectory + ".mdl";

            qc.WriteBasicCmd("modelname", modelNameStr);
            qc.WriteBasicCmd("upaxis", "y");
            qc.WriteBasicCmd("cdmaterials", mtlDir);

            QCommand collision = new QCommand("collisionjoints", "Asset.smd");

            collision.AddParameter("$mass", 115.0);
            collision.AddParameter("$inertia", 2.00);
            collision.AddParameter("$damping", 0.01);
            collision.AddParameter("$rotdamping", 0.40);
            qc.AddCommand(collision);

            QCommand sequence = new QCommand("sequence", "reference", "Reference.smd");

            sequence.AddParameter("fps", 1);
            sequence.AddParameter("loop");
            qc.AddCommand(sequence);

            string qcFile = qc.BuildFile();
            string qcPath = Path.Combine(modelDir, "Compile.qc");

            FileUtility.WriteFile(qcPath, qcFile);
            Rbx2Source.MarkTaskCompleted("BuildCompilerScript");

            AssemblerData data = new AssemblerData();

            data.ModelData         = writer;
            data.TextureData       = texAssembly;
            data.CompilerScript    = qcPath;
            data.RootDirectory     = rootDir;
            data.MaterialDirectory = materialsDir;
            data.TextureDirectory  = texturesDir;
            data.CompileDirectory  = compileDirectory;
            data.ModelName         = modelNameStr;

            return(data);
        }