在Galgame引擎Favorite View Point System(FVP)立绘合成的适配工作基本完成后,紧接着开始Galgame引擎Siglus Engine立绘合成的适配工作。

探索引擎Siglus Engine的立绘合成方式

对于Galgame引擎Siglus Engine来说,它的大部分资源基本都没有进行封包处理,而是直接按照文件类型直接放到各个文件夹内。这里以Summer Pockets REFLECTION BLUE为例,根目录里g00文件夹下的文件分布如下:

Galgame Image Cover Tools开发日记四——引擎Siglus Engine的适配1.jpg

我们可以发现,文件夹里的文件都为.g00文件,这是引擎Siglus Engine里一种特别的文件格式,专门用来存储图像资源的。

还是老样子,把其中一个文件使用16进制编辑器打开,发现文件头部分还是那样子,全是乱糟糟的16进制数据。在没有弄清楚文件头那一大堆数据如何读取之前,还是根本无从下手……

这里,我们还是请教了我们伟大的GitHub老师,在Garbro项目里,也就是这个Galagme基本万能的解包工具,里面就有相关.g00文件的解析源代码,使用C#语言编写:

//! \file       ImageG00.cs
//! \date       Mon Apr 18 14:06:48 2016
//! \brief      RealLive engine image format.
//
// Copyright (C) 2016 by morkt
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
//

using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.IO;
using System.Linq;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using GameRes.Utility;

namespace GameRes.Formats.RealLive
{
    internal class G00MetaData : ImageMetaData
    {
        public int  Type;
    }

    [Export(typeof(ImageFormat))]
    public class G00Format : ImageFormat
    {
        public override string         Tag { get { return "G00"; } }
        public override string Description { get { return "RealLive engine image format"; } }
        public override uint     Signature { get { return 0; } }

        public override ImageMetaData ReadMetaData (IBinaryStream file)
        {
            int type = file.ReadByte();
            if (type > 2)
                return null;
            uint width  = file.ReadUInt16();
            uint height = file.ReadUInt16();
            if (0 == width || width > 0x8000 || 0 == height || height > 0x8000)
                return null;
            if (2 == type)
            {
                int count = file.ReadInt32();
                if (count <= 0 || count > 0x1000)
                    return null;
            }
            else
            {
                uint length = file.ReadUInt32();
                if (length + 5 != file.Length)
                    return null;
            }
            return new G00MetaData {
                Width  = width,
                Height = height,
                BPP    = 1 == type ? 8 : 24,
                Type   = type,
            };
        }

        public override ImageData Read (IBinaryStream stream, ImageMetaData info)
        {
            using (var reader = new G00Reader (stream, (G00MetaData)info))
            {
                reader.Unpack();
                return ImageData.Create (info, reader.Format, reader.Palette, reader.Data);
            }
        }

        public override void Write (Stream file, ImageData image)
        {
            throw new NotImplementedException ("G00Format.Write not implemented");
        }
    }

    internal class Tile
    {
        public int  X;
        public int  Y;
        public uint Offset;
        public int  Length;
    }

    internal sealed class G00Reader : IDisposable
    {
        IBinaryStream   m_input;
        byte[]          m_output;
        int             m_width;
        int             m_height;
        int             m_type;

        public byte[]           Data { get { return m_output; } }
        public PixelFormat    Format { get; private set; }
        public BitmapPalette Palette { get; private set; }

        public G00Reader (IBinaryStream input, G00MetaData info)
        {
            m_width = (int)info.Width;
            m_height = (int)info.Height;
            m_type = info.Type;
            m_input = input;
        }

        public void Unpack ()
        {
            m_input.Position = 5;
            if (0 == m_type)
                UnpackV0();
            else if (1 == m_type)
                UnpackV1();
            else
                UnpackV2();
        }

        void UnpackV0 ()
        {
            m_output = LzDecompress (m_input, 1, 3);
            Format = PixelFormats.Bgr24;
        }

        void UnpackV1 ()
        {
            m_output = LzDecompress (m_input, 2, 1);
            int colors = LittleEndian.ToUInt16 (m_output, 0);
            int src = 2;
            var palette = new Color[colors];
            for (int i = 0; i < colors; ++i)
            {
                palette[i] = Color.FromArgb (m_output[src+3], m_output[src+2], m_output[src+1], m_output[src]);
                src += 4;
            }
            Palette = new BitmapPalette (palette);
            Format = PixelFormats.Indexed8;
            Buffer.BlockCopy (m_output, src, m_output, 0, m_output.Length-src);
        }

