Space Engineers Paint-Gun Mod Skin Patch (listed) 09.12.2025 02:01:30 (456 lines) [Edit] [Raw] [View]

using System; using System.Collections.Generic; using Digi.ComponentLib; using Digi.PaintGun.Utilities; using Sandbox.Game.Entities; using Sandbox.ModAPI; using VRage.Game; using VRage.Game.ModAPI; using VRage.Utils; using VRageMath;

namespace Digi.PaintGun.Features.Palette { public class Painting : ModComponent { struct CheckData { public readonly MyStringHash SkinId; public readonly int ReadAtTick;

        public CheckData(MyStringHash skinId)
        {
            SkinId = skinId;
            ReadAtTick = PaintGunMod.Instance.Tick + Constants.TICKS_PER_SECOND * 2;
        }
    }

    class QueueReplaceData
    {
        public const int ChunkSize = 10000;
        public const int TickDelay = 30;

        public readonly HashSet Grids = new HashSet();
        public readonly List Blocks = new List(1024);
        public int BlockIndex = 0;

        public readonly PaintMaterial Paint;
        public readonly bool ByLocalPlayer;
        public readonly bool Sync;

        public int NextTick;

        public QueueReplaceData(PaintMaterial paint, bool byLocalPlayer, bool sync)
        {
            Paint = paint;
            ByLocalPlayer = byLocalPlayer;
            Sync = sync;
            NextTick = PaintGunMod.Instance.Tick + TickDelay;
        }
    }

    readonly List QueueReplaceRequests = new List(0);

    readonly Dictionary CheckSkinned = new Dictionary(0);
    readonly List RemoveCheckKeys = new List(0);

    readonly HashSet TempConnectedGrids = new HashSet(0);

    public Painting(PaintGunMod main) : base(main)
    {
    }

    protected override void RegisterComponent()
    {
    }

    protected override void UnregisterComponent()
    {
    }

    protected override void UpdateAfterSim(int tick)
    {
        bool a = CheckSkinsApplied();
        bool b = ProcessQueueReplace();

        if(!a && !b)
        {
            SetUpdateMethods(UpdateFlags.UPDATE_AFTER_SIM, false);
        }
    }

    bool CheckSkinsApplied()
    {
        if(CheckSkinned.Count == 0)
            return false;

        int tick = Main.Tick;
        if(tick % 30 != 0)
            return true;

        // see if skin was applied and warn accordingly.
        foreach(KeyValuePair kv in CheckSkinned)
        {
            IMySlimBlock slim = kv.Key;
            CheckData data = kv.Value;

            if(data.ReadAtTick > tick)
                continue;

            if(slim.SkinSubtypeId != data.SkinId)
            {
                SkinInfo skinInfo = Main.Palette.GetSkinInfo(data.SkinId);
                string name = skinInfo?.Name ?? $"Unknown:{data.SkinId.String}";
                Main.Notifications.Show(3, $"[{name}] skin not applied, likely not owned. Consider hiding it in PaintGun's config (Chat then F2).", MyFontEnum.Red, 5000);
            }

            RemoveCheckKeys.Add(slim);
        }

        foreach(IMySlimBlock key in RemoveCheckKeys)
        {
            CheckSkinned.Remove(key);
        }

        RemoveCheckKeys.Clear();

        return CheckSkinned.Count > 0;
    }

