Extending SkoolKit

Extension modules

While creating a disassembly of a game, you may find that SkoolKit’s suite of skool macros is inadequate for certain tasks. For example, the game might have large tile-based sprites that you want to create images of for the HTML disassembly, and composing long #UDGARRAY macros for them would be too tedious. Or you might want to insert a timestamp in the header of the ASM disassembly so that you (or others) can keep track of when your ASM files were written.

One way to solve these problems is to add custom methods that could be called by a #CALL macro. But where to add the methods? SkoolKit’s core HTML-writing and ASM-writing classes are skoolkit.skoolhtml.HtmlWriter and skoolkit.skoolasm.AsmWriter, so you could add the methods to those classes. But a better way is to subclass HtmlWriter and AsmWriter in a separate extension module, and add the methods there; then that extension module can be easily used with different versions of SkoolKit, and shared with other people.

A minimal extension module would look like this:

# Extension module in the skoolkit package directory
from .skoolhtml import HtmlWriter
from .skoolasm import AsmWriter

class GameHtmlWriter(HtmlWriter):
    pass

class GameAsmWriter(AsmWriter):
    pass

The next step is to get SkoolKit to use the extension module for your game. First, place the extension module (let’s call it game.py) in the skoolkit package directory; to locate this directory, run skool2html.py with the -p option:

$ skool2html.py -p
/usr/lib/python2.7/dist-packages/skoolkit

(The package directory may be different on your system.) With game.py in place, add the following line to the [Config] section of your disassembly’s ref file:

HtmlWriterClass=skoolkit.game.GameHtmlWriter

If you don’t have a ref file yet, create one (ideally named game.ref, assuming the skool file is game.skool); if the ref file doesn’t have a [Config] section yet, add one.

Now whenever skool2html.py is run on your skool file (or ref file), SkoolKit will use the GameHtmlWriter class instead of the core HtmlWriter class.

To get skool2asm.py to use GameAsmWriter instead of the core AsmWriter class when it’s run on your skool file, add the following @writer ASM directive somewhere after the @start directive, and before the @end directive (if there is one):

; @writer=skoolkit.game.GameAsmWriter

The skoolkit package directory is a reasonable place for an extension module, but it could be placed in another package, or somewhere else as a standalone module. For example, if you wanted to keep a standalone extension module in ~/.skoolkit, it should look like this:

# Standalone extension module
from skoolkit.skoolhtml import HtmlWriter
from skoolkit.skoolasm import AsmWriter

class GameHtmlWriter(HtmlWriter):
    pass

class GameAsmWriter(AsmWriter):
    pass

Then, assuming the extension module is game.py, the HtmlWriterClass parameter should be set thus:

HtmlWriterClass=~/.skoolkit:game.GameHtmlWriter

and the @writer directive should be set thus:

; @writer=~/.skoolkit:game.GameAsmWriter

#CALL methods

Implementing a method that can be called by a #CALL macro is done by adding the method to the HtmlWriter or AsmWriter subclass in the extension module.

One thing to be aware of when adding a #CALL method to a subclass of HtmlWriter is that the method must accept an extra parameter in addition to those passed from the #CALL macro itself: cwd. This parameter is set to the current working directory of the file from which the #CALL macro is executed, which may be useful if the method needs to provide a hyperlink to some other part of the disassembly (as in the case where an image is being created).

Let’s say your sprite-image-creating method will accept two parameters (in addition to cwd): sprite_id (the sprite identifier) and fname (the image filename). The method (let’s call it sprite) would look something like this:

from .skoolhtml import HtmlWriter

class GameHtmlWriter(HtmlWriter):
    def sprite(self, cwd, sprite_id, fname):
        img_path = self.image_path(fname)
        if self.need_image(img_path):
            udgs = self.build_sprite(sprite_id)
            self.write_image(img_path, udgs)
        return self.img_element(cwd, img_path)

