Resources Game Viewer
Viewing of games resources

Creating a plugin for the game

Description of a plugin

Each plugin represents group of the supported games which have, as a rule, identical structure of resources. For example, the games created on the same engine should be placed in one group if unpacking algorithms of these games don't differ. It is worth noting that there is a description of the elementary plugin below which games resources are in archive.
The structure of the plugin is quite simple: import of necessary libraries, the announcement of constants (parameters), and the description of a class with functions.

LetТs analyze a g_simple.py plugin. The plugin is presented as an example and is located in the plugins folder in the program folder.

Import of libraries

import io, struct # Import of the main libraries
from PIL import Image # Import of the Image module from PIL library for work with images
import numpy as np # Import of NumPy library for work with arrays.

For work with graphics in QuestViewer the PIL (Pillow) library is used. The unpacked images have to represent objects of Image of PIL library. NumPy is used for creation of the array with data of the image which is in turn used for creation of an Image object. NumPy can also considerably accelerate unpacking of graphic resources.

Announcement of constants

LetТs take a look at the following part of a code:

NAME = "Simple" # # Group name
FORMATS_ARCHIVE = ['dat'] # the list of supported archives formats
TYPES_ARCHIVE = [('Game Pack DAT', ('*.dat'))] # Types of files in the window "Open Archive"
GAMES = ["Simple Game 1", # # List of group games
         "Simple Game 2"]
AUTHOR = "Author author@email.com" # Information on the author

Description of a class and its functions

All functions for work with resources register in the class Game_Res.

class Game_Res:
    def __init__(self,api):
        self.file = None
        self.data = [] # List of tuples or lists: (resource name, offset, size, format, ...)
        
        # Supported formats from the archive
        self.sup_formats = ["png",
                            "anm",
                            "wav"]
        # Types of resources
        # 1 - Image, 2 - Animation, 3 - Sound
        self.sup_types = {"png":1,
                          "anm":2,
                          "wav":3}
        
        self.images = []   # Array of Images PIL.Image | []
        self.sound = None  # Buffer with sound | io.BytesIO(bytes)

Function constructor __ init __ accepts one argument Ц an object of API by means of which it is possible to interact with the program.

The following two objects Ц the list and the dictionary - are used for sorting of the resources list.

Main functions

The open_data function is displayed when opening archive and accepts one argument - a full way to archive on a disk.

    def open_data(self,file):
        format = file.split(".")[-1].lower()
        if format == "dat":
            self.OpenArchiveDAT(file)

A file extension in a lowercase is brought in the format variable. Then there is a choice of the necessary unpacking archive function. This condition can be withdrawed if only one archive format is used in a game.

The unpack function is displayed at a clique on a resource in the program. It accepts one argument Ц a tuple from the self.data list: (name, offset, size, format).

    def unpack(self,data_res):
        # Clear objects self.images and self.sound
        self.images = []
        self.sound = None
        
        name = data_res[0]
        offset = data_res[1]
        size = data_res[2]
        format = data_res[3]
        self.file.seek(offset)
        if format == "png":
            self.Unpack_PNG(io.BytesIO(self.file.read(size)))
        elif format == "anm":
            self.Unpack_ANM(io.BytesIO(self.file.read(size)))
        elif format == "wav":
            self.Unpack_WAV(io.BytesIO(self.file.read(size)))

Before unpacking of resource self.images and self.sound objects are assigned empty value. For convenience we define the variables name, offset, size, format. We pass on offset in archive to a resource and we trigger the necessary function of unpacking, transferring by an argument the buffer with the read resource.

Instead of transferring of the buffer (it just already became a "standard" during creation of a plugin) it is possible to transfer only resource offset, and to carry out reading from archive in function. Such way allows to get rid of creation of the temporary buffer that will a little accelerate unpacking of resources of big size.

Example of the Unpack function with offset transferring:

    def unpack(self,data_res):
        self.images = []
        self.sound = None

        offset = data_res[1]
        format = data_res[3]
        if format == "png":
            self.Unpack_PNG(offset)
        elif format == "anm":
            self.Unpack_ANM(offset)
        elif format == "wav":
            self.Unpack_WAV(offset)

Archive reading

The OpenArchiveDAT function is triggered when opening archive by the open_data function considered above. It accepts one argument - a full way to archive.

    def OpenArchiveDAT(self,file):
        f = open(file,"rb")
        num_res = struct.unpack("<I",f.read(4))[0] # Number of files

        for i in range(num_res):
            filename = f.read(32)
            filename = filename.split(b"\x00")[0].decode("utf8")
            format = filename.split(".")[-1].lower()
            offset = struct.unpack("<I",f.read(4))[0]
            size = struct.unpack("<I",f.read(4))[0]
            self.data.append((filename,offset,size,format))
        self.file = f

The archive opens in the read mode in a binary form. For converting of the bytes given from lines the Struct module is used. The quantity of resources is brought in the num_res variable. In the following part of a code by means of the cycle FOR the table of pseudo-archive resources is read in the self.data list.
Pay attention to the following two lines:

filename = filename.split(b"\x00")[0].decode("utf8")
format = filename.split(".")[-1].lower()

