#! /usr/bin/perl
#
# convert2chroma.pl
# Convert various other games into a format playable by Chroma
#
# The following inputs are recognised:
#
# BBC, BBC Designer     - disc image, memory snapshot
# C64                   - memory snapshot
# Spectrum              - tape image, memory snapshot
# CPC                   - memory snapshot
# Atari                 - memory snapshot
# Amiga                 - xor_lib directory
#
###############################################################################
#
# Copyright (C) 2010-2019 Amf
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version. 
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
###############################################################################
#
# BBC levels
# ==========
# The BBC XOR Designer file format is as follows:
# 
#     in file                                                     in memory
#     0x000 - 0x1c1 Map data                                      0x900 - 0xbff
#     0x1c2 - 0x1c3 Address of top left map piece - 32            0x78 - 0x79
#     0x1c4 - 0x1c5 Address of top right map piece - 32           0x7a - 0x7b
#     0x1c6 - 0x1c7 Address of bottom left map piece - 32         0x7c - 0x7d
#     0x1c8 - 0x1c9 Address of bottom right map piece - 32        0x7e - 0x7f
#     0x1ca - 0x1cb Address of Player1 - 32                       0x80 - 0x81
#     0x1cc - 0x1cd Offset of Player1 from View1 (x,y)            0x82 - 0x83
#     0x1ce - 0x1cf View1 (x,y)                                   0x84 - 0x85
#     0x1d0 - 0x1d1 Address of View1                              0x86 - 0x87
#     0x1d2 - 0x1d3 Address of Player2 - 32                       0x88 - 0x89
#     0x1d4 - 0x1d5 Offset of Player2 from View2 (x,y)            0x8a - 0x8b
#     0x1d6 - 0x1d7 View2 (x, y)                                  0x8c - 0x8d
#     0x1d8 - 0x1d9 Address of View2                              0x8e - 0x8f
#     0x1da         Current player                 (must be 0x02) 0x90
#     0x1db         Unused - Other player alive   (ignored, 0x02) 0x91
#     0x1dc         Masks total                                   0x92
#     0x1dd         Masks collected                (must be 0x00) 0x93
#     0x1de         Unused - List length           (must be 0x00) 0x94
#     0x1df         Unused - Explosion number     (ignored, 0x00) 0x95
#     0x1e0         Unused - Switched             (ignored, 0x00) 0x96
#     0x1e1         Unused                                 (0x00) 0x97
#     0x1e2 - 0x1e3 Address of Teleport1                          0x98 - 0x99
#     0x1e4 - 0x1e5 Offset of Teleport1 from ViewTeleport1 (x,y)  0x9a - 0x9b
#     0x1e6 - 0x1e7 ViewTeleport1 (x,y)                           0x9c - 0x9d
#     0x1e8 - 0x1e9 Address of ViewTeleport1                      0x9e - 0x9f
#     0x1ea - 0x1eb Address of Teleport2                          0xa0 - 0xa1
#     0x1ec - 0x1ed Offset of Teleport2 from ViewTeleport2 (x,y)  0xa2 - 0xa3
#     0x1ee - 0x1ef ViewTeleport2 (x,y)                           0xa4 - 0xa5
#     0x1f0 - 0x1f1 Address of ViewTeleport2                      0xa6 - 0xa7
#     0x1f2 - 0x1ff Unused                                 (0x00)
#     0x200 - 0x211 Title
#     0x220 - 0x283 Completion message
#
# For BBC XOR, the level data is split into two chunks, 0x000 - 0x1ff and
# 0x200 - 0x211, located in different parts of the file. There is no
# completion message.
#
# In memory, the map data is stored in a 32 * 32 byte array, starting at 0x900.
# The address for the piece at (x, y) is therefore 0x900 + x + (32 * y). In the
# file, the map data defines only the inner grid from (1,1) to (30,30). It runs
# from left to right, top to bottom, and should be read as nibbles, with the
# least significant nibble coming first. The nibbles translate as follows:
# 
#     0x0  Space                          PIECE_SPACE
#     0x1  Wall                    %      PIECE_WALL
#     0x2  Magus                   1      PIECE_PLAYER_ONE 
#     0x3  Questor                 2      PIECE_PLAYER_TWO
#     0x4  Map                     MmNn   PIECE_MAP_[TOP|BOTTOM]_[LEFT|RIGHT]
#     0x5  Dots                    -      PIECE_DOTS_X
#     0x6  Waves                   |      PIECE_DOTS_Y
#     0x7  Fish                    d      PIECE_ARROW_RED_DOWN
#     0x8  Chicken                 a      PIECE_ARROW_RED_LEFT
#     0x9  H Bomb                  D      PIECE_BOMB_RED_DOWN
#     0xa  V Bomb                  A      PIECE_BOMB_RED_LEFT
#     0xb  Mask                    *      PIECE_STAR
#     0xc  Door                    /      PIECE_DOOR
#     0xd  Fat Doll                o      PIECE_CIRCLE
#     0xe  Beam Me Up Scotty       T      PIECE_TELEPORT
#     0xf  Switch                  S      PIECE_SWITCH
#
# The map pieces are stored ambiguously in the map data. To resolve this, the
# address of each map piece is stored separately in 0x1c2 - 0x1c9. Note that
# these addresses, as well as those for the players, are offset by 32 bytes to
# allow for relative addressing - disassembling the code makes this clearer.
#
# Contrary to what the value of 0x1da might suggest, Questor moves first,
# thanks to a call to 0x17b7 when setting up the level - this is a hack.
# 
# The completion message is XORed with 0x7e to prevent casual reading.
# 
# There are fourteen common levels shared between BBC XOR and BBC XOR Designer:
# 
#         title                         differences
#      1: Dots And Waves                View2
#      2: Something Fishy
#      3: Chicken Supreme               View1 View2 
#      4: Explosive Mixture             View1
#      5: Henrys Anguish                View1 View2 
#      6: The Dolls House               View1 View2 
#      7: Dollys Revenge                View1 View2 
#      8: Enlightenment                 View2
#      9: The Challenge                 View1 View2 
#     10: Patience Pending              View1 View2 ViewTeleport1 ViewTeleport2
#     11: Razor Edge                    View2 ViewTeleport1 ViewTeleport2
#     12: The Happy Hour                View2 ViewTeleport1
#     13: Deja Vu                       View2
#     14: Penultimate                   View1 View2 ViewTeleport1
# 
# BBC XOR has a fifteenth level not present in XOR Designer:
# 
#     15: The Decoder
# 
# BBC XOR Designer has two hidden levels which aren't directly accessible:
# 
#     x1: Wheel Of Fortune
#     x2: The Hidden Cavern
# 
# BBC graphics
# ============
# The XOR Designer graphics format contains 16 sprites, following the order
# used for map data. Each sprite is stored in 72 bytes, being 12 x 24 pixels
# in standard BBC four colour mode. For XOR, the sprites for the walls are
# stored separately to the sprites for the pieces. There are also two extra
# wall sprites which we add to the end of the standard 16.
#
# Commodore 64 levels and graphics
# ================================
# The Commodore 64 XOR levels and graphics are stored in an identical format
# to those of the BBC version, and thus may be extracted in a similar way.
# Again, Questor move first. There are no differences between the levels and
# those of the BBC version.
#
# ZX Spectrum levels
# ==================
# The ZX Spectrum version splits the level data into two different chunks -
# the map data itself, and the location data, which takes the following format:
# 
#     0x00 - 0x01 Player1 (x, y)
#     0x02 - 0x03 Address of Player1
#     0x04 - 0x05 Offset of Player1 from View1 (x, y)
#     0x06 - 0x07 Address of View1
#     0x08 - 0x09 Player2 (x, y)
#     0x0a - 0x0b Address of Player2
#     0x0c - 0x0d Offset of Player2 from View1 (x, y)
#     0x0e - 0x0f Address of View2
#     0x10        Masks total (binary packed decimal)
#     0x11 - 0x02 Address of Teleport2
#     0x13 - 0x14 Teleport1 (x, y)
#     0x15 - 0x16 Address of Teleport1
#     0x17 - 0x18 Offset of Teleport1 from ViewTeleport1 (x, y)
#     0x19 - 0x1a Address of ViewTeleport1
#     0x1b - 0x1c Address of Teleport1
#     0x1d - 0x1e Teleport2 (x, y)
#     0x1f - 0x20 Address of Teleport2
#     0x21 - 0x22 Offset of Teleport2 from ViewTeleport2 (x, y)
#     0x23 - 0x24 Address of ViewTeleport2
#     0x25 - 0x26 Address of top left map piece
#     0x25 - 0x26 Address of top right map piece
#     0x25 - 0x26 Address of bottom left map piece
#     0x25 - 0x26 Address of bottom right map piece
#     0x2d - 0x2e Address of map data
#
# The map data is stored as per BBC XOR, and is unpacked to memory at 0xe500.
# The address for the piece at (x, y) is therefore 0xe500 + x + (32 * y).
# Unlike the BBC version, none of the addresses are offset in any way, and
# Magus is the first to move.
#
#         title                         differences from the BBC version
#      1: Dots And Waves                View1 View2
#      2: Something Fishy               1 (extra wall)
#      3: Chicken Supreme              
#      4: Explosive Mixture             3 (maps swapped), View1
#      5: Henrys Anguish                 
#      6: The Dolls House                
#      7: Dollys Revenge                 
#      8: Enlightenment                   
#      9: The Challenge                 View1 View2
#     10: Patience Pending              
#     11: Razor Edge                    2 (maps swapped)
#     12: The Happy Hour                
#     13: Deja Vu                       
#     14: Penultimate                   2 (players swapped), View1 View2
#     15: The Decoder
#
# Amstrad CPC levels
# ==================
# The Amstrad CPC XOR levels are stored in a similar format to those of the
# ZX Spectrum version, with the exception that the map data is unpacked to
# memory at 0x3c00, which affects the addresses stored in the location data.
# Again, Magus is the first to move.
#
#         title                         differences from the BBC version
#      1: Dots And Waves                View1 View2
#      2: Something Fishy               1 (extra wall)
#      3: Chicken Supreme
#      4: Explosive Mixture             3 (maps swapped), View1
#      5: Henrys Anguish
#      6: The Dolls House
#      7: Dollys Revenge
#      8: Enlightenment
#      9: The Challenge                 View1 View2
#     10: Patience Pending                
#     11: Razor Edge                    2 (maps swapped)
#     12: The Happy Hour                  
#     13: Deja Vu
#     14: Penultimate                   2 (players swapped), View1 View2
#     15: The Decoder                   10 (corruption of final line)
#
#         title                         differences from the Spectrum version
#     15: The Decoder                   10 (corruption of final line)
#
# Atari ST levels
# ===============
# The Atari ST level format is as follows:
#
#     0x000 - 0x3ff Map data
#     0x400 - 0x401 View1x
#     0x402 - 0x403 View1y
#     0x404 - 0x405 Offset from View1x to Player1x
#     0x406 - 0x407 Offset from View1y to Player1y
#     0x408 - 0x409 0x00 0x11
#     0x40a - 0x40b View2x
#     0x40c - 0x40d View2y
#     0x40e - 0x40f Offset from View2x to Player2x
#     0x410 - 0x411 Offset from View2y to Player2y
#     0x412 - 0x413 0x00 0x12
#     0x414 - 0x415 Masks total
#     0x416 - 0x417 ViewTeleport1x
#     0x418 - 0x419 ViewTeleport1y
#     0x41a - 0x41b 0x00 0x00
#     0x41c - 0x41d Address of Teleport1
#     0x41e - 0x41f Offset from ViewTeleport1x to Teleport1x
#     0x420 - 0x421 Offset from ViewTeleport1y to Teleport1y
#     0x422 - 0x423 ViewTeleport2x
#     0x424 - 0x425 ViewTeleport2y
#     0x426 - 0x427 0x00 0x00
#     0x428 - 0x429 Address of Teleport2
#     0x42a - 0x42b Offset from ViewTeleport2x to Teleport2x
#     0x42c - 0x42d Offset from ViewTeleport2y to Teleport2y
#     0x42e - 0x431 0x1b 0x59 0x21 0x23
#     0x432 - 0x445 Title
#
# The address for (x,y) is stored as x + (32 * y) - there is no offset.
# The map data defines the whole grid from (0, 0) to (31, 31). It runs from
# left to right, then from top to bottom. It should be read as bytes, which
# are translated as follows:
#
#     0x00        Space                     PIECE_SPACE
#     0x01        Wall               %      PIECE_WALL
#     0x02        Magus              1      PIECE_PLAYER_ONE
#     0x03        Questor            2      PIECE_PLAYER_TWO
#     0x04        Map top left       M      PIECE_MAP_TOP_LEFT
#     0x05        Dots               -      PIECE_DOTS_X
#     0x06        Waves              |      PIECE_DOTS_Y
#     0x07        Fish               d      PIECE_ARROW_RED_DOWN
#     0x08        Chicken            a      PIECE_ARROW_RED_LEFT
#     0x09        H Bomb             D      PIECE_BOMB_RED_DOWN
#     0x0a        V Bomb             A      PIECE_BOMB_RED_LEFT
#     0x0b        Mask               *      PIECE_STAR
#     0x0c        Door               /      PIECE_DOOR
#     0x0d        Fat Doll           o      PIECE_CIRCLE
#     0x0e        Map top right      m      PIECE_MAP_TOP_RIGHT
#     0x0f        Map bottom left    N      PIECE_MAP_BOTTOM_LEFT
#     0x10        Map bottom right   n      PIECE_MAP_BOTTOM_RIGHT
#     0x11        Beam Me Up Scotty  T      PIECE_TELEPORT
#     0x12        Switch             S      PIECE_SWITCH
#     0x13        Fish               d      PIECE_ARROW_RED_DOWN
#     0x14        Chicken            a      PIECE_ARROW_RED_LEFT
#     0x15        Chicken            a      PIECE_ARROW_RED_LEFT
#     0x16        Fish               d      PIECE_ARROW_RED_DOWN
#     0x17        Fish               d      PIECE_ARROW_RED_DOWN
#     0x18        Chicken            a      PIECE_ARROW_RED_LEFT
#     0x19        Chicken            a      PIECE_ARROW_RED_LEFT
#     0x1a        Fish               d      PIECE_ARROW_RED_DOWN
#     0x21 - 0x5c Wall               %      PIECE_WALL
#     0x61        End piece          ?      PIECE_UNKNOWN
#     0x62        Wall               %      PIECE_WALL
#
# Magus moves first.
#
#         title                         differences from the BBC version
#      1: Dots And Waves                1 View1 View2
#      2: Something Fishy               3 (players swapped)
#      3: Chicken Supreme
#      4: Explosive Mixture             
#      5: Henrys Anguish                2 (players swapped) View1 View2
#      6: The Dolls House
#      7: Dollys Revenge
#      8: Enlightenment
#      9: The Challenge                 1 View1 View2
#     10: Patience Pending              ViewTeleport1 ViewTeleport2
#     11: Razor Edge                    
#     12: The Happy Hour                View2 ViewTeleport1 ViewTeleport2
#     13: Deja Vu
#     14: Penultimate                   View1 View2 ViewTeleport1
#     15: The Decoder                   Ending ViewTeleport1 ViewTeleport2
#
# Amiga levels
# ============
# Amiga .maze files have fifteen levels, sequentially, each with the following
# format:
# 
#     0x000 - 0x01e Title
#     0x01f - 0x41e Map Data
#     0x420 - 0x423 View 1 x
#     0x424 - 0x427 View 1 y
#     0x428 - 0x42b Player 1 x
#     0x42c - 0x42f Player 1 y
#     0x430 - 0x433 View 2 x
#     0x434 - 0x437 View 2 y
#     0x438 - 0x43b Player 2 x
#     0x43c - 0x43f Player 2 y
#     0x440 - 0x443 Mask Total
#     0x444 - 0x447 ViewTeleport 1 x
#     0x448 - 0x44b ViewTeleport 1 y
#     0x44c - 0x44f Teleport 1 x
#     0x450 - 0x453 Teleport 1 y
#     0x454 - 0x457 ViewTeleport 1 x
#     0x458 - 0x45b ViewTeleport 1 y
#     0x45c - 0x45f Teleport 1 x
#     0x460 - 0x463 Teleport 1 y
# 
# The map data defines the whole grid from (0, 0) to (31, 31). It runs from
# left to right, then from top to bottom. It should be read as bytes; the
# lowest six bits of each byte determine the piece:
# 
#     0x00  Space                        PIECE_SPACE
#     0x01  Magus                 1      PIECE_PLAYER_ONE
#     0x02  Questor               2      PIECE_PLAYER_TWO 
#     0x03  Fish                  v      PIECE_ARROW_RED_DOWN
#     0x04  Chicken               <      PIECE_ARROW_RED_LEFT
#     0x05  Fat Doll              o      PIECE_CIRCLE
#     0x06  Mask                  *      PIECE_STAR
#     0x07  Switch                S      PIECE_SWITCH
#     0x08  Map TL                M      PIECE_MAP_TOP_LEFT
#     0x09  Map TR                m      PIECE_MAP_TOP_RIGHT
#     0x0a  Map BL                N      PIECE_MAP_BOTTOM_LEFT
#     0x0b  Map BR                n      PIECE_MAP_BOTTOM_RIGHT
#     0x0c  Beam Me Up Scotty     T      PIECE_TELEPORT
#     0x0d  Waves                 |      PIECE_EARTH_Y
#     0x0e  Dots                  -      PIECE_EARTH_X
#     0x0f  V Bomb                Z      PIECE_BOMB_RED_LEFT
#     0x10  H Bomb                X      PIECE_BOMB_RED_DOWN
#     0x11  Door                  /      PIECE_DOOR
#     0x20  Wall                  %      PIECE_WALL
#
# Questor is the first to move.
# 
# There are two sets of fifteen levels:
# 
#     XOR's Mazes:
# 
#         Amiga title        BBC equivalent     differences from BBC version
#      1: Olaf's Warm Up     Dots and Waves     2 View1 View2
#      2: Rocks & Zeppelins  Something Fishy    3 (players swapped)
#      3: Something Tricky   Chicken Supreme    
#      4: Explosive Mixture  Explosive Mixture  
#      5: Erik's Anguish     Henrys Anguish     3 (players swapped) View1 View2
#      6: The Ball's House   The Doll's House   
#      7: Rolling Revenge    Dollys Revenge     
#      8: Enlightenment      Enlighenment       
#      9: The Challenge      The Challenge      1 View1 View2
#     10: Patience Pending   Patience Pending   ViewTeleport1 ViewTeleport2
#     11: Razor Edge         Razor Edge         8 (last line corrupted)
#     12: The Happy Hour     The Happy Hour     View2 ViewTeleport1 ViewTeleport2
#     13: Deja Vu            Deja Vu            
#     14: Penultimate        Penultimate        View1 View ViewTeleport2
#     15: The Decoder        The Decoder        Ending ViewTeleport1 ViewTeleport2
#  
#
#     Procyon's Mazes:
# 
#         Amiga title        BBC equivalent     differences from BBC version
#      1: Taurus                  
#      2: Andromeda                       
#      3: Perseus                         
#      4: Cameloparous                    
#      5: Ursa Nagor                            insoluble!
#      6: Draco                   
#      7: Hercules                        
#      8: Delphinus                       
#      9: Aquila                  
#     10: Ophiuchus          Wheel of Fortune   2 View1 View2 ViewTeleport1 ViewTeleport2
#     11: Corvus                  
#     12: Canis Maior                     
#     13: Hydrus                  
#     14: Vela Carina                           insoluble!
#     15: Crux               The Hidden Cavern  View1 View2 ViewTeleport2
# 
# Amiga Graphics
# ==============
# There are fifteen sets of 20 background sprites in the "vimages*" files. The
# format begins with a 32 colour palette, with each entry taking two bytes, of
# the form 0x0R 0xGB. This is followed by a number of 960 byte blocks, with
# each block encoding one bit of colour data for ten sprites. Within each
# block, the first three bytes of a word encode 24 pixels for a sprite. The
# first two blocks encode bit 1 for sprites 0 - 9 and 10 - 19, the next two
# encode bit 2, and so on.
# 
# The "cimages" file contains one set of 60 character sprites. It takes a
# similar format to the "vimages*" files, but contains no palette information.
# 
# Enigma
# ======
# Enigma has five different wall pieces, but these are merely cosmetic. We
# merge them all into the single wall piece present in Chroma. Enigma also has
# "certain death" piece, which we also replace with a wall, on the basis that 
# in a successful game, the player will interact with this piece in exactly the
# same manner as a wall.
# 

