示例#1
0
        public static void ServerReplaceConstructionSiteWithStructure(
            IStaticWorldObject worldObject,
            IProtoObjectStructure protoStructure,
            ICharacter byCharacter)
        {
            if (worldObject?.IsDestroyed ?? true)
            {
                throw new Exception("Construction site doesn't exist or already destroyed: " + worldObject);
            }

            var tilePosition = worldObject.TilePosition;

            // destroy construction site
            Server.World.DestroyObject(worldObject);

            // create structure
            var structure = ConstructionSystem.ServerCreateStructure(
                protoStructure,
                tilePosition,
                byCharacter: byCharacter);

            if (byCharacter == null)
            {
                return;
            }

            Instance.ServerNotifyOnStructurePlacedOrRelocated(structure, byCharacter);
            Api.SafeInvoke(() => ServerStructureBuilt?.Invoke(byCharacter, structure));
        }
示例#2
0
        public override void SharedUpdate(double deltaTime)
        {
            if (this.CharacterPublicState.SelectedItem != this.ItemConstructionTool)
            {
                this.AbortAction();
                return;
            }

            if (!ConstructionSystem.SharedCheckCanInteract(
                    this.Character,
                    this.WorldObject,
                    writeToLog: true))
            {
                this.AbortAction();
                return;
            }

            this.currentStageTimeRemainsSeconds -= deltaTime;
            if (this.currentStageTimeRemainsSeconds <= 0)
            {
                this.SharedOnStageCompleted();
            }
            else if (Api.IsClient)
            {
                if (this.ObjectPublicState.StructurePointsCurrent >= this.structurePointsMax)
                {
                    // apparently the building finished construction/repair before the client simulation was complete
                    this.AbortAction();
                }
            }

            this.UpdateProgress();
        }
示例#3
0
        public override void SharedUpdate(double deltaTime)
        {
            if (this.CharacterPublicState.SelectedHotbarItem != this.ItemConstructionTool)
            {
                this.AbortAction();
                return;
            }

            if (!ConstructionSystem.SharedCheckCanInteract(
                    this.Character,
                    this.WorldObject,
                    writeToLog: true))
            {
                this.AbortAction();
                return;
            }

            this.currentStageTimeRemainsSeconds -= deltaTime;
            if (this.currentStageTimeRemainsSeconds <= 0)
            {
                this.SharedOnStageCompleted();
            }

            this.UpdateProgress();
        }
示例#4
0
        private static void ClientValidateCanBuild(
            Vector2Ushort tilePosition,
            bool logErrors,
            out string errorMessage,
            out bool canPlace,
            out bool isTooFar)
        {
            var protoStructure = currentSelectedProtoConstruction;

            if (Client.World.GetTile(tilePosition)
                .StaticObjects
                .Any(so => so.ProtoStaticWorldObject == protoStructure ||
                     ProtoObjectConstructionSite.SharedIsConstructionOf(so, protoStructure)))
            {
                // this building is already built here
                canPlace     = false;
                isTooFar     = false;
                errorMessage = null;
                return;
            }

            var character = Client.Characters.CurrentPlayerCharacter;

            if (!protoStructure.CheckTileRequirements(
                    tilePosition,
                    character,
                    errorCodeOrMessage: out var errorCodeOrMessage,
                    logErrors: logErrors))
            {
                // time requirements are not valid
                canPlace     = false;
                isTooFar     = false;
                errorMessage = ConstructionSystem.SharedConvertCodeOrErrorMessageToString(errorCodeOrMessage);
                return;
            }

            errorMessage = null;

            var configBuild = protoStructure.ConfigBuild;

            if (configBuild.CheckStageCanBeBuilt(character))
            {
                canPlace = true;
                isTooFar = SharedIsTooFarToPlace(protoStructure,
                                                 tilePosition,
                                                 character,
                                                 logErrors);
                return;
            }

            // not enough items to build the stage
            // close construction menu
            ClientCloseConstructionMenu();
            canPlace = false;
            isTooFar = false;
        }
