网页功能: 加入收藏 设为首页 网站搜索  
浅谈LZSS与游戏图片破解
发表日期:2006-09-03作者:[转贴] 出处:  

  业余游戏制作者最头疼的就是没有美工的支持了。很多业余游戏制作所使用的图片都是来自于网上的很有限的一些图片资源,然而这些图片并不能完整配套,所以业余游戏的画面往往显得单调或者搭配不协调(使用多个不属于一系列的图片资源)。基于此,也有不少业余游戏采用“窃取”商业游戏图片于己用的方式(反正业余游戏一般都不用于商业目的),这种方法使用的就是一系列完整、配套的图片,画面就会显得专业、协调得多,但是,前提是能够破解商业游戏的图片格式。多数商业游戏并不会将图片资源以可以直接打开的常用格式存放,而是会做一定的压缩处理,这样做有两个好处:其一,图片不易被用户直接修改或用于其他用途;其二,减小游戏图片资源所占用的磁盘空间。

  最近,我为了获取图片资源,研究了一些游戏的图片压缩格式,发现有不少游戏采用了LZSS压缩,例如《天使音乐会》、《神奇传说系列》(只针对《时空道标》和《远征奥德赛》两作,因为没有其他的在手边)。于是,我在网上查阅了一些关于LZSS压缩的资料,实现了这些游戏的图片资源破解。

  LZSS压缩是LZ77压缩的改进方式,相对于LZ77减少了冗余度。LZSS在压缩比率上相对其它压缩并没有太大优势,然而它的压缩/解压缩速度却非常快,因此往往用在速度优先的场合(当然,游戏图片解压缩就是速度优先的)。基于这个优势,LZSS被大量采用,例如微软以前常使用的compress.exe/expand.exe就是采用LZSS实现的(这里顺便提一下,《神奇传说时空道标》的图片压缩完全就是用compress.exe的方式压缩的,连文件头都完整存在,因此可以直接用expand.exe来解压缩,就不用自己忙活了^_^)。

  在文章的末尾我给出了一个用LZSS压缩/解压缩的源程序,是Haruhiko Okumura在1989年所写,后来被广泛使用。但是由于其源代码相当晦涩,所以我在这里先把LZSS压缩的原理大致介绍一下:

  LZSS采用了一个大小为N的滑动窗口用于在文件中滑动,其中后F大小作为一个前向缓冲,在窗口中前N-F字节内容是已处理部分,而后F字节也就是前向缓冲是待处理部分。如下图示:



  压缩过程就是用前向缓冲中的F字节长的串和前面的N-F个长F的串作比较,例如当F如上图为10的时候,将前向缓冲的qrstabcdfk串分别和前面的zqrstabcdf、yzqrstabcd、xyzqrstabc、wxyzqrstab……总共N-10个串进行比较,寻找最长匹配。在上例中,qrstabcdfk的最长匹配出现在qrstuvwxyz时(即箭头所指位置N-F-10),匹配长度为4即qrst。然后记录下二元组〈匹配位置,匹配长度〉(在上例中是〈N-20, 4〉),放入到输出缓冲区;如果匹配长度少于等于2个字节(例如上例中匹配f时,匹配长度为1),这时用上述二元组记录,反而会造成浪费,因此,直接把原字符放入输出缓冲区。当放入输出缓冲区以后,应该将滑动窗口向后滑动,后F字节中处理过了的字节滑入N-F字节区中,同时从文件中补充相应字节数至后F字节,重复上述处理,直至文件结束。解压缩的过程与此类似,在此不再赘述。

上面只是一个大概的LZSS实现原理,下面我针对所附的源代码中的部分问题做一个解释:

1. 大家知道,作字符串比较是比较耗时的,为了提高效率,程序中使用了256棵查找二叉树,每一棵表示一个字节值开头的串,以快速查找到所需要比较的串的所在地。

2. 程序中将滑动窗口做成了一个环状缓冲,以免造成滑动的不便。