    bool ProcessQueueReplace()
    {
        if(QueueReplaceRequests.Count == 0)
            return false;

        QueueReplaceData request = QueueReplaceRequests[0];

        if(request.ByLocalPlayer)
        {
            string text = $"Replacing... {request.Blocks.Count - request.BlockIndex} remaining";
            Main.SelectionGUI.SetGUIStatus(0, text);
            Main.Notifications.Show(0, text, MyFontEnum.Debug, 16 * 30 + 100);
        }
        //else
        //{
        //    Main.SelectionGUI.SetGUIStatus(0, "Other replace request in progress...", "red");
        //}

        int tick = Main.Tick;
        if(request.NextTick > tick)
            return true;

        request.NextTick = tick + QueueReplaceData.TickDelay;

        PaintMaterial paint = request.Paint;
        bool sync = request.Sync;
        bool queueCheck = paint.Skin.HasValue; // only care if new paint affects skin

        int maxLen = Math.Min(request.BlockIndex + QueueReplaceData.ChunkSize, request.Blocks.Count);

        for(int i = request.BlockIndex; i < maxLen; i++)
        {
            IMySlimBlock block = request.Blocks[i];

            if(sync)
            {
                block.CubeGrid.SkinBlocks(block.Min, block.Min, paint.ColorMask, paint.Skin?.String);

                if(queueCheck)
                {
                    queueCheck = false; // only check first block
                    CheckSkinned[block] = new CheckData(paint.Skin.Value); // replace, in case they swap out skins quickly
                }
            }
            else
            {
                var internalGrid = (MyCubeGrid)block.CubeGrid;
                internalGrid.ChangeColorAndSkin(internalGrid.GetCubeBlock(block.Min), paint.ColorMask, paint.Skin);

                if(queueCheck)
                {
                    queueCheck = false; // only check first block
                    CheckSkinned.Remove(block);
                }
            }
        }

        request.BlockIndex += QueueReplaceData.ChunkSize;

        if(request.BlockIndex < request.Blocks.Count)
        {
            return true;
        }
        else
        {
            if(request.ByLocalPlayer)
            {
                Main.SelectionGUI.SetGUIStatus(0, "Finished replacing", "lime");
                Main.Notifications.Show(2, "Finished replacing!", MyFontEnum.Green, 3000);
            }

            QueueReplaceRequests.RemoveAtFast(0);
            return QueueReplaceRequests.Count > 0;
        }
    }

    public void ToolPaintBlock(IMyCubeGrid grid, Vector3I gridPosition, PaintMaterial paint, bool useMirroring)
    {
        if(paint.Skin.HasValue)
        {
            // vanilla DLC-locked skins should use API, mod-added should use the packet to force skin change.
            SkinInfo skin = Main.Palette.GetSkinInfo(paint.Skin.Value);
            if(!skin.AlwaysOwned)
            {
                if(useMirroring && (grid.XSymmetryPlane.HasValue || grid.YSymmetryPlane.HasValue || grid.ZSymmetryPlane.HasValue))
                {
                    MirrorData mirrorData = new MirrorData(grid);
                    PaintBlockSymmetry(true, grid, gridPosition, paint, mirrorData, MyAPIGateway.Multiplayer.MyId);
                    // no ammo consumption, it's creative usage anyway
                }
                else
                {
                    //Main.Notifications.Show(3, "DEBUG Force Skin", MyFontEnum.Green, 1000);
                    PaintBlock(false, grid, gridPosition, paint, MyAPIGateway.Multiplayer.MyId); //XXX: RR
                    Main.NetworkLibHandler.PacketPaint.Send(grid, gridPosition, paint, false); //XXX: RR
                    Main.NetworkLibHandler.PacketConsumeAmmo.Send();
                }

                return;
            }
        }

        // for mod-added skins:
        Main.NetworkLibHandler.PacketPaint.Send(grid, gridPosition, paint, useMirroring);
    }

    public void ToolReplacePaint(IMyCubeGrid grid, BlockMaterial oldPaint, PaintMaterial paint, bool includeSubgrids)
    {
        // TODO: a way to do this for clients receiving game-synced color requests...
        foreach(QueueReplaceData request in QueueReplaceRequests)
        {
            if(request.Grids.Contains(grid))
            {
                Main.SelectionGUI.SetGUIStatus(0, "Previous replace still in progress...", "red");
                Main.Notifications.Show(2, "Grid has replace color in progress!", MyFontEnum.Red, 3000);
                return;
            }
        }

        // no longer using this because game crashes from too many messages with emissive blocks...
        //if(paint.Skin.HasValue)
        //{
        //    // vanilla DLC-locked skins should use API, mod-added should use the packet to force skin change.
        //    SkinInfo skin = Main.Palette.GetSkinInfo(paint.Skin.Value);
        //    if(!skin.AlwaysOwned)
        //    {
        //        ReplaceColorInGrid(true, grid, oldPaint, paint, includeSubgrids, MyAPIGateway.Multiplayer.MyId);
        //        return;
        //    }
        //}

        // for mod-added skins:
        Main.NetworkLibHandler.PacketReplacePaint.Send(grid, oldPaint, paint, includeSubgrids);
    }