@commands = (
        # Convert XOR levels and graphics
        ["bbc", \&convert_bbc_all],
        ["designer", \&convert_designer_all],
        ["c64", \&convert_c64_all],
        ["spectrum", \&convert_spectrum_all],
        ["cpc", \&convert_cpc_all],
        ["atari", \&convert_atari_all],
        ["amiga", \&convert_amiga_all],

        # Convert XOR levels
        ["bbc-levels", \&convert_bbc_levels],
        ["c64-levels", \&convert_bbc_levels],
        ["designer-levels", \&convert_designer_levels],
        ["designer-level-file", \&convert_designer_level],
        ["spectrum-levels", \&convert_spectrum_levels],
        ["cpc-levels", \&convert_spectrum_levels],
        ["atari-levels", \&convert_atari_levels],
        ["amiga-levels", \&convert_amiga_levels],

        # Convert XOR graphics
        ["bbc-graphics", \&convert_bbc_graphics],
        ["c64-graphics", \&convert_c64_graphics],
        ["designer-graphics", \&convert_bbc_graphics],
        ["designer-graphics-file", \&convert_bbc_graphics_file],
        ["spectrum-graphics", \&convert_spectrum_graphics],
        ["cpc-graphics", \&convert_cpc_graphics],
        ["atari-graphics", \&convert_atari_graphics],
        ["amiga-graphics", \&convert_amiga_graphics],
        ["amiga-graphics-file", \&convert_amiga_graphics_file],

        # Convert BBC XOR music
        ["bbc-music", \&convert_bbc_music],
        # Patch BBC XOR disc image to show all levels
        ["bbc-patch", \&convert_bbc_patch],
        # Patch Amiga disc image to avoid manual "copy protection" check
        ["amiga-patch", \&convert_amiga_patch],

        # Convert Chroma XOR levels back into 8bit binary files
        ["chroma2xor", \&convert_chroma2xor],

        # Convert third party XOR files
        ["zanten-file", \&convert_zanten_level], # http://www.rvvz.demon.nl/xor/
        ["ovine-file", \&convert_ovine_level],   # http://xor.ovine.net/

        # Convert Engima level(s)
        ["enigma", \&convert_enigma],
        ["enigma-file", \&convert_enigma_file],
        ["chroma2enigma", \&convert_chroma2enigma]

        );

@titles = (
        "Dots and Waves",
        "Something Fishy",
        "Chicken Supreme",
        "Explosive Mixture",
        "Henrys Anguish",
        "The Dolls House",
        "Dollys Revenge",
        "Enlightenment",
        "The Challenge",
        "Patience Pending",
        "Razor Edge",
        "The Happy Hour",
        "Deja Vu",
        "Penultimate",
        "The Decoder"
        );