        void UnpackV2 ()
        {
            Format = PixelFormats.Bgra32;
            int tile_count = m_input.ReadInt32();
            var tiles = new List<Tile> (tile_count);
            for (int i = 0; i < tile_count; ++i)
            {
                var tile = new Tile();
                tile.X = m_input.ReadInt32();
                tile.Y = m_input.ReadInt32();
                tiles.Add (tile);
                m_input.Seek (0x10, SeekOrigin.Current);
            }
            using (var input = new BinMemoryStream (LzDecompress (m_input, 2, 1)))
            {
                if (input.ReadInt32() != tile_count)
                    throw new InvalidFormatException();
                int dst_stride = m_width * 4;
                m_output = new byte[m_height * dst_stride];
                for (int i = 0; i < tile_count; ++i)
                {
                    tiles[i].Offset = input.ReadUInt32();
                    tiles[i].Length = input.ReadInt32();
                }
                var tile = tiles.First (t => t.Length != 0);

                input.Position = tile.Offset;
                int tile_type = input.ReadUInt16();
                int count = input.ReadUInt16();
                if (tile_type != 1)
                    throw new InvalidFormatException();
                input.Seek (0x70, SeekOrigin.Current);
                for (int i = 0; i < count; ++i)
                {
                    int tile_x = input.ReadUInt16();
                    int tile_y = input.ReadUInt16();
                    input.ReadInt16();
                    int tile_width = input.ReadUInt16();
                    int tile_height = input.ReadUInt16();
                    input.Seek (0x52, SeekOrigin.Current);

                    tile_x += tile.X;
                    tile_y += tile.Y;
                    if (tile_x + tile_width > m_width || tile_y + tile_height > m_height)
                        throw new InvalidFormatException();
                    int dst = tile_y * dst_stride + tile_x * 4;
                    int tile_stride = tile_width * 4;
                    for (int row = 0; row < tile_height; ++row)
                    {
                        input.Read (m_output, dst, tile_stride);
                        dst += dst_stride;
                    }
                }
            }
        }

        public static byte[] LzDecompress (IBinaryStream input, int min_count, int bytes_pp)
        {
            int packed_size = input.ReadInt32() - 8;
            int output_size = input.ReadInt32();
            var output = new byte[output_size];
            int dst = 0;
            int bits = 2;
            while (dst < output.Length && packed_size > 0)
            {
                bits >>= 1;
                if (1 == bits)
                {
                    bits = input.ReadUInt8() | 0x100;
                    --packed_size;
                }
                if (0 != (bits & 1))
                {
                    input.Read (output, dst, bytes_pp);
                    dst += bytes_pp;
                    packed_size -= bytes_pp;
                }
                else
                {
                    if (packed_size < 2)
                        break;
                    int offset = input.ReadUInt16();
                    packed_size -= 2;
                    int count = (offset & 0xF) + min_count;
                    offset >>= 4;
                    offset *= bytes_pp;
                    count *= bytes_pp;
                    Binary.CopyOverlapped (output, dst-offset, dst, count);
                    dst += count;
                }
            }
            return output;
        }

        #region IDisposable Members
        public void Dispose ()
        {
        }
        #endregion
    }
}

经过对上面源码前半部分的分析后,我们可以很清楚地知道了如何读取文件头那一大堆乱糟糟的16进制数据了:

Galgame Image Cover Tools开发日记四——引擎Siglus Engine的适配2.jpg

其中,红色框内2个字节为图像类型数据,0代表CG类型,2代表人物立绘类型(包括表情和身体差分)或者CG差分类型,3代表场景立绘类型;橙色框内2个字节代表该图像文件画布的X值;蓝色框内2个字节代表该图像文件画布的Y值;紧接着,如果图像类型数据为2,则绿色框内4个字节为该文件里包含多少图像(一般都为1个),否则为图像文件大小;紫色框内2个字节为合成立绘偏移坐标的X值;粉色框内2个字节为合成立绘偏移坐标的Y值。