示例#5
0
        /// <summary>
        /// Verifies that the character has all the required items to use this action.
        /// </summary>
        public bool ValidateRequiredItemsAvailable()
        {
            if (this.Config.CheckStageCanBeBuilt(this.Character))
            {
                return(true);
            }

            Api.Logger.Warning(
                $"Building/repairing is not possible - not all required items are available: {this.WorldObject} by {this.Character}",
                this.Character);

            ConstructionSystem.SharedOnNotEnoughItemsAvailable(this);

            return(false);
        }
示例#6
0
        private void ServerRemote_PlaceStructure(
            IProtoObjectStructure protoStructure,
            Vector2Ushort tilePosition)
        {
            var character = ServerRemoteContext.Character;

            if (!protoStructure.SharedIsTechUnlocked(character))
            {
                Logger.Error(
                    $"Cannot build {protoStructure} at {tilePosition}: player character doesn't have unlocked tech node for this structure.",
                    character);
                return;
            }

            if (SharedIsTooFarToPlace(protoStructure,
                                      tilePosition,
                                      character,
                                      logErrors: true))
            {
                return;
            }

            // validate if the structure can be placed there
            if (!protoStructure.CheckTileRequirements(tilePosition, character, logErrors: true))
            {
                return;
            }

            // validate if there are enough required items/resources to build the structure
            var configBuild = protoStructure.ConfigBuild;

            if (!configBuild.CheckStageCanBeBuilt(character))
            {
                Logger.Error(
                    $"Cannot build {protoStructure} at {tilePosition}: player character doesn't have enough resources (or not allowed).",
                    character);
                return;
            }

            var selectedHotbarItem = PlayerCharacter.GetPublicState(character).SelectedItem;

            if (!(selectedHotbarItem?.ProtoItem is IProtoItemToolToolbox))
            {
                Logger.Error(
                    $"Cannot build {protoStructure} at {tilePosition}: player character doesn't have selected construction tool.",
                    character);
                return;
            }

            // consume required items/resources (for 1 stage)
            configBuild.ServerDestroyRequiredItems(character);

            if (configBuild.StagesCount > 1)
            {
                if (AllowInstantPlacementInCreativeMode &&
                    CreativeModeSystem.SharedIsInCreativeMode(character))
                {
                    // instant placement allowed
                }
                else
                {
                    // there are multiple construction stages - spawn and setup a construction site
                    var constructionSite = this.ServerCreateConstructionSite(tilePosition, protoStructure, character);
                    this.ServerNotifyOnStructurePlacedOrRelocated(constructionSite, character);
                    return;
                }
            }

            ServerDecalsDestroyHelper.DestroyAllDecals(tilePosition, protoStructure.Layout);

            // there is only one construction stage - simply spawn the structure
            var structure = ConstructionSystem.ServerCreateStructure(
                protoStructure,
                tilePosition,
                character);

            this.ServerNotifyOnStructurePlacedOrRelocated(structure, character);
            Api.SafeInvoke(() => ServerStructureBuilt?.Invoke(character, structure));
        }