With this method (and an appropriate implementation of the build_sprite method) in place, it’s possible to use a #CALL macro like this:

#UDGTABLE
{ #CALL:sprite(3,jumping) }
{ Sprite 3 (jumping) }
TABLE#

Adding a #CALL method to the AsmWriter subclass is equally simple. The timestamp-creating method (let’s call it timestamp) would look something like this:

import time
from .skoolasm import AsmWriter

class GameAsmWriter(AsmWriter):
    def timestamp(self):
        return time.strftime("%a %d %b %Y %H:%M:%S %Z")

With this method in place, it’s possible to use a #CALL macro like this:

; This ASM file was generated on #CALL:timestamp()

Skool macros

Another way to add a custom method is to implement it as a skool macro. The main differences between a skool macro and a #CALL method are:

  • a #CALL macro’s parameters are automatically evaluated and passed to the #CALL method; a skool macro’s parameters must be parsed and evaluated manually (typically by using one or more of the macro-parsing utility functions)
  • every optional parameter in a skool macro can be assigned a default value if omitted; in a #CALL method, only the optional arguments at the end can be assigned default values if omitted, whereas any others are set to None
  • numeric parameters in a #CALL macro are automatically converted to numbers before being passed to the #CALL method; no automatic conversion is done on the parameters of a skool macro

In summary: a #CALL method is generally simpler to implement than a skool macro, but skool macros are more flexible.

Implementing a skool macro is done by adding a method named expand_macroname to the HtmlWriter or AsmWriter subclass in the extension module. So, to implement a #SPRITE or #TIMESTAMP macro, we would add a method named expand_sprite or expand_timestamp.

A skool macro method must accept either two or three parameters, depending on whether it is implemented on a subclass of AsmWriter or HtmlWriter:

  • text - the text that contains the skool macro
  • index - the index of the character after the last character of the macro name (that is, where to start looking for the macro’s parameters)
  • cwd - the current working directory of the file from which the macro is being executed; this parameter must be supported by skool macro methods on an HtmlWriter subclass

A skool macro method must return a 2-tuple of the form (end, string), where end is the index of the character after the last character of the macro’s parameter string, and string is the HTML or text to which the macro should be expanded.

The expand_sprite method on GameHtmlWriter may therefore look something like this:

from .skoolhtml import HtmlWriter

class GameHtmlWriter(HtmlWriter):
    # #SPRITEspriteId[{X,Y,W,H}](fname)
    def expand_sprite(self, text, index, cwd):
        end, img_path, crop_rect, sprite_id = self.parse_image_params(text, index, 1)
        if self.need_image(img_path):
            udgs = self.build_sprite(sprite_id)
            self.write_image(img_path, udgs, crop_rect)
        return end, self.img_element(cwd, img_path)

With this method (and an appropriate implementation of the build_sprite method) in place, the #SPRITE macro might be used like this:

#UDGTABLE
{ #SPRITE3(jumping) }
{ Sprite 3 (jumping) }
TABLE#

The expand_timestamp method on GameAsmWriter would look something like this:

import time
from .skoolasm import AsmWriter

class GameAsmWriter(AsmWriter):
    def expand_timestamp(self, text, index):
        return index, time.strftime("%a %d %b %Y %H:%M:%S %Z")

Parsing skool macros

The skoolkit.skoolmacro module provides some utility functions that may be used to parse the parameters of a skool macro.

skoolkit.skoolmacro.parse_ints(text, index, num, defaults=())

Parse a string of comma-separated integer parameters. The string will be parsed until either the end is reached, or an invalid character is encountered. The set of valid characters consists of the comma, ‘$’, the digits 0-9, and the letters A-F and a-f.

Parameters:
  • text – The text to parse.
  • index – The index at which to start parsing.
  • num – The maximum number of parameters to parse.
  • defaults – The default values of the optional parameters.
Returns:

A list of the form [end, value1, value2...], where:

  • end is the index at which parsing terminated
  • value1, value2 etc. are the parameter values

skoolkit.skoolmacro.parse_params(text, index, p_text=None, chars='', except_chars='', only_chars='')

Parse a string of the form params[(p_text)]. The parameter string params will be parsed until either the end is reached, or an invalid character is encountered. The default set of valid characters consists of ‘$’, ‘#’, the digits 0-9, and the letters A-Z and a-z.

Parameters:
  • text – The text to parse.
  • index – The index at which to start parsing.
  • p_text – The default value to use for text found in parentheses.
  • chars – Characters to consider valid in addition to those in the default set.
  • except_chars – If not empty, all characters except those in this string are considered valid.
  • only_chars – If not empty, only the characters in this string are considered valid.
Returns:

A 3-tuple of the form (end, params, p_text), where:

  • end is the index at which parsing terminated
  • params is the parameter string
  • p_text is the text found in parentheses (if any)

New in version 3.6: The except_chars and only_chars parameters.

HtmlWriter also provides a method for parsing the parameters of an image-creating skool macro.

HtmlWriter.parse_image_params(text, index, num, defaults=(), path_id='UDGImagePath', fname='', chars='', ints=None)

Parse a string of the form params[{X,Y,W,H}][(fname)]. The parameter string params may contain comma-separated values and will be parsed until either the end is reached, or an invalid character is encountered. The default set of valid characters consists of the comma, ‘$’, the digits 0-9, and the letters A-F and a-f.

Parameters:
  • text – The text to parse.
  • index – The index at which to start parsing.
  • num – The maximum number of parameters to parse.
  • defaults – The default values of the optional parameters.
  • path_id – The ID of the target directory for the image file (as defined in the [Paths] section of the ref file).
  • fname – The default base name of the image file.
  • chars – Characters to consider valid in addition to those in the default set.
  • ints – A list of the indexes (0-based) of the parameters that must evaluate to an integer; if None, every parameter must evaluate to an integer.
Returns:

A list of the form [end, image_path, crop_rect, value1, value2...], where:

  • end is the index at which parsing terminated
  • image_path is either the full path of the image file (relative to the root directory of the disassembly) or fname (if path_id is blank or None)
  • crop_rect is (X, Y, W, H)
  • value1, value2 etc. are the parameter values

Changed in version 3.6: If path_id is blank or None, image_path is equal to fname.

New in version 3.6: The ints parameter.

Parsing ref files

HtmlWriter provides some convenience methods for extracting text and data from ref files. These methods are described below.

HtmlWriter.get_section(section_name, paragraphs=False, lines=False)

Return the contents of a ref file section.

Parameters:
  • section_name – The section name.
  • paragraphs – If True, return the contents as a list of paragraphs.
  • lines – If True, return the contents (or each paragraph) as a list of lines; otherwise return the contents (or each paragraph) as a single string.
HtmlWriter.get_sections(section_type, paragraphs=False, lines=False)

Return a list of 2-tuples of the form (suffix, contents) or 3-tuples of the form (infix, suffix, contents) derived from ref file sections whose names start with section_type followed by a colon. suffix is the part of the section name that follows either the first colon (when there is only one) or the second colon (when there is more than one); infix is the part of the section name between the first and second colons (when there is more than one).

Parameters:
  • section_type – The section name prefix.
  • paragraphs – If True, return the contents of each section as a list of paragraphs.
  • lines – If True, return the contents (or each paragraph) of each section as a list of lines; otherwise return the contents (or each paragraph) as a single string.
HtmlWriter.get_dictionary(section_name)

Return a dictionary built from the contents of a ref file section. Each line in the section should be of the form X=Y.

HtmlWriter.get_dictionaries(section_type)

Return a list of 2-tuples of the form (suffix, dict) derived from ref file sections whose names start with section_type followed by a colon. suffix is the part of the section name that follows the first colon, and dict is a dictionary built from the contents of that section; each line in the section should be of the form X=Y.

Memory snapshots

The snapshot attribute on HtmlWriter and AsmWriter is a 65536-element list that is populated with the contents of any DEFB, DEFM, DEFS and DEFW statements in the skool file.

A simple #PEEK macro that expands to the value of the byte at a given address might be implemented by using snapshot like this:

from .skoolhtml import HtmlWriter
from .skoolasm import AsmWriter
from .skoolmacro import parse_ints

class GameHtmlWriter(HtmlWriter):
    # #PEEKaddress
    def expand_peek(self, text, index, cwd):
        end, address = parse_ints(text, index, 1)
        return end, str(self.snapshot[address])

class GameAsmWriter(AsmWriter):
    # #PEEKaddress
    def expand_peek(self, text, index):
        end, address = parse_ints(text, index, 1)
        return end, str(self.snapshot[address])

HtmlWriter also provides some methods for saving and restoring memory snapshots, which can be useful for temporarily changing graphic data or the contents of data tables. These methods are described below.

HtmlWriter.push_snapshot(name='')

Save the current memory snapshot for later retrieval (by pop_snapshot()), and put a copy in its place.

Parameters:name – An optional name for the snapshot.
HtmlWriter.pop_snapshot()

Discard the current memory snapshot and replace it with the one that was most recently saved (by push_snapshot()).

HtmlWriter.get_snapshot_name()

Return the name of the current memory snapshot.

Graphics

If you are going to implement custom image-creating #CALL methods or skool macros, you will need to make use of the skoolkit.skoolhtml.Udg class.

The Udg class represents an 8x8 graphic (8 bytes) with a single attribute byte, and an optional mask.

class skoolkit.skoolhtml.Udg(attr, data, mask=None)

Initialise the UDG.

Parameters:
  • attr – The attribute byte.
  • data – The graphic data (sequence of 8 bytes).
  • mask – The mask data (sequence of 8 bytes).

A simple #INVERSE macro that creates an inverse image of a UDG might be implemented like this:

from .skoolhtml import HtmlWriter, Udg
from .skoolmacro import parse_ints

class GameHtmlWriter(HtmlWriter):
    # #INVERSEaddress,attr
    def expand_inverse(self, text, index, cwd):
        end, address, attr = parse_ints(text, index, 2)
        img_path = self.image_path('inverse{0}_{1}'.format(address, attr))
        if self.need_image(img_path):
            udg_data = [b ^ 255 for b in self.snapshot[address:address + 8]]
            udg = Udg(attr, udg_data)
            self.write_image(img_path, [[udg]])
        return end, self.img_element(cwd, img_path)

The Udg class provides two methods for manipulating an 8x8 graphic: flip and rotate.

Udg.flip(flip=1)

Flip the UDG.

Parameters:flip – 1 to flip horizontally, 2 to flip vertically, or 3 to flip horizontally and vertically.
Udg.rotate(rotate=1)

Rotate the UDG 90 degrees clockwise.

Parameters:rotate – The number of rotations to perform.

If you are going to implement #CALL methods or skool macros that create animated images, you will need to make use of the skoolkit.skoolhtml.Frame class.

The Frame class represents a single frame of an animated image.

class skoolkit.skoolhtml.Frame(udgs, scale=1, mask=False, x=0, y=0, width=None, height=None, delay=32)

Create a frame of an animated image.

Parameters:
  • udgs – The two-dimensional array of tiles (instances of Udg) from which to build the frame.
  • scale – The scale of the frame.
  • mask – Whether to apply masks to the tiles in the frame.
  • x – The x-coordinate of the top-left pixel to include in the frame.
  • y – The y-coordinate of the top-left pixel to include in the frame.
  • width – The width of the frame; if None, the maximum width (derived from x and width of the array of tiles) is used.
  • height – The height of the frame; if None, the maximum height (derived from y and height of the array of tiles) is used.
  • delay – The delay between this frame and the next in 1/100ths of a second.

New in version 3.6.

HtmlWriter provides the following image-related convenience methods.

HtmlWriter.image_path(fname, path_id='UDGImagePath')

Return the full path of an image file relative to the root directory of the disassembly. If fname does not end with ‘.png’ or ‘.gif’, an appropriate suffix will be appended (depending on the default image format). If fname starts with a ‘/’, it will be removed and the remainder returned (in which case path_id is ignored). If fname is blank, None is returned.

Parameters:
  • fname – The name of the image file.
  • path_id – The ID of the target directory (as defined in the [Paths] section of the ref file).
HtmlWriter.need_image(image_path)

Return whether an image file needs to be created. This will be true only if the file doesn’t already exist, or all images are being rebuilt. Well-behaved image-creating methods will call this to check whether an image file needs to be written, and thus avoid building an image when it is not necessary.

Parameters:image_path – The full path of the image file relative to the root directory of the disassembly.
HtmlWriter.write_image(image_path, udgs, crop_rect=(), scale=2, mask=False)

Create an image and write it to a file.

Parameters:
  • image_path – The full path of the file to which to write the image (relative to the root directory of the disassembly).
  • udgs – The two-dimensional array of tiles (instances of Udg) from which to build the image.
  • crop_rect – The cropping rectangle, (x, y, width, height), where x and y are the x- and y-coordinates of the top-left pixel to include in the final image, and width and height are the width and height of the final image.
  • scale – The scale of the image.
  • mask – Whether to apply masks to the tiles in the image.
HtmlWriter.write_animated_image(image_path, frames)

Create an animated image and write it to a file.

Parameters:
  • image_path – The full path of the file to which to write the image (relative to the root directory of the disassembly).
  • frames – A list of the frames (instances of Frame) from which to build the image.

New in version 3.6.

HtmlWriter.img_element(cwd, image_path, alt=None)

Return an <img .../> element for an image file.

Parameters:
  • cwd – The current working directory (from which the relative path of the image file will be computed).
  • image_path – The full path of the image file relative to the root directory of the disassembly.
  • alt – The alt text to use for the image; if None, the base name of the image file (with the ‘.png’ or ‘.gif’ suffix removed) will be used.
HtmlWriter.screenshot(x=0, y=0, w=32, h=24, df_addr=16384, af_addr=22528)

Return a two-dimensional array of tiles (instances of Udg) built from the display file and attribute file of the current memory snapshot.

Parameters:
  • x – The x-coordinate of the top-left tile to include (0-31).
  • y – The y-coordinate of the top-left tile to include (0-23).
  • w – The width of the array (in tiles).
  • h – The height of the array (in tiles).
  • df_addr – The display file address to use.
  • af_addr – The attribute file address to use.
HtmlWriter.flip_udgs(udgs, flip=1)

Flip a 2D array of UDGs (instances of Udg).

Parameters:
  • udgs – The array of UDGs.
  • flip – 1 to flip horizontally, 2 to flip vertically, or 3 to flip horizontally and vertically.
HtmlWriter.rotate_udgs(udgs, rotate=1)

Rotate a 2D array of UDGs (instances of Udg) 90 degrees clockwise.

Parameters:
  • udgs – The array of UDGs.
  • rotate – The number of rotations to perform.

HtmlWriter initialisation

If your HtmlWriter subclass needs to perform some initialisation tasks, such as creating instance variables, or parsing ref file sections, the place to do that is the init() method.

HtmlWriter.init()

Perform post-initialisation operations. This method is called after __init__() has completed. By default the method does nothing, but subclasses may override it.

For example:

from .skoolhtml import HtmlWriter

class GameHtmlWriter(HtmlWriter):
    def init(self):
        # Get character names from the ref file
        self.characters = self.get_dictionary('Characters')