然后,上面源码中的其中一段是这么写的:

public void Unpack ()
{
    m_input.Position = 5;
    if (0 == m_type)
        UnpackV0();
    else if (1 == m_type)
        UnpackV1();
    else
        UnpackV2();
}    

我们发现,源代码针对不同的图像类型进行了不同的处理,共三种。但经过分析后发现,这三种处理方式全是作者本人自己写的算法,而且比较复杂,甚至连里面的LZ解压算法都是自己写的,而且C#和Python的许多结构体构成大不相同,所以将这段C#代码直接写成Python可以说难度非常大……(真的不是一般的大,甚至都去请教了现役程序员,结果还是太难)

然后,我考虑着可不可以将这段代码修改一下,然后编译出dll直接让Python调用。但因为个人能力有限以及跨语言调用会产生各种BUG,所以还是放弃了这种方法……

之后的近一个星期里,我基本都是在这种迷茫中度过的,那时确实都快要放弃了……

最后,Listder酱无意间在GitHub上面找到了某位大佬使用Python语言写的解析.g00文件的项目,这让我重新看到了希望!

该作者将解析代码分成了三个模块:

分析类型模块:

import struct
from PIL import Image
from CoverExtend import SiglusLzss as siglz
from CoverExtend import SiglusImage

#We use pillow to convert the decoded raw bitmap to PNG/BMP

#Header: byte "type" followed by uint16_t width and height.
#the 24-bit variant has an extra uint32_t pair for compressed and uncompressed length.

#There are 3 main types of G00 file: 8-bit image, 24-bit RGB, and directory, which are likely multiple image files in one.
#denoted by a start byte of 0, 1, or 2, respectively.

def extract(imagepath):
    infile = open(imagepath,'rb')
    type = infile.read(1)[0] #is a byte. not exactly much to process here...
    width = struct.unpack("<H",infile.read(2))[0]
    height = struct.unpack("<H",infile.read(2))[0]
    if (type == 0):
      #24-bit!
      complen, decomplen = struct.unpack("<II", infile.read(8))
      decomp = bytearray(decomplen) 
      siglz.decompress_24(infile.read(complen), decomp, decomplen)
      img = Image.frombytes("RGBA", [width,height], bytes(decomp), "raw" , "BGRA")
    if (type == 1):
      #8-bit!
      complen, decomplen = struct.unpack("<II", infile.read(8))
      decomp = bytearray(decomplen)
      siglz.decompress_8(infile.read(complen), decomp, decomplen)
      img = Image.frombytes("L", [width,height], bytes(decomp))
    if (type == 2):
      #A directory. recursion time!
      images = SiglusImage.decodedir(infile, [width,height])
      img = images[0]
    return img

解析图像模块:

#provides a PIL decode function for basic g00 images,
#and more advanced "ImageFactory" methods for complex images (multi-part images; usually face data)
from CoverExtend import SiglusLzss as siglz
import struct
from io import BytesIO
from PIL import Image

def decode24(fp): #takes a file-like object pointing to the compression header; is broken off for comples image decoding
        complen = struct.unpack("<I", fp.read(4))[0]
        decomplen = struct.unpack("<I", fp.read(4))[0]
        if (complen > decomplen):
          raise IndexError("WTF: Compressed file length is more the the decompressed?")
        #decomp = lzss.decode(infile.read(complen))
        decomp = siglz.decompress_24(fp.read(complen), decomplen) #is lzss, but on whole pixels
        if (len(decomp) != decomplen):
          raise IndexError("Decompressed less data than expected!") #the "more" condition is never true; the decoder yields or raises an IndexError 
                                                                    #(TODO: check if it's for the output array and raise it higher if so)
        return decomp

