/// <summary>
        /// Quickly replace a certain file in rom
        /// Does some hacks, leaves old file contents, so it should not be used repeatedly
        /// </summary>
        public static void ReplaceFile(string inromPath, Stream outrom, Stream injfile, string targetName)
        {
            using var inrom       = File.OpenRead(inromPath);
            using var readableRom = new FileReadableGameArchive(inromPath);

            var header = new RomHeader();

            inrom.Read(SpanUtil.AsBytes(ref header));

            outrom.Seek(0, SeekOrigin.Begin);
            inrom.Seek(0, SeekOrigin.Begin);
            inrom.CopyTo(outrom);

            var fileEntry         = readableRom.FindFile(targetName);
            var rawEntry          = new RomEntry();
            var rawEntryOffset    = Marshal.SizeOf <RomHeader>() + fileEntry.SourceOffset;
            var rawEntryBytesSpan = SpanUtil.AsBytes(ref rawEntry);

            outrom.Seek(rawEntryOffset, SeekOrigin.Begin);
            outrom.Read(rawEntryBytesSpan);
            outrom.Seek(rawEntryOffset, SeekOrigin.Begin);

            var newDataOffset = outrom.Length;

            while (newDataOffset % header.offsetMultiplier != 0)
            {
                newDataOffset++;
            }

            rawEntry.RawDataOffset = newDataOffset / header.offsetMultiplier;
            rawEntry.DataSize      = (int)injfile.Length;

            outrom.Write(rawEntryBytesSpan);

            outrom.Seek(newDataOffset, SeekOrigin.Begin);

            injfile.CopyTo(outrom);
        }