示例#7
0
        public static void ClientStartRelocation(IStaticWorldObject objectStructure)
        {
            var protoStructure = objectStructure.ProtoStaticWorldObject;
            var character      = Client.Characters.CurrentPlayerCharacter;

            if (IsInObjectPlacementMode ||
                ConstructionPlacementSystem.IsInObjectPlacementMode)
            {
                // already relocating/placing something
                return;
            }

            if (!SharedIsRelocatable(objectStructure))
            {
                return;
            }

            if (!CreativeModeSystem.SharedIsInCreativeMode(character) &&
                !LandClaimSystem.SharedIsOwnedLand(objectStructure.TilePosition,
                                                   character,
                                                   requireFactionPermission: true,
                                                   out var hasNoFactionPermission,
                                                   out _))
            {
                // the building location or destination is in an area that is not owned by the player
                SharedShowCannotRelocateNotification(
                    character,
                    protoStructure,
                    hasNoFactionPermission);
                return;
            }

            var isPvEorWithinInteractionArea = PveSystem.SharedIsPve(false) ||
                                               protoStructure.SharedIsInsideCharacterInteractionArea(
                Api.Client.Characters.CurrentPlayerCharacter,
                objectStructure,
                writeToLog: false);

            if (!isPvEorWithinInteractionArea)
            {
                CannotInteractMessageDisplay.ClientOnCannotInteract(
                    character,
                    CoreStrings.Notification_TooFar,
                    isOutOfRange: true);
                return;
            }

            if (LandClaimSystem.SharedIsUnderRaidBlock(character, objectStructure))
            {
                // the building is in an area under the raid
                ConstructionSystem.SharedShowCannotBuildNotification(
                    character,
                    LandClaimSystem.ErrorRaidBlockActionRestricted_Message,
                    protoStructure);
                return;
            }

            if (LandClaimShieldProtectionSystem.SharedIsUnderShieldProtection(objectStructure))
            {
                // the building is in an area under shield protection
                LandClaimShieldProtectionSystem.SharedSendNotificationActionForbiddenUnderShieldProtection(
                    character);
                return;
            }

            ClientDisableConstructionRelocation();

            var sceneObject = Client.Scene.CreateSceneObject("StructureRelocationHelper");

            componentObjectPlacementHelper = sceneObject.AddComponent <ClientComponentObjectPlacementHelper>();
            componentRelocationHelper      = sceneObject.AddComponent <ClientComponentObjectRelocationHelper>();

            componentObjectPlacementHelper
            .Setup(protoStructure,
                   isCancelable: true,
                   isRepeatCallbackIfHeld: false,
                   isDrawConstructionGrid: true,
                   isBlockingInput: true,
                   validateCanPlaceCallback: ClientValidateCanRelocate,
                   placeSelectedCallback: ClientConstructionPlaceSelectedCallback,
                   delayRemainsSeconds: 0.1);
            componentObjectPlacementHelper.HideBlueprintOnOverlapWithTheSameObject = false;

            componentRelocationHelper.Setup(objectStructure);

            void ClientValidateCanRelocate(
                Vector2Ushort tilePosition,
                bool logErrors,
                out string errorMessage,
                out bool canPlace,
                out bool isTooFar)
            {
                if (tilePosition == objectStructure.TilePosition)
                {
                    canPlace     = true;
                    isTooFar     = false;
                    errorMessage = null;
                    return;
                }

                if (!SharedCheckTileRequirementsForRelocation(character,
                                                              objectStructure,
                                                              tilePosition,
                                                              out errorMessage,
                                                              logErrors: logErrors))
                {
                    // time requirements are not valid
                    canPlace = false;
                    isTooFar = false;
                    return;
                }

                if (!SharedValidateCanCharacterRelocateStructure(character,
                                                                 objectStructure,
                                                                 tilePosition,
                                                                 out errorMessage,
                                                                 logErrors: logErrors))
                {
                    canPlace = true;
                    isTooFar = true;
                    return;
                }

                if (SharedHasObstacle(
                        character,
                        objectStructure,
                        tilePosition.ToVector2D() + protoStructure.Layout.Center))
                {
                    if (logErrors)
                    {
                        CannotInteractMessageDisplay.ClientOnCannotInteract(
                            character,
                            CoreStrings.Notification_ObstaclesOnTheWay,
                            isOutOfRange: true);
                    }

                    errorMessage = CoreStrings.Notification_ObstaclesOnTheWay;
                    canPlace     = true;
                    isTooFar     = true;
                    return;
                }

                canPlace = true;
                isTooFar = false;
            }

            void ClientConstructionPlaceSelectedCallback(Vector2Ushort tilePosition)
            {
                if (SharedHasObstacle(
                        character,
                        objectStructure,
                        tilePosition.ToVector2D() + protoStructure.Layout.Center))
                {
                    CannotInteractMessageDisplay.ClientOnCannotInteract(
                        character,
                        CoreStrings.Notification_ObstaclesOnTheWay,
                        isOutOfRange: true);
                    return;
                }

                ClientTimersSystem.AddAction(0.1, ClientDisableConstructionRelocation);
                if (tilePosition != objectStructure.TilePosition)
                {
                    Instance.CallServer(_ => _.ServerRemote_RelocateStructure(objectStructure, tilePosition));
                }
            }
        }