def decodedir(fp,size=None): #takes file-like, returns dict of numbered SiglusImages, with further dirs nesting.
    output = []
    infos = []
    count = struct.unpack("<I", fp.read(4))[0] #uint32_t
    for i in range(count):
      info = {}
      info["orig"] = struct.unpack("<II", fp.read(8)) #x/y tuple
      info["end"] = struct.unpack("<II", fp.read(8))
      fp.read(8) #skip two more
      infos.append(info)
    complen, decomplen = struct.unpack("<II", fp.read(8))
    debuf = bytearray(decomplen)
    siglz.decompress_8(fp.read(complen), debuf, decomplen)
    fp = BytesIO(debuf) #and fake a stream from here.
    entry_count = struct.unpack("<I", fp.read(4))[0]
    if entry_count != count:
      print("WARN: different counts of placed tiles to actual tiles!")
    for i in range(entry_count): #each panel?
      infos[i]["off"] = struct.unpack("<I", fp.read(4))[0]
      infos[i]["len"] = struct.unpack("<I", fp.read(4))[0]
    for i in infos:
      image = {}
      t = struct.unpack("<H", fp.read(2))[0] #note: NOT THE SAME AS G00 types!
      blocks = struct.unpack("<H", fp.read(2))[0]
      image["orig_x"], image["orig_y"] = struct.unpack("<II", fp.read(8))
      width,height = struct.unpack("<II", fp.read(8))
      image["disp_x"], image["disp_y"] = struct.unpack("<II", fp.read(8))
      image["full_width"], image["full_height"] = struct.unpack("<II", fp.read(8))
      fp.read(20*4) #skip some unknown structures
      if (t == 0):
        continue
      if (t > 2):
        raise IndexError("Invalid Type!")
      if size is not None:
        partcanvas = Image.new("RGBA", size, (0,0,0,0)) #pad to full-sized canvas
      else:
        partcanvas = Image.new("RGBA", [width,height], (0,0,0,0))
      for i in range(blocks): #number of chunks in the image.
        startx, starty = struct.unpack("<HH", fp.read(4))
        info = struct.unpack("<H", fp.read(2))
        bwidth, bheight = struct.unpack("<HH", fp.read(4))
        fp.read(41*2) #skip empty shit
        block = Image.frombytes("RGBA", [bwidth,bheight], fp.read(4*bwidth*bheight), "raw", "BGRA")
        #block.show()
        if size is not None:
          partcanvas.paste(block, [startx,starty]) #we're putting it on a full-sized image canvas, not a part-sized canvas.
        else:
          partcanvas.paste(block, [startx - image["orig_x"],starty - image["orig_y"]])
        #input("Next Block")
#      partcanvas.show()
#      input("Next Canvas")
      output.append(partcanvas)
    return output

以及使用到的各种LZ压缩算法模块:

import struct
lzflag = False

#tweaked lzss that operates on pixels instead of bytes. file pixels are 24 bits BGR, memory pixels are 32-bit BGRA (assumed opaque)
#stoppixel is intended as a crude "roll back to start of block" function. index 0 of the buffer should be the returned index from the last call.
#unfortunately, it doesn't work properly. might decide to calculate the length of a given block on the fly and only process it if it's complete
def decompress_24(buf, outbuf, decomplen, stoppixel=0):
    outcount = stoppixel
    count = 0
    blockp = 0
    try:
     if (outcount >= decomplen):
       print ("Done!")
       return (stoppixel,-1)
     while outcount < decomplen:
#      print(decomplen,outcount)
      blockp = count
      stoppixel = outcount
      tagmask = buf[count] #get a block of 8 items
      count += 1
      tagcount = 0x8
      while outcount < decomplen and tagcount > 0:
        if (tagmask % 2) == 0x01: #is pixel literal? append RGB to RGBA.
          outbuf[outcount] = buf[count]
          outbuf[outcount+1] = buf[count+1]
          outbuf[outcount+2] = buf[count+2]
          outbuf[outcount+3] = 0xff
          count += 3
          outcount += 4
        else: #is sequence; fish it out and play it back
          seekback = struct.unpack("<H",buf[count:count+2])[0] #seekback; could probably speed this step up by doing manual conversion...
          count += 2
          seqlen = seekback
          seekback = seekback >> 4 #strip off the 4-bit sequence length to get the 12-bit "walk-back" distance (in pixels)
          seekback = seekback << 2  #multiply by pixel size to get walk-back in bytes
          seqlen &= 0xf
          seqlen += 1
          backseek = outcount
          backseek -= seekback
          while (seqlen > 0):
            seqlen -= 1