The method split(b"\x00")[0] cuts off excess bytes from a name. In our case 32 bytes are allocated for a name in the table, and empty bytes after a name are filled 0x00. For transformation of bytes to a line the decode method is used, the coding utf8 is transferred by the argument. The resource format in a lowercase is brought in the format variable.

Unpacking of resources

LetТs analyze resources unpacking functions: Unpack_PNG, Unpack_ANM, self.Unpack_WAV. All of them accept the only argument - the buffer with the read resource.
The Unpack_PNG and Unpack_WAV functions are very simple:

    def Unpack_PNG(self, f):
        image = Image.open(f)
        self.images = [image]

    def Unpack_WAV(self, f):
        self.sound = f

self.images - the list of PIL.Image.Image objects. Pillow supports all main graphic formats (PNG, JPEG, BMP, etc.) therefore it is possible to use the Image.open() function which will return the image object. It is necessary to add it to the self.images list.
For unpacking of a sound (if it is the standard formats supported by Pygame (WAV, OGG, MP3)), it is enough to assign to the self.sound variable the buffer with the read resource.

The last Unpack_ANM function unpacks the animation pseudo-format having the following structure:

    def Unpack_ANM(self,f):
        num_frames = struct.unpack("<I",f.read(4))[0] # Number of frames
        for i in range(num_frames):
            rgb = []
            w = struct.unpack("<I",f.read(4))[0] # Frame Width
            h = struct.unpack("<I",f.read(4))[0] # Frame height
            for j in range(w*h):
                p = struct.unpack("4B",f.read(4)) # R G B A
                rgb.extend(p)
            rgb = np.array(rgb,np.uint8).reshape(h, w, 4)
            self.images.append(Image.fromarray(rgb,"RGBA"))

During each iteration an empty rgb list is created. A variable p is assigned a value of the tuple (r, g, b, a) in the enclosed cycle. By means of the extend method all elements of the p tuple are added to the end of the rgb list.
It is worth paying attention to the following two last lines:

rgb = np.array(rgb,np.uint8).reshape(h, w, 4)
self.images.append(Image.fromarray(rgb,"RGBA"))

The np.array() function creates the NumPy array from the rgb list. The reshape method changes an array form: in our case the matrix of w*h size is created each element of which is RGBA component. The Image.fromarray() functions return an Image object Ц the image which is added to the self.images list.
If the image was coded by three RGB components, the last lines would be like:

rgb = np.array(rgb,np.uint8).reshape(h, w, 3)
self.images.append(Image.fromarray(rgb,"RGB"))

In similar cases when images aren't compressed at all, it is possible to use NumPy for acceleration of unpacking in order to read data immediately, without using cycles. By using the NumPy the code would be like:

    def Unpack_ANM(self,f):
        num_frames = struct.unpack("<I",f.read(4))[0]
        for i in range(num_frames):
            w = struct.unpack("<I",f.read(4))[0]
            h = struct.unpack("<I",f.read(4))[0]
            rgb = np.frombuffer(f.read(w*h*4), dtype=np.uint8)
            rgb = np.array(rgb,np.uint8).reshape(h, w, 4)
            self.images.append(Image.fromarray(rgb,"RGBA"))

If components of RGB are written in reverse order, then after form change it is necessary to change a matrix. For example, data are written down as BGRA, then after "reshape (h, w, 4)" it is possible to write:

b, g, r, a = np.rollaxis(rgb, axis = -1)
rgb = np.dstack([r, g, b, a])

Resources without archive

It is not unusual that resources aren't packed in archive. For work with them in the program it is necessary to change a little a code and to add a constants.

Constants

By default "File -> Open Files..." clause is inactive. For its activation it is necessary to add to a plugin the following constants. They are analogs of the constants FORMATS_ARCHIVE and TYPES_ARCHIVE.

FORMATS_FILES = ['png','wav'] # the List of expansions of the supported resources
TYPES_FILES = [('PNG Images', ('*.png'))] # Types of files in the УOpen FilesФ window

Main functions

The open_files function is triggered when opening files or folders and accepts one argument - the list of files in a format [(a way to the file, None, the file size, expansion),...]. The program creates the list of files itself and transfers it to function. It is necessary only to save it. Pay attention that offset in files without archive is equal to None.

def open_files(self,files):
    self.data = files

The unpack function needs also to be changed.

    def unpack(self,data_res):
        # Clear objects self.images and self.sound
        self.images = []
        self.sound = None
        
        name = data_res[0]
        offset = data_res[1]
        size = data_res[2]
        format = data_res[3]
        
        f = open(name,"rb")
        data_res = io.BytesIO(f.read(size))
        f.close()
        
        if format == "png":
            self.Unpack_PNG(data_res)
        elif format == "wav":
            self.Unpack_WAV(data_res)

If resources are packed in archives, but at the same time there are resources without archive, then it is possible to add a small condition:

    if offset == None:
        # Open the file
        f = open(name,"rb")
        data_res = io.BytesIO(f.read(size))
        f.close()
    else:
        # Read the archive
        self.file.seek(offset)
        data_res = io.BytesIO(self.file.read(size))

Conclusion

For acceleration of resources unpacking it is possible to use Cython, but the description of these methods is beyond this article. It is worth noting some requirements to plugins:

Useful links