示例#8
0
        private static bool SharedCheckTileRequirementsForRelocation(
            ICharacter character,
            IStaticWorldObject objectStructure,
            Vector2Ushort toPosition,
            out string errorMessage,
            bool logErrors)
        {
            if (!(objectStructure.ProtoGameObject is IProtoObjectStructure protoStructure))
            {
                errorMessage = null;
                return(false);
            }

            var world = Api.IsServer
                            ? (IWorldService)Server.World
                            : (IWorldService)Client.World;

            var tileRequirements      = protoStructure.TileRequirements;
            var occupiedTilePositions = new HashSet <Vector2Ushort>(objectStructure.OccupiedTilePositions);

            foreach (var tileOffset in protoStructure.Layout.TileOffsets)
            {
                var tilePosition = new Vector2Ushort((ushort)(toPosition.X + tileOffset.X),
                                                     (ushort)(toPosition.Y + tileOffset.Y));
                if (occupiedTilePositions.Contains(tilePosition))
                {
                    // no need to check that tile as the object player want to move is already there
                    continue;
                }

                var tile = world.GetTile(tilePosition,
                                         logOutOfBounds: false);

                if (tile.IsOutOfBounds)
                {
                    errorMessage = "Out of bounds";
                }
                else
                {
                    var context = new ConstructionTileRequirements.Context(tile,
                                                                           character,
                                                                           protoStructure,
                                                                           tileOffset,
                                                                           startTilePosition: toPosition,
                                                                           objectToRelocate: objectStructure);
                    if (tileRequirements.Check(context, out errorMessage))
                    {
                        // valid tile
                        continue;
                    }
                }

                // check failed
                if (!logErrors)
                {
                    return(false);
                }

                if (Api.IsServer)
                {
                    Api.Logger.Warning(
                        $"Cannot move {protoStructure} at {toPosition} - check failed:"
                        + Environment.NewLine
                        + errorMessage);
                }

                ConstructionSystem.SharedShowCannotPlaceNotification(
                    character,
                    errorMessage,
                    protoStructure);

                return(false);
            }

            errorMessage = null;
            return(true);
        }
示例#9
0
        public static bool SharedValidateCanCharacterRelocateStructure(
            ICharacter character,
            IStaticWorldObject objectStructure,
            Vector2Ushort toPosition,
            out string errorMessage,
            bool logErrors)
        {
            if (!SharedIsRelocatable(objectStructure))
            {
                errorMessage = null;
                return(false);
            }

            if (!SharedCheckTileRequirementsForRelocation(character,
                                                          objectStructure,
                                                          toPosition,
                                                          out errorMessage,
                                                          logErrors))
            {
                return(false);
            }

            if (!(objectStructure.ProtoGameObject is IProtoObjectStructure protoStructure))
            {
                return(false);
            }

            var maxRelocationDistance = PveSystem.SharedIsPve(true)
                                            ? MaxRelocationDistancePvE
                                            : MaxRelocationDistancePvP;

            if (objectStructure.TilePosition.TileSqrDistanceTo(toPosition)
                > maxRelocationDistance * maxRelocationDistance)
            {
                if (logErrors)
                {
                    ConstructionSystem.SharedShowCannotPlaceNotification(
                        character,
                        CoreStrings.Notification_TooFar,
                        protoStructure);
                }

                errorMessage = CoreStrings.Notification_TooFar;
                return(false);
            }

            var itemInHands = character.SharedGetPlayerSelectedHotbarItem();

            if (!(itemInHands.ProtoGameObject is IProtoItemToolToolbox))
            {
                return(false);
            }

            if (CreativeModeSystem.SharedIsInCreativeMode(character))
            {
                errorMessage = null;
                return(true);
            }

            if (!LandClaimSystem.SharedIsOwnedLand(objectStructure.TilePosition,
                                                   character,
                                                   requireFactionPermission: true,
                                                   out var hasNoFactionPermission,
                                                   ownedArea: out _) ||
                !IsOwnedLand(toPosition, out hasNoFactionPermission))
            {
                errorMessage = string.Format(CoreStrings.Faction_Permission_Required_Format,
                                             CoreStrings.Faction_Permission_LandClaimManagement_Title);

                // the building location or destination is in an area that is not owned by the player
                if (logErrors)
                {
                    SharedShowCannotRelocateNotification(
                        character,
                        protoStructure,
                        hasNoFactionPermission);
                }

                return(false);
            }

            if (LandClaimSystem.SharedIsUnderRaidBlock(character, objectStructure))
            {
                // the building is in an area under the raid
                errorMessage = LandClaimSystem.ErrorRaidBlockActionRestricted_Message;
                if (logErrors)
                {
                    ConstructionSystem.SharedShowCannotPlaceNotification(
                        character,
                        LandClaimSystem.ErrorRaidBlockActionRestricted_Message,
                        protoStructure);
                }

                return(false);
            }

            if (LandClaimShieldProtectionSystem.SharedIsUnderShieldProtection(objectStructure))
            {
                // the building is in an area under shield protection
                errorMessage = CoreStrings.ShieldProtection_ActionRestrictedBaseUnderShieldProtection;
                if (logErrors)
                {
                    LandClaimShieldProtectionSystem.SharedSendNotificationActionForbiddenUnderShieldProtection(
                        character);
                }

                return(false);
            }

            errorMessage = null;
            return(true);

            bool IsOwnedLand(
                Vector2Ushort startTilePosition,
                out bool hasNoFactionPermission)
            {
                var worldObjectLayoutTileOffsets = objectStructure.ProtoStaticWorldObject.Layout.TileOffsets;

                foreach (var tileOffset in worldObjectLayoutTileOffsets)
                {
                    if (!LandClaimSystem.SharedIsOwnedLand(startTilePosition.AddAndClamp(tileOffset),
                                                           character,
                                                           requireFactionPermission: true,
                                                           out hasNoFactionPermission,
                                                           out _))
                    {
                        return(false);
                    }
                }

                hasNoFactionPermission = false;
                return(true);
            }
        }