#            print(tmpcount,hold)
            for i in range(4): #we're copying dwords here, not bytes. (0,1,2,3)
              outbuf[outcount] = outbuf[backseek]
              outcount += 1
              backseek += 1
        tagmask = tagmask >> 1 #get next bit in the mask
        tagcount -= 1
    except (IndexError, struct.error):
      print ("Incomplete Block")
      return (stoppixel, blockp) #this means the block is done for now...
    stoppixel = outcount
    blockp = count
    print ("Decompressed")
    return (stoppixel, blockp) #we're done, but it needs one more pass before echoing -1

#standard byte-level lzss?
def decompress_8(buf, outbuf, decomplen, stoppixel=0):
    #can't use the lzss library, no support for partial decode as far as I can tell, which is practically required for PIL plugins.
    outcount = stoppixel
    count = 0
    blockp = 0
    try:
     while outcount < decomplen:
#      print(decomplen,outcount)
      blockp = count
      tagmask = buf[count] #get a block of 8 items
      count += 1
      tagcount = 0x8
      while outcount < decomplen and tagcount > 0:
        if (tagmask % 2) == 0x01: #is literal? append.
          outbuf[outcount] = buf[count]
          count += 1
          outcount += 1
        else: #is sequence; fish it out and play it back
          seekback = struct.unpack("<H",buf[count:count+2])[0] #seekback; could probably speed this step up by doing manual conversion...
          count += 2
          seqlen = seekback
          seekback = seekback >> 4 #strip the sequence length to get a 12-bit walkback (in bytes)
          seqlen &= 0xf
          seqlen += 2 #minimum byte sequence is 2 bytes, to avoid the tag taking more space than the sequence...
          backseek = outcount
          backseek -= seekback
          while (seqlen > 0):
            seqlen -= 1
#            print(tmpcount,hold)
            outbuf[outcount] = outbuf[backseek]
            outcount += 1
            backseek += 1
        tagmask = tagmask >> 1 #get next bit in the mask
        tagcount -= 1
    except (IndexError) as e: #incomplete block (or malformed stream...)
      return (stoppixel, blockp)
    except struct.error:
      return (stoppixel, blockp) #return the recorded state with the number of bytes consumed
    return (stoppixel, -1)

def compress_24(buf):
    #this isn't exactly "compression" but it abuses the format enough for the engine to pick it up...
    #takes a flat ***BGR*** image. *NOT* BGRA. CONVERT IT FIRST.
    #also, this does *not* include the compression header.
    out = b''
    count = 0
    while count < len(buf):
      b += [0xff]
      b += buf[count:count+(8*3)]
      count += (8*3)
    return out

说实话,我是真的没大看懂解析图像模块和使用到的各种LZ压缩算法模块,所以这里简单的进行了修改,然后在GICT里面直接当作模块进行调用了。

当然,合成立绘还得需要获得偏移坐标。根据分析,引擎Siglus Engine的差分坐标计算方式和引擎Artemis Engine基本一致,也就存储X和Y的位置不同罢了:

def SiglusEnginefg(partimage,baseplate):
    with open(partimage,"rb") as f:
        f.seek(25)
        datax1 = f.read(2)
        f.read(2)
        datay1 = f.read(2)
        f.close()
    with open(baseplate,"rb") as f:
        f.seek(25)
        datax2 = f.read(2)
        f.read(2)
        datay2 = f.read(2)
        f.close()
    datax1s = int.from_bytes(datax1,"little")
    datay1s = int.from_bytes(datay1,"little")
    datax2s = int.from_bytes(datax2,"little")
    datay2s = int.from_bytes(datay2,"little")

    x = abs(datax1s-datax2s)
    y = abs(datay1s-datay2s)
    return x,y

从这里我们可以看出,引擎Siglus Engine的图像资源文件因为没有进行集中封包,所以为了防止小白,单个.g00文件解析起来会特别特别特别麻烦……

分析图像资源

Galgame Image Cover Tool在开发之前的基本规划是用户直接导入整个Galgame人物立绘的解包后资源,文件分布都是以默认的方式进行读取。所以需要对解包后资源进行固定形式的检索。

下面是Summer Pockets REFLECTION BLUE的立绘文件:

Galgame Image Cover Tools开发日记四——引擎Siglus Engine的适配3.jpg

