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
- NAME defines the group name displayed in the list of all groups in the program.
- FORMATS_ARCHIVE represents the list of archive formats and supported games.
- TYPES_ARCHIVE - the list of the tuples consisting of two elements: a line and a tuple. Designation of archive which is displayed in the dropping-out УFile typeФ list file of selection dialogue box is stored in a line. Masks of expansions of archives are stored in a tuple.
- GAMES - the list of games of this group (it is displayed at the choice of group in the program).
- AUTHOR - summary of the author of a plugin: name (or nickname) and contact information
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.
- self.file - an open archive stream . After reading archive you shouldnТt close it!
- self.data - the list of tuples or lists. Each list in self.data can have unlimited numbers
of elements, but the first four elements always have to have values: resource name, offset, size, format.
- Resource name - a full name of a resource in archive (or a relative way);
- Offset - resource offset in archive;
- Size - the resource size;
- Format - a resource format.
The following two objects Ц the list and the dictionary - are used for sorting of the resources list.
- self.sup_formats - the list of the supported formats.
- self.sup_types - the dictionary, where a key is a format, and value is a type. At the moment
the program supports 3 types of resources:
- 1 - Image
- 2 - Animation
- 3 - Sound
- self.images - the list of images - PIL.Image objects
- self.sound - the buffer with a sound - io.BytesIO ()
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:
- Number of frames
- for each frame:
- Frame Width
- Frame height
- RGBA data of the image
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:
- Archives and resources expansions are have to written in a lower case;
- The following names of functions, constants, variables shouldn't change:
- Game_Res
- open_data
- unpack
- self.file
- self.data
- self.sup_formats
- self.sup_types
- self.images
- self.sound