    /// 
    ///  arg determines if it sends the paint request using the API, and automatically checks skin ownership. Must be false for mod-added skins.
    /// 
    public void PaintBlock(bool sync, IMyCubeGrid grid, Vector3I gridPosition, PaintMaterial paint, ulong originalSenderSteamId)
    {
        IMySlimBlock slim = grid.GetCubeBlock(gridPosition);

        if(sync)
        {
            grid.SkinBlocks(gridPosition, gridPosition, paint.ColorMask, paint.Skin?.String);

            if(paint.Skin.HasValue)
            {
                // check if skin was applied to alert player
                CheckSkinned[slim] = new CheckData(paint.Skin.Value); // add or replace

                SetUpdateMethods(UpdateFlags.UPDATE_AFTER_SIM, true);
            }
        }
        else
        {
            // NOTE getting a MySlimBlock and sending it straight to arguments avoids getting prohibited errors.
            MyCubeGrid gridInternal = (MyCubeGrid)grid;
            gridInternal.ChangeColorAndSkin(gridInternal.GetCubeBlock(gridPosition), paint.ColorMask, paint.Skin);

            if(paint.Skin.HasValue)
            {
                CheckSkinned.Remove(slim); // prevent alerting if skin gets changed into an always-owned one
            }
        }
    }

    /// 
    ///  arg determines if it sends the paint request using the API, and automatically checks skin ownership. Must be false for mod-added skins.
    /// 
    public void PaintBlockSymmetry(bool sync, IMyCubeGrid grid, Vector3I gridPosition, PaintMaterial paint, MirrorData mirrorData, ulong originalSenderSteamId)
    {
        PaintBlock(sync, grid, gridPosition, paint, originalSenderSteamId);

        List alreadyMirrored = Main.Caches.AlreadyMirrored;
        alreadyMirrored.Clear();

        Vector3I? mirrorX = MirrorPaint(sync, grid, 0, mirrorData, mirrorData.OddX, gridPosition, paint, alreadyMirrored, originalSenderSteamId); // X
        Vector3I? mirrorY = MirrorPaint(sync, grid, 1, mirrorData, mirrorData.OddY, gridPosition, paint, alreadyMirrored, originalSenderSteamId); // Y
        Vector3I? mirrorZ = MirrorPaint(sync, grid, 2, mirrorData, mirrorData.OddZ, gridPosition, paint, alreadyMirrored, originalSenderSteamId); // Z
        Vector3I? mirrorYZ = null;

        if(mirrorX.HasValue && mirrorData.Y.HasValue) // XY
            MirrorPaint(sync, grid, 1, mirrorData, mirrorData.OddY, mirrorX.Value, paint, alreadyMirrored, originalSenderSteamId);

        if(mirrorX.HasValue && mirrorData.Z.HasValue) // XZ
            MirrorPaint(sync, grid, 2, mirrorData, mirrorData.OddZ, mirrorX.Value, paint, alreadyMirrored, originalSenderSteamId);

        if(mirrorY.HasValue && mirrorData.Z.HasValue) // YZ
            mirrorYZ = MirrorPaint(sync, grid, 2, mirrorData, mirrorData.OddZ, mirrorY.Value, paint, alreadyMirrored, originalSenderSteamId);

        if(mirrorData.X.HasValue && mirrorYZ.HasValue) // XYZ
            MirrorPaint(sync, grid, 0, mirrorData, mirrorData.OddX, mirrorYZ.Value, paint, alreadyMirrored, originalSenderSteamId);
    }

    Vector3I? MirrorPaint(bool sync, IMyCubeGrid grid, int axis, MirrorData mirrorPlanes, bool odd, Vector3I originalPosition, PaintMaterial paint, List alreadyMirrored, ulong originalSenderSteamId)
    {
        Vector3I? mirrorPosition = null;

        switch(axis)
        {
            case 0:
                if(mirrorPlanes.X.HasValue)
                    mirrorPosition = originalPosition + new Vector3I(((mirrorPlanes.X.Value - originalPosition.X) * 2) - (odd ? 1 : 0), 0, 0);
                break;
            case 1:
                if(mirrorPlanes.Y.HasValue)
                    mirrorPosition = originalPosition + new Vector3I(0, ((mirrorPlanes.Y.Value - originalPosition.Y) * 2) - (odd ? 1 : 0), 0);
                break;
            case 2:
                if(mirrorPlanes.Z.HasValue)
                    mirrorPosition = originalPosition + new Vector3I(0, 0, ((mirrorPlanes.Z.Value - originalPosition.Z) * 2) + (odd ? 1 : 0)); // reversed on odd
                break;
        }

        if(mirrorPosition.HasValue && originalPosition != mirrorPosition.Value && !alreadyMirrored.Contains(mirrorPosition.Value) && grid.CubeExists(mirrorPosition.Value))
        {
            alreadyMirrored.Add(mirrorPosition.Value);
            PaintBlock(sync, grid, mirrorPosition.Value, paint, originalSenderSteamId);
        }

        return mirrorPosition;
    }

    /// 
    ///  arg determines if it sends the paint request using the API, and automatically checks skin ownership. Must be false for mod-added skins.
    /// 
    public void ReplaceColorInGrid(bool sync, IMyCubeGrid selectedGrid, BlockMaterial oldPaint, PaintMaterial paint, bool includeSubgrids, ulong originalSenderSteamId)
    {
        //long timeStart = Stopwatch.GetTimestamp();

        TempConnectedGrids.Clear();

        if(includeSubgrids)
            MyAPIGateway.GridGroups.GetGroup(selectedGrid, GridLinkTypeEnum.Mechanical, TempConnectedGrids);
        else
            TempConnectedGrids.Add(selectedGrid);

        bool checkFirstSkinned = paint.Skin.HasValue; // only care if new paint affects skin
        bool byLocalPlayer = originalSenderSteamId == MyAPIGateway.Multiplayer.MyId;

        //int total = 0;
        int affected = 0;

        QueueReplaceData queue = null;

        foreach(IMyCubeGrid grid in TempConnectedGrids)
        {
            MyCubeGrid internalGrid = (MyCubeGrid)grid;

            // avoiding GetCubeBlock() lookup by feeding MySlimBlock directly
            // must remain `var` because it's uses a prohibited type in the generic.
            var enumerator = internalGrid.CubeBlocks.GetEnumerator();
            try
            {
                while(enumerator.MoveNext())
                {
                    IMySlimBlock block = enumerator.Current;
                    BlockMaterial blockMaterial = new BlockMaterial(block);

                    if(paint.Skin.HasValue && blockMaterial.Skin != oldPaint.Skin)
                        continue;

                    if(paint.ColorMask.HasValue && !Utils.ColorMaskEquals(blockMaterial.ColorMask, oldPaint.ColorMask))
                        continue;

                    affected++;

                    if(affected > QueueReplaceData.ChunkSize) // can crash with too many blocks, painting them in chunks after a certain amount
                    {
                        if(queue == null)
                        {
                            queue = new QueueReplaceData(paint, byLocalPlayer, sync);
                            QueueReplaceRequests.Add(queue);
                        }

                        queue.Blocks.Add(block);
                        queue.Grids.Add(grid);
                        continue;
                    }

                    if(sync)
                    {
                        grid.SkinBlocks(block.Min, block.Min, paint.ColorMask, paint.Skin?.String);

                        if(checkFirstSkinned)
                        {
                            checkFirstSkinned = false; // only check first block
                            CheckSkinned[block] = new CheckData(paint.Skin.Value); // replace, in case they swap out skins quickly
                        }
                    }
                    else
                    {
                        internalGrid.ChangeColorAndSkin(enumerator.Current, paint.ColorMask, paint.Skin);

                        if(checkFirstSkinned)
                        {
                            checkFirstSkinned = false; // only check first block
                            CheckSkinned.Remove(block);
                        }
                    }
                }
            }
            finally
            {
                enumerator.Dispose();
            }

            //total += grid.CubeBlocks.Count;
        }

        if(queue != null || CheckSkinned.Count > 0)
        {
            SetUpdateMethods(UpdateFlags.UPDATE_AFTER_SIM, true);
        }

        //long timeEnd = Stopwatch.GetTimestamp();

        if(byLocalPlayer)
        {
            if(queue != null)
                Main.Notifications.Show(2, $"Queued replaced color for {affected.ToString()} blocks.", MyFontEnum.Debug, 5000);
            else
                Main.Notifications.Show(2, $"Replaced color for {affected.ToString()} blocks.", MyFontEnum.Debug, 5000);

            //double seconds = (timeEnd - timeStart) / (double)Stopwatch.Frequency;

            //if(affected == total)
            //    Main.Notifications.Show(2, $"Replaced color for all {affected.ToString()} blocks in {(seconds * 1000).ToString("0.######")}ms", MyFontEnum.White, 5000);
            //else
            //    Main.Notifications.Show(2, $"Replaced color for {affected.ToString()} of {total.ToString()} blocks in {(seconds * 1000).ToString("0.######")}ms", MyFontEnum.White, 5000);
        }

        TempConnectedGrids.Clear();
    }
}

}