根据分析,上面图片中,红色框内为立绘类型,且一般分为bs1、bs2和bs3,在身体差分中又多出了f_bs1、f_bs2和f_bs3,共6种类型,这根据Galgame制作的质量的不同而不同;橙色框内为角色名称,蓝色框中为表情差分的类型标识,褐色框内为身体差分中按照服装分类的标识,绿色框内为同类型的排列序号。在表情差分中,它们之间都用下划线分开;在身体差分中,只有立绘尺寸类型和其它项目之间是用下划线分开的。

根据上面的分析,看样子我们只要根据表情和身体差分的标识和序列号,进行统一的一一对应就可以正确合成出图像了,但事实上并非那么简单。

这是Summer Pockets REFLECTION BLUE里神山识的解析转化后的人物立绘文件分布:

Galgame Image Cover Tools开发日记四——引擎Siglus Engine的适配4.jpg

根据图像位置对应关系,神山识人物立绘差分的对应关系如上图所示。

这是Summer Pockets REFLECTION BLUE里久岛鸥的解析转化后的人物立绘文件分布:

Galgame Image Cover Tools开发日记四——引擎Siglus Engine的适配5.jpg

根据图像位置对应关系,久岛鸥人物立绘差分的对应关系如上图所示。

这是Summer Pockets REFLECTION BLUE里鸣濑白羽的解析转化后的人物立绘文件分布:

Galgame Image Cover Tools开发日记四——引擎Siglus Engine的适配6.jpg

根据图像位置对应关系,鸣濑白羽人物立绘差分的对应关系如上图所示。

由此可见,Summer Pockets REFLECTION BLUE里每位人物立绘差分的对应方式大相径庭,甚至会出现立绘类型不同身体差分个数不同的情况,所以这里还是努力写出了针对Summer Pockets REFLECTION BLUE的人物立绘合成的规则字典:

SiglusEngineCharactersImageDict = {"sk_f01":["01","02"],"sk_f02":["03"],"sk_f11":["11","12"],"um_f01":["01","02","03"],"um_f11":["11","12","13"],"tm_f01":["01","02","03"],"tm_f11":["0111","0112","0113","0211","0212","0213"
                                                                                                                                                                                   ,"0311","0313","0411","0412","0413","0511","0512","0513"]
                                ,"tm_f12":["0312"],"ts_f01":["01","02"],"in_f01":["01","02","03","04"],"tn_f01":["01","02","03"],"tn_f02":["0104"],"tn_f03":["0204","0304","0704"],"tn_f04":["0401","0402","0403","0501","0502","0503","0601","0602","0603"],"tn_f05":["0404"],"tn_f06":["0504","0604"]
                                ,"tn_f07":["01","02","03"],"tn_f08":["01","02","03"],"tn_f09":["0101","0102","0103","0201","0202","0203","0301","0302","0303","0701","0702","0703"],"ky_f01":["01"],"ky_f02":["01"],"ky_f03":["0102","0202"],"ky_f04":["0102","0202"],"ky_f05":["0302","0402"],"ky_f06":["0302","0402"]
                                ,"ko_f01":["01"],"ko_f02":["02"],"km_f01":["01"],"km_f02":["02"],"km_f03":["03"],"km_f04":["09"],"km_f11":["11","12","13"],"ao_f01":["01","02","03"],"ao_f02":["04"],"ao_f11":["11","12","13","14"],"sr_f01":["0101","0102","0201",
                                                                                                                                                                                                                                "0202","0301","0302","0401"]
                                ,"sr_f02":["0103","0203","0303"],"sr_f03":["0101","0102","0201","0202","0301","0302"],"sr_f04":["0103","0203","0303"],"sr_f05":["0501","0502"],"sr_f06":["0601","0602"],"sr_f07":["0603"],"sr_f11":["11","12"]
                                ,"sr_f12":["13"],"sr_f21":["21","22"],"sc_f01":["01","02","03"],"sj_f01":["01","02"],"sj_f02":["01","02"],"ru_f01":["01","02","03"],"ru_f02":["01","02","03"],"sz_f01":["01","03"],"sz_f02":["02"],"sz_f11":["11","12","13"]
                                ,"mk_f01":["01","02"],"mk_f11":["11","12","13"],"mk_f14":["14","15"]}

检索图像资源

角色名称的获取

接下来,开始编写角色名称获取代码。

