Exemplo n.º 1
0
 public ProtoTileDecal(
     string localTextureFilePath,
     Vector2Ushort size,
     INoiseSelector noiseSelector,
     Vector2Ushort?interval           = null,
     DecalHidingSetting hidingSetting = DecalHidingSetting.StructureOrFloorObject,
     DrawOrder drawOrder  = DrawOrder.GroundDecals,
     Vector2Ushort offset = default,
     bool requiresCompleteNoiseSelectorCoverage = false,
     bool canFlipHorizontally = true,
     bool requiresCompleteProtoTileCoverage = false,
     IReadOnlyList <ProtoTileGroundTexture> requiredGroundTextures = null)
     : this(CollectTextures(localTextureFilePath),
            size,
            noiseSelector,
            interval,
            hidingSetting,
            drawOrder,
            offset,
            requiresCompleteNoiseSelectorCoverage,
            canFlipHorizontally,
            requiresCompleteProtoTileCoverage,
            requiredGroundTextures)
 {
 }
Exemplo n.º 2
0
        public ProtoTileDecal(
            IReadOnlyList <ITextureResource> textureResources,
            Vector2Ushort size,
            INoiseSelector noiseSelector,
            Vector2Ushort?interval           = null,
            DecalHidingSetting hidingSetting = DecalHidingSetting.StructureOrFloorObject,
            DrawOrder drawOrder  = DrawOrder.GroundDecals,
            Vector2Ushort offset = default,
            bool requiresCompleteNoiseSelectorCoverage = false,
            bool canFlipHorizontally = true,
            bool requiresCompleteProtoTileCoverage = false,
            IReadOnlyList <ProtoTileGroundTexture> requiredGroundTextures = null)
        {
            this.Size   = size;
            this.Offset = offset;
            this.RequiresCompleteNoiseSelectorCoverage = requiresCompleteNoiseSelectorCoverage;
            this.NoiseSelector          = noiseSelector;
            this.TextureResources       = textureResources;
            this.DrawOrder              = drawOrder;
            this.HidingSetting          = hidingSetting;
            this.RequiredGroundTextures = requiredGroundTextures?.Count > 0
                                              ? requiredGroundTextures
                                              : null;

            this.Interval = new Vector2Ushort((ushort)(this.Size.X + (interval?.X ?? 0)),
                                              (ushort)(this.Size.Y + (interval?.Y ?? 0)));

            this.CanFlipHorizontally = canFlipHorizontally;
            this.RequiresCompleteProtoTileCoverage = requiresCompleteProtoTileCoverage;
        }
        public static void PlacePlayer(ICharacter character, bool isRespawn)
        {
            // please ensure that we don't touch any private properties as they're not initialized at the first spawn request
            // (player placed in the world and only then it's initialized)
            var           random        = Api.Random;
            Vector2Ushort?spawnPosition = null;

            var spawnZone = protoSpawnZone.ServerZoneInstance;

            if (!spawnZone.IsEmpty)
            {
                // TODO: this could be slow and might require distributing across the multiple frames
                if (isRespawn)
                {
                    spawnPosition = TryFindZoneSpawnPosition(character, spawnZone, random, isRespawn: true);
                }

                if (!spawnPosition.HasValue)
                {
                    spawnPosition = TryFindZoneSpawnPosition(character, spawnZone, random, isRespawn: false);
                }
            }

            if (!spawnPosition.HasValue)
            {
                // fallback - spawn in the center of the world
                var worldBounds = worldService.WorldBounds;
                var offset      = worldBounds.Offset;
                var size        = worldBounds.Size;
                spawnPosition = new Vector2Ushort((ushort)(offset.X + size.X / 2),
                                                  (ushort)(offset.Y + size.Y / 2));
            }

            worldService.SetPosition(character, spawnPosition.Value.ToVector2D());
        }
 private static ProceduralTexture CreateComposedTextureInternal(
     string name,
     bool isTransparent,
     bool isUseCache,
     TextureResourceWithOffset[] textureResources,
     ITextureResource[] textureResourcesWithoutOffets,
     Vector2Ushort?customSize)
 {
     return(new(
                name,
                generateTextureCallback : request => Compose(request, customSize, textureResources),
                isTransparent : isTransparent,
                isUseCache : isUseCache,
                dependsOn : textureResourcesWithoutOffets));
 }
        public static ProceduralTexture CreateComposedTexture(
            string name,
            bool isTransparent,
            bool isUseCache,
            Vector2Ushort?customSize = null,
            params TextureResourceWithOffset[] textureResourcesWithOffsets)
        {
            var textureResources = new ITextureResource[textureResourcesWithOffsets.Length];

            for (var index = 0; index < textureResourcesWithOffsets.Length; index++)
            {
                var textureResourceWithOffset = textureResourcesWithOffsets[index];
                textureResources[index] = textureResourceWithOffset.TextureResource;
            }

            return(CreateComposedTextureInternal(name,
                                                 isTransparent,
                                                 isUseCache,
                                                 textureResourcesWithOffsets,
                                                 textureResources,
                                                 customSize));
        }
        public static bool ClientTryDropItemToGroundContainerNearby(
            Tile startTile,
            IItem item,
            ushort countToDrop,
            out Vector2Ushort?fallbackDropPosition,
            out IItemsContainer resultItemsContainer)
        {
            var character             = Client.Characters.CurrentPlayerCharacter;
            var objectGroundContainer = startTile.StaticObjects.FirstOrDefault(
                so => so.ProtoGameObject is ObjectGroundItemsContainer);

            if (objectGroundContainer != null)
            {
                if (TryDropTo(objectGroundContainer, out var droppedTo))
                {
                    // dropped successfully
                    fallbackDropPosition = null;
                    resultItemsContainer = droppedTo;
                    return(true);
                }
            }
            else if (instance.CheckTileRequirements(startTile.Position, character, logErrors: false))
            {
                // can create a new ground items container there
                fallbackDropPosition = startTile.Position;
                resultItemsContainer = null;
                return(false); // returning false - will try to create a new container there
            }

            // collect neighbor tiles list which are accessible by the player
            var neighborTiles = startTile.EightNeighborTiles
                                .SelectMany(t => t.EightNeighborTiles)
                                .Where(t => SharedIsWithinInteractionDistance(character, t.Position, out _))
                                .OrderBy(t => t.Position.TileSqrDistanceTo(startTile.Position))
                                .Distinct()
                                .ToList();

            // try drop to an existing ground container nearby
            foreach (var neighborTile in neighborTiles)
            {
                if (neighborTile.Height != startTile.Height)
                {
                    // different tile height
                    continue;
                }

                objectGroundContainer = neighborTile.StaticObjects.FirstOrDefault(
                    so => so.ProtoGameObject is ObjectGroundItemsContainer);
                if (objectGroundContainer is null)
                {
                    continue;
                }

                if (TryDropTo(objectGroundContainer, out var droppedTo))
                {
                    // dropped successfully
                    fallbackDropPosition = null;
                    resultItemsContainer = droppedTo;
                    return(true);
                }
            }

            // cannot find any ground container nearby
            // let's find any tile nearby suitable for a new ground container
            neighborTiles.Shuffle(); // randomize neighbors

            foreach (var neighborTile in neighborTiles)
            {
                if (neighborTile.Height != startTile.Height)
                {
                    // different tile height
                    continue;
                }

                if (instance.CheckTileRequirements(neighborTile.Position, character, logErrors: false))
                {
                    // this neighbor tile is suitable for a new ground container
                    fallbackDropPosition = neighborTile.Position;
                    resultItemsContainer = null;
                    return(false); // returning false - will try to create a new container there
                }
            }

            fallbackDropPosition = null;
            resultItemsContainer = null;
            return(false);

            bool TryDropTo(IStaticWorldObject staticWorldObject, out IItemsContainer droppedTo)
            {
                droppedTo = GetPublicState(staticWorldObject).ItemsContainer;
                return(Client.Items.MoveOrSwapItem(item,
                                                   droppedTo,
                                                   allowSwapping: false,
                                                   countToMove: countToDrop,
                                                   isLogErrors: false));
            }
        }
        public static async void ClientTryDropItemOnGround(
            IItem itemToDrop,
            ushort countToDrop,
            Vector2Ushort?dropTilePosition = null)
        {
            countToDrop = Math.Min(countToDrop, itemToDrop.Count);

            var character = Client.Characters.CurrentPlayerCharacter;

            if (!dropTilePosition.HasValue)
            {
                if (ClientTryDropItemToGroundContainerNearby(
                        character.Tile,
                        itemToDrop,
                        countToDrop,
                        out dropTilePosition,
                        out var resultItemsContainer))
                {
                    OnSuccess(resultItemsContainer);
                    return;
                }

                countToDrop = Math.Min(countToDrop, itemToDrop.Count);

                var obstaclesOnTheWay = false;
                if (!dropTilePosition.HasValue ||
                    !SharedIsWithinInteractionDistance(
                        character,
                        dropTilePosition.Value,
                        out obstaclesOnTheWay))
                {
                    NotificationSystem.ClientShowNotification(
                        obstaclesOnTheWay
                            ? CoreStrings.Notification_ObstaclesOnTheWay
                            : NotificationNoFreeSpaceToDrop,
                        color: NotificationColor.Bad,
                        icon: TextureResourceSack);
                    return;
                }
            }

            var tilePosition = dropTilePosition.Value;

            if (!SharedIsWithinInteractionDistance(
                    character,
                    tilePosition,
                    out var obstaclesOnTheWay2))
            {
                NotificationSystem.ClientShowNotification(
                    obstaclesOnTheWay2
                        ? CoreStrings.Notification_ObstaclesOnTheWay
                        : CoreStrings.Notification_TooFar,
                    NotificationCannotDropItemThere,
                    NotificationColor.Bad,
                    TextureResourceSack);
                return;
            }

            var tile = Client.World.GetTile(tilePosition);
            var objectGroundContainer = tile.StaticObjects.FirstOrDefault(_ => _.ProtoGameObject == instance);

            if (objectGroundContainer is null)
            {
                if (!instance.CheckTileRequirements(tilePosition, character, logErrors: false))
                {
                    // cannot drop item here
                    NotificationSystem.ClientShowNotification(
                        CoreStrings.Notification_ObstaclesOnTheWay,
                        NotificationCannotDropItemThere,
                        NotificationColor.Bad,
                        TextureResourceSack);
                    return;
                }

                Logger.Info(
                    $"Requested placing item on the ground (new ground container needed): {itemToDrop}. Count={countToDrop}.");
                objectGroundContainer = await instance.CallServer(
                    _ => _.ServerRemote_DropItemOnGround(
                        itemToDrop,
                        countToDrop,
                        tilePosition));

                if (objectGroundContainer != null)
                {
                    // successfully placed on ground
                    OnSuccess(GetPublicState(objectGroundContainer).ItemsContainer);
                    return;
                }

                // we're continue the async call - the context might have been changed
                if (itemToDrop.IsDestroyed)
                {
                    return;
                }

                // was unable to place the item on the ground - maybe it was already placed with an earlier call
                if (itemToDrop.Container?.OwnerAsStaticObject?.ProtoStaticWorldObject is ObjectGroundItemsContainer)
                {
                    // it seems to be on the ground now
                    return;
                }

                // the action is definitely failed
                instance.SoundPresetObject.PlaySound(ObjectSound.InteractFail);
                return;
            }

            if (!instance.SharedCanInteract(character, objectGroundContainer, writeToLog: true))
            {
                return;
            }

            // get items container instance
            var groundItemsContainer = GetPublicState(objectGroundContainer).ItemsContainer;

            // try move item to the ground items container
            if (!Client.Items.MoveOrSwapItem(
                    itemToDrop,
                    groundItemsContainer,
                    countToMove: countToDrop,
                    isLogErrors: false))
            {
                // cannot move - open container UI
                ClientOpenContainerExchangeUI(objectGroundContainer);
                return;
            }

            // item moved successfully
            OnSuccess(groundItemsContainer);

            void OnSuccess(IItemsContainer resultGroundItemsContainer)
            {
                itemToDrop.ProtoItem.ClientOnItemDrop(itemToDrop, resultGroundItemsContainer);

                NotificationSystem.ClientShowItemsNotification(
                    itemsChangedCount: new Dictionary <IProtoItem, int>()
                {
                    { itemToDrop.ProtoItem, -countToDrop }
                });

                if (Api.Client.Input.IsKeyHeld(InputKey.Shift, evenIfHandled: true))
                {
                    // open container UI to allow faster items exchange with it
                    ClientOpenContainerExchangeUI(resultGroundItemsContainer.OwnerAsStaticObject);
                }
            }
        }