示例#10
0
        private void SharedOnStageCompleted()
        {
            if (!this.ValidateRequiredItemsAvailable())
            {
                // don't have required items - cannot do building/repairing action
                this.AbortAction();
                return;
            }

            if (Api.IsServer)
            {
                // items are removing only on the Server-side
                this.Config.ServerDestroyRequiredItems(this.Character);

                // notify tool was used
                ServerItemUseObserver.NotifyItemUsed(this.Character, this.ItemConstructionTool);

                // reduce tool durability
                ItemDurabilitySystem.ServerModifyDurability(this.ItemConstructionTool, delta: -1);
            }

            this.currentStageDurationSeconds     = this.CalculateStageDurationSeconds(this.Character, isFirstStage: false);
            this.currentStageTimeRemainsSeconds += this.currentStageDurationSeconds;

            var currentStructurePoints = this.ObjectPublicState.StructurePointsCurrent;
            var newStructurePoints     = currentStructurePoints
                                         + this.stageStructureAddValue;

            // Please note: as we're using floating number (StructurePointsCurrent) to track the construction progress
            // it might cause some inaccuracy. In order to avoid this inaccuracy we're adding some tolerance.
            // The tolerance is also needed to handle the case when the blueprint was damaged only slightly.
            var completionTolerance = this.stageStructureAddValue / 2.0;

            // Please note: client will simply always construct until finished
            // Server will keep building stages until the completion tolerance reached.
            if ((Api.IsClient && currentStructurePoints < this.structurePointsMax) ||
                (Api.IsServer && newStructurePoints + completionTolerance < this.structurePointsMax))
            {
                // repairing/building is still possible - more stages are available
                if (Api.IsServer)
                {
                    this.ObjectPublicState.StructurePointsCurrent = (float)newStructurePoints;
                    Api.Logger.Important(
                        $"Building/repairing progressed: {this.WorldObject} structure points: {newStructurePoints}/{this.structurePointsMax}; by {this.Character}");

                    if (this.IsRepair)
                    {
                        ((IProtoObjectStructure)this.WorldObject.ProtoStaticWorldObject)
                        .ServerOnRepairStageFinished(this.WorldObject, this.Character);
                    }
                }

                this.UpdateProgress();

                if (!this.ValidateRequiredItemsAvailable())
                {
                    // don't have enough required items - cannot continue building/repairing action
                    this.AbortAction();
                    return;
                }

                if (Api.IsServer &&
                    this.ItemConstructionTool.IsDestroyed)
                {
                    // tool was destroyed (durability 0)
                    this.AbortAction();
                    return;
                }

                return;
            }

            // repairing/building is completed
            if (Api.IsServer)
            {
                newStructurePoints = this.structurePointsMax;
                Api.Logger.Important(
                    $"Building/repairing completed: {this.WorldObject} structure points: {newStructurePoints}/{this.structurePointsMax}; by {this.Character}");
                this.ObjectPublicState.StructurePointsCurrent = (float)newStructurePoints;

                if (this.IsRepair)
                {
                    ((IProtoObjectStructure)this.WorldObject.ProtoStaticWorldObject)
                    .ServerOnRepairStageFinished(this.WorldObject, this.Character);
                }

                if (this.ObjectPublicState is ConstructionSitePublicState constructionSiteState)
                {
                    constructionSiteState.LastBuildActionDoneByCharacter = this.Character;
                    var constructionProto = ProtoObjectConstructionSite.SharedGetConstructionProto(this.WorldObject);

                    // add skill experience for building
                    this.CharacterPrivateState.Skills.ServerAddSkillExperience <SkillBuilding>(
                        SkillBuilding.ExperienceAddWhenBuildingFinished
                        * constructionProto.BuildingSkillExperienceMultiplier);
                }
                else
                {
                    // add skill experience for repair
                    this.CharacterPrivateState.Skills.ServerAddSkillExperience <SkillBuilding>(
                        SkillBuilding.ExperienceAddWhenRepairFinished
                        * ((IProtoObjectStructure)this.WorldObject.ProtoGameObject).BuildingSkillExperienceMultiplier);
                }
            }

            this.SetCompleted(isCancelled: false);
            ConstructionSystem.SharedActionCompleted(this.Character, this);
        }