3. 程序的输出格式如下:用一位表示一个单元的类型,该位为1表示字符未经处理直接输出(一个字节)、为0表示经过了处理,输出上面所说的〈匹配位置,匹配长度〉二元组(两个字节),把这样的8位合在一起(一个字节)表示后面输出的八组元素的类型,其后就是经过处理或未经处理的八组数据,每组一或二个字节,当八组数据满时,将输出缓冲区中的数据输出到文件。二元组的两个字节是这样安排的:第一个字节表示匹配位置的低八位,第二个字节的高四位表示匹配位置的高四位,第二个字节的第四位表示匹配长度(程序中定义N为4096,因此位置值占用12位,F值定义为18,除去匹配长度为1和2的两种情况,共16种情况占4位)。

4. 微软的compress.exe/expand.exe采用的是N=4096、F=16的定义,因此这个程序只要修改F值就可以解压缩compress.exe压缩的文件(事先需去除文件头)。


  有了这个程序,我们就可以解压不少游戏的图片或其他资源,用于自己的业余游戏开发。如何判断一个文件是否采用LZSS压缩呢?其实很简单,上面我们也说到,在匹配长度小于等于2的时候,LZSS是原样输出的,以BMP文件为例,其文件头前几个字节ASCII为“BM6>[1]”由于BM6等几个字符匹配长度为1,因此在压缩的文件中也肯定是明文出现的,如果其后有类似F3 F0的字节值,就和上面介绍的二元组格式一致了,那么多半就是LZSS压缩的了。

/**************************************************************
    LZSS.C -- A Data Compression Program
    (tab = 4 spaces)
***************************************************************
    4/6/1989 Haruhiko Okumura
    Use, distribute, and modify this program freely.
    Please send me your improved versions.
        PC-VAN        SCIENCE
        NIFTY-Serve    PAF01022
        CompuServe    74050,1022
**************************************************************/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>

#define N         4096    /* size of ring buffer */
#define F         18    /* upper limit for match_length */
#define THRESHOLD    2 /* encode string into position and length
                         if match_length is greater than this */

#define NIL            N    /* index for root of binary search trees */

unsigned long int
        textsize = 0,    /* text size counter */
        codesize = 0,    /* code size counter */
        printcount = 0;    /* counter for reporting progress every 1K bytes */
unsigned char
        text_buf[N + F - 1];    /* ring buffer of size N,
            with extra F-1 bytes to facilitate string comparison */

int        match_position, match_length, /* of longest match. These are
            set by the InsertNode() procedure. */

        lson[N + 1], rson[N + 257], dad[N + 1]; /* left & right children &
            parents -- These constitute binary search trees. */

FILE    *infile, *outfile; /* input & output files */

void InitTree(void) /* initialize trees */
{
    int i;

    /* For i = 0 to N - 1, rson[i] and lson[i] will be the right and
     left children of node i. These nodes need not be initialized.
     Also, dad[i] is the parent of node i. These are initialized to
     NIL (= N), which stands for 'not used.'
     For i = 0 to 255, rson[N + i + 1] is the root of the tree
     for strings that begin with character i. These are initialized
     to NIL. Note there are 256 trees. */


    for (i = N + 1; i <= N + 256; i++) rson[i] = NIL;
    for (i = 0; i < N; i++) dad[i] = NIL;
}

void InsertNode(int r)
    /* Inserts string of length F, text_buf[r..r+F-1], into one of the
     trees (text_buf[r]'th tree) and returns the longest-match position
     and length via the global variables match_position and match_length.
     If match_length = F, then removes the old node in favor of the new
     one, because the old one will be deleted sooner.
     Note r plays double role, as tree node and position in buffer. */