在_tmp.py代码块读取Setting.ini文件后,直接循环检索g00文件夹下的所有文件,然后根据人物立绘文件名开头的特殊字符串进行筛选,紧接着使用指定字符串(下划线)分割的方式得到角色名称,以列表形式存储,并传递给主窗口的CharactersCombobox,供用户选择角色:

elif GalEngine == "Siglus Engine":
    FilesList = []
    CharactersList = []
    if ModleChoice == "人物立绘":
        for i in os.listdir(ResourcePath):
            if i[:2] == "bs" and "bs_" not in i:
                FilesList.append(i)
                if i[4:6] not in CharactersList:
                    CharactersList.append(i[4:6])
            elif i[:4] == "f_bs":
                FilesList.append(i)
                if i[6:8] not in CharactersList:
                    CharactersList.append(i[4:6])
    a = 0
    for i in CharactersList:
        if Galgame == "Summer Pockets REFLECTION BLUE":
            if i in CharactersDict.SummerPocketsRBCharactersDict.keys():
                CharactersList[a] = CharactersDict.SummerPocketsRBCharactersDict[i]
                PartimageDict = True
        else:
            PartimageDict = False
        a = a + 1
    for i in CharactersList:
        if i == "None":
            CharactersList.remove("None")

在这里,因为本人已经推过Summer Pockets REFLECTION BLUE了,所以顺手把Summer Pockets REFLECTION BLUE单独适配了出来,也就是将角色的英文缩写和角色全称对应,使用字典存储,便于用户更好的分辨角色并选择:

SummerPocketsRBCharactersDict = {"tm":"䌷文德斯(紬 ヴェンダース)","ts":"䌷文德斯(幼)","in":"稻荷(イナリ)","tn":"加纳天善(かのう てんざん)","um":"加藤羽未(加藤うみ)"
                              ,"ky":"岬镜子(みさき きょうこ)","ko":"岬镜子(年轻时)","km":"久岛鸥(久島鴎)","ao":"空门苍(空門蒼)","sr":"鸣濑白羽(鳴瀬白羽)"
                              ,"sc":"鸣濑白羽(幼)","sj":"鸣濑小鸠(なるせ こばと)","ru":"三谷良一(みたに りょういち)","sk":"神山识(神山識)","sz":"水织静久(みずおり しずく)"
                              ,"tr":"None","mk":"野村美希(のむら みき)","tu":"None"}

立绘类型的获取

在前面对图像资源的分析中,我们可以知道立绘类型在文件名的最前面,所以这里依然指定字符串切割,并排除其它非人物立绘的图像文件:

if _tmp.Galgame == "灵感满溢的甜蜜创想":
    ###……
elif _tmp.Galgame == "Summer Pockets REFLECTION BLUE":
    CharactersDictList = CharactersDict.SummerPocketsRBCharactersDict
###……
def CharactersCombobox(self,event):
    ###……
    CharactersName = event.GetString()
    ###……
    elif _tmp.GalEngine == "Siglus Engine":
            if _tmp.ModleChoice == "人物立绘":
                CharactersFileList = []
                for i in _tmp.FilesList:
                    if _tmp.Galgame == "None" or CharactersName not in CharactersDictList.values():
                        if CharactersName in i:
                            CharactersFileList.append(i.replace(".g00",""))
                            if i[:2] == "bs":
                                if i[:3] not in StyleList:
                                    StyleList.append(i[:3])
                            elif i[:4] == "f_bs":
                                if i[:5] not in StyleList:
                                    StyleList.append(i[:5])
                    else:
                        if list(CharactersDictList.keys()) [list (CharactersDictList.values()).index (CharactersName)] in i:
                            CharactersFileList.append(i.replace(".g00",""))
                            if i[:2] == "bs":
                                if i[:3] not in StyleList:
                                    StyleList.append(i[:3])
                            elif i[:4] == "f_bs":
                                if i[:5] not in StyleList:
                                    StyleList.append(i[:5])

罗列身体差分

在选择了立绘类型后,接下来就要罗列出身体差分了,也就是人物立绘图像合成的底板,这样就可以和再下一步的表情差分的罗列相对应。

根据前面分析,身体差分图像和角色差分图像文件名称唯一不同的地方就是身体差分文件名后面并不像表情差分那样各个项目使用下划线分隔开来,所以还是可以直接根据特殊字符串切割并判断的方式罗列出身体差分:

def StyleCombobox(self,event):
    ###……
    StyleName = event.GetString()
    ###……
    elif _tmp.GalEngine == "Siglus Engine":
            for i in CharactersFileList:
                if "_" not in i[-4:]:
                    if i[:3] == StyleName:
                        BaseplateList.append(i)
                    elif i[:5] == StyleName:
                        BaseplateList.append(i)

罗列表情差分

最后,就是要罗列出表情差分了。

通过前面对表情和身体差分对应关系的分析,我们需要分两种情况进行编写代码:

  1. 对于已经适配出立绘合成规则字典的Galgame来说,直接在字典里寻找Key对应的Value进行精细筛选然后罗列即可。

  2. 对于未适配出立绘合成规则字典的Galgame来说,无法进行精细筛选,只能将立绘类型和前面所选择的身体差分相同的表情差分全部罗列出来。

所以,按照这个思路,以字符串切割为基本方法,通过不断的条件判断(我也不想这样写成屎山呜呜……X﹏X)进行系统性的罗列表情差分:

def BaseplateCombobox(self,event):
    ###……
    BaseplateName = event.GetString()
    ###……
    elif _tmp.GalEngine == "Siglus Engine":
            for i in CharactersFileList:
                if "_" in i[-4:]:
                    if len(StyleName) == 5:
                        if BaseplateName[2:5] == i[:3]:
                            if _tmp.PartimageDict:
                                if len(CharactersDict.SiglusEngineCharactersImageDict.get(i[4:10])[0]) == 4:
                                    if BaseplateName[-6:-2] in CharactersDict.SiglusEngineCharactersImageDict.get(i[4:10]):
                                        PartImageList.append(i)
                                else:
                                    if BaseplateName[-4:-2] in CharactersDict.SiglusEngineCharactersImageDict.get(i[4:10]):
                                        PartImageList.append(i)
                            else:
                                PartImageList.append(i)
                    elif len(StyleName) == 3:
                        if BaseplateName[:3] == i[:3]:
                            if _tmp.PartimageDict:
                                if len(CharactersDict.SiglusEngineCharactersImageDict.get(i[4:10])[0]) == 4:
                                    if BaseplateName[-6:-2] in CharactersDict.SiglusEngineCharactersImageDict.get(i[4:10]):
                                        PartImageList.append(i)
                                else:
                                    if BaseplateName[-4:-2] in CharactersDict.SiglusEngineCharactersImageDict.get(i[4:10]):
                                        PartImageList.append(i)
                            else:
                                PartImageList.append(i)
            PartImagechoice.SetItems(PartImageList)
            BaseplatePath = _tmp.ResourcePath + "\\" + BaseplateName + ".g00"
            image2 = SiglusArt.extract(BaseplatePath) PartImageList.append(i)

在罗列完表情差分后,因为.g00文件解析转化起来比较慢,所以这里先把身体差分解析转化出来,这样后面预览图象切换表情差分时速度更快。

合成图像

最后,当用户选择完表情差分后,就可以根据偏移坐标合成出图像了:

def PartImageCombobox(self,event):
    ###……
    PartImageName = event.GetString()
    ###……
    elif _tmp.GalEngine == "Siglus Engine":
        PartImagePath = _tmp.ResourcePath + "\\" + PartImageName + ".g00"
        BaseplatePath = _tmp.ResourcePath + "\\" + BaseplateName + ".g00"
        image1 = SiglusArt.extract(PartImagePath)
        image3 = image2.copy()
        image3.alpha_composite(image1,(SiglusXY.SiglusEnginefg(PartImagePath,BaseplatePath)))

在黑暗中经过了不断地摸索,再摸索,经过了长达一个多星期,到这里,史诗级难度较大的引擎Siglus Engine的适配代码编写工作终于,终于,终于结束了🎉🎉🎉

毕竟难度特别大,心中的成就感可以说油然而生了。

Galgame Image Cover Tools开发日记四——引擎Siglus Engine的适配7.jpg

根据后期测试,该立绘合成方式对同样引擎为Siglus Engine的爱因斯坦携爱敬上同样适用,所以除了立绘合成规则得单独适配以外,基本可以认为该立绘合成方式对使用引擎Siglus Engine的所有Galgame都适用!✨✨✨