Exemplo n.º 8
0
        public static async Task<ITextureResource> GenerateHeadSprite(
            CharacterHeadSpriteData data,
            ProceduralTextureRequest request,
            bool isMale,
            HeadSpriteType headSpriteType,
            bool isPreview,
            Vector2Ushort? customTextureSize = null,
            sbyte spriteQualityOffset = 0)
        {
            var isFrontFace = headSpriteType == HeadSpriteType.Front;
            var renderingTag = request.TextureName;
            var side = isFrontFace ? "Front" : "Back";

            var style = data.FaceStyle;

            var faceStylesProvider = SharedCharacterFaceStylesProvider.GetForGender(isMale);

            var facePath = $"{faceStylesProvider.FacesFolderPath}{style.FaceId}/{side}";
            var faceShapePath = facePath + ".png";

            if (!IsFileExists(faceShapePath))
            {
                Api.Logger.Error("Face sprite not found: " + faceShapePath);
                // try fallback
                facePath = faceStylesProvider.FacesFolderPath + "Face01/" + side;
                faceShapePath = facePath;
                if (!IsFileExists(faceShapePath))
                {
                    // no fallback
                    return TextureResource.NoTexture;
                }
            }

            var faceTopPath = $"{facePath}Top{style.TopId}.png";
            var faceBottomPath = $"{facePath}Bottom{style.BottomId}.png";

            if (isFrontFace)
            {
                if (!IsFileExists(faceTopPath))
                {
                    Api.Logger.Error("Face top sprite not found: " + faceTopPath);
                    // try fallback
                    faceTopPath = $"{facePath}Top01.png";
                    if (!IsFileExists(faceTopPath))
                    {
                        // no fallback
                        return TextureResource.NoTexture;
                    }
                }

                if (!IsFileExists(faceBottomPath))
                {
                    Api.Logger.Error("Face bottom sprite not found: " + faceBottomPath);

                    // try fallback
                    faceBottomPath = $"{facePath}Bottom01.png";
                    if (!IsFileExists(faceBottomPath))
                    {
                        // no fallback
                        return TextureResource.NoTexture;
                    }
                }
            }

            var protoItemHeadEquipment = data.HeadEquipmentItemProto;
            var isHairVisible = protoItemHeadEquipment?.IsHairVisible ?? true;
            isHairVisible &= style.HairId is not null;

            string hair = null, hairBehind = null;
            if (isHairVisible
                && !string.IsNullOrEmpty(style.HairId))
            {
                var hairBase = faceStylesProvider.HairFolderPath + $"{style.HairId}/{side}";
                hair = hairBase + ".png";
                hairBehind = hairBase + "Behind.png";
            }

            string skinTone = null;
            if (!string.IsNullOrEmpty(style.SkinToneId))
            {
                skinTone = SharedCharacterFaceStylesProvider.GetSkinToneFilePath(style.SkinToneId);
            }

            string hairColor = null;
            if (!string.IsNullOrEmpty(style.HairColorId))
            {
                hairColor = SharedCharacterFaceStylesProvider.HairColorRootFolderPath + $"{style.HairColorId}" + ".png";
            }

            string helmetFront = null, helmetBehind = null;
            TextureResource helmetFrontMaskTextureResource = null;

            if (protoItemHeadEquipment is not null)
            {
                protoItemHeadEquipment.ClientGetHeadSlotSprites(data.HeadEquipmentItem,
                                                                isMale,
                                                                data.SkeletonResource,
                                                                isFrontFace,
                                                                isPreview,
                                                                out helmetFront,
                                                                out helmetBehind);

                if (helmetFront is null)
                {
                    throw new Exception("Helmet attachment is not available for " + protoItemHeadEquipment);
                }

                if (isFrontFace)
                {
                    helmetFrontMaskTextureResource = new TextureResource(
                        helmetFront.Substring(0, helmetFront.Length - ".png".Length) + "Mask.png",
                        qualityOffset: spriteQualityOffset);

                    if (!Api.Shared.IsFileExists(helmetFrontMaskTextureResource.FullPath))
                    {
                        helmetFrontMaskTextureResource = null;
                    }
                }
            }

            // let's combine all the layers (if some elements are null - they will not be rendered)
            List<ComposeLayer> layers;
            if (protoItemHeadEquipment is null
                || protoItemHeadEquipment.IsHeadVisible)
            {
                var faceLayer = await CreateFaceTexture(
                                    request,
                                    renderingTag,
                                    customTextureSize,
                                    new List<ComposeLayer>()
                                    {
                                        new(faceShapePath, spriteQualityOffset),
                                        new(faceTopPath, spriteQualityOffset),
                                        new(faceBottomPath, spriteQualityOffset)
                                    },
        private static async Task <ITextureResource> Compose(
            ProceduralTextureRequest request,
            Vector2Ushort?customSize,
            params TextureResourceWithOffset[] textureResources)
        {
            var rendering    = Api.Client.Rendering;
            var renderingTag = request.TextureName;

            var qualityScaleCoef = rendering.CalculateCurrentQualityScaleCoefWithOffset(0);

            Vector2Ushort size;

            if (customSize is not null)
            {
                size = customSize.Value;
                if (qualityScaleCoef > 1)
                {
                    size = new Vector2Ushort((ushort)(size.X / qualityScaleCoef),
                                             (ushort)(size.Y / qualityScaleCoef));
                }
            }
            else
            {
                size = await rendering.GetTextureSize(textureResources[0].TextureResource);
            }

            request.ThrowIfCancelled();

            // create camera and render texture
            var renderTexture = rendering.CreateRenderTexture(renderingTag, size.X, size.Y);
            var cameraObject  = Api.Client.Scene.CreateSceneObject(renderingTag);
            var camera        = rendering.CreateCamera(cameraObject,
                                                       renderingTag,
                                                       drawOrder: -100);

            camera.RenderTarget = renderTexture;
            camera.SetOrthographicProjection(size.X, size.Y);

            // create and prepare sprite renderers
            foreach (var entry in textureResources)
            {
                var positionOffset = entry.Offset;
                if (positionOffset.HasValue &&
                    qualityScaleCoef > 1)
                {
                    positionOffset /= qualityScaleCoef;
                }

                rendering.CreateSpriteRenderer(
                    cameraObject,
                    entry.TextureResource,
                    positionOffset: positionOffset,
                    spritePivotPoint: entry.PivotPoint ?? (0, 1), // draw down by default
                    renderingTag: renderingTag);
            }

            await camera.DrawAsync();

            cameraObject.Destroy();

            request.ThrowIfCancelled();

            var generatedTexture = await renderTexture.SaveToTexture(isTransparent : true);

            renderTexture.Dispose();
            request.ThrowIfCancelled();
            return(generatedTexture);
        }
        public static async Task <ITextureResource> GenerateHeadSprite(
            CharacterHeadSpriteData data,
            ProceduralTextureRequest request,
            bool isMale,
            bool isFrontFace,
            Vector2Ushort?customTextureSize = null,
            sbyte spriteQualityOffset       = 0)
        {
            var renderingTag = request.TextureName;
            var side         = isFrontFace ? "Front" : "Back";

            var style = data.FaceStyle;

            var faceStylesProvider = SharedCharacterFaceStylesProvider.GetForGender(isMale);

            var facePath      = $"{faceStylesProvider.FacesFolderPath}{style.FaceId}/{side}";
            var faceShapePath = facePath + ".png";

            if (!IsFileExists(faceShapePath))
            {
                Api.Logger.Error("Face sprite not found: " + faceShapePath);
                // try fallback
                facePath      = faceStylesProvider.FacesFolderPath + "Face01/" + side;
                faceShapePath = facePath;
                if (!IsFileExists(faceShapePath))
                {
                    // no fallback
                    return(TextureResource.NoTexture);
                }
            }

            var faceTopPath    = $"{facePath}Top{style.TopId}.png";
            var faceBottomPath = $"{facePath}Bottom{style.BottomId}.png";

            if (isFrontFace)
            {
                if (!IsFileExists(faceTopPath))
                {
                    Api.Logger.Error("Face top sprite not found: " + faceTopPath);
                    // try fallback
                    faceTopPath = $"{facePath}Top01.png";
                    if (!IsFileExists(faceTopPath))
                    {
                        // no fallback
                        return(TextureResource.NoTexture);
                    }
                }

                if (!IsFileExists(faceBottomPath))
                {
                    Api.Logger.Error("Face bottom sprite not found: " + faceBottomPath);

                    // try fallback
                    faceBottomPath = $"{facePath}Bottom01.png";
                    if (!IsFileExists(faceBottomPath))
                    {
                        // no fallback
                        return(TextureResource.NoTexture);
                    }
                }
            }

            var itemHeadEquipment      = data.HeadEquipment;
            var protoItemHeadEquipment = (IProtoItemEquipmentHead)itemHeadEquipment?.ProtoItem;
            var isHairVisible          = protoItemHeadEquipment?.IsHairVisible ?? true;

            isHairVisible &= style.HairId != null;

            string hair = null, hairBehind = null;

            if (isHairVisible)
            {
                var hairBase = faceStylesProvider.HairFolderPath + $"{style.HairId}/{side}";
                hair       = hairBase + ".png";
                hairBehind = hairBase + "Behind.png";
            }

            string helmetFront = null, helmetBehind = null;

            if (protoItemHeadEquipment != null)
            {
                protoItemHeadEquipment.ClientGetHeadSlotSprites(
                    itemHeadEquipment,
                    isMale,
                    data.SkeletonResource,
                    isFrontFace,
                    out helmetFront,
                    out helmetBehind);

                if (helmetFront == null)
                {
                    throw new Exception("Helmet attachment is not available for " + protoItemHeadEquipment);
                }
            }

            var isHeadVisible = protoItemHeadEquipment?.IsHeadVisible ?? true;

            // let's combine all the layers (if some elements are null - they will not be rendered)
            List <ComposeLayer> layers;

            if (isHeadVisible)
            {
                layers = new List <ComposeLayer>()
                {
                    new ComposeLayer(helmetBehind, spriteQualityOffset),
                    new ComposeLayer(hairBehind, spriteQualityOffset),
                    new ComposeLayer(faceShapePath, spriteQualityOffset),
                    new ComposeLayer(faceTopPath, spriteQualityOffset),
                    new ComposeLayer(faceBottomPath, spriteQualityOffset),
                    new ComposeLayer(hair, spriteQualityOffset),
                    new ComposeLayer(helmetFront, spriteQualityOffset)
                };
            }
            else // if head is not visible (defined by head equipment item)
            {
                layers = new List <ComposeLayer>()
                {
                    new ComposeLayer(helmetBehind, spriteQualityOffset),
                    new ComposeLayer(helmetFront, spriteQualityOffset)
                };
            }

            // load only those layers which had the according file
            layers.RemoveAll(
                t => t.TextureResource == null ||
                !IsFileExists(t.TextureResource.FullPath));

            if (layers.Count == 0)
            {
                Api.Logger.Error("No sprites for face rendering: " + request.TextureName);
                return(TextureResource.NoTexture);
            }

            // load all the layers data
            var resultTextureSize = await PrepareLayers(request, layers);

            if (customTextureSize.HasValue)
            {
                resultTextureSize = customTextureSize.Value;
            }

            var referencePivotPos = new Vector2Ushort(
                (ushort)(resultTextureSize.X / 2),
                (ushort)(resultTextureSize.Y / 2));

            // create camera and render texture
            var renderTexture = Rendering.CreateRenderTexture(renderingTag, resultTextureSize.X, resultTextureSize.Y);
            var cameraObject  = Api.Client.Scene.CreateSceneObject(renderingTag);
            var camera        = Rendering.CreateCamera(cameraObject,
                                                       renderingTag,
                                                       drawOrder: -100);

            camera.RenderTarget = renderTexture;
            camera.SetOrthographicProjection(resultTextureSize.X, resultTextureSize.Y);

            // create and prepare renderer for each layer
            foreach (var layer in layers)
            {
                var pivotPos = layer.PivotPos;
                var offsetX  = referencePivotPos.X - pivotPos.X;
                var offsetY  = pivotPos.Y - referencePivotPos.Y;
                var offset   = (offsetX, offsetY);

                Rendering.CreateSpriteRenderer(
                    cameraObject,
                    layer.TextureResource,
                    positionOffset: offset,
                    // draw down
                    spritePivotPoint: (0, 1),
                    renderingTag: renderingTag);
            }

            // ReSharper disable once CoVariantArrayConversion
            request.ChangeDependencies(layers.Select(l => l.TextureResource).ToArray());

            await camera.DrawAsync();

            cameraObject.Destroy();

            request.ThrowIfCancelled();

            var generatedTexture = await renderTexture.SaveToTexture(
                isTransparent : true,
                qualityScaleCoef : Rendering.CalculateCurrentQualityScaleCoefWithOffset(
                    spriteQualityOffset));

            renderTexture.Dispose();
            request.ThrowIfCancelled();
            return(generatedTexture);
        }
        public static async Task <ITextureResource> GenerateHeadSprite(
            CharacterHeadSpriteData data,
            ProceduralTextureRequest request,
            bool isMale,
            HeadSpriteType headSpriteType,
            Vector2Ushort?customTextureSize = null,
            sbyte spriteQualityOffset       = 0)
        {
            var isFrontFace  = headSpriteType == HeadSpriteType.Front;
            var renderingTag = request.TextureName;
            var side         = isFrontFace ? "Front" : "Back";

            var style = data.FaceStyle;

            var faceStylesProvider = SharedCharacterFaceStylesProvider.GetForGender(isMale);

            var facePath      = $"{faceStylesProvider.FacesFolderPath}{style.FaceId}/{side}";
            var faceShapePath = facePath + ".png";

            if (!IsFileExists(faceShapePath))
            {
                Api.Logger.Error("Face sprite not found: " + faceShapePath);
                // try fallback
                facePath      = faceStylesProvider.FacesFolderPath + "Face01/" + side;
                faceShapePath = facePath;
                if (!IsFileExists(faceShapePath))
                {
                    // no fallback
                    return(TextureResource.NoTexture);
                }
            }

            var faceTopPath    = $"{facePath}Top{style.TopId}.png";
            var faceBottomPath = $"{facePath}Bottom{style.BottomId}.png";

            if (isFrontFace)
            {
                if (!IsFileExists(faceTopPath))
                {
                    Api.Logger.Error("Face top sprite not found: " + faceTopPath);
                    // try fallback
                    faceTopPath = $"{facePath}Top01.png";
                    if (!IsFileExists(faceTopPath))
                    {
                        // no fallback
                        return(TextureResource.NoTexture);
                    }
                }

                if (!IsFileExists(faceBottomPath))
                {
                    Api.Logger.Error("Face bottom sprite not found: " + faceBottomPath);

                    // try fallback
                    faceBottomPath = $"{facePath}Bottom01.png";
                    if (!IsFileExists(faceBottomPath))
                    {
                        // no fallback
                        return(TextureResource.NoTexture);
                    }
                }
            }

            var itemHeadEquipment      = data.HeadEquipment;
            var protoItemHeadEquipment = (IProtoItemEquipmentHead)itemHeadEquipment?.ProtoItem;
            var isHairVisible          = protoItemHeadEquipment?.IsHairVisible ?? true;

            isHairVisible &= style.HairId != null;

            string hair = null, hairBehind = null;

            if (isHairVisible &&
                !string.IsNullOrEmpty(style.HairId))
            {
                var hairBase = faceStylesProvider.HairFolderPath + $"{style.HairId}/{side}";
                hair       = hairBase + ".png";
                hairBehind = hairBase + "Behind.png";
            }

            string skinTone = null;

            if (!string.IsNullOrEmpty(style.SkinToneId))
            {
                skinTone = SharedCharacterFaceStylesProvider.GetSkinToneFilePath(style.SkinToneId);
            }

            string hairColor = null;

            if (!string.IsNullOrEmpty(style.HairColorId))
            {
                hairColor = SharedCharacterFaceStylesProvider.HairColorRootFolderPath + $"{style.HairColorId}" + ".png";
            }

            string helmetFront = null, helmetBehind = null;

            if (protoItemHeadEquipment != null)
            {
                protoItemHeadEquipment.ClientGetHeadSlotSprites(
                    itemHeadEquipment,
                    isMale,
                    data.SkeletonResource,
                    isFrontFace,
                    out helmetFront,
                    out helmetBehind);

                if (helmetFront is null)
                {
                    throw new Exception("Helmet attachment is not available for " + protoItemHeadEquipment);
                }
            }

            // let's combine all the layers (if some elements are null - they will not be rendered)
            List <ComposeLayer> layers;

            if (protoItemHeadEquipment is null ||
                protoItemHeadEquipment.IsHeadVisible)
            {
                var faceLayer = await CreateFaceTexture(
                    request,
                    renderingTag,
                    customTextureSize,
                    new List <ComposeLayer>()
                {
                    new ComposeLayer(faceShapePath, spriteQualityOffset),
                    new ComposeLayer(faceTopPath, spriteQualityOffset),
                    new ComposeLayer(faceBottomPath, spriteQualityOffset)
                },
                    skinTone);

                layers = new List <ComposeLayer>();

                if (isHairVisible)
                {
                    var(layerHair, layerHairBehind) = await GetHairLayers(request,
                                                                          hair,
                                                                          hairBehind,
                                                                          hairColor,
                                                                          spriteQualityOffset);

                    if (headSpriteType != HeadSpriteType.BackOverlay)
                    {
                        layers.Add(new ComposeLayer(helmetBehind, spriteQualityOffset));
                        layers.Add(layerHairBehind);
                        layers.Add(faceLayer);
                    }

                    if (headSpriteType != HeadSpriteType.Back)
                    {
                        if (layerHair.TextureResource is null &&
                            helmetFront is null &&
                            layers.Count == 0)
                        {
                            return(TransparentTexturePlaceholder);
                        }

                        layers.Add(layerHair);
                        layers.Add(new ComposeLayer(helmetFront, spriteQualityOffset));
                    }
                }
                else // hair is not visible
                {
                    if (headSpriteType == HeadSpriteType.BackOverlay)
                    {
                        return(TransparentTexturePlaceholder);
                    }

                    layers.Add(new ComposeLayer(helmetBehind, spriteQualityOffset));
                    layers.Add(faceLayer);
                    layers.Add(new ComposeLayer(helmetFront, spriteQualityOffset));
                }
            }