示例#11
0
 protected override void AbortAction()
 {
     ConstructionSystem.SharedAbortAction(
         this.Character,
         this.WorldObject);
 }
示例#12
0
        public static bool SharedValidateCanCharacterRelocateStructure(
            ICharacter character,
            IStaticWorldObject objectStructure,
            Vector2Ushort toPosition,
            bool logErrors)
        {
            if (!SharedIsRelocatable(objectStructure))
            {
                return(false);
            }

            if (!SharedCheckTileRequirementsForRelocation(character,
                                                          objectStructure,
                                                          toPosition,
                                                          logErrors))
            {
                return(false);
            }

            if (!(objectStructure.ProtoGameObject is IProtoObjectStructure protoStructure))
            {
                return(false);
            }

            if (objectStructure.TilePosition.TileSqrDistanceTo(toPosition)
                > MaxRelocationDistance * MaxRelocationDistance)
            {
                if (logErrors)
                {
                    ConstructionSystem.SharedShowCannotPlaceNotification(
                        character,
                        CoreStrings.Notification_TooFar,
                        protoStructure);
                }

                return(false);
            }

            var itemInHands = character.SharedGetPlayerSelectedHotbarItem();

            if (!(itemInHands.ProtoGameObject is IProtoItemToolToolbox))
            {
                return(false);
            }

            if (CreativeModeSystem.SharedIsInCreativeMode(character))
            {
                return(true);
            }

            if (!LandClaimSystem.SharedIsOwnedLand(objectStructure.TilePosition, character, out _) ||
                !IsOwnedLand(toPosition))
            {
                // the building location or destination is in an area that is not owned by the player
                if (logErrors)
                {
                    ConstructionSystem.SharedShowCannotPlaceNotification(
                        character,
                        LandClaimSystem.ErrorNotLandOwner_Message,
                        protoStructure);
                }

                return(false);
            }

            if (LandClaimSystem.SharedIsUnderRaidBlock(character, objectStructure))
            {
                // the building is in an area under the raid
                if (logErrors)
                {
                    ConstructionSystem.SharedShowCannotPlaceNotification(
                        character,
                        LandClaimSystem.ErrorRaidBlockActionRestricted_Message,
                        protoStructure);
                }

                return(false);
            }

            if (LandClaimShieldProtectionSystem.SharedIsUnderShieldProtection(objectStructure))
            {
                // the building is in an area under shield protection
                if (logErrors)
                {
                    LandClaimShieldProtectionSystem.SharedSendNotificationActionForbiddenUnderShieldProtection(
                        character);
                }

                return(false);
            }

            return(true);

            bool IsOwnedLand(Vector2Ushort startTilePosition)
            {
                var worldObjectLayoutTileOffsets = objectStructure.ProtoStaticWorldObject.Layout.TileOffsets;

                foreach (var tileOffset in worldObjectLayoutTileOffsets)
                {
                    if (!LandClaimSystem.SharedIsOwnedLand(startTilePosition.AddAndClamp(tileOffset), character, out _))
                    {
                        return(false);
                    }
                }

                return(true);
            }
        }