{
    int i, p, cmp;
    unsigned char *key;

    cmp = 1; key = &text_buf[r]; p = N + 1 + key[0];
    rson[r] = lson[r] = NIL; match_length = 0;
    for ( ; ; ) {
        if (cmp >= 0) {
            if (rson[p] != NIL) p = rson[p];
            else { rson[p] = r; dad[r] = p; return; }
        } else {
            if (lson[p] != NIL) p = lson[p];
            else { lson[p] = r; dad[r] = p; return; }
        }
        for (i = 1; i < F; i++)
            if ((cmp = key[i] - text_buf[p + i]) != 0) break;
        if (i > match_length) {
            match_position = p;
            if ((match_length = i) >= F) break;
        }
    }
    dad[r] = dad[p]; lson[r] = lson[p]; rson[r] = rson[p];
    dad[lson[p]] = r; dad[rson[p]] = r;
    if (rson[dad[p]] == p) rson[dad[p]] = r;
    else lson[dad[p]] = r;
    dad[p] = NIL; /* remove p */
}

void DeleteNode(int p) /* deletes node p from tree */
{
    int q;
    
    if (dad[p] == NIL) return; /* not in tree */
    if (rson[p] == NIL) q = lson[p];
    else if (lson[p] == NIL) q = rson[p];
    else {
        q = lson[p];
        if (rson[q] != NIL) {
            do { q = rson[q]; } while (rson[q] != NIL);
            rson[dad[q]] = lson[q]; dad[lson[q]] = dad[q];
            lson[q] = lson[p]; dad[lson[p]] = q;
        }
        rson[q] = rson[p]; dad[rson[p]] = q;
    }
    dad[q] = dad[p];
    if (rson[dad[p]] == p) rson[dad[p]] = q; else lson[dad[p]] = q;
    dad[p] = NIL;
}

void Encode(void)
{
    int i, c, len, r, s, last_match_length, code_buf_ptr;
    unsigned char code_buf[17], mask;
    
    InitTree(); /* initialize trees */
    code_buf[0] = 0; /* code_buf[1..16] saves eight units of code, and
        code_buf[0] works as eight flags, "1" representing that the unit
        is an unencoded letter (1 byte), "0" a position-and-length pair
        (2 bytes). Thus, eight units require at most 16 bytes of code. */

    code_buf_ptr = mask = 1;
    s = 0; r = N - F;
    for (i = s; i < r; i++) text_buf[i] = ' '; /* Clear the buffer with
        any character that will appear often. */

    for (len = 0; len < F && (c = getc(infile)) != EOF; len++)
        text_buf[r + len] = c; /* Read F bytes into the last F bytes of
            the buffer */

    if ((textsize = len) == 0) return; /* text of size zero */
    for (i = 1; i <= F; i++) InsertNode(r - i); /* Insert the F strings,
        each of which begins with one or more 'space' characters. Note
        the order in which these strings are inserted. This way,
        degenerate trees will be less likely to occur. */

    InsertNode(r); /* Finally, insert the whole string just read. The
        global variables match_length and match_position are set. */

    do {
        if (match_length > len) match_length = len; /* match_length
            may be spuriously long near the end of text. */

        if (match_length <= THRESHOLD) {
            match_length = 1; /* Not long enough match. Send one byte. */
            code_buf[0] |= mask; /* 'send one byte' flag */
            code_buf[code_buf_ptr++] = text_buf[r]; /* Send uncoded. */
        } else {
            code_buf[code_buf_ptr++] = (unsigned char) match_position;
            code_buf[code_buf_ptr++] = (unsigned char)
                (((match_position >> 4) & 0xf0)
             | (match_length - (THRESHOLD + 1))); /* Send position and
                    length pair. Note match_length > THRESHOLD. */

        }
        if ((mask <<= 1) == 0) { /* Shift mask left one bit. */
            for (i = 0; i < code_buf_ptr; i++) /* Send at most 8 units of */
                putc(code_buf[i], outfile); /* code together */
            codesize += code_buf_ptr;
            code_buf[0] = 0; code_buf_ptr = mask = 1;
        }
        last_match_length = match_length;
        for (i = 0; i < last_match_length &&
                (c = getc(infile)) != EOF; i++) {
            DeleteNode(s);        /* Delete old strings and */
            text_buf[s] = c;    /* read new bytes */
            if (s < F - 1) text_buf[s + N] = c; /* If the position is
                near the end of buffer, extend the buffer to make
                string comparison easier. */

            s = (s + 1) & (N - 1); r = (r + 1) & (N - 1);
                /* Since this is a ring buffer, increment the position
                 modulo N. */

            InsertNode(r);    /* Register the string in text_buf[r..r+F-1] */
        }
        if ((textsize += i) > printcount) {
            printf("%12ld\r", textsize); printcount += 1024;
                /* Reports progress each time the textsize exceeds
                 multiples of 1024. */

        }
        while (i++ < last_match_length) {    /* After the end of text, */
            DeleteNode(s);                    /* no need to read, but */
            s = (s + 1) & (N - 1); r = (r + 1) & (N - 1);
            if (--len) InsertNode(r);        /* buffer may not be empty. */
        }
    } while (len > 0);    /* until length of string to be processed is zero */
    if (code_buf_ptr > 1) {        /* Send remaining code. */
        for (i = 0; i < code_buf_ptr; i++) putc(code_buf[i], outfile);
        codesize += code_buf_ptr;
    }
    printf("In : %ld bytes\n", textsize);    /* Encoding is done. */
    printf("Out: %ld bytes\n", codesize);
    printf("Out/In: %.3f\n", (double)codesize / textsize);
}