예제 #2
0
        public static unsafe void EncodePicture(Stream outpic, Image <Rgba32> image,
                                                int effectiveWidth, int effectiveHeight, uint pictureId, Origin origin, ShinTextureCompress.FragmentCompressionConfig fragmentCompressionConfig)
        {
            Trace.Assert(effectiveWidth > 0 && effectiveHeight > 0);

            // Split image into fragments
            // this algorithm does not match the one used originally __exactly__, but seems to give close results
            // probably the order of checks and size transformations is different in the tool
            // this is good enough for me :shrug:
            int minX = int.MaxValue,
                maxX = int.MinValue,
                minY = int.MaxValue,
                maxY = int.MinValue;

            Trace.Assert(effectiveHeight <= image.Height);
            Trace.Assert(effectiveWidth <= image.Width);

            for (var j = 0; j < effectiveHeight; j++)
            {
                var row = image.GetPixelRowSpan(j);
                for (var i = 0; i < effectiveWidth; i++)
                {
                    if (row[i].A > 0)
                    {
                        minX = Math.Min(minX, i);
                        minY = Math.Min(minY, j);
                        maxX = Math.Max(maxX, i);
                        maxY = Math.Max(maxY, j);
                    }
                }
            }

            var preliminaryFragments = new List <Rectangle>();

            for (var j = minY; j <= maxY; j += MagicHeight)
            {
                for (var i = minX; i <= maxX; i += MagicWidth)
                {
                    var h = Math.Min(j + MagicHeight, effectiveHeight) - j;

                    while (i <= maxX && IsBorderEmptyV(image, i, j, h))
                    {
                        i++;
                    }
                    if (i > maxX)
                    {
                        break;
                    }

                    var w = Math.Min(i + MagicWidth, effectiveWidth) - i;

                    preliminaryFragments.Add(new Rectangle(i, j, w, h));
                }
            }

            var fragments = preliminaryFragments
                            .Select(frag =>
            {
                // make sure it's in the image bounds
                var shrinkY  = frag.Bottom - Math.Min(frag.Bottom, maxY + 1);
                var shrinkX  = frag.Right - Math.Min(frag.Right, maxX + 1);
                frag.Width  -= shrinkX;
                frag.Height -= shrinkY;
                if (frag.Width <= 0 && frag.Height <= 0)
                {
                    return(frag);
                }

                // shrink the top
                while (frag.Width > 0 && frag.Height > 0 && IsBorderEmptyH(image, frag.X, frag.Top, frag.Width))
                {
                    frag.Y++;
                    frag.Height--;
                }

                // shrink the bottom
                while (frag.Width > 0 && frag.Height > 0 && IsBorderEmptyH(image, frag.X, frag.Bottom - 1, frag.Width))
                {
                    frag.Height--;
                }

                // shrink the left
                while (frag.Width > 0 && frag.Height > 0 && IsBorderEmptyV(image, frag.Left, frag.Y, frag.Height))
                {
                    frag.X++;
                    frag.Width--;
                }

                // shrink the right
                while (frag.Width > 0 && frag.Height > 0 && IsBorderEmptyV(image, frag.Right - 1, frag.Y, frag.Height))
                {
                    frag.Width--;
                }

                return(frag);
            })
                            .Where(frag => frag.Width > 0 && frag.Height > 0)
                            .ToImmutableArray();

            // we have one more padding to add: in case if fragment does not have directly adjacent fragments to the right or bottom,
            //   it's width or height (respectively) needs to be incremented

            // unfortunately linear, which makes the algorithm O(n^2)
            // but hey, nobody will pass huge pictures here, right?..
            bool CheckAdj(bool isBottom, Rectangle rect)
            {
                var rectButt = isBottom
                    ? new Rectangle(rect.X, rect.Bottom - 1, rect.Width, 1)
                    : new Rectangle(rect.Right - 1, rect.Y, 1, rect.Height);

                //? new Rectangle(rect.X + 1, rect.Bottom - 1, rect.Width - 2, 1)
                //: new Rectangle(rect.Right - 1, rect.Y + 1, 1, rect.Height - 2);

                Trace.Assert(rectButt.Width > 0 && rectButt.Height > 0);

                for (var i = 0; i < fragments.Length; i++)
                {
                    if (fragments[i] != rect && fragments[i].IntersectsWith(rectButt))
                    {
                        return(true);
                    }
                }
                return(false);
            }

            fragments = fragments
                        .Select(frag =>
            {
                // add another bit of padding to match the original encoder results
                frag.X      -= 1;
                frag.Width  += 2;
                frag.Y      -= 1;
                frag.Height += 2;
                if (frag.X < 0)
                {
                    frag.X = 0;
                }
                if (frag.Y <= 0)
                {
                    frag.Y = 0;
                }
                return(frag);
            })
                        .Select(frag =>
            {
                // hello, O(n^2), my old friend...
                if (!CheckAdj(false, frag))
                {
                    frag.Width++;
                }
                if (!CheckAdj(true, frag))
                {
                    frag.Height++;
                }
                return(frag);
            }).ToImmutableArray();

            //Console.WriteLine(JsonConvert.SerializeObject(fragments.Select(f => new
            //    {f.X, f.Y, f.Width, f.Height}), Formatting.Indented));

            // now we need to do the dedup
            // notice: dedup works before the quantization, so it might not catch all cases
            // nah, should be fine
            uint HashFragment(Rectangle rect)
            {
                rect.Deconstruct(out var dx, out var dy, out var width, out var height);
                if (dx + width > image.Width)
                {
                    width -= dx + width - image.Width;
                }
                if (dy + height > image.Height)
                {
                    height -= dy + height - image.Height;
                }
                HashSet <Rgba32> values = new();

                uint hash = 5381;

                for (var j = dy; j < dy + height; j++)
                {
                    for (var i = dx; i < dx + width; i++)
                    {
                        var v = image[i, j];
                        if (v.A == 0)
                        {
                            v = Rgba32.Transparent;
                        }
                        hash = ((hash << 5) + hash) + v.PackedValue;
                    }
                }

                return(hash);
            }

            bool CompareFragments(Rectangle a, Rectangle b)
            {
                a.Intersect(image.Bounds());
                b.Intersect(image.Bounds());
                if (a.Size != b.Size)
                {
                    return(false);
                }

                for (var j = 0; j < a.Height; j++)
                {
                    var rowA = image.GetPixelRowSpan(a.Y + j).Slice(a.X, a.Width);
                    var rowB = image.GetPixelRowSpan(b.Y + j).Slice(b.X, b.Width);
                    if (!rowA.SequenceEqual(rowB))
                    {
                        return(false);
                    }
                }

                return(true);
            }

            var hashToIndex = new Dictionary <uint, List <int> >();

            for (var i = 0; i < fragments.Length; i++)
            {
                var frag      = fragments[i];
                var hashValue = HashFragment(frag);
                if (!hashToIndex.TryGetValue(hashValue, out var list))
                {
                    hashToIndex[hashValue] = list = new List <int>();
                }
                list.Add(i);
            }

            var physicalFragments = new List <Rectangle>();
            //var virtualFragmentsToPhysical = new Dictionary<int, int>();
            var physicalFragmentsToVirtual = new Dictionary <int, List <int> >();

            foreach (var(_, v) in hashToIndex)
            {
                var hs = v.ToHashSet();
                while (hs.Count > 0)
                {
                    var index      = hs.First();
                    var sameValues = new List <int> {
                        index
                    };
                    sameValues.AddRange(hs
                                        .Where(i => i != index && CompareFragments(fragments[i], fragments[index])));
                    foreach (var i in sameValues)
                    {
                        hs.Remove(i);
                    }

                    var physicalIndex = physicalFragments.Count;
                    physicalFragments.Add(fragments[index]);
                    physicalFragmentsToVirtual[physicalIndex] = sameValues;
                    //foreach (var i in sameValues)
                    //    virtualFragmentsToPhysical.Add(i, physicalIndex);
                }
            }


            var(originX, originY) = origin switch
            {
                Origin.TopLeft => (0, 0),
                Origin.Top => (effectiveWidth / 2, 0),
                Origin.TopRight => (effectiveWidth, 0),
                Origin.Left => (0, effectiveHeight / 2),
                Origin.Center => (effectiveWidth / 2, effectiveHeight / 2),
                Origin.Right => (effectiveWidth, effectiveHeight / 2),
                Origin.BottomLeft => (0, effectiveHeight),
                Origin.Bottom => (effectiveWidth / 2, effectiveHeight),
                Origin.BottomRight => (effectiveWidth, effectiveHeight),
                _ => throw new ArgumentOutOfRangeException(nameof(origin), origin, null)
            };

            var header = new PicHeader
            {
                magic   = 0x34434950,
                version = 2,
                // fileSize!
                effectiveHeight = checked ((ushort)effectiveHeight),
                effectiveWidth  = checked ((ushort)effectiveWidth),
                entryCount      = checked ((ushort)fragments.Length),
                originX         = checked ((ushort)originX),
                originY         = checked ((ushort)originY),
                field20         = 1, // this value is set in __most__ pictures, excluding __some__ from /picture/e/ directory
                pictureId       = pictureId
            };

            var dataOffset    = sizeof(PicHeader) + fragments.Length * sizeof(PicHeaderFragmentEntry);
            var currentOffset = dataOffset;

            outpic.Seek(dataOffset, SeekOrigin.Begin);

            var fragmentEntries = new PicHeaderFragmentEntry[fragments.Length];

            foreach (var(i, frag) in physicalFragments.Select((x, i) => (i, x)))
            {
                var p1 = outpic.Position;
                var sz = ShinTextureCompress.EncodeImageFragment(outpic, image, frag.X, frag.Y,
                                                                 0, 0, frag.Width, frag.Height, fragmentCompressionConfig);
                var p2 = outpic.Position;
                Debug.Assert(p2 - p1 == sz);
                foreach (var virtualIndex in physicalFragmentsToVirtual[i])
                {
                    var virtualRect = fragments[virtualIndex];
                    fragmentEntries[virtualIndex] = new PicHeaderFragmentEntry
                    {
                        x      = checked ((ushort)virtualRect.X),
                        y      = checked ((ushort)virtualRect.Y),
                        offset = checked ((uint)currentOffset),
                        size   = checked ((uint)sz),
                    };
                }
                currentOffset += sz;
            }

            Trace.Assert(currentOffset == outpic.Length);

            var fragmentEntriesArray = fragmentEntries.ToImmutableArray();

            outpic.Seek(0, SeekOrigin.Begin);
            header.fileSize = checked ((uint)currentOffset);
            outpic.Write(SpanUtil.AsBytes(ref header));
            outpic.Write(MemoryMarshal.Cast <PicHeaderFragmentEntry, byte>(fragmentEntriesArray.AsSpan()));

            Trace.Assert(dataOffset == outpic.Position);
        }