if($#ARGV >= 2)
{
    for($i = 0; $i <= $#commands; $i ++)
    {
        if($ARGV[0] eq "--".$commands[$i][0])
        {
            $command = $commands[$i][1];
            &$command($ARGV[1], $ARGV[2]);
            exit(0);
        }
    }
}

print STDERR << "EOF";
Usage: convert2chroma.pl <mode> <input> <output>

    convert2chroma.pl --bbc <input> <output>            # BBC XOR
    convert2chroma.pl --designer <input> <output>        # BBC XOR Designer
    convert2chroma.pl --c64 <input> <output>            # Commodore 64 XOR
    convert2chroma.pl --spectrum <input> <output>        # Spectrum XOR
    convert2chroma.pl --cpc <input> <output>            # Amstrad CPC XOR
    convert2chroma.pl --amiga <input> <output>          # Amiga XOR
    convert2chroma.pl --atari <input> <output>          # Amiga ST XOR

    convert2chroma.pl --enigma <input> <output>         # Enigma

There are various other options available; see inside the script for details.

EOF

exit(0);

###############################################################################

sub read_file
{
    my $file = shift(@_);
    my $data;

    open(INPUT, $file) || die "Couldn't open file ($file): $!\n";
    binmode INPUT;
    undef $/;
    $data = <INPUT>;
    close INPUT;

    return $data;
}

sub locate_string
{
    my $data = shift(@_);
    my $string = shift(@_);
    my $name = shift(@_);
    my $index;

    $index = index($$data, $string);
    
    if($index != -1 && index($$data, $string, $index + 1) != -1)
    {
        print "Ambiguous match";
        return -2;
    }

    return $index;
}

sub byte
{
    my $data = shift(@_);
    my $offset = shift(@_);

    return ord(substr($$data, $offset, 1));
} 

sub short
{
    my $data = shift(@_);
    my $offset = shift(@_);

    return ord(substr($$data, $offset, 1)) + 256 * ord(substr($$data, $offset + 1, 1));
}

sub long
{
    my $data = shift(@_);
    my $offset = shift(@_);

    return ord(substr($$data, $offset + 3, 1)) + 256 * ord(substr($$data, $offset + 2, 1));
}

sub convert_address
{
    my $address = shift(@_);
    my $x = shift(@_);
    my $y = shift(@_);

    $$x = $address % 32;
    $$y = int($address / 32);
}

sub make_directory
{
    my $directory = shift(@_);

    if(! -e $directory)
    {   
        mkdir($directory, 0755) || die "Couldn't make directory ($directory): $!\n";
    }
}

###############################################################################

sub create_level
{
    my $level = shift(@_);
    my $file_output = shift(@_);

                   #  12M
    $translation = " %???-|daDA*/oTS";

    print "Creating level '$file_output': ";

    for($j = 0; $j < 32; $j ++)
    {
        for($i = 0; $i < 32; $i ++)
        {
            $map[$i][$j] = "%";
        }   
    }

    $title2number{"Andromeda"} = 2;
    $title2number{"Aquila"} = 9;
    $title2number{"Cameloparous"} = 4;
    $title2number{"Canis Maior"} = 12;
    $title2number{"Chicken Supreme"} = 3;
    $title2number{"Corvus"} = 11;
    $title2number{"Crux"} = 15;
    $title2number{"Deja Vu"} = 13;
    $title2number{"Delphinus"} = 8;
    $title2number{"Dollys Revenge"} = 7;
    $title2number{"Dots And Waves"} = 1;
    $title2number{"Dots and Waves"} = 1;
    $title2number{"Draco"} = 6;
    $title2number{"Enlightenment"} = 8;
    $title2number{"Erik's Anguish"} = 5;
    $title2number{"Explosive Mixture"} = 4;
    $title2number{"Henrys Anguish"} = 5;
    $title2number{"Hercules"} = 7;
    $title2number{"Hydrus"} = 13;
    $title2number{"Olaf's Warm Up"} = 1;
    $title2number{"Ophiuchus"} = 10;
    $title2number{"Patience Pending"} = 10;
    $title2number{"Penultimate"} = 14;
    $title2number{"Perseus"} = 3;
    $title2number{"Razor Edge"} = 11;
    $title2number{"Rocks & Zeppelins"} = 2;
    $title2number{"Rolling Revenge"} = 7;
    $title2number{"Something Fishy"} = 2;
    $title2number{"Something Tricky"} = 3;
    $title2number{"Taurus"} = 1;
    $title2number{"The Ball's House"} = 6;
    $title2number{"The Challenge"} = 9;
    $title2number{"The Decoder"} = 15;
    $title2number{"The Dolls House"} = 6;
    $title2number{"The Happy Hour"} = 12;
#   $title2number{"The Hidden Cavern"} = 15;
    $title2number{"Ursa Nagor"} = 5;
    $title2number{"Vela Carina"} = 14;
#   $title2number{"Wheel Of Fortune"} = 10;

    # Populate map from data
    $teleports = 0;

    # Compressed map - four bits per piece
    if($$level{"mode"} eq "8bit")
    {
        $offset = 0;
        for($j = 1; $j < 31; $j ++)
        {   
            for($i = 1; $i < 31; $i += 2)
            {   
                $c = byte(\$$level{"map"}, $offset);
                $offset ++;

                $c1 = $c % 16;
                $c2 = ($c - %c1) / 16;

                $map[$i][$j] = substr($translation, $c1, 1);
                $map[$i + 1][$j] = substr($translation, $c2, 1);

                if($map[$i][$j] eq "T" || $map[$i + 1][$j] eq "T")
                {
                    $teleports ++;
                }
            }
        }
    }

    # Uncompressed map - one byte per piece
    if($$level{"mode"} eq "amiga")
    {
        $offset = 0;
        for($j = 0; $j < 32; $j ++)
        {
            for($i = 0; $i < 32; $i ++)
            {   
                $c = byte(\$$level{"map"}, $offset);
                $c = $c & 0x3f; # ?
                $offset ++;
                $m = "?";

                $m = " " if($c == 0x00);
                $m = "1" if($c == 0x01);
                $m = "2" if($c == 0x02);
                $m = "d" if($c == 0x03);
                $m = "a" if($c == 0x04);
                $m = "o" if($c == 0x05);
                $m = "*" if($c == 0x06);
                $m = "S" if($c == 0x07);
                $m = "M" if($c == 0x08);
                $m = "m" if($c == 0x09);
                $m = "N" if($c == 0x0a);
                $m = "n" if($c == 0x0b);
                $m = "T" if($c == 0x0c);
                $m = "|" if($c == 0x0d);
                $m = "-" if($c == 0x0e);
                $m = "A" if($c == 0x0f);
                $m = "D" if($c == 0x10);
                $m = "/" if($c == 0x11);
                $m = "%" if($c == 0x20);

                if($m eq "T")
                {   
                    $teleports ++;
                }

                if($m eq "?")
                {
                    print "FAILED\n  Unknown map piece 0x".sprintf("%02x", $c)." at ($i, $j)\n";
                return;
                }

                $map[$i][$j] = $m;
            }
        }
    }

    if($$level{"mode"} eq "atari")
    {
        for($j = 0; $j < 32; $j ++)
        {
              for($i = 0; $i < 32; $i ++)
            {
                $p = "?";
                $c = byte(\$level_data, $j*32+$i);

                $p = " " if($c == 0x00);
                    $p = "1" if($c == 0x02);
                $p = "2" if($c == 0x03);
                $p = "M" if($c == 0x04);
                $p = "-" if($c == 0x05);
                $p = "|" if($c == 0x06);
                $p = "d" if($c == 0x07 || $c == 0x13 || $c == 0x16 || $c == 0x17 || $c == 0x1a);
                $p = "a" if($c == 0x08 || $c == 0x14 || $c == 0x15 || $c == 0x18 || $c == 0x19);
                $p = "D" if($c == 0x09);
                $p = "A" if($c == 0x0a);
                $p = "*" if($c == 0x0b);
                $p = "/" if($c == 0x0c);
                $p = "o" if($c == 0x0d);
                $p = "m" if($c == 0x0e);
                $p = "N" if($c == 0x0f);
                $p = "n" if($c == 0x10);
                $p = "T" if($c == 0x11);
                $p = "S" if($c == 0x12);

                $p = "%" if($c == 0x55 || $c==0x56 || $c == 0x57 || $c == 0x58); # 1
                    $p = "%" if($c == 0x49 || $c==0x4a || $c == 0x4b || $c == 0x4c); # 2
                $p = "%" if($c == 0x4d || $c==0x4e || $c == 0x4f || $c == 0x50); # 3
                $p = "%" if($c == 0x51 || $c==0x52 || $c == 0x53 || $c == 0x54); # 4
                $p = "%" if($c == 0x2d || $c==0x2e || $c == 0x2f || $c == 0x30 || $c == 0x62); # 5 
                $p = "%" if($c == 0x31 || $c==0x32 || $c == 0x33 || $c == 0x34); # 6
                $p = "%" if($c == 0x27 || $c==0x28 || $c == 0x29 || $c == 0x2a); # 7
                $p = "%" if($c == 0x25 || $c==0x26 || $c == 0x2b || $c == 0x2c); # 8
                $p = "%" if($c == 0x01 || $c == 0x21 || $c==0x22 || $c == 0x23 || $c == 0x24); # 9
                $p = "%" if($c == 0x35 || $c==0x36 || $c == 0x37 || $c == 0x38); # 10
                $p = "%" if($c == 0x39 || $c==0x3a || $c == 0x3b || $c == 0x3c); # 11
                $p = "%" if($c == 0x3d || $c==0x3e || $c == 0x3f || $c == 0x40); # 12
                $p = "%" if($c == 0x41 || $c==0x42 || $c == 0x43 || $c == 0x44); # 13
                $p = "%" if($c == 0x45 || $c==0x46 || $c == 0x47 || $c == 0x48); # 14
                $p = "%" if($c == 0x59 || $c==0x5a || $c == 0x5b || $c == 0x5c); # 15

                $p = "?" if($c == 0x61);

                $teleports ++ if($p eq "T");

                $map[$i][$j] = $p;
            }
        }
    }

    # Populate players
    for($i = 1; $i <=2; $i ++)
    {
        $p = $map[$$level{"player".$i."x"}][$$level{"player".$i."y"}];
        if($p ne $i && $p ne " " && $p ne "?")
        {
            print "FAILED\n  player $i is unmatched (map says '".$map[$$level{"player".$i."x"}][$$level{"player".$i."y"}]."')\n";
            return;
        }
        $map[$$level{"player".$i."x"}][$$level{"player".$i."y"}] = $i;
    }

    # Populate maps
    if($$level{"mode"} eq "8bit")
    {
        foreach $i ("TL", "TR", "BL", "BR")
        {
            if($map[$$level{"map".$i."x"}][$$level{"map".$i."y"}] ne "?")
            {
                    print "FAILED\n  map $i is unmatched (map says '".$map[$$level{"map".$i."x"}][$$level{"map".$i."y"}]."')\n";
                return;
            }
        }
        $map[$$level{"mapTLx"}][$$level{"mapTLy"}] = "M";
        $map[$$level{"mapTRx"}][$$level{"mapTRy"}] = "m";
        $map[$$level{"mapBLx"}][$$level{"mapBLy"}] = "N";
        $map[$$level{"mapBRx"}][$$level{"mapBRy"}] = "n";
    }

    # Sanity check
    $failed = 0;
    for($j = 1; $j < 31; $j ++)
    {   
        for($i = 1; $i < 31; $i ++)
        {   
            if($map[$i][$j] eq "<" || $map[$i][$j] eq "Z")
            {
                if($map[$i-1][$j] eq " " || $map[$i-1][$j] eq "-")
                {
                    $failed ++;
                }
            }
            if($map[$i][$j] eq "v" || $map[$i][$j] eq "X")
            {
                if($map[$i][$j+1] eq " " || $map[$i][$j+1] eq "|")
                {
                    $failed ++;
                }
            }
        }
    }
    if($failed > 0)
    {
        print "FAILED\n  $failed unsupported pieces\n";
        return;
    }

    open(OUTPUT, ">".$file_output) || die "Unable to open file ($file_output) for writing: $!\n";

    print OUTPUT "chroma level\n";
    print OUTPUT "mode: xor\n\n";
    print OUTPUT "title: XOR: ".$$level{"title"}."\n";
    print OUTPUT "size: 32 32\n";
    print OUTPUT "player: ".$$level{"player"}."\n" if($$level{"player"});
    if($title2number{$$level{"title"}})
    {
        print OUTPUT "level: ".$title2number{$$level{"title"}}."\n";
    }
    print OUTPUT "view1: ".$$level{"view1x"}." ".$$level{"view1y"}."\n";
    print OUTPUT "view2: ".$$level{"view2x"}." ".$$level{"view2y"}."\n";

    if($teleports > 0)
    {
        print OUTPUT "viewteleport1: ".$$level{"viewteleport1x"}." ".$$level{"viewteleport1y"}." (".$$level{"teleport1x"}." ".$$level{"teleport1y"}.")\n";
        print OUTPUT "viewteleport2: ".$$level{"viewteleport2x"}." ".$$level{"viewteleport2y"}." (".$$level{"teleport2x"}." ".$$level{"teleport2y"}.")\n";
    }

    if($message ne "")
    {
        print OUTPUT "message: $message\n";
    }

    print OUTPUT "\ndata:\n";

    for($j = 0; $j < 32; $j ++)
    {
        for($i = 0; $i < 32; $i ++)
        {
            print OUTPUT $map[$i][$j];
        }
        print OUTPUT "\n";
    }

    close(OUTPUT);

    print "OK\n";
}

sub create_set
{
    my $file_output = shift(@_);
    my $title = shift(@_);

    open(OUTPUT, ">".$file_output) || die "Unable to open file ($file_output) for writing: $!\n";

    print OUTPUT << "EOF";
<chroma type="set">
<head>
<title>$title</title>
</head>
</chroma>
EOF
    close(OUTPUT);
}

sub create_bmp
{
    my $image = shift(@_);
    my $file_output = shift(@_);
    my $scale_x = shift(@_);
    my $scale_y = shift(@_);

    $bpp = $$image{"colours"} <= 16 ? 4 : 8;

    $size_x = $$image{"x"} * $scale_x;
    $size_y = $$image{"y"} * $scale_y;
    $offset = 54 + ($$image{"colours"} * 4);
    $size_data = $size_x * $size_y * $bpp / 8;
    $size_file = $size_data + $offset;

    print "Creating graphic '$file_output'\n";

    open(BMP, ">".$file_output) || die "Unable to open file ($file_output) for writing: $!\n";
    binmode BMP;

    print BMP "BM";                             # header
    print BMP pack("L", $size_file);            # file length
    print BMP pack("S", 0);                     # reserved
    print BMP pack("S", 0);                     # reserved
    print BMP pack("L", $offset);               # offset to data

    print BMP pack("L", 0x28);                  # size of BITMAPINFOHEADER
    print BMP pack("L", $size_x);               # width
    print BMP pack("L", $size_y);               # height
    print BMP pack("S", 1);                     # number of planes
    print BMP pack("S", $bpp);                  # bits per pixel
    print BMP pack("L", 0);                     # compression
    print BMP pack("L", $size_data);            # image size in bytes
    print BMP pack("L", 0xb12);                 # pixels per metre horizontal
    print BMP pack("L", 0xb12);                 # pixels per metre vertical
    print BMP pack("L", $$image{"colours"});    # colours used
    print BMP pack("L", $$image{"colours"});    # colours important

    for($i = 0; $i < $$image{"colours"}; $i ++)
    {
        print BMP pack("L", $image{"palette"}[$i]);
    }

    for($y = $$image{"y"} - 1; $y >= 0; $y --)
    {
        for($ys = 0; $ys < $scale_y; $ys ++)
        {
            $byte = 0; $bits = 0;
            for($x = 0; $x < $$image{"x"} ; $x ++ )
            {
            for($xs = 0; $xs < $scale_x; $xs ++)
            {
                $byte = $byte * (2 ** $bits);
                $byte += $$image{"pixels"}[$x][$y];
                $bits += $bpp;

                if($bits == 8)
                {
                    print BMP pack("C", $byte);
                $byte = 0;
                $bits = 0;
                }
            }
            }
            if($bits != 0)
            {
            print BMP pack("C", $byte);
            }
        }
    }

    close BMP;
}

sub create_graphics_set
{
    my $file_output = shift(@_);
    my $stub = shift(@_);
    my $title = shift(@_);
    my $size = shift(@_);

    open(OUTPUT, ">".$file_output) || die "Unable to open file ($file_output) for writing: $!\n";

    print OUTPUT << "EOF";
<chroma type="graphics">

<head>

<title>$title</title>

<sizes>
<!-- Everything is scaled from 24x24 -->
<size x="12" y="12" small="yes" pieces="yes" />
<size x="24" y="24" small="yes" pieces="yes" />
<size x="48" y="48" small="yes" pieces="yes" />
<size x="72" y="72" small="yes" pieces="yes" />
<size x="96" y="96" small="yes" pieces="yes" />
<size x="144" y="144" small="yes" pieces="yes" />
</sizes>

</head>

<background colour="#808080" />

<pieces path="$stub">

<piece name="space" scale="yes">
<image file="$stub\_00.bmp" />
</piece>

<piece name="wall" scale="yes">
<image file="$stub\_01.bmp" />
</piece>

<piece name="player_one" scale="yes">
<image file="$stub\_02.bmp" />
<image type="small" file="$stub\_02.bmp" />
</piece>

<piece name="player_two" scale="yes">
<image file="$stub\_03.bmp" />
<image type="small" file="$stub\_03.bmp" />
</piece>

<piece name="map_top_left" scale="yes">
<image file="$stub\_04.bmp" />
</piece>

<piece name="map_top_right" scale="yes">
<image file="$stub\_04.bmp" />
</piece>

<piece name="map_bottom_left" scale="yes">
<image file="$stub\_04.bmp" />
</piece>

<piece name="map_bottom_right" scale="yes">
<image file="$stub\_04.bmp" />
</piece>

<piece name="dots_x" scale="yes">
<image file="$stub\_05.bmp" />
</piece>

<piece name="dots_y" scale="yes">
<image file="$stub\_06.bmp" />
</piece>

<piece name="arrow_red_down" scale="yes">
<image file="$stub\_07.bmp" />
</piece>

<piece name="arrow_red_left" scale="yes">
<image file="$stub\_08.bmp" />
</piece>

<piece name="bomb_red_down" scale="yes">
<image file="$stub\_09.bmp" />
</piece>

<piece name="bomb_red_left" scale="yes">
<image file="$stub\_10.bmp" />
</piece>

<piece name="star" scale="yes">
<image file="$stub\_11.bmp" />
<image type="small" file="$stub\_11.bmp" />
</piece>

<piece name="door" scale="yes">
<image file="$stub\_12.bmp" />
<image type="small" file="$stub\_12.bmp" />
</piece>

<piece name="circle" scale="yes">
<image file="$stub\_13.bmp" />
</piece>

<piece name="teleport" scale="yes">
<image file="$stub\_14.bmp" />
</piece>

<piece name="switch" scale="yes">
<image file="$stub\_15.bmp" />
</piece>

<piece name="explosion_red_left" scale="yes">
<image colour="#ffffff" />
</piece>
<piece name="explosion_red_horizontal">
<clone piece="explosion_red_left" />
</piece>
<piece name="explosion_red_right">
<clone piece="explosion_red_left" />
</piece>
<piece name="explosion_red_top">
<clone piece="explosion_red_left" />
</piece>
<piece name="explosion_red_vertical">
<clone piece="explosion_red_left" />
</piece>
<piece name="explosion_red_bottom">
<clone piece="explosion_red_left" />
</piece>

<piece name="darkness" scale="yes">
<image colour="#000000" />
</piece>

</pieces>
</chroma>
EOF

    close(OUTPUT);
}

###############################################################################

sub convert_spectrum_levels
{
    my $file_input = shift(@_);
    my $output_prefix = shift(@_);

    $data = read_file($file_input);

    # spectrum 0x9748
    $base = 0xe500; # Spectrum
    $offset_locations = locate_string(\$data, "\x47\x22\xe5\x12\x01\x32\xe5\x02") - 16;
    if($offset_locations < 0)
    {
        $base = 0x3c00; # CPC
        $offset_locations = locate_string(\$data, "\x47\x22\x3c\x12\x01\x32\x3c\x02") - 16;
        if($offset_locations < 0)
        {
            die "Unable to locate locations in input file\n";
        }
    }

    # spectrum 0x6400
    $offset_maps= locate_string(\$data, "\x11\x11\x01\x10\x6b\x00\x40\x00") - 27;
    if($offset_maps < 0)
    {
        die "Unable to locate maps in input file\n";
    }

    for($number = 1; $number < 16; $number ++)
    {
        undef %level;
        $level{"map"} = substr($data, $offset_maps + (($number - 1) * 0x1c2), 0x1c2);
        $locations = substr($data, $offset_locations + (($number - 1) * 0x2f), 0x2f);

        $level{"player1x"} = byte(\$locations, 0x00);
        $level{"player1y"} = byte(\$locations, 0x01);
        convert_address(short(\$locations, 0x06) - $base, \$level{"view1x"}, \$level{"view1y"});
        $level{"player2x"} = byte(\$locations, 0x08);
        $level{"player2y"} = byte(\$locations, 0x09);
        convert_address(short(\$locations, 0x0e) - $base, \$level{"view2x"}, \$level{"view2y"});

        $level{"teleport1x"} = byte(\$locations, 0x13);
        $level{"teleport1y"} = byte(\$locations, 0x14);
        convert_address(short(\$locations, 0x19) - $base, \$level{"viewteleport1x"}, \$level{"viewteleport1y"});

        $level{"teleport2x"} = byte(\$locations, 0x1d);
        $level{"teleport2y"} = byte(\$locations, 0x1e);
        convert_address(short(\$locations, 0x23) - $base, \$level{"viewteleport2x"}, \$level{"viewteleport2y"});

        convert_address(short(\$locations, 0x25) - $base, \$level{"mapTLx"}, \$level{"mapTLy"});
        convert_address(short(\$locations, 0x27) - $base, \$level{"mapTRx"}, \$level{"mapTRy"});
        convert_address(short(\$locations, 0x29) - $base, \$level{"mapBLx"}, \$level{"mapBLy"});
        convert_address(short(\$locations, 0x2b) - $base, \$level{"mapBRx"}, \$level{"mapBRy"});

        $level{"title"} = $titles[$number - 1];

        $file_output = sprintf("%s%02d%s", $output_prefix, $number, ".chroma");

        $level{"mode"} = "8bit";

        create_level(\%level, $file_output);
    }
}

sub convert_bbc_levels
{
    my $file_input = shift(@_);
    my $output_prefix = shift(@_);

    $data = read_file($file_input);

    $offset_data = locate_string(\$data, "\xb7\x09\x48\x09\x9d\x0c\x85\x0a") - 0x1c2;
    if($offset_data < 0)
    {
        $offset_data = locate_string(\$data, "\x85\x0b\x81\x0a\xfa\x09\xb6\x0b") - 0x1c2;
    }
    if($offset_data < 0)
    {
        die "Unable to locate maps in input file\n";
    }

    $offset_title = locate_string(\$data, "   DOTS AND WAVES   ");
    if($offset_title < 0)
    {
        $offset_title = locate_string(\$data, "      TAURUS       ");
    }
    $offset_title ++;

    for($number = 1; $number < 16; $number ++)
    {
        $level_data = substr($data, $offset_data, 0x200);
        $offset_data -= 0x200;

        if($offset_title != 0)
        {
            $level_data .= substr($data, $offset_title, 18);
            $offset_title += 0x17;
        }
        else
        {
            $level_data .= pack("A18", $titles[$number - 1]);
        }

        $file_output = sprintf("%s%02d%s", $output_prefix, $number, ".chroma");

        convert_bbc_level($level_data, $file_output);
    }

}

sub convert_designer_levels
{
    my $file_input = shift(@_);
    my $output_prefix = shift(@_);

    $data = read_file($file_input);

    if($data =~ /(.{512}  DOTS AND WAVES  .{114})/s)
    {   
        convert_bbc_level($1, $output_prefix."01.chroma");
    }
    if($data =~ /(.{512} SOMETHING FISHY  .{114})/s)
    {   
        convert_bbc_level($1, $output_prefix."02.chroma");
    }
    if($data =~ /(.{512} CHICKEN SUPREME  .{114})/s)
    {   
        convert_bbc_level($1, $output_prefix."03.chroma");
    }
    if($data =~ /(.{512}EXPLOSIVE MIXTURE .{114})/s)
    {   
        convert_bbc_level($1, $output_prefix."04.chroma");
    }
    if($data =~ /(.{512}  HENRYS ANGUISH  .{114})/s)
    {   
        convert_bbc_level($1, $output_prefix."05.chroma");
    }
    if($data =~ /(.{512} THE DOLLS HOUSE  .{114})/s)
    {   
        convert_bbc_level($1, $output_prefix."06.chroma");
    }
    if($data =~ /(.{512}  DOLLYS REVENGE  .{114})/s)
    {   
        convert_bbc_level($1, $output_prefix."07.chroma");
    }
    if($data =~ /(.{512}  ENLIGHTENMENT   .{114})/s)
    {
        convert_bbc_level($1, $output_prefix."08.chroma");
    }
    if($data =~ /(.{512}  THE CHALLENGE   .{114})/s)
    {
        convert_bbc_level($1, $output_prefix."09.chroma");
    }
    if($data =~ /(.{512} PATIENCE PENDING .{114})/s)
    {
        convert_bbc_level($1, $output_prefix."10.chroma");
    }
    if($data =~ /(.{512}    RAZOR EDGE    .{114})/s)
    {
        convert_bbc_level($1, $output_prefix."11.chroma");
    }
    if($data =~ /(.{512}  THE HAPPY HOUR  .{114})/s)
    {
        convert_bbc_level($1, $output_prefix."12.chroma");
    }
    if($data =~ /(.{512}     DEJA VU      .{114})/s)
    {
        convert_bbc_level($1, $output_prefix."13.chroma");
    }
    if($data =~ /(.{512}   PENULTIMATE    .{114})/s)
    {
        convert_bbc_level($1, $output_prefix."14.chroma");
    }
    if($data =~ /(.{512} WHEEL OF FORTUNE .{114})/s)
    {
        convert_bbc_level($1, $output_prefix."x1.chroma");
    }
    if($data =~ /(.{512}THE HIDDEN CAVERN .{114})/s)
    {
        convert_bbc_level($1, $output_prefix."x2.chroma");
    }
}

sub convert_designer_level
{
    my $file_input = shift(@_);
    my $file_output = shift(@_);

    $data = read_file($file_input);

    convert_bbc_level($data, $file_output);
}

sub convert_bbc_level
{
    my $level_data = shift(@_);
    my $file_output = shift(@_);

    undef %level;

    $level{"map"} = substr($level_data, 0, 0x1c2);

    $base = 0x900;
    convert_address(short(\$level_data, 0x1c2) - $base + 32, \$level{"mapTLx"}, \$level{"mapTLy"});
    convert_address(short(\$level_data, 0x1c4) - $base + 32, \$level{"mapTRx"}, \$level{"mapTRy"});
    convert_address(short(\$level_data, 0x1c6) - $base + 32, \$level{"mapBLx"}, \$level{"mapBLy"});
    convert_address(short(\$level_data, 0x1c8) - $base + 32, \$level{"mapBRx"}, \$level{"mapBRy"});

    convert_address(short(\$level_data, 0x1ca) - $base + 32, \$level{"player1x"}, \$level{"player1y"});
    convert_address(short(\$level_data, 0x1d0) - $base, \$level{"view1x"}, \$level{"view1y"});
    convert_address(short(\$level_data, 0x1d2) - $base + 32, \$level{"player2x"}, \$level{"player2y"});
    convert_address(short(\$level_data, 0x1d8) - $base, \$level{"view2x"}, \$level{"view2y"});

    convert_address(short(\$level_data, 0x1e2) - $base, \$level{"teleport1x"}, \$level{"teleport1y"});
    convert_address(short(\$level_data, 0x1e8) - $base, \$level{"viewteleport1x"}, \$level{"viewteleport1y"});
    convert_address(short(\$level_data, 0x1ea) - $base, \$level{"teleport2x"}, \$level{"teleport2y"});
    convert_address(short(\$level_data, 0x1f0) - $base, \$level{"viewteleport2x"}, \$level{"viewteleport2y"});

    $title = substr($level_data, 0x200, 18);
    $title = " ".$title." ";
    $title = lc($title);
    $title =~ s/ (\w)/' '.uc($1)/eg;
    $title =~ s/^\s+//;
    $title =~ s/\s+$//;
    $level{"title"} = $title;

    $cryptedmessage = substr($level_data, 0x220, 100);
    $message = "";
    if(ord($cryptedmessage) != 0)
    {
        for($i = 0; $i < length($cryptedmessage); $i++)
        {   
            $c = byte(\$cryptedmessage, $i);
            $c = $c ^ 0x7e;
            if($c > 31 && $c <127)
            {   
                $message .= chr($c);
            }
            else
            {   
                last;
            }
        }
    }
    if($message ne "")
    {
        $level{"message"} = $message;
    }

    $level{"mode"} = "8bit";

    $level{"player"} = 2;

    create_level(\%level, $file_output)
}

sub convert_amiga_levels
{
    my $file_input = shift(@_);
    my $output_prefix = shift(@_);

    $data = read_file($file_input);

    for($number = 1; $number< 16; $number++)
    {   
        $level_data= substr($data, ($number- 1) * 0x464, 0x464);
        $file_output = sprintf("%s%02d%s", $output_prefix, $number, ".chroma");

        convert_amiga_level($level_data, $file_output);
    }
}

sub convert_amiga_level
{
    my $level_data = shift(@_);
    my $file_output = shift(@_);

    undef %level;

    $offset = 0;
    while(byte(\$level_data, $offset) > 31)
    {
        $level{"title"} .= substr($level_data, $offset, 1);
        $offset ++;
    }

    $level{"map"} = substr($level_data, 0x1f, 0x400);

    $level{"view1x"} = long(\$level_data, 0x420);
    $level{"view1y"} = long(\$level_data, 0x424);
    $level{"player1x"} = long(\$level_data, 0x428);
    $level{"player1y"} = long(\$level_data, 0x42c);
    $level{"view2x"} = long(\$level_data, 0x430);
    $level{"view2y"} = long(\$level_data, 0x434);
    $level{"player2x"} = long(\$level_data, 0x438);
    $level{"player2y"} = long(\$level_data, 0x43c);
    $level{"viewteleport1x"} = long(\$level_data, 0x444);
    $level{"viewteleport1y"} = long(\$level_data, 0x448);
    $level{"teleport1x"} = long(\$level_data, 0x44c);
    $level{"teleport1y"} = long(\$level_data, 0x450);
    $level{"viewteleport2x"} = long(\$level_data, 0x454);
    $level{"viewteleport2y"} = long(\$level_data, 0x458);
    $level{"teleport2x"} = long(\$level_data, 0x45c);
    $level{"teleport2y"} = long(\$level_data, 0x460);
    $level{"player"} = 2;

    $level{"mode"} = "amiga";

    create_level(\%level, $file_output);
}

sub convert_atari_levels
{
    my $file_input = shift(@_);
    my $output_prefix = shift(@_);

    $data = read_file($file_input);

    $number = 1;

    foreach $title (
            "    DOTS AND WAVES    ",
            "   SOMETHING FISHY    ",
            "   CHICKEN SUPREME    ",
            "  EXPLOSIVE MIXTURE   ",
            "    HENRYS ANGUISH    ",
            "    THE DOLLS HOUSE   ",
            "    DOLLYS REVENGE    ",
            "    ENLIGHTENMENT     ",
            "    THE CHALLENGE     ",
            "   PATIENCE PENDING   ",
            "      RAZOR EDGE      ",
            "   THE HAPPY HOUR     ",
            "       DEJA VU        ",
            "     PENULTIMATE      ",
            "     THE DECODER      "
            )
    {

        $offset_title = locate_string(\$data, $title);
        if($offset_title > 0)
        {
            $level_data = substr($data, $offset_title - 1074, 1096);
            $file_output = sprintf("%s%02d%s", $output_prefix, $number, ".chroma");
            convert_atari_level($level_data, $file_output);
        }

        $number ++;
    }
}

sub convert_atari_level
{
    my $level_data = shift(@_);
    my $file_output = shift(@_);

    my $title;

    $level{"view1x"} = byte(\$level_data, 0x401);
    $level{"view1y"} = byte(\$level_data, 0x403);
    $level{"player1x"} = $level{"view1x"} + byte(\$level_data, 0x405);
    $level{"player1y"} = $level{"view1y"} + byte(\$level_data, 0x407);

    $level{"view2x"} = byte(\$level_data, 0x40b);
    $level{"view2y"} = byte(\$level_data, 0x40d);
    $level{"player2x"} = $level{"view2x"} + byte(\$level_data, 0x40f);
    $level{"player2y"} = $level{"view2y"} + byte(\$level_data, 0x411);

    $level{"viewteleport1x"} = byte(\$level_data, 0x417);
    $level{"viewteleport1y"} = byte(\$level_data, 0x419);
    $level{"viewteleport2x"} = byte(\$level_data, 0x423);
    $level{"viewteleport2y"} = byte(\$level_data, 0x425);

    $level{"teleport1x"} = $level{"viewteleport1x"} + byte(\$level_data, 0x41f);
    $level{"teleport1y"} = $level{"viewteleport1y"} + byte(\$level_data, 0x421);
    $level{"teleport2x"} = $level{"viewteleport2x"} + byte(\$level_data, 0x42b);
    $level{"teleport2y"} = $level{"viewteleport2y"} + byte(\$level_data, 0x42d);

    $level{"map"} = substr($level_data, 0, 0x400);

    $title = substr($level_data, 0x432, 20);
    $title = " ".$title." ";
    $title = lc($title);
    $title =~ s/ (\w)/' '.uc($1)/eg;
    $title =~ s/^\s+//;
    $title =~ s/\s+$//;
    $level{"title"} = $title;

    $cryptedmessage = substr($level_data, 0x220, 100);

    $level{"mode"} = "atari";

    create_level(\%level, $file_output);
}

###############################################################################

sub convert_bbc_graphics
{
    my $file_input = shift(@_);
    my $output_prefix = shift(@_);
    my $mode = shift(@_);

    $data = read_file($file_input);

    if($data =~ /(\x00{72}\x1d\x1d\xdd\x00\x47\x47\x77\x00.{208})/s)
    {
        $walls = $1;
        $walls1 = substr($walls, 0, 144);
        $walls2 = substr($walls, 144, 144);
        if($walls2 =~ /^\x00\x70\x70\x70/s)
        {
            $walls2 = "";
        }
    }
    else
    {
        die "Unable to locate graphics in input file\n";
    }

    if($data =~ /(\x00\x70\x70\x70\x70\x71\x73\x77\x00\xf0\xf0\xf4\xfe\xff\xfb\xf1.{992})/s)
    {
        $pieces = $1;
        $match = $walls1.$pieces.$walls2;
        convert_bbc_graphics_data($match, $output_prefix, "bbc");
    }
    else
    {
        die "Unable to locate graphics in input file\n";
    }

}  

sub convert_c64_graphics
{
    my $file_input = shift(@_);
    my $output_prefix = shift(@_);

    $data = read_file($file_input);

    if($data =~ /(\x53\x53\xf3\x00.{68}\x55\x55\x15\x15.{68}\x00\x1c\x57\x55.{68})/)
    {
        $walls = substr($1, 72, 144);
    }
    else
    {
        $walls = "";
    }

    if($data =~ /(\x00{72}\x53\x53\xf3\x00\x35\x35\x3f\x00.{1072})/s)
    {   
        $pieces = $1.$walls;
        convert_bbc_graphics_data($pieces, $output_prefix, "c64");
    }
    else
    {
        die "Unable to locate graphics in input file\n";
    }

}

sub convert_bbc_graphics_file
{
    my $file_input = shift(@_);
    my $output_prefix = shift(@_);

    $data = read_file($file_input);

    convert_bbc_graphics_data($data, $output_prefix, "bbc");
}

sub convert_bbc_graphics_data
{
    my $data = shift(@_);
    my $output_prefix = shift(@_);
    my $mode = shift(@_);
    my $offset;

    $x = 0;
    $y = 0;
    $j = 0;
    $offset = 0;
    $number = 0;

    $levels = 1 if($levels < 1);
    for($level = 0; $level < $levels; $level ++)
    { # 1 2 4
        @colours =( [0x00000000, 0x00ff0000, 0x000000ff, 0x00ffffff], # 1
                    [0x00000000, 0x00ff0000, 0x000000ff, 0x00ffffff], # 2
                    [0x00000000, 0x00ff0000, 0x000000ff, 0x00ffffff], # 3
                    [0x00000000, 0x000000ff, 0x00ff00ff, 0x00ffffff], # 4
                    [0x00000000, 0x000000ff, 0x00ff00ff, 0x00ffffff], # 5
                    [0x00000000, 0x000000ff, 0x00ff00ff, 0x00ffffff], # 6
                    [0x00000000, 0x000000ff, 0x00ff0000, 0x00ffffff], # 7
                    [0x00000000, 0x000000ff, 0x00ff0000, 0x00ffffff], # 8
                    [0x00000000, 0x000000ff, 0x00ff0000, 0x00ffffff], # 9
                    [0x00000000, 0x0000ff00, 0x00ff0000, 0x00ffffff], # 10
                    [0x00000000, 0x0000ff00, 0x00ff0000, 0x00ffffff], # 11
                    [0x00000000, 0x00ff00ff, 0x000000ff, 0x0000ff00], # 12
                    [0x00000000, 0x00ff00ff, 0x000000ff, 0x0000ff00], # 13
                    [0x000000ff, 0x00ff0000, 0x00000000, 0x00ffffff], # 14
                    [0x000000ff, 0x00ff0000, 0x00000000, 0x00ffffff]  # 15
                );

        $image{"colours"} = 4; #   RRGGBB
        $image{"palette"}[0] = $colours[$level][0];
        $image{"palette"}[1] = $colours[$level][1];
        $image{"palette"}[2] = $colours[$level][2];
        $image{"palette"}[3] = $colours[$level][3];

        $image{"x"} = 12;
        $image{"y"} = 24;

        $number = 0;
        $o = $offset;
        while($o < length($data))
        {
                $c = ord(substr($data, $o, 1));

            if($mode eq "bbc")
            {
                $b3 = ($c & 1 ? 1 : 0) + ($c & 16 ? 2 : 0);
                $b2 = ($c & 2 ? 1 : 0) + ($c & 32 ? 2 : 0);
                $b1 = ($c & 4 ? 1 : 0) + ($c & 64 ? 2 : 0);
                $b0 = ($c & 8 ? 1 : 0) + ($c & 128 ? 2 : 0);
            }
            if($mode eq "c64")
            {
                $b3 = ($c & 1 ? 1 : 0) + ($c & 2 ? 2 : 0);
                $b2 = ($c & 4 ? 1 : 0) + ($c & 8 ? 2 : 0);
                $b1 = ($c & 16 ? 1 : 0) + ($c & 32 ? 2 : 0);
                $b0 = ($c & 64 ? 1 : 0) + ($c & 128 ? 2 : 0);
            }
        
            $image{"pixels"}[$x + 0][$y + $j] = $b0;
            $image{"pixels"}[$x + 1][$y + $j] = $b1;
            $image{"pixels"}[$x + 2][$y + $j] = $b2;
            $image{"pixels"}[$x + 3][$y + $j] = $b3;

            $o++;

            $j++;

            if($j == 8)
            {
                $j = 0;
                $x += 4; 
            }
            if($x == 12)
            {
                $x = 0;
                $y += 8;
            }
            if($y == 24)
            {
                if($levels > 1)
                {
                         $file_output = sprintf("%s%d_%02d%s", $output_prefix, $level + 1, $number, ".bmp");
                }
                else
                {
                         $file_output = sprintf("%s%02d%s", $output_prefix, $number, ".bmp");
                }
                create_bmp(\%image, $file_output, 2, 1);

                $y = 0;
                $x = 0;
                $j = 0;
                $number ++;
            }
        }
    }
}

sub convert_cpc_graphics
{
    my $file_input = shift(@_);
    my $output_prefix = shift(@_);

    $data = read_file($file_input);

    if($data =~ /(\xcc{144}\x0c\xc8\x0c\x0c.{2156})/s)
    {   
        convert_cpc_graphics_data($1, $output_prefix);
    }
    else
    {
        die "Unable to locate graphics in input file\n";
    }

}

sub convert_cpc_graphics_data
{
    my $data = shift(@_);
    my $output_prefix = shift(@_);

    my $offset;

    $x = 0;
    $y = 0;
    $offset = 0;
    $number = 0;

    $image{"colours"} = 16;   #  RRGGBB
    $image{"palette"}[0x0] = 0x00000081;
    $image{"palette"}[0x1] = 0x00ff8100;
    $image{"palette"}[0x2] = 0x00ff0000;
    $image{"palette"}[0x3] = 0x00ff0083;
    $image{"palette"}[0x4] = 0x008300ff;
    $image{"palette"}[0x5] = 0x00830000;
    $image{"palette"}[0x6] = 0x00ffff00;
    $image{"palette"}[0x7] = 0x00ff81ff;
    $image{"palette"}[0x8] = 0x00ffffff;
    $image{"palette"}[0x9] = 0x000000ff;
    $image{"palette"}[0xa] = 0x00000000;
    $image{"palette"}[0xb] = 0x00838183;
    $image{"palette"}[0xc] = 0x008381ff;
    $image{"palette"}[0xd] = 0x00ff8183;
    $image{"palette"}[0xe] = 0x00ffff83;
    $image{"palette"}[0xf] = 0x00ffffff;

    $image{"x"} = 12;
    $image{"y"} = 24;

    while($offset < length($data))
    {
        $c = byte(\$data, $offset);

        $b1 = ($c & 1 ? 1 : 0) + ($c & 4 ? 2 : 0) + ($c & 16 ? 4 : 0) + ($c & 64 ? 8 : 0);
        $b0 = ($c & 2 ? 1 : 0) + ($c & 8 ? 2 : 0) + ($c & 32 ? 4 : 0) + ($c & 128 ? 8 : 0);
        
        $image{"pixels"}[$x + 0][$y] = $b0;
        $image{"pixels"}[$x + 1][$y] = $b1;

        $offset ++;

        $x+=2;

        if($x == 12)
        {
            $x = 0;
            $y += 1;
        }
        if($y == 24)
        {
            $file_output = sprintf("%s%02d%s", $output_prefix, $number, ".bmp");
            create_bmp(\%image, $file_output, 2, 1);

            $y = 0;
            $x = 0;
            $number ++;
        }
    }
}

sub convert_spectrum_graphics
{
    my $file_input = shift(@_);
    my $output_prefix = shift(@_);

    $data = read_file($file_input);

    if($data =~ /(\x00{81}\x00\xc0\x00\x00\xc0\x00.{1209})/s)
    {   
        convert_spectrum_graphics_data($1, $output_prefix);
    }
    else
    {
        die "Unable to locate graphics in input file\n";
    }
}

sub convert_spectrum_graphics_data
{
    my $data = shift(@_);
    my $output_prefix = shift(@_);
    my $mode = shift(@_);

    my $offset;

    $x = 0;
    $y = 0;
    $offset = 0;
    $number = 0;

    $image{"x"} = 24;
    $image{"y"} = 24;

    $image{"colours"} = 16;   #  RRGGBB
    $image{"palette"}[0x0] = 0x00000000;
    $image{"palette"}[0x1] = 0x0000007f;
    $image{"palette"}[0x2] = 0x007f0000;
    $image{"palette"}[0x3] = 0x007f007f;
    $image{"palette"}[0x4] = 0x00007f00;
    $image{"palette"}[0x5] = 0x00007f7f;
    $image{"palette"}[0x6] = 0x007f007f;
    $image{"palette"}[0x7] = 0x007f7f7f;
    $image{"palette"}[0x8] = 0x00000000;
    $image{"palette"}[0x9] = 0x000000ff;
    $image{"palette"}[0xa] = 0x00ff0000;
    $image{"palette"}[0xb] = 0x00ff00ff;
    $image{"palette"}[0xc] = 0x0000ff00;
    $image{"palette"}[0xd] = 0x0000ffff;
    $image{"palette"}[0xe] = 0x00ffff00;
    $image{"palette"}[0xf] = 0x00ffffff;

    while($offset < length($data))
    {
        for($y = 0; $y < 24; $y ++)
        {
            for($x = 0; $x < 3; $x ++)
            {
            $c = ord(substr($data, $offset + $y * 3 + $x, 1));

            $px = $x * 8;
            if($y % 2 == 1) { $px = 16 - $px; }

            $col = ord(substr($data, $offset + 72 + (int($y / 8)*3) + $x));
            $fg = $col % 8;
            $bg = int($col / 8) % 8;

            if($col & 64)
            {
                $fg +=8; $bg+=8;
            }
            
            $image{"pixels"}[$px + 0][$y] = ($c & 128 ? $fg : $bg);
            $image{"pixels"}[$px + 1][$y] = ($c & 64 ? $fg : $bg);
            $image{"pixels"}[$px + 2][$y] = ($c & 32 ? $fg : $bg);
            $image{"pixels"}[$px + 3][$y] = ($c & 16 ? $fg : $bg);
            $image{"pixels"}[$px + 4][$y] = ($c & 8 ? $fg : $bg);
            $image{"pixels"}[$px + 5][$y] = ($c & 4 ? $fg : $bg);
            $image{"pixels"}[$px + 6][$y] = ($c & 2 ? $fg : $bg);
            $image{"pixels"}[$px + 7][$y] = ($c & 1 ? $fg : $bg);
            }
        }

        $file_output = sprintf("%s%02d%s", $output_prefix, $number, ".bmp");
        create_bmp(\%image, $file_output, 1, 1);

        $offset += 81;
        $number++;
    }
}

sub convert_amiga_graphics
{
    my $file_input = shift(@_);
    my $output_prefix = shift(@_);

    for($level = 0; $level < 15; $level ++)
    {
        $file_in = $file_input . "/vimages".$level;
        $file_out = sprintf("%s/back_%d_", $output_prefix, $level + 1);
        convert_amiga_graphics_file($file_in, $file_out, 0);
    }

    $file_in = $file_input . "/cimages";
    $file_out = sprintf("%s/char_", $output_prefix);
    convert_amiga_graphics_file($file_in, $file_out, 1);
}

sub convert_amiga_graphics_file
{
    my $file_input = shift(@_);
    my $output_prefix = shift(@_);
    my $mode = shift(@_);
    
    open(INPUT, $file_input) || die "Couldn't open input file ($file_input): $!\n";
    binmode INPUT;
    undef $/;
    $data = <INPUT>;
    close INPUT;

    $image{"x"} = 24;
    $image{"y"} = 24;
    $image{"colours"} = 32;

    # Read palette
    if($mode == 0)
    {
        for($i = 0; $i < 32; $i ++)
        {
            $p = 256 * ord(substr($data, $i * 2, 1)) + ord(substr($data, $i * 2 + 1, 1));

            $blue = $p % 16;
            $green = (($p - ($p % 16)) / 16) % 16;
            $red= (($p - ($p % 256)) / 256) % 16;

            $blue *= 0x11;
            $green *= 0x11;
            $red *= 0x11;

            $image{"palette"}[$i] = $blue + ($green * 0x100) + ($red * 0x10000);
        }
    }

    if($mode == 0)
    {
        $sprites = 20;
    }
    else
    {
        $sprites = 60;
    }

    for($sprite = 0; $sprite < $sprites; $sprite ++)
    {
        undef $image{"pixels"};
        
        for($colour = 0; $colour < 5; $colour ++)
        {
            for($row = 0; $row < 24; $row ++)
            {
                    if($mode == 0)
                {
                    $block = ($sprite - ($sprite % 10)) / 10;
                    $block += 2 * $colour;
                    $offset = 64 + ($sprite % 10) * 4 + $row * 40 + $block * 960;
                }
                else
                {
                    $block = ($sprite - ($sprite % 10)) / 10;
                    $block += 6 * $colour;
                    $offset = 0 + ($sprite % 10) * 4 + $row * 40 + $block * 960;
                }

                for($byte = 0; $byte < 3; $byte ++)
                {
                    $b = ord(substr($data, $offset + $byte, 1));
                    for($bit = 0; $bit < 8; $bit ++)
                    {
                        if($b & (2 ** (7 - $bit)))
                    {
                        $image{"pixels"}[$byte * 8 + $bit][$row] |= 2 ** $colour;
                    }
                    }
                }
            }
        }

        $file_output = sprintf("%s%02d%s", $output_prefix, $sprite, ".bmp");
        create_bmp(\%image, $file_output, 1, 1);
    }
    
}

sub convert_atari_graphics
{
    my $file_input = shift(@_);
    my $output_prefix = shift(@_);

    $data = read_file($file_input);

    $palette = "\x02\x27\x00\x40\x00\x00\x03\x33\x07\x44\x07\x11\x03\x37\x07\x70\x02\x25\x05\x57\x07\x50\x03\x53\x04\x44\x05\x55\x06\x66\x07\x77";

    $ok = 0;
    $offset_palette = 0;
    while(!$ok && $offset_palette != -1)
    {
        $offset_palette = index($data, $palette, $offset_palette);
        $offset_palette2 = index($data, $palette, $offset_palette);

        if($offset_palette2 = $offset_palette + 96)
        {
            $ok = 1;
        }
    }
    if($ok == 0)
    {
        die "Unable to locate palette data in file";
    }

    $offset_base = $offset_palette - 0x1b6e4;
    $offset_pieces = $offset_base + 0x68000;
    $offset_backgrounds = $offset_base + 0x60000;

    @graphics_pieces = (
          # ["space", 0, 0, 0],
          # ["wall", 2, 0, 0],
          # ["player_one", 4, 0, 1]
          # ["player_two", 6, 0, 1]
            ["map_top_left", 8, 0, 1],
            ["dots", 10, 0, 1],

            ["waves", 0, 1, 1],
            ["fish", 2, 1, 1],
            ["chicken", 4, 1, 1],
            ["h_bomb", 6, 1, 1],
            ["v_bomb", 8, 1, 1],
            ["mask", 10, 1, 1],

            ["door", 0, 2, 1],
            ["doll", 2, 2, 1],
            ["map_top_right", 4, 2, 1],
            ["map_bottom_left", 6, 2, 1],
            ["map_bottom_right", 8, 2, 1],
            ["teleport", 10, 2, 1],

            ["switch", 0, 3, 1],
            ["fish_2", 8, 3, 1],
            ["chicken_2", 10, 3, 1],

            ["chicken_3", 0, 4, 1],
            ["fish_3", 2, 4, 1],
            ["fish_4", 4, 4, 1],
            ["chicken_4", 6, 4, 1],
            ["chicken_5", 8, 4, 1],
            ["fish_5", 10, 4, 1],

            ["explosion_0", 0, 5, 1],
            ["explosion_1", 2, 5, 1],
            ["explosion_2", 4, 5, 1],
            ["explosion_3", 8, 5, 1],
            ["explosion_4", 10, 5, 1],

            ["player_one_1", 0, 6, 2],
            ["player_two_1", 1, 6, 2],
            ["player_one_2", 2, 6, 2],
            ["player_two_2", 3, 6, 2],
            ["player_one_3", 4, 6, 2],
            ["player_two_3", 5, 6, 2],
            ["player_one_4", 6, 6, 2],
            ["player_two_4", 7, 6, 2],
            ["player_one_5", 8, 6, 2],
            ["player_two_5", 9, 6, 2],
            ["player_one_6", 10, 6, 2],
            ["player_two_6", 11, 6, 2],

            ["player_one_7", 0, 7, 2],
            ["player_two_7", 1, 7, 2],
            ["player_one_8", 2, 7, 2],
            ["player_two_8", 3, 7, 2],
            ["player_one_9", 4, 7, 2],
            ["player_two_9", 5, 7, 2],
            ["player_one_10", 6, 7, 2],
            ["player_two_10", 7, 7, 2],
            ["player_one_11", 8, 7, 2],
            ["player_two_11", 9, 7, 2],
            ["player_one_12", 10, 7, 2],
            ["player_two_12", 11, 7, 2],

            ["player_one_13", 0, 8, 2],
            ["player_two_13", 1, 8, 2],
            ["player_one_14", 2, 8, 2],
            ["player_two_14", 3, 8, 2],
            ["player_one_15", 4, 8, 2],
            ["player_two_15", 5, 8, 2],
            );

    for($graphic = 0; $graphic <= $#graphics_pieces; $graphic ++)
    {
        convert_atari_graphic($output_prefix."/".$graphics_pieces[$graphic][0], \$data, $offset_pieces, $graphics_pieces[$graphic][1] * 24, $graphics_pieces[$graphic][2] * 22, 24, 22, $graphics_pieces[$graphic][3]);
    }

    @graphics_backgrounds = (
            ["background_1", 0, 0, 0],
            ["background_2", 2, 0, 0],
            ["background_3", 4, 0, 0],
            ["background_4", 6, 0, 0],
            ["background_5", 8, 0, 0],
            ["background_6", 10, 0, 0],
            ["background_7", 0, 2, 0],
            ["background_8", 2, 2, 0],
            ["background_9", 4, 2, 0],
            ["background_10", 6, 2, 0],
            ["background_11", 8, 2, 0],
            ["background_12", 10, 2, 0],
            ["background_13", 6, 4, 0],
            ["background_14", 8, 4, 0],
            ["background_15", 10, 4, 0],
            );

    for($graphic = 0; $graphic <= $#graphics_backgrounds; $graphic ++)
    {
        convert_atari_graphic($output_prefix."/".$graphics_backgrounds[$graphic][0], \$data, $offset_backgrounds, $graphics_backgrounds[$graphic][1] * 24, $graphics_backgrounds[$graphic][2] * 22, 48, 44, $graphics_backgrounds[$graphic][3]);
    }

    @graphics_walls = (
            ["wall_1_1", 5, 8, 3],
            ["wall_1_2", 6, 8, 3],
            ["wall_1_3", 7, 8, 3],
            ["wall_1_4", 8, 8, 3],

            ["wall_2_1", 7, 6, 3],
            ["wall_2_2", 8, 6, 3],
            ["wall_2_3", 7, 7, 3],
            ["wall_2_4", 8, 7, 3],

            ["wall_3_1", 9, 6, 3],
            ["wall_3_2", 10, 6, 3],
            ["wall_3_3", 9, 7, 3],
            ["wall_3_4", 10, 7, 3],

            ["wall_4_1", 11, 6, 3],
            ["wall_4_2", 12, 6, 3],
            ["wall_4_3", 11, 7, 3],
            ["wall_4_4", 12, 7, 3],

            ["wall_5_1", 12, 0, 4],
            ["wall_5_2", 12, 1, 4],
            ["wall_5_3", 12, 2, 4],
            ["wall_5_4", 12, 3, 4],
            ["wall_5_5", 12, 5, 3],

            ["wall_6_1", 12, 4, 4],
            ["wall_6_2", 12, 5, 4],
            ["wall_6_3", 12, 6, 4],
            ["wall_6_4", 12, 7, 4],

            ["wall_7_1", 6, 8, 4],
            ["wall_7_2", 7, 8, 4],
            ["wall_7_3", 8, 8, 4],
            ["wall_7_4", 9, 8, 4],

            ["wall_8_1", 6, 3, 4],
            ["wall_8_2", 7, 3, 4],
            ["wall_8_3", 10, 8, 4],
            ["wall_8_4", 11, 8, 4],

            ["wall_9_1", 2, 3, 4],
            ["wall_9_2", 3, 3, 4],
            ["wall_9_3", 4, 3, 4],
            ["wall_9_4", 5, 3, 4],

            ["wall_10_1", 12, 8, 4],
            ["wall_10_2", 2, 8, 3],
            ["wall_10_3", 3, 8, 3],
            ["wall_10_4", 4, 8, 3],

            ["wall_11_1", 0, 4, 3],
            ["wall_11_2", 1, 4, 3],
            ["wall_11_3", 0, 5, 3],
            ["wall_11_4", 1, 5, 3],

            ["wall_12_1", 2, 4, 3],
            ["wall_12_2", 3, 4, 3],
            ["wall_12_3", 2, 5, 3],
            ["wall_12_4", 3, 5, 3],

            ["wall_13_1", 4, 4, 3],
            ["wall_13_2", 5, 4, 3],
            ["wall_13_3", 4, 5, 3],
            ["wall_13_4", 5, 5, 3],

            ["wall_14_1", 5, 6, 3],
            ["wall_14_2", 6, 6, 3],
            ["wall_14_3", 5, 7, 3],
            ["wall_14_4", 6, 7, 3],

            ["wall_15_1", 9, 8, 3],
            ["wall_15_2", 10, 8, 3],
            ["wall_15_3", 11, 8, 3],
            ["wall_15_4", 12, 8, 3],

            );

    for($graphic = 0; $graphic <= $#graphics_walls; $graphic ++)
    {
        $size_x = defined($graphics_walls[$graphic][4]) ? $graphics_walls[$graphic][4] : 1;
        $size_y = defined($graphics_walls[$graphic][5]) ? $graphics_walls[$graphic][5] : 1;

        if($graphics_walls[$graphic][3] == 3)
        {
            convert_atari_graphic($output_prefix."/".$graphics_walls[$graphic][0], \$data, $offset_backgrounds, $graphics_walls[$graphic][1] * 24, $graphics_walls[$graphic][2] * 22, $size_x * 24, $size_y * 22, 0);
        }
        elsif($graphics_walls[$graphic][3] == 4)
        {
            convert_atari_graphic($output_prefix."/".$graphics_walls[$graphic][0], \$data, $offset_pieces, $graphics_walls[$graphic][1] * 24, $graphics_walls[$graphic][2] * 22, $size_x * 24, $size_y * 22, 0);
        }
    }

}

sub convert_atari_graphic
{
    my $file_output = shift(@_);
    my $data = shift(@_);
    my $offset = shift(@_);
    my $offset_x = shift(@_);
    my $offset_y = shift(@_);
    my $size_x = shift(@_);
    my $size_y = shift(@_);
    my $mask = shift(@_);

    @colour_map = (0, 36, 73, 109, 146, 182, 219, 255);

    $image{"colours"} = 16;
    $image{"x"} = $size_x;
    $image{"y"} = $size_y;

    # Setup palette
    for($i = 0; $i < 16; $i ++)
    {
        $c = byte(\$palette, $i * 2) * 256 + byte(\$palette, $i * 2 + 1);
        $r = $colour_map[int($c / 256) % 8];
        $g = $colour_map[int($c / 16) % 8];
        $b = $colour_map[$c % 8];
        $image{"palette"}[$i] = $r * 0x10000 + $g * 0x100 + $b;
    }

    # Transparent colour
    if($mask != 0)
    {
        $image{"colours"} = 17;
        $image{"palette"}[16] = 0x00deface; 
    }

    undef $image{"pixels"};

    for($y = 0; $y < $size_y; $y ++)
    {
        for($x = 0; $x < $size_x; $x += 8)
        {
            $o = $offset + ($y + $offset_y) * 160 + 8 * int(($x + $offset_x) / 16);
            $o++ if(($x + $offset_x) % 16 == 8);

            for($colour = 0; $colour < 4; $colour ++)
            {
                $byte = byte(\$$data, $o + $colour * 2);
                for($b = 0; $b < 8; $b ++)
                {
                    if($byte & (2 ** (7 - $b)))
                    {   
                        $image{"pixels"}[$x + $b][$y] |= 2 ** $colour;
                    }
                }
            }
        }
    }

    if($mask)
    {
        if($mask == 2)
        {
            $mask_x = 5 * 24;
            $mask_y = 0;
        }
        else
        {
            $mask_x = $offset_x + 24;
            $mask_y = $offset_y;
        }

        for($y = 0; $y < $size_y; $y ++)
        {
                for($x = 0; $x < $size_x; $x += 8)
            {
                $o = $offset + ($y + $mask_y) * 160 + 8 * int(($x + $mask_x) / 16);
                $o++ if(($x + $mask_x) % 16 == 8);

                $byte = byte(\$$data, $o);

                for($b = 0; $b < 8; $b ++)
                {
                    if($byte & (2 ** (7 - $b)))
                    {   
                        $image{"pixels"}[$x + $b][$y] = 16;
                    }
                }
            }
        }
    }

    create_bmp(\%image, $file_output.".bmp", 1, 1);
}
    
###############################################################################

sub convert_bbc_music
{
    my $file_input = shift(@_);
    my $file_output = shift(@_);

    # If debug is set, the notes are displayed on screen
    $debug = 0;

    # Translation table for displaying notes on screen
    $debug_table[0x00] = "---";
    $debug_table[0x11] = "D#2";
    $debug_table[0x1d] = "F#2";
    $debug_table[0x25] = "G#2";
    $debug_table[0x2d] = "A#2";
    $debug_table[0x35] = "C-3";
    $debug_table[0x39] = "C#3";
    $debug_table[0x41] = "D#3";
    $debug_table[0x49] = "F-3";
    $debug_table[0x4d] = "F#3";
    $debug_table[0x55] = "G#3";
    $debug_table[0x5d] = "A#3";
    $debug_table[0x65] = "C-4";
    $debug_table[0x69] = "C#4";
    $debug_table[0x71] = "D#4";
    $debug_table[0x79] = "F-4";
    $debug_table[0x7d] = "F#4";
    $debug_table[0x85] = "G#4";
    $debug_table[0x8d] = "A#4";

    # Translation table between BBC SOUND values and note values
    $note_table[0x00] = 0;
    $note_table[0x11] = 0x1c;
    $note_table[0x1d] = 0x1f;
    $note_table[0x25] = 0x21;
    $note_table[0x2d] = 0x23;
    $note_table[0x35] = 0x25;
    $note_table[0x39] = 0x26;
    $note_table[0x41] = 0x28;
    $note_table[0x49] = 0x2a;
    $note_table[0x4d] = 0x2b;
    $note_table[0x55] = 0x2d;
    $note_table[0x5d] = 0x2f;
    $note_table[0x65] = 0x31;
    $note_table[0x69] = 0x32;
    $note_table[0x71] = 0x34;
    $note_table[0x79] = 0x36;
    $note_table[0x7d] = 0x37;
    $note_table[0x85] = 0x39;
    $note_table[0x8d] = 0x3b;

    $data = read_file($file_input);

    # Search for various data structures in file
    if($data =~ /(\x11\x35\x1d\x35.{75}\x39)/s)
    {
        $data_channel12barnotes = $1;
    }
    else
    {
        print "ERROR: Couldn't locate data in input file (channel12barnotes)\n";
        return;
    }

    if($data =~ /(\x00\x00\x46\x0a.{50}\x50)/s)
    {
        $data_channel12barorder = $1;
    }
    else
    {
        print "ERROR: Couldn't locate data in input file (channel12barorder)\n";
        return;
    }

    if($data =~ /(\x69\x0c\x65\x0c.{204}\xff)/s)
    {
        $data_channel3barnotes = $1;
    }
    else
    {
        print "ERROR: Couldn't locate data in input file (channel3barnotes)\n";
        return;
    }

    if($data =~ /(\x21\x4f\x21\x4f.{47}\x4f)/s)
    {
        $data_channel3barorder = $1;
    }
    else
    {
        print "ERROR: Couldn't locate data in input file (channel3barorder)\n";
        return;
    }

    $duration = 648;
    $patterns = ($duration / 64) + 1;

    open(OUTPUT, ">".$file_output) || die "Couldn't open file for output: $!\n";
    binmode OUTPUT;

    # Module header
    print OUTPUT "Extended Module: ";
    print OUTPUT pack("Z20", "XOR Theme");          # Module name
    print OUTPUT pack("c", 0x1a);
    print OUTPUT pack("Z20", "convert2chroma.pl");  # Tracker name
    print OUTPUT pack("v", 0x0104);                 # Version
    print OUTPUT pack("V", 0x114);                  # Header size
    print OUTPUT pack("v", $patterns);              # Song length
    print OUTPUT pack("v", 0);                      # Restart position
    print OUTPUT pack("v", 4);                      # Channels
    print OUTPUT pack("v", $patterns);              # Patterns
    print OUTPUT pack("v", 2);                      # Instruments
    print OUTPUT pack("v", 1);                      # Flags (linear frequency table)
    print OUTPUT pack("v", 6);                      # Default tempo
    print OUTPUT pack("v", 125);                    # Default BPM

    # Pattern table
    for($i = 0; $i < 256; $i ++)
    {
        print OUTPUT pack("c", $i < ($patterns - 1) ? $i : 0); 
    }

    # Generate patterns from tune
    $time = 0;
    $time_channel3 = 24;

    $offset_channel12 = 0;
    $offset_channel12bar = 0;
    $offset_channel3 = 0;
    $offset_channel3bar = 0;

    for($time = 0; $time < $duration; $time ++)
    {
        $play_1 = 0;
        $play_2 = 0;
        $play_3 = 0;

        if($time % 3 == 0)
        {
                $note = $offset_channel12 + ($time % 12) / 3;
            $play_1 = ord(substr($data_channel12barnotes, $note, 1));
        }

        if($time % 2 == 0)
        {
            $note = $offset_channel12 + 4 + ($time % 12) / 2;
            $play_2 = ord(substr($data_channel12barnotes, $note, 1));
        }

        if($time_channel3 == 0)
        {
                $play_3 = ord(substr($data_channel3barnotes, $offset_channel3, 1));
            if($play_3 == 255)
            {
                $offset_channel3 = ord(substr($data_channel3barorder, $offset_channel3bar, 1));

                $offset_channel3bar += 2;
            if($offset_channel3bar > length($data_channel3barorder))
            {
                # End of channel 3 bar data
                # This should happen just after the end of the tune, so never gets called.
                print "ERROR: Premature end of channel 3 bar data\n";
            }

                $play_3 = ord(substr($data_channel3barnotes, $offset_channel3, 1));

            }
            $offset_channel3 ++;

            $time_channel3 = ord(substr($data_channel3barnotes, $offset_channel3, 1));
            $offset_channel3 ++;

        }

        $time_channel3 --;

        if($play_1 != 0) { $pattern[1][$time % 64] = $note_table[$play_1]; }
        if($play_2 != 0) { $pattern[2][$time % 64] = $note_table[$play_2]; }
        if($play_3 != 0) { $pattern[3][$time % 64] = $note_table[$play_3]; }

        if($debug)
        {
            printf("%8d%8s%8s%8s\n", $time, $debug_table[$play_1], $debug_table[$play_2], $debug_table[$play_3]);
        }

        if($time % 12 == 11)
        {
            if($debug)
            {
                # Bar separator
                print "\n";
            }

            $offset_channel12bar ++;
            $offset_channel12 = ord(substr($data_channel12barorder, $offset_channel12bar, 1));
            if($offset_channel12 == 0x50)
            {
                # End of channel 1/2 bar data
                # This should happen at the very end of the tune. 
            }
        }

        if($time % 64 == 63)
        {
            dump_pattern(0);
        }
    }

    # Dump any partial final pattern
    if($duration % 64 != 0)
    {
        dump_pattern(1);
    }

    for($instrument =0; $instrument < 2; $instrument ++)
    {
        # Instrument
        print OUTPUT pack("V", 0x107);              # Instrument size
        print OUTPUT pack("Z22", "Instrument $instrument"); # Instrument name
        print OUTPUT pack("c", 0);                  # Instrument type
        print OUTPUT pack("v", 1);                  # Number of samples

        print OUTPUT pack("V", 40);                 # Sample header size
        print OUTPUT pack("Z96", "");               # Sample number for each note
        print OUTPUT pack("Z48", "\x00\x00\x40");   # Points for volume envelope
        print OUTPUT pack("Z48", "\x00\x00\x20");   # Points for panning envelope
        print OUTPUT pack("c", 1);                  # Number of volume points
        print OUTPUT pack("c", 1);                  # Number of panning points
        print OUTPUT pack("c", 0);                  # Volume sustain point
        print OUTPUT pack("c", 0);                  # Volume loop start point
        print OUTPUT pack("c", 0);                  # Volume loop end point
        print OUTPUT pack("c", 0);                  # Panning sustain point
        print OUTPUT pack("c", 0);                  # Panning loop start point
        print OUTPUT pack("c", 0);                  # Panning loop end point
        print OUTPUT pack("c", 0);                  # Volume type
        print OUTPUT pack("c", 0);                  # Panning type
        print OUTPUT pack("c", 0);                  # Vibrato type
        print OUTPUT pack("c", 0);                  # Vibrato sweep
        print OUTPUT pack("c", 0);                  # Vibrato depth
        print OUTPUT pack("c", 0);                  # Vibrato rate
        print OUTPUT pack("v", 0);                  # Volume fadeout
        print OUTPUT pack("v", 0);                  # Reserved
        print OUTPUT pack("Z20", "");               # ?

        # Sample
        if($instrument == 0)
        {
            # Used for channels 1 and 2 for a backing beat
            $sample_length = 2048;
            $phase_attack = 0.0;
            $phase_decay = 0.0;
            $relative_note = 12;
        }
        else
        {
            # Used for channel 3 for the melody
            $sample_length = 4096;
            $phase_attack = 0.05;
            $phase_decay = 0.1;
            $relative_note = 0;
        }

        print OUTPUT pack("V", $sample_length); # Sample length
        print OUTPUT pack("V", 0);              # Sample loop start
        print OUTPUT pack("V", 1);              # Sample loop end
        print OUTPUT pack("c", 64);             # Volume
        print OUTPUT pack("c", 0 );             # Finetune
        print OUTPUT pack("c", 0);              # Type
        print OUTPUT pack("c", 128);            # Panning
        print OUTPUT pack("c", $relative_note); # Relative note number
        print OUTPUT pack("c", 0);              # Reserved
        print OUTPUT pack("Z22", "Sample $instrument"); # Sample name

        # Sample data
        $sample = "";
        $previous = 0;
        for($i = 0; $i < $sample_length; $i ++)
        {
            # The XM format documentation gives 8363Hz for C-4. Conversely, xmp's
            # output suggests 8287Hz for C-4. C-0 should be 262Hz. Either way, that
            # doesn't affect sample generation, as it is independent of C-4s frequency:
            # 
            # $value = sin($PI * ($C4 / 8 ) * ( 2 ** - ($relative_note / 12) * $i / $C4));
            
            $value = sin(3.14159265 * (2 ** - ($relative_note / 12)) * $i / 8);

            # Convert to a square wave 
            $value = $value > 0 ? 1 : -1;

            # Volume envelope
            $volume = 124;

            $phase = ($i / $sample_length);

            if($phase < $phase_attack)
            {
                $value = $volume * $value * $phase / $phase_attack;
            }
            elsif($phase < $phase_decay)
            {
                $value = $volume * $value;
            }
            else
            {
                $value = $volume * $value * (1 - (($phase - $phase_decay) / (1 - $phase_decay)));
            }

            # Convert to delta sample data
            $value = int($value);
            $delta = $value - $previous;
            $previous = $value;

            $sample .= pack("c", $delta);
        }

        print OUTPUT $sample;
    }

    close(OUTPUT);
}

sub dump_pattern
{
    my $final = shift(@_);

    $pattern_data = "";

    for($row = 0; $row < 64; $row ++)
    {
        for($channel = 1; $channel <=4; $channel ++)
        {
            if($pattern[$channel][$row] != 0)
            {
                $pattern_data .= pack("c3", 0x83, $pattern[$channel][$row], $channel == 3 ? 0x2 : 0x1);
            }
            else
            {
                if($final && $row == (($duration - 1) % 64) && $channel == 4)
                {
                    # Loop back to pattern 0 at end of tune
                    $pattern_data .= pack("cc", 0x88, 0xb);
                }
                else
                {
                    $pattern_data .= pack("c", 0x80);
                }
            }
        }
    }

    print OUTPUT pack("V", 9);                     # Pattern header size
    print OUTPUT pack("c", 0);                     # Packing type
    print OUTPUT pack("v", 64);                    # Number of rows
    print OUTPUT pack("v", length($pattern_data)); # Packed pattern size
    print OUTPUT $pattern_data;

    undef @pattern;
}

###############################################################################

sub convert_bbc_all
{
    my $file_input = shift(@_);
    my $output_prefix = shift(@_);

    make_directory($output_prefix);
    make_directory($output_prefix."/levels/");
    make_directory($output_prefix."/levels/xor_bbc/");
    make_directory($output_prefix."/graphics/");
    make_directory($output_prefix."/graphics/xor_bbc/");

    convert_bbc_levels($file_input, $output_prefix."/levels/xor_bbc/xor_bbc_");
    create_set($output_prefix."/levels/xor_bbc/set.chroma", "XOR (BBC)");

    $levels = 15;
    convert_bbc_graphics($file_input, $output_prefix."/graphics/xor_bbc/xor_bbc_");

    open(FILE, ">".$output_prefix."/graphics/xor_bbc.chroma");

print FILE << "EOF";
<chroma type="graphics">

<head>

<title>XOR (BBC)</title>

<sizes>
<!-- Everything is scaled from 24x24 -->
<size x="12" y="12" small="yes" pieces="yes" />
<size x="24" y="24" small="yes" pieces="yes" />
<size x="48" y="48" small="yes" pieces="yes" />
<size x="72" y="72" small="yes" pieces="yes" />
<size x="96" y="96" small="yes" pieces="yes" />
<size x="144" y="144" small="yes" pieces="yes" />
</sizes>

</head>

<background colour="#808080" />

<pieces path="xor_bbc" levels="15">

<piece name="space" scale="yes">
<image file="xor_bbc_%l_00.bmp" />
</piece>

<piece name="wall" scale="yes">
<if condition="l=1">
<image file="xor_bbc_%l_01.bmp" />
</if>
<if condition="l=2">
<image file="xor_bbc_%l_16.bmp" />
</if>
<if condition="l=3">
<image file="xor_bbc_%l_17.bmp" />
</if>
<if condition="l=4">
<image file="xor_bbc_%l_01.bmp" />
</if>
<if condition="l=5">
<image file="xor_bbc_%l_16.bmp" />
</if>
<if condition="l=6">
<image file="xor_bbc_%l_17.bmp" />
</if>
<if condition="l=7">
<image file="xor_bbc_%l_01.bmp" />
</if>
<if condition="l=8">
<image file="xor_bbc_%l_16.bmp" />
</if>
<if condition="l=9">
<image file="xor_bbc_%l_17.bmp" />
</if>
<if condition="l=10">
<image file="xor_bbc_%l_17.bmp" />
</if>
<if condition="l=11">
<image file="xor_bbc_%l_17.bmp" />
</if>
<if condition="l=12">
<image file="xor_bbc_%l_16.bmp" />
</if>
<if condition="l=13">
<image file="xor_bbc_%l_16.bmp" />
</if>
<if condition="l=14">
<image file="xor_bbc_%l_01.bmp" />
</if>
<if condition="l=15">
<image file="xor_bbc_%l_01.bmp" />
</if>
</piece>

<piece name="player_one" scale="yes">
<image file="xor_bbc_%l_02.bmp" />
<image type="small" file="xor_bbc_%l_02.bmp" />
</piece>

<piece name="player_two" scale="yes">
<image file="xor_bbc_%l_03.bmp" />
<image type="small" file="xor_bbc_%l_03.bmp" />
</piece>

<piece name="map_top_left" scale="yes">
<image file="xor_bbc_%l_04.bmp" />
</piece>

<piece name="map_top_right" scale="yes">
<image file="xor_bbc_%l_04.bmp" />
</piece>

<piece name="map_bottom_left" scale="yes">
<image file="xor_bbc_%l_04.bmp" />
</piece>

<piece name="map_bottom_right" scale="yes">
<image file="xor_bbc_%l_04.bmp" />
</piece>

<piece name="dots_x" scale="yes">
<image file="xor_bbc_%l_05.bmp" />
</piece>

<piece name="dots_y" scale="yes">
<image file="xor_bbc_%l_06.bmp" />
</piece>

<piece name="arrow_red_down" scale="yes">
<image file="xor_bbc_%l_07.bmp" />
</piece>

<piece name="arrow_red_left" scale="yes">
<image file="xor_bbc_%l_08.bmp" />
</piece>

<piece name="bomb_red_down" scale="yes">
<image file="xor_bbc_%l_09.bmp" />
</piece>

<piece name="bomb_red_left" scale="yes">
<image file="xor_bbc_%l_10.bmp" />
</piece>

<piece name="star" scale="yes">
<image file="xor_bbc_%l_11.bmp" />
<image type="small" file="xor_bbc_%l_11.bmp" />
</piece>

<piece name="door" scale="yes">
<image file="xor_bbc_%l_12.bmp" />
<image type="small" file="xor_bbc_%l_12.bmp" />
</piece>

<piece name="circle" scale="yes">
<image file="xor_bbc_%l_13.bmp" />
</piece>

<piece name="teleport" scale="yes">
<image file="xor_bbc_%l_14.bmp" />
</piece>

<piece name="switch" scale="yes">
<image file="xor_bbc_%l_15.bmp" />
</piece>

<piece name="explosion_red_left" scale="yes">
<image colour="#ffffff" />
</piece>
<piece name="explosion_red_horizontal">
<clone piece="explosion_red_left" />
</piece>
<piece name="explosion_red_right">
<clone piece="explosion_red_left" />
</piece>
<piece name="explosion_red_top">
<clone piece="explosion_red_left" />
</piece>
<piece name="explosion_red_vertical">
<clone piece="explosion_red_left" />
</piece>
<piece name="explosion_red_bottom">
<clone piece="explosion_red_left" />
</piece>

<piece name="darkness" scale="yes">
<image colour="#000000" />
</piece>

</pieces>
</chroma>
EOF

    convert_bbc_music($file_input, $output_prefix."/xor-theme.xm");
}

sub convert_designer_all
{
    my $file_input = shift(@_);
    my $output_prefix = shift(@_);

    make_directory($output_prefix);
    make_directory($output_prefix."/levels/");
    make_directory($output_prefix."/levels/xor_bbc_designer/");
    make_directory($output_prefix."/graphics/");
    make_directory($output_prefix."/graphics/xor_bbc_designer/");

    convert_designer_levels($file_input, $output_prefix."/levels/xor_bbc_designer/xor_bbc_designer_");
    create_set($output_prefix."/levels/xor_bbc_designer/set.chroma", "XOR (BBC Designer)");

    convert_bbc_graphics($file_input, $output_prefix."/graphics/xor_bbc_designer/xor_bbc_designer_");
    create_graphics_set($output_prefix."/graphics/xor_bbc_designer.chroma", "xor_bbc_designer", "XOR (BBC Designer)", 24);

    convert_bbc_music($file_input, $output_prefix."/xor-theme.xm");
}


sub convert_c64_all
{
    my $file_input = shift(@_);
    my $output_prefix = shift(@_);

    make_directory($output_prefix);
    make_directory($output_prefix."/levels/");
    make_directory($output_prefix."/levels/xor_c64/");
    make_directory($output_prefix."/graphics/");
    make_directory($output_prefix."/graphics/xor_c64/");

    convert_bbc_levels($file_input, $output_prefix."/levels/xor_c64/xor_c64_");
    create_set($output_prefix."/levels/xor_c64/set.chroma", "XOR (C64)");

    convert_c64_graphics($file_input, $output_prefix."/graphics/xor_c64/xor_c64_");
    create_graphics_set($output_prefix."/graphics/xor_c64.chroma", "xor_c64", "XOR (C64)", 24);
}

sub convert_spectrum_all
{
    my $file_input = shift(@_);
    my $output_prefix = shift(@_);

    make_directory($output_prefix);
    make_directory($output_prefix."/levels/");
    make_directory($output_prefix."/levels/xor_spectrum/");
    make_directory($output_prefix."/graphics/");
    make_directory($output_prefix."/graphics/xor_spectrum/");

    convert_spectrum_levels($file_input, $output_prefix."/levels/xor_spectrum/xor_spectrum_");
    create_set($output_prefix."/levels/xor_spectrum/set.chroma", "XOR (Spectrum)");

    convert_spectrum_graphics($file_input, $output_prefix."/graphics/xor_spectrum/xor_spectrum_");
    create_graphics_set($output_prefix."/graphics/xor_spectrum.chroma", "xor_spectrum", "XOR (Spectrum)", 24);
}

sub convert_cpc_all
{
    my $file_input = shift(@_);
    my $output_prefix = shift(@_);

    make_directory($output_prefix);
    make_directory($output_prefix."/levels/");
    make_directory($output_prefix."/levels/xor_cpc/");
    make_directory($output_prefix."/graphics/");
    make_directory($output_prefix."/graphics/xor_cpc/");

    convert_spectrum_levels($file_input, $output_prefix."/levels/xor_cpc/xor_cpc_");
    create_set($output_prefix."/levels/xor_cpc/set.chroma", "XOR (CPC)");

    convert_cpc_graphics($file_input, $output_prefix."/graphics/xor_cpc/xor_cpc_");
    create_graphics_set($output_prefix."/graphics/xor_cpc.chroma", "xor_cpc", "XOR (CPC)", 24);
}

sub convert_amiga_all
{
    my $file_input = shift(@_);
    my $output_prefix = shift(@_);

    make_directory($output_prefix);

    make_directory($output_prefix."/levels/");
    make_directory($output_prefix."/levels/xor_amiga_1/");
    make_directory($output_prefix."/levels/xor_amiga_2/");

    convert_amiga_levels($file_input."/standard.mazes", $output_prefix."/levels/xor_amiga_1/xor_amiga_1_");

    convert_amiga_levels($file_input."/standard2.mazes", $output_prefix."/levels/xor_amiga_2/xor_amiga_2_");

    create_set($output_prefix."/levels/xor_amiga_1/set.chroma", "XOR (Amiga Set 1)");
    create_set($output_prefix."/levels/xor_amiga_2/set.chroma", "XOR (Amiga Set 2)");

    make_directory($output_prefix."/graphics/");
    make_directory($output_prefix."/graphics/xor_amiga/");

    convert_amiga_graphics($file_input, $output_prefix."/graphics/xor_amiga");

    open(FILE, ">".$output_prefix."/graphics/xor_amiga.chroma");
    print FILE << "EOF";
<chroma type="graphics">

<head>

<title>XOR (Amiga)</title>

<sizes>
<!-- Everything is scaled from 24x24 -->
<size x="12" y="12" small="yes" pieces="yes" />
<size x="24" y="24" small="yes" pieces="yes" />
<size x="48" y="48" small="yes" pieces="yes" />
<size x="72" y="72" small="yes" pieces="yes" />
<size x="96" y="96" small="yes" pieces="yes" />
<size x="144" y="144" small="yes" pieces="yes" />
</sizes>

</head>

<background colour="#808080" />

<pieces path="xor_amiga" levels="15">

<piece name="space" scale="yes" level="yes" random="yes">
<image file="back_%l_16.bmp" />
<image file="back_%l_17.bmp" />
<image file="back_%l_18.bmp" />
<image file="back_%l_19.bmp" />
</piece>

<piece name="wall" scale="yes" bevel="16" level="no">
<image file="back_%l_00.bmp" />
<image file="back_%l_01.bmp" />
<image file="back_%l_02.bmp" />
<image file="back_%l_03.bmp" />
<image file="back_%l_04.bmp" />
<image file="back_%l_05.bmp" />
<image file="back_%l_06.bmp" />
<image file="back_%l_07.bmp" />
<image file="back_%l_08.bmp" />
<image file="back_%l_09.bmp" />
<image file="back_%l_10.bmp" />
<image file="back_%l_11.bmp" />
<image file="back_%l_12.bmp" />
<image file="back_%l_13.bmp" />
<image file="back_%l_14.bmp" />
<image file="back_%l_15.bmp" />
</piece>

<piece name="player_one" scale="yes" mover="yes">
<image file="char_01.bmp" key="#000000" />
<image file="char_34.bmp" key="#000000" />
<image file="char_48.bmp" key="#000000" />
<image file="char_37.bmp" key="#000000" />
<image file="char_47.bmp" key="#000000" />
<image type="small" file="char_01.bmp" key="#000000" />
</piece>

<piece name="player_two" scale="yes" mover="yes">
<image file="char_00.bmp" key="#000000" />
<image file="char_30.bmp" key="#000000" />
<image file="char_09.bmp" key="#000000" />
<image file="char_33.bmp" key="#000000" />
<image file="char_08.bmp" key="#000000" />
<image type="small" file="char_00.bmp" key="#000000" />
</piece>

<piece name="map_top_left" scale="yes">
<image file="char_07.bmp" key="#000000" />
</piece>

<piece name="map_top_right" scale="yes">
<image file="char_19.bmp" key="#000000" />
</piece>

<piece name="map_bottom_left" scale="yes">
<image file="char_38.bmp" key="#000000" />
</piece>

<piece name="map_bottom_right" scale="yes">
<image file="char_39.bmp" key="#000000" />
</piece>

<piece name="dots_x" scale="yes">
<image file="char_13.bmp" key="#000000" />
</piece>

<piece name="dots_y" scale="yes">
<image file="char_12.bmp" key="#000000" />
</piece>

<piece name="arrow_red_down" scale="yes">
<image file="char_02.bmp" key="#000000" />
</piece>

<piece name="arrow_red_left" scale="yes">
<image file="char_03.bmp" key="#000000" />
</piece>

<piece name="bomb_red_down" scale="yes">
<image file="char_15.bmp" key="#000000" />
</piece>

<piece name="bomb_red_left" scale="yes">
<image file="char_14.bmp" key="#000000" />
</piece>

<piece name="star" scale="yes">
<image file="char_05.bmp" key="#000000" />
<image type="small" file="char_05.bmp" key="#000000" />
</piece>

<piece name="door" scale="yes">
<image file="char_16.bmp" key="#000000" />
<image type="small" file="char_16.bmp" key="#000000" />
</piece>

<piece name="circle" scale="yes">
<image file="char_04.bmp" key="#000000" />
</piece>

<piece name="teleport" scale="yes">
<image file="char_11.bmp" key="#000000" />
</piece>

<piece name="switch" scale="yes">
<image file="char_06.bmp" key="#000000" />
</piece>

<piece name="explosion_red_left" scale="yes" animate="yes">
<image file="char_20.bmp" key="#000000" />
<image file="char_21.bmp" key="#000000" />
<image file="char_22.bmp" key="#000000" />
<image file="char_23.bmp" key="#000000" />
<image file="char_24.bmp" key="#000000" />
<image file="char_25.bmp" key="#000000" />
<image file="char_26.bmp" key="#000000" />
<image file="char_27.bmp" key="#000000" />
<image file="char_28.bmp" key="#000000" />
<image file="char_29.bmp" key="#000000" />
</piece>
<piece name="explosion_red_horizontal" scale="yes" animate="yes">
<clone piece="explosion_red_left" />
</piece>
<piece name="explosion_red_right" scale="yes" animate="yes">
<clone piece="explosion_red_left" />
</piece>
<piece name="explosion_red_top" scale="yes" animate="yes">
<clone piece="explosion_red_left" />
</piece>
<piece name="explosion_red_vertical" scale="yes" animate="yes">
<clone piece="explosion_red_left" />
</piece>
<piece name="explosion_red_bottom" scale="yes" animate="yes">
<clone piece="explosion_red_left" />
</piece>

<piece name="darkness" scale="yes">
<image colour="#000000" />
</piece>

</pieces>
</chroma>
EOF
    close(FILE);
}

sub convert_atari_all
{
    my $file_input = shift(@_);
    my $output_prefix = shift(@_);

    make_directory($output_prefix);
    make_directory($output_prefix."/levels/");
    make_directory($output_prefix."/levels/xor_atari/");
    make_directory($output_prefix."/graphics/");
    make_directory($output_prefix."/graphics/xor_atari/");

    convert_atari_levels($file_input, $output_prefix."/levels/xor_atari/xor_atari_");
    create_set($output_prefix."/levels/xor_atari/set.chroma", "XOR (Atari ST)");

    convert_atari_graphics($file_input, $output_prefix."/graphics/xor_atari");

    open(FILE, ">".$output_prefix."/graphics/xor_atari.chroma");

print FILE << "EOF";
<chroma type="graphics">

<head>

<title>XOR (Atari ST)</title>

<sizes>
<!-- Everything is scaled from 24x22 -->
<size x="12" y="11" small="yes" pieces="yes" />
<size x="24" y="22" small="yes" pieces="yes" />
<size x="48" y="44" small="yes" pieces="yes" />
<size x="72" y="66" small="yes" pieces="yes" />
<size x="96" y="88" small="yes" pieces="yes" />
<size x="144" y="132" small="yes" pieces="yes" />
</sizes>

</head>

<background colour="#808080" />

<pieces path="xor_atari" levels="15">

<piece name="space" tile="yes" scale="yes" level="yes">
<image file="background_%l.bmp" />
</piece>

<piece name="wall" tile="yes" scale="yes" level="yes">
<image file="wall_%l_1.bmp" />
<image file="wall_%l_2.bmp" />
<image file="wall_%l_3.bmp" />
<image file="wall_%l_4.bmp" />
</piece>

<piece name="player_one" scale="yes" level="yes">
<image file="player_one_%l.bmp" key="#deface" />
<image type="small" file="player_one_%l.bmp" key="#deface" />
</piece>

<piece name="player_two" scale="yes" level="yes">
<image file="player_two_%l.bmp" key="#deface" />
<image type="small" file="player_two_%l.bmp" key="#deface" />
</piece>

<piece name="map_top_left" scale="yes">
<image file="map_top_left.bmp" key="#deface" />
</piece>

<piece name="map_top_right" scale="yes">
<image file="map_top_right.bmp" key="#deface" />
</piece>

<piece name="map_bottom_left" scale="yes">
<image file="map_bottom_left.bmp" key="#deface" />
</piece>

<piece name="map_bottom_right" scale="yes">
<image file="map_bottom_right.bmp" key="#deface" />
</piece>

<piece name="dots_x" scale="yes">
<image file="dots.bmp" key="#deface" />
</piece>

<piece name="dots_y" scale="yes">
<image file="waves.bmp" key="#deface" />
</piece>

<piece name="arrow_red_down" scale="yes" random="yes">
<image file="fish.bmp" key="#deface" />
<image file="fish_2.bmp" key="#deface" />
<image file="fish_3.bmp" key="#deface" />
<image file="fish_4.bmp" key="#deface" />
<image file="fish_5.bmp" key="#deface" />
</piece>

<piece name="arrow_red_left" scale="yes" random="yes">
<image file="chicken.bmp" key="#deface" />
<image file="chicken_2.bmp" key="#deface" />
<image file="chicken_3.bmp" key="#deface" />
<image file="chicken_4.bmp" key="#deface" />
<image file="chicken_5.bmp" key="#deface" />
</piece>

<piece name="bomb_red_down" scale="yes">
<image file="h_bomb.bmp" key="#deface" />
</piece>

<piece name="bomb_red_left" scale="yes">
<image file="v_bomb.bmp" key="#deface" />
</piece>

<piece name="star" scale="yes">
<image file="mask.bmp" key="#deface" />
<image type="small" file="mask.bmp" key="#deface" />
</piece>

<piece name="door" scale="yes">
<image file="door.bmp" key="#deface" />
<image type="small" file="door.bmp" key="#deface" />
</piece>

<piece name="circle" scale="yes">
<image file="doll.bmp" key="#deface" />
</piece>

<piece name="teleport" scale="yes">
<image file="teleport.bmp" key="#deface" />
</piece>

<piece name="switch" scale="yes">
<image file="switch.bmp" key="#deface" />
</piece>

<piece name="explosion_red_left" scale="yes" animate="yes" >
<image file="explosion_0.bmp" key="#deface" />
<image file="explosion_1.bmp" key="#deface" />
<image file="explosion_2.bmp" key="#deface" />
<image file="explosion_3.bmp" key="#deface" />
<image file="explosion_4.bmp" key="#deface" />
</piece>
<piece name="explosion_red_horizontal" scale="yes" animate="yes" >
<clone piece="explosion_red_left" />
</piece>
<piece name="explosion_red_right" scale="yes" animate="yes" >
<clone piece="explosion_red_left" />
</piece>
<piece name="explosion_red_top" scale="yes" animate="yes" >
<clone piece="explosion_red_left" />
</piece>
<piece name="explosion_red_vertical" scale="yes" animate="yes" >
<clone piece="explosion_red_left" />
</piece>
<piece name="explosion_red_bottom" scale="yes" animate="yes" >
<clone piece="explosion_red_left" />
</piece>

<piece name="darkness" scale="yes">
<image colour="#000000" />
</piece>

</pieces>
</chroma>
EOF
    close(FILE);
}

###############################################################################

sub convert_enigma
{
    my $file_input = shift(@_);
    my $file_output = shift(@_);

    # single file
    if(-f $file_input)
    {
        convert_enigma_file($file_input, $file_output);
        return;
    }

    if(-d $file_input)
    {
        opendir(DIR, $file_input) || die "Couldn't open input directory ($file_input): $!\n";

        $output_prefix = $file_output;
        make_directory($output_prefix);
        make_directory($output_prefix."/levels/");
        make_directory($output_prefix."/levels/enigma/");

        create_set($output_prefix."/levels/enigma/set.chroma", "Enigma");

        while(($entry = readdir(DIR)) ne "")
        {
            next if($entry eq "." || $entry eq "..");
            next if($entry !~ /.level$/);

            $output = $entry;
            $output =~ s/.level$/.chroma/;

            convert_enigma_file($file_input."/".$entry, $file_output."/levels/enigma/".$output);
        }

        closedir(DIR);
    }
    else
    {
        die "Couldn't open input directory ($file_input)\n";
    }

}

###############################################################################

sub convert_enigma_file
{
    my $file_input = shift(@_);
    my $file_output = shift(@_);

    print "Converting '$file_input'\n";

    $title = ""; $height = 0; $width = 0; $map = "";

    open(INPUT, $file_input) || die "Couldn't open input file ($file_input): $!\n";
    while(<INPUT>)
    {
        if(/^Title: (.*)/)
        {
            $title = $1;
        }
        if(/^Height: (\d+)/)
        {
            $height = $1;
        }
        if(/^Width: (\d+)/)
        {
            $width = $1;
        }
        if(/^Map: (.*)/)
        {
            $line = $1;
            $line =~ tr/-+|&~@E$<^>vYZWX/%%%%%1\/*abcdABCD/;
            $map .= $line."\n";
        }
    }

    close(INPUT);

    open(OUTPUT, ">".$file_output) || die "Couldn't open output file ($file_output): $!\n";
    print OUTPUT << "EOF";
chroma level

title: Enigma: $title
mode: enigma
size: $width $height

data:
$map
EOF
    close(OUTPUT);

}

###############################################################################

sub convert_chroma2enigma
{
    my $file_input = shift(@_);
    my $file_output = shift(@_);

    open(INPUT, $file_input) || die "Couldn't open input file ($file_input): $!\n";

    $size_x = 0;
    $size_y = 0;
    $title = "";

    $state = 0;
    while(<INPUT>)
    {
        $state = 1 if(/^chroma level/);
        next if($state == 0);

        if(/^size: (\d+) (\d+)/)
        {
            $size_x = $1;
            $size_y = $2;
        }
        if(/^title: (.*)/)
        {
            $title = $1;
        }
        if(/^data:/)
        {
            $state = 2;
            last;
        }
 
    }

    if($state != 2)
    {
        die "Unable to locate data in input file";
    }

    $x = 0;
    $y = 0;

    while(!eof(INPUT) && $y < $size_y)
    {
        $c = getc(INPUT);
        next if($c eq "\r" || $c eq "\n");

        $map[$x][$y] = $c;
        $x ++;
        if($x == $size_x)
        {
            $x = 0; $y ++;
        }
    }

    close(INPUT);

    open(OUTPUT, ">".$file_output) || die "Unable to open output file: $!\n";
    print OUTPUT "Title: $title\n";
    print OUTPUT "Height: $size_y\n";
    print OUTPUT "Width: $size_x\n";
    for($y = 0; $y < $size_y; $y ++)
    {
        print OUTPUT "Map: ";
        for($x = 0; $x < $size_x; $x ++)
        {
            $c = $map[$x][$y];
            $c =~ tr/1\/*abcdABCD/@E$<^>vYZWX/;
            print OUTPUT $c;
        }
        print OUTPUT "\n";
    }
    close(OUTPUT);

}

###############################################################################
#
# To replace level 1 of the BBC XOR disc image with the XOR regression tests:
#     convert2chroma.pl --chroma2xor levels/regression/xor-regression.chroma regression.xor xor-bbc.ssd 1

sub convert_chroma2xor
{
    my $file_input = shift(@_);
    my $file_output = shift(@_);

    open(INPUT, $file_input) || die "Couldn't open input file ($file_input): $!\n";

    $size_x = 0;
    $size_y = 0;
    $title = "";

    $state = 0;
    while(<INPUT>)
    {
        $state = 1 if(/^chroma level/);
        next if($state == 0);

        if(/^size: (\d+) (\d+)/)
        {
            $size_x = $1;
            $size_y = $2;
        }
        if(/^title: (.*)/)
        {
            $title = $1;
        }
        if(/^view1: (\d+) (\d+)/)
        {
            $data{"view1x"} = $1;
            $data{"view1y"} = $2;
        }
        if(/^view2: (\d+) (\d+)/)
        {
            $data{"view2x"} = $1;
            $data{"view2y"} = $2;
        }
        if(/^viewteleport1: (\d+) (\d+) \((\d+) (\d+)\)/)
        {
            $data{"viewteleport1x"} = $1;
            $data{"viewteleport1y"} = $2;
            $data{"teleport1x"} = $3;
            $data{"teleport1y"} = $4;
            $data{"teleports"} ++;
        }
        if(/^viewteleport2: (\d+) (\d+) \((\d+) (\d+)\)/)
        {
            $data{"viewteleport2x"} = $1;
            $data{"viewteleport2y"} = $2;
            $data{"teleport2x"} = $3;
            $data{"teleport2y"} = $4;
            $data{"teleports"} ++;
        }
        if(/^data:/)
        {
            $state = 2;
            last;
        }
 
    }

    if($state != 2)
    {
        die "Unable to locate data in input file";
    }

    $x = 0;
    $y = 0;

    $base = 0x900;

    while(!eof(INPUT) && $y < $size_y)
    {
        $c = getc(INPUT);
        next if($c eq "\r" || $c eq "\n");

        $p = 0x0;
        $p = 0x1 if($c eq "%");
        $p = 0x2 if($c eq "1");
        $p = 0x3 if($c eq "2");
        $p = 0x4 if($c eq "M" || $c eq "m" || $c eq "N" || $c eq "n");
        $p = 0x5 if($c eq "-");
        $p = 0x6 if($c eq "|");
        $p = 0x7 if($c eq "d");
        $p = 0x8 if($c eq "a");
        $p = 0x9 if($c eq "D");
        $p = 0xa if($c eq "A");
        $p = 0xb if($c eq "*");
        $p = 0xc if($c eq "/");
        $p = 0xd if($c eq "o");
        $p = 0xe if($c eq "T");
        $p = 0xf if($c eq "S");

        if($c eq "M") { $data{"mapTLx"} = $x; $data{"mapTLy"} = $y; }
        if($c eq "m") { $data{"mapTRx"} = $x; $data{"mapTRy"} = $y; }
        if($c eq "N") { $data{"mapBLx"} = $x; $data{"mapBLy"} = $y; }
        if($c eq "n") { $data{"mapBRx"} = $x; $data{"mapBRy"} = $y; }
        if($c eq "1") { $data{"player1x"} = $x; $data{"player1y"} = $y; }
        if($c eq "2") { $data{"player2x"} = $x; $data{"player2y"} = $y; }
        if($c eq "*") { $data{"stars"} ++; }
        if($c eq "T" && $data{"teleports"} != 2)
        {
            $data{"teleports"} ++;
            $data{"teleport".$data{"teleports"}."x"} = $x;
            $data{"teleport".$data{"teleports"}."y"} = $y;
        }

        $map[$x][$y] = $p;
        $x ++;
        if($x == $size_x)
        {
            $x = 0; $y ++;
        }
    }

    close(INPUT);

    open(OUTPUT, ">".$file_output) || die "Unable to open output file: $!\n";
    binmode OUTPUT;

    for($y = 1; $y < 31; $y ++)
    {
        for($x = 1; $x < 31; $x += 2)
        {
            print OUTPUT chr($map[$x][$y] + 16 * $map[$x + 1][$y]);
        }
    }

    # Sanitise viewpoints
    if(!defined($data{"view1x"}))
    {
        $data{"view1x"} = $data{"player1x"} - 3; 
        $data{"view1y"} = $data{"player1y"} - 3; 
    }
    if(!defined($data{"view2x"}))
    {
        $data{"view2x"} = $data{"player2x"} - 3; 
        $data{"view2y"} = $data{"player2y"} - 3; 
    }
    if(!defined($data{"viewteleport1x"}))
    {
        $data{"viewteleport1x"} = $data{"teleport1x"} - 3; 
        $data{"viewteleport1y"} = $data{"teleport1y"} - 3; 
    }
    if(!defined($data{"viewteleport2x"}))
    {
        $data{"viewteleport2x"} = $data{"teleport2x"} - 3; 
        $data{"viewteleport2y"} = $data{"teleport2y"} - 3; 
    }

    # Ensure viewpoints are not offscreen
    foreach $i ("view1x", "view1y", "view2x", "view2y", "viewteleport1x", "viewteleport1y", "viewteleport2x", "viewteleport2y")
    {
        if($data{$i} < 0)
        {
            $data{$i} = 0;
        }
        if($data{$i} > 24)
        {
            $data{$i} = 24;
        }
    }

    # Sanitise title
    $title =~ s/^XOR: //;
    $title =~ tr/a-z/A-Z/;
    $title =~ tr/A-Z ?//cd;
    $title = substr($title, 0, 19);
    if(length($title < 19))
    {
        $l = length($title);
        $title = " "x((19 - $l)/2).$title." "x((19 - $l)/2);
        $title .= " " if(length($title != 19));
    }
    $title .= "\r";

    # Map data
    foreach $i ("TL", "TR", "BL", "BR")
    { 
        if(defined($data{"map".$i."x"}))
        {
            print OUTPUT pack("v", $base + $data{"map".$i."x"} + $data{"map".$i."y"} * 32 - 32);
        }
        else
        {
            print OUTPUT pack("v", 0);
        }
    }

    # Player and view data
    for($i = 1; $i <= 2; $i ++)
    {
        print OUTPUT pack("v", $base + $data{"player".$i."x"} + $data{"player".$i."y"} * 32 - 32);
        print OUTPUT pack("c", $data{"player".$i."x"} - $data{"view".$i."x"});
        print OUTPUT pack("c", $data{"player".$i."y"} - $data{"view".$i."y"});
        print OUTPUT pack("c", $data{"view".$i."x"});
        print OUTPUT pack("c", $data{"view".$i."y"});
        print OUTPUT pack("v", $base + $data{"view".$i."x"} + $data{"view".$i."y"} * 32);
    }

    # Star count and padding
    print OUTPUT pack("c", 0x02);
    print OUTPUT pack("c", 0x02);
    print OUTPUT pack("c", $data{"stars"});
    print OUTPUT pack("c", 0x00);
    print OUTPUT pack("c", 0x00);
    print OUTPUT pack("c", 0x00);
    print OUTPUT pack("c", 0x00);
    print OUTPUT pack("c", 0x00);

    # Teleport data
    if($data{"teleports"} != 2 )
    {
        print OUTPUT pack("Z30", "");
    }
    else
    {
        for($i = 1; $i <= 2; $i ++)
        {
            print OUTPUT pack("v", $base + $data{"teleport".$i."x"} + $data{"teleport".$i."y"} * 32);
            print OUTPUT pack("c", $data{"teleport".$i."x"} - $data{"viewteleport".$i."x"});
            print OUTPUT pack("c", $data{"teleport".$i."y"} - $data{"viewteleport".$i."y"});
            print OUTPUT pack("c", $data{"viewteleport".$i."x"});
            print OUTPUT pack("c", $data{"viewteleport".$i."y"});
            print OUTPUT pack("v", $base + $data{"viewteleport".$i."x"} + $data{"viewteleport".$i."y"} * 32);
        }

        print OUTPUT pack("Z14", "");
    }

    close(OUTPUT);

    # Optionally insert the file into the BBC XOR disc image
    if($ARGV[3] ne "")
    {
        $file_discimage = $ARGV[3];

        if($ARGV[4] ne "")
        {
            $level = $ARGV[4];
        }
        if($level < 1 || $level >= 15)
        {
            $level = 1;
        }

        $xor = read_file($file_discimage);
        $insert = read_file($file_output);

        $index_numbers  = locate_string(\$xor, "\x00\x4d\x00\x01\x00\x02\x00\x03\x00\x04\x00\x05\x00\x06\x00\x07\x00\x08\x00\x09\x01\x00\x01\x01\x01\x02\x01\x03\x01\x04\x01\x05");

        # BBC XOR
        if($index_numbers >= 0)
        {
            $offset = $index_numbers + 0x179 + (15 - $level) * 0x200;
            insert(\$xor, \$insert, $offset);

            $offset = $index_numbers + 0x22 + ($level - 1) * 23;
            insert(\$xor, \$title, $offset);

            open(OUTPUT, ">".$file_discimage) || die "Unable to open output file: $!\n";
            binmode OUTPUT;
            print OUTPUT $xor;
            close(OUTPUT);
        }
        else
        {
            die "Unable to identify file\n";
        }
    }

}

sub insert
{
    my $data = shift(@_);
    my $string = shift(@_);
    my $offset = shift(@_);

    my $tmp = shift(@_);

    $tmp = substr($$data, 0, $offset);
    $tmp .= $$string;
    $tmp .= substr($$data, $offset + length($$string));

    $$data = $tmp;
}


###############################################################################

sub convert_bbc_patch
{
    my $file_input = shift(@_);
    my $file_output = shift(@_);

    $xor = read_file($file_input);

    $index_skip = locate_string(\$xor, "\xad\x6e\x25\xc9\x0b\x30\x06\xee\x6e\x25");
    # BBC XOR
    if($index_skip != 0)
    {
        $xor =~ s/\xad\x6e\x25\xc9\x0b\x30\x06\xee\x6e\x25/\xad\x6e\x25\xc9\x0b\x30\x06\xea\xea\xea/;
        $xor =~ s/\xc9\x0b\x30\x07\x29\x01\xf0\x03\xce\x6e\x25/\xc9\x0b\x30\x07\x29\x01\xf0\x03\xea\xea\xea/;
        open(OUTPUT, ">".$file_output) || die "Unable to open output file: $!\n";
        binmode OUTPUT;
        print OUTPUT $xor;
        close(OUTPUT);
    }
    else
    {
        die "Unable to identify file\n";
    }
}


###############################################################################

sub convert_amiga_patch
{
    my $file_input = shift(@_);
    my $file_output = shift(@_);

    open(IN, $file_input) || die "Unable to open input file: $!\n";
    open(OUT,">".$file_output) || die "Unable to open outfile: $!\n";
    binmode IN;
    binmode OUT;
    while(<IN>)
    {
        $line = $_;
        # Patch protection routine
        if($line =~ /\x00\x00\x00\x06\x8c\xc0\x60\x00\xfc\x84\x4a\xac\x8c\xa8\x6f\x24/)
        {  
            $line =~ s/\x00\x00\x00\x06\x8c\xc0\x60\x00\xfc\x84\x4a\xac\x8c\xa8\x6f\x24/\x00\x00\x00\x06\x8c\xc0\x60\x00\xfc\x84\x4a\xac\x8c\xa8\x60\x24/;
            $ok |= 1;
        }
        # Patch checksum
        if($line =~ /\xc0\xf8\x37\xb4/)
        {  
            $line =~ s/\xc0\xf8\x37\xb4/\xc0\xf8\x46\xb4/;
            $ok |= 2;
        }
        print OUT $line;
    }
    close OUT;
    close IN;

    if($ok != 3)
    {
        die "Unable to identify file\n";
    }
}

###############################################################################
#
# http://www.rvvz.demon.nl/xor/
# A JavaScript version by Rob Veldhuyzen van Zanten
#
sub convert_zanten_level
{
    my $file_input = shift(@_);
    my $file_output = shift(@_);

    print "Converting '$file_input'\n";

    $title = ""; $height = 0; $width = 0; $map = "";

    open(INPUT, $file_input) || die "Couldn't open input file ($file_input): $!\n";
    while(<INPUT>)
    {
        if(/^spel='(.*)'/)
        {
            $title = $1;
        }
        if(/^'(.*)'/)
        {
            $line = $1;
            $width = length($line);
            $line =~ tr/ #UWpqrs\-|<v@omZxBg/ %12MmNn\-|adAD*\/oTS/;
            $map .= $line."\n";
            $height ++;
        }
    }

    close(INPUT);

    open(OUTPUT, ">".$file_output) || die "Couldn't open output file ($file_output): $!\n";
    print OUTPUT << "EOF";
chroma level

title: XOR: $title
mode: xor
size: $width $height

data:
$map
EOF
    close(OUTPUT);

}

###############################################################################
#
# http://xor.ovine.net
# Ovine's "remake" has the worst XOR engine I've come across - thoroughly
# broken precedence and bombs that can be pushed against their gravity. Quite
# how well levels written for such an engine will work with Chroma is anyone's
# guess, but converting their level files is simple enough:
#
# 0x000 - 0x3ff Map data (32 * 32, bytes as per BBC XOR except for maps)
# 0x400 - 0x401 Masks total
# 0x402 - 0x411 Title
# 0x412 - 0x437 Unused?

sub convert_ovine_level
{
    my $file_input = shift(@_);
    my $file_output = shift(@_);

    print "Converting '$file_input'\n";

    $level_data = read_file($file_input);
    $bad = 1 if(length($level_data) != 0x438);

    $title = ""; $map = "";

    $o = 0;
    for($y = 0; $y < 32; $y ++)
    {
        for($x = 0; $x < 32; $x ++)
        {
            $c = byte(\$level_data, $o);
            $map .= " " if($c == 0x00);
            $map .= "%" if($c == 0x01);
            $map .= "1" if($c == 0x02);
            $map .= "2" if($c == 0x03);
            $map .= "-" if($c == 0x05);
            $map .= "|" if($c == 0x06);
            $map .= "d" if($c == 0x07);
            $map .= "a" if($c == 0x08);
            $map .= "D" if($c == 0x09);
            $map .= "A" if($c == 0x0a);
            $map .= "*" if($c == 0x0b);
            $map .= "/" if($c == 0x0c);
            $map .= "o" if($c == 0x0d);
            $map .= "T" if($c == 0x0e);
            $map .= "S" if($c == 0x0f);
            $map .= "M" if($c == 0x19);
            $map .= "m" if($c == 0x16);
            $map .= "N" if($c == 0x18);
            $map .= "n" if($c == 0x17);
            $bad = 1 if($c >= 0x1a || $c == 0x04);
            $o ++;
        }
        $map .= "\n";
    }

    if($bad)
    {
        print "ERROR: Couldn't convert file\n";
        return;
    }

    $o = 0x402;
    while(byte(\$level_data, $o) != 0 && $o < 0x412)
    {
        $title .= chr(byte(\$level_data, $o));
        $o ++;
    }

    open(OUTPUT, ">".$file_output) || die "Couldn't open output file ($file_output): $!\n";
    print OUTPUT << "EOF";
chroma level

title: XOR: $title
mode: xor
size: 32 32

data:
$map
EOF
    close(OUTPUT);

}