示例#13
0
        private void SharedOnStageCompleted()
        {
            if (!this.ValidateRequiredItemsAvailable())
            {
                // don't have required items - cannot do building/repairing action
                this.AbortAction();
                return;
            }

            if (Api.IsServer)
            {
                // items are removing only on the Server-side
                this.Config.ServerDestroyRequiredItems(this.Character);

                // notify tool was used
                ServerItemUseObserver.NotifyItemUsed(this.Character, this.ItemConstructionTool);

                // reduce tool durability
                ItemDurabilitySystem.ServerModifyDurability(this.ItemConstructionTool, delta: -1);
            }

            this.currentStageDurationSeconds     = this.CalculateStageDurationSeconds(this.Character, isFirstStage: false);
            this.currentStageTimeRemainsSeconds += this.currentStageDurationSeconds;

            var newStructurePoints = this.ObjectPublicState.StructurePointsCurrent
                                     + this.stageStructureAddValue;

            // Please note: as we're using floating number (StructurePointsCurrent) to track the construction progress
            // it might cause some inaccuracy. In order to avoid this inaccuracy we're adding some tolerance.
            // The tolerance is also needed to handle the case when the blueprint was damaged only slightly.
            var completionTolerance = this.stageStructureAddValue / 2.0;

            if (newStructurePoints + completionTolerance < this.structurePointsMax)
            {
                // repairing/building is still possible - more stages are available
                if (Api.IsServer)
                {
                    this.ObjectPublicState.StructurePointsCurrent = (float)newStructurePoints;
                    Api.Logger.Important(
                        $"Building/repairing progressed: {this.WorldObject} structure points: {newStructurePoints}/{this.structurePointsMax}; by {this.Character}");
                }

                this.UpdateProgress();

                if (!this.ValidateRequiredItemsAvailable())
                {
                    // don't have enough required items - cannot continue building/repairing action
                    this.AbortAction();
                    return;
                }

                if (Api.IsServer &&
                    this.ItemConstructionTool.IsDestroyed)
                {
                    // tool was destroyed (durability 0)
                    this.AbortAction();
                    return;
                }

                return;
            }

            // repairing/building is completed
            if (Api.IsServer)
            {
                // add building skill experience
                this.CharacterPrivateState.Skills.ServerAddSkillExperience <SkillBuilding>(
                    SkillBuilding.ExperienceAddWhenBuildingFinished);

                newStructurePoints = this.structurePointsMax;
                Api.Logger.Important(
                    $"Building/repairing completed: {this.WorldObject} structure points: {newStructurePoints}/{this.structurePointsMax}; by {this.Character}");
                this.ObjectPublicState.StructurePointsCurrent = (float)newStructurePoints;

                if (this.ObjectPublicState is ConstructionSitePublicState constructionSiteState)
                {
                    constructionSiteState.LastBuildActionDoneByCharacter = this.Character;
                }
            }
            else
            {
                // play success sound
                /*this.TargetWorldObject.ProtoWorldObject.*/
                Api.GetProtoEntity <ObjectConstructionSite>().SharedGetObjectSoundPreset()
                .PlaySound(ObjectSound.InteractSuccess);
            }

            this.SetCompleted(isCancelled: false);
            ConstructionSystem.SharedActionCompleted(this.Character, this);
        }