void Decode(void)    /* Just the reverse of Encode(). */
{
    int i, j, k, r, c;
    unsigned int flags;
    
    for (i = 0; i < N - F; i++) text_buf[i] = ' ';
    r = N - F; flags = 0;
    for ( ; ; ) {
        if (((flags >>= 1) & 256) == 0) {
            if ((c = getc(infile)) == EOF) break;
            flags = c | 0xff00;        /* uses higher byte cleverly */
        }                            /* to count eight */
        if (flags & 1) {
            if ((c = getc(infile)) == EOF) break;
            putc(c, outfile); text_buf[r++] = c; r &= (N - 1);
        } else {
            if ((i = getc(infile)) == EOF) break;
            if ((j = getc(infile)) == EOF) break;
            i |= ((j & 0xf0) << 4); j = (j & 0x0f) + THRESHOLD;
            for (k = 0; k <= j; k++) {
                c = text_buf[(i + k) & (N - 1)];
                putc(c, outfile); text_buf[r++] = c; r &= (N - 1);
            }
        }
    }
}

int main(int argc, char *argv[])
{
    char *s;
    
    if (argc != 4) {
        printf("'lzss e file1 file2' encodes file1 into file2.\n"
             "'lzss d file2 file1' decodes file2 into file1.\n");
        return EXIT_FAILURE;
    }
    if ((s = argv[1], s[1] || strpbrk(s, "DEde") == NU

我来说两句】 【加入收藏】 【返加顶部】 【打印本页】 【关闭窗口
中搜索 浅谈LZSS与游戏图片破解
本类热点文章
  浅谈LZSS与游戏图片破解
  “传奇”研究之二(传奇地图格式)
  关于阿玛迪斯战记的图片文件格式分析
  外挂制作介绍篇
  游戏外挂技术分析
  “传奇”研究之一(图象数据存储方式)
  游戏修改器
  “传奇”研究之三 人物和地图显示篇
  从游戏中得到动态内存数据
  “变速齿轮”再研究
  DXSDK8-Visual Basic教程
最新分类信息我要发布 
最新招聘信息

关于我们 / 合作推广 / 给我留言 / 版权举报 / 意见建议 / 广告投放  
Copyright ©2003-2024 Lihuasoft.net webmaster(at)lihuasoft.net
网站编程QQ群   京ICP备05001064号 页面生成时间:0.00506