PUL (Puzzle) files

06/11/2012 19:31 Zeroxelli#1
Just wondering if anyone has any information on their structure. I wrote a basic DMap reader (took about 30 minutes, DMap/ani structure is quite simple.) and I'm trying to parse the PUL files to get the tiles for the map. It's not hard at all to get it to work if you get the offsets right, but some PUL files are half the size of the arena map (the map I worked with first) and the offsets are not absolute at all. Is there something I'm missing about calculating the offset of the tile amounts (x and y)? Of course the tile information is after them, so there's no need for those offsets to be exact.

Edit: And I know that I can calculate the number of tiles by using the tile size and coordinate width/height of the map (given a simple calculation to convert the size to isometric format), but that would still leave the offset for the tile data unknown. So I'd rather just read the data from the file the right way.

Here's an example:
[Only registered and activated users can see links. Click Here To Register...]
As you can see, the highlighted byte (4) is the amount of tiles for the x axis, while the 4th byte to the right of it (3) is the amount of tiles for the y axis. So, 4x3 tiles, pretty simple. And the data follows immediately after that. The problem is the garbage before the tile amounts, it doesn't seem to be a consistent size, so skipping over it can be a pain. That's the only thing I need a nudge about. :)

Edit: Scratch that, it does seem to have some uniformity to it. Tile amount for the X axis has been at 263 for 4 maps now, and the amount for the y axis is at 267. They're both UInt16 (ushort).
06/11/2012 20:43 nTL3fTy#2
Taken from the EO client source:
Code:
var version = 0;
var header = reader.ReadCString(8);

if (header == "PUZZLE") version = 1;
else if (header == "PUZZLE2") version = 2;

if (version == 0)
    return null;

var aniFile = reader.ReadCString(256);

var size = reader.ReadTqSize(); // two ints (Width and Height)
file.Size = size;

var amount = file.PuzzleAmount; // size.Width * size.Height
for (var i = 0; i < amount; i++)
{
    file.PuzzleIndex[i] = reader.ReadInt16();
}

file.RollSpeedX = 0;
file.RollSpeedY = 0;

if (version == 2)
{
    file.RollSpeedX = reader.ReadInt32();
    file.RollSpeedY = reader.ReadInt32();
}
06/11/2012 20:51 Zeroxelli#3
Quote:
Originally Posted by nTL3fTy View Post
Taken from the EO client source:
Code:
var version = 0;
var header = reader.ReadCString(8);

if (header == "PUZZLE") version = 1;
else if (header == "PUZZLE2") version = 2;

if (version == 0)
    return null;

var aniFile = reader.ReadCString(256);

var size = reader.ReadTqSize(); // two ints (Width and Height)
file.Size = size;

var amount = file.PuzzleAmount; // size.Width * size.Height
for (var i = 0; i < amount; i++)
{
    file.PuzzleIndex[i] = reader.ReadInt16();
}

file.RollSpeedX = 0;
file.RollSpeedY = 0;

if (version == 2)
{
    file.RollSpeedX = reader.ReadInt32();
    file.RollSpeedY = reader.ReadInt32();
}
Ah, perfect, I was off by a few bytes. :) Is there a public download of the EO source (unmodified) still around? I formatted my usb drive by accident.

Also, here's a screenshot of TC (newplain) at .025f scale.
[Only registered and activated users can see links. Click Here To Register...]
06/11/2012 21:19 nTL3fTy#4
Quote:
Originally Posted by Zeroxelli View Post
Ah, perfect, I was off by a few bytes. :) Is there a public download of the EO source (unmodified) still around? I formatted my usb drive by accident.
[Only registered and activated users can see links. Click Here To Register...]

Quote:
Originally Posted by Zeroxelli View Post
Also, here's a screenshot of TC (newplain) at .025f scale.
I notice there's no bridges. ;)
06/11/2012 22:32 pro4never#5
Yahh it's a very simple file as already shown. Now you just need a full dmap structure as well as displaying scene objects and then converting between screen and game space based on zoom/camera location and mouse offset.

Are you planning on turning this into a map editor? Mine is basically just missing scene objects right now as well as the FULL dmap structure (just access info right now).
06/12/2012 01:09 unknownone#6
Full dmap structure for anyone who wants it.

Code:
        internal enum MapObjectType
        {
            Scene = 0x01,
            TerrainObject = 0x04,
            Backdrop = 0x08,
            Effect = 0x0A,
            Sound = 0x0F
        }

        public static MapData Load(Stream file) 
        {
            MapData dmap = new MapData() {
                TerrainObjects = new List<MapTerrainObject>(),
                Scenes = new List<MapScene>(),
                Sounds = new List<MapSound>(),
                Effects = new List<Map3DEffect>()
            };
            using (BinaryReader br = new BinaryReader(file))
            {
                string dmapheader = br.ReadASCIIString(8);
                dmap.PuzzlePath = br.ReadASCIIString(260);
                dmap.Bounds = br.ReadSize();
                dmap.Cells = new MapCellCollection(dmap.Bounds) { CollectionSize = dmap.Bounds };
                for (int j = 0 ; j < dmap.Bounds.Height ; j++) {
                    for (int i = 0 ; i < dmap.Bounds.Width ; i++) {
                        dmap.Cells[i, j] = new MapCell() {
                            Access = br.ReadInt16(),
                            Surface = br.ReadInt16(),
                            Height = br.ReadInt16()
                        };
                    }
                    dmap.Cells.OffsetArray.Add(br.ReadInt32());
                }
                
                Int32 portalcount = br.ReadInt32();
                dmap.Portals = new List<MapPortal>();
                for (int i = 0 ; i < portalcount ; i++) {
                    dmap.Portals.Add(new MapPortal() {
                        Location = br.ReadPoint(),
                        PortalType = br.ReadInt32()
                    });
                }
                Int32 objCount = br.ReadInt32();
                for (int i = 0 ; i < objCount ; i++) {
                    MapObjectType type;
                    try {
                        type = (MapObjectType)br.ReadInt32();
                    }
                    catch (EndOfStreamException ex) {
                        throw ex;
                    }
                    switch (type) {
                        case MapObjectType.Scene:
                            dmap.Scenes.Add(new MapScene() {
                                ScenePath = br.ReadASCIIString(260),
                                Location = br.ReadPoint()
                            });
                            break;
                        case MapObjectType.TerrainObject:
                            dmap.TerrainObjects.Add(new MapTerrainObject() {
                                AniPath = br.ReadASCIIString(260),
                                AniName = br.ReadASCIIString(128),
                                Location = br.ReadPoint(),
                                Size = br.ReadSize(),
                                ImageOffset = br.ReadPoint(),
                                Interval = br.ReadInt32()
                            });
                            break;
                        case MapObjectType.Effect:
                            dmap.Effects.Add(new Map3DEffect() {
                                Effect = br.ReadASCIIString(64),
                                Location = br.ReadPoint()
                            });
                            break;
                        case MapObjectType.Sound:
                            dmap.Sounds.Add(new MapSound() {
                                SoundPath = br.ReadASCIIString(260),
                                Location = br.ReadPoint(),
                                Volume = br.ReadInt32(),
                                Range = br.ReadInt32() 
                            });
                            break;
                        default:
                            throw new UnknownMapDataException();
                    }
                }
                Int32 nLayers = br.ReadInt32();
                dmap.Layers = new List<MapLayer>(nLayers);
                for (int i = 0 ; i < nLayers ; i++) {
                    MapLayer layer = new MapLayer() {
                        Backdrops = new List<MapBackdrop>(),
                        TerrainObjects = new List<MapTerrainObject>(),
                        index = br.ReadInt32(),
                        layertype = br.ReadInt32(),
                        xInt = br.ReadInt32(),
                        yInt = br.ReadInt32()
                    };
                    Int32 nItems = br.ReadInt32();
                    for (int j = 0 ; j < nItems ; j++) {
                        MapObjectType nType = (MapObjectType)br.ReadInt32();
                        switch (nType)
                        {
                            case MapObjectType.Backdrop:
                                layer.Backdrops.Add(new MapBackdrop() { 
                                    PuzzlePath = br.ReadASCIIString(260) 
                                });
                                break;
                            case MapObjectType.TerrainObject:
                                layer.TerrainObjects.Add(new MapTerrainObject() {
                                    AniPath = br.ReadASCIIString(260),
                                    AniName = br.ReadASCIIString(128),
                                    Location = br.ReadPoint(),
                                    Size = br.ReadSize(),
                                    ImageOffset = br.ReadPoint(),
                                    Interval = br.ReadInt32() 
                                });
                                break;
                            default:
                                throw new UnknownMapDataException();
                        }
                        
                    }
                    dmap.Layers.Add(layer);
                }
            }
            return dmap;
        }
06/12/2012 01:26 Zeroxelli#7
Quote:
Originally Posted by nTL3fTy View Post
[Only registered and activated users can see links. Click Here To Register...]



I notice there's no bridges. ;)
Yep, haven't even touched scene files yet. And thanks for the link to the EO source, I missed having it as a reference.

Quote:
Originally Posted by pro4never View Post
Yahh it's a very simple file as already shown. Now you just need a full dmap structure as well as displaying scene objects and then converting between screen and game space based on zoom/camera location and mouse offset.

Are you planning on turning this into a map editor? Mine is basically just missing scene objects right now as well as the FULL dmap structure (just access info right now).
Hmm, possibly. I just did it to get the coordinates of certain spots in maps in real-time, but being able to make/edit maps would definitely be a plus...

Quote:
Originally Posted by unknownone View Post
Full dmap structure for anyone who wants it.

Code:
        internal enum MapObjectType
        {
            Scene = 0x01,
            TerrainObject = 0x04,
            Backdrop = 0x08,
            Effect = 0x0A,
            Sound = 0x0F
        }

        public static MapData Load(Stream file) 
        {
            MapData dmap = new MapData() {
                TerrainObjects = new List<MapTerrainObject>(),
                Scenes = new List<MapScene>(),
                Sounds = new List<MapSound>(),
                Effects = new List<Map3DEffect>()
            };
            using (BinaryReader br = new BinaryReader(file))
            {
                string dmapheader = br.ReadASCIIString(8);
                dmap.PuzzlePath = br.ReadASCIIString(260);
                dmap.Bounds = br.ReadSize();
                dmap.Cells = new MapCellCollection(dmap.Bounds) { CollectionSize = dmap.Bounds };
                for (int j = 0 ; j < dmap.Bounds.Height ; j++) {
                    for (int i = 0 ; i < dmap.Bounds.Width ; i++) {
                        dmap.Cells[i, j] = new MapCell() {
                            Access = br.ReadInt16(),
                            Surface = br.ReadInt16(),
                            Height = br.ReadInt16()
                        };
                    }
                    dmap.Cells.OffsetArray.Add(br.ReadInt32());
                }
                
                Int32 portalcount = br.ReadInt32();
                dmap.Portals = new List<MapPortal>();
                for (int i = 0 ; i < portalcount ; i++) {
                    dmap.Portals.Add(new MapPortal() {
                        Location = br.ReadPoint(),
                        PortalType = br.ReadInt32()
                    });
                }
                Int32 objCount = br.ReadInt32();
                for (int i = 0 ; i < objCount ; i++) {
                    MapObjectType type;
                    try {
                        type = (MapObjectType)br.ReadInt32();
                    }
                    catch (EndOfStreamException ex) {
                        throw ex;
                    }
                    switch (type) {
                        case MapObjectType.Scene:
                            dmap.Scenes.Add(new MapScene() {
                                ScenePath = br.ReadASCIIString(260),
                                Location = br.ReadPoint()
                            });
                            break;
                        case MapObjectType.TerrainObject:
                            dmap.TerrainObjects.Add(new MapTerrainObject() {
                                AniPath = br.ReadASCIIString(260),
                                AniName = br.ReadASCIIString(128),
                                Location = br.ReadPoint(),
                                Size = br.ReadSize(),
                                ImageOffset = br.ReadPoint(),
                                Interval = br.ReadInt32()
                            });
                            break;
                        case MapObjectType.Effect:
                            dmap.Effects.Add(new Map3DEffect() {
                                Effect = br.ReadASCIIString(64),
                                Location = br.ReadPoint()
                            });
                            break;
                        case MapObjectType.Sound:
                            dmap.Sounds.Add(new MapSound() {
                                SoundPath = br.ReadASCIIString(260),
                                Location = br.ReadPoint(),
                                Volume = br.ReadInt32(),
                                Range = br.ReadInt32() 
                            });
                            break;
                        default:
                            throw new UnknownMapDataException();
                    }
                }
                Int32 nLayers = br.ReadInt32();
                dmap.Layers = new List<MapLayer>(nLayers);
                for (int i = 0 ; i < nLayers ; i++) {
                    MapLayer layer = new MapLayer() {
                        Backdrops = new List<MapBackdrop>(),
                        TerrainObjects = new List<MapTerrainObject>(),
                        index = br.ReadInt32(),
                        layertype = br.ReadInt32(),
                        xInt = br.ReadInt32(),
                        yInt = br.ReadInt32()
                    };
                    Int32 nItems = br.ReadInt32();
                    for (int j = 0 ; j < nItems ; j++) {
                        MapObjectType nType = (MapObjectType)br.ReadInt32();
                        switch (nType)
                        {
                            case MapObjectType.Backdrop:
                                layer.Backdrops.Add(new MapBackdrop() { 
                                    PuzzlePath = br.ReadASCIIString(260) 
                                });
                                break;
                            case MapObjectType.TerrainObject:
                                layer.TerrainObjects.Add(new MapTerrainObject() {
                                    AniPath = br.ReadASCIIString(260),
                                    AniName = br.ReadASCIIString(128),
                                    Location = br.ReadPoint(),
                                    Size = br.ReadSize(),
                                    ImageOffset = br.ReadPoint(),
                                    Interval = br.ReadInt32() 
                                });
                                break;
                            default:
                                throw new UnknownMapDataException();
                        }
                        
                    }
                    dmap.Layers.Add(layer);
                }
            }
            return dmap;
        }
Ahh thank you, references are always great to have. :)
06/12/2012 10:15 Korvacs#8
Just a few corrections on Sparkie's code for the structure.

The value that Sparkie stores is actually a checksum for that row.

Code:
                for (int j = 0 ; j < dmap.Bounds.Height ; j++) {
                    for (int i = 0 ; i < dmap.Bounds.Width ; i++) {
                        dmap.Cells[i, j] = new MapCell() {
                            Access = br.ReadInt16(),
                            Surface = br.ReadInt16(),
                            Height = br.ReadInt16()
                        };
                    }
                    [B]dmap.Cells.OffsetArray.Add(br.ReadInt32());[/B]
                }
So here is TQ's checksum calculation in C++.

Code:
                for (int j = 0 ; j < dmap.Bounds.Height ; j++) {
                    for (int i = 0 ; i < dmap.Bounds.Width ; i++) {
                        dmap.Cells[i, j] = new MapCell() {
                            Access = br.ReadInt16(),
                            Surface = br.ReadInt16(),
                            Height = br.ReadInt16()
                        };
                        [B]//dwCheckData += pLayerInfo->usMask * (pLayerInfo->usTerrain+j+1) + (pLayerInfo->sAltitude+2)*(i+1+pLayerInfo->usTerrain);
                    }
                    int ActualCheckSum = br.ReadInt32();
                    if (RowCheckSum != ActualCheckSum)
                        return;[/B]
                }
The value labelled PortalType, is actually the PortalsID within that map.

Code:
                Int32 portalcount = br.ReadInt32();
                dmap.Portals = new List<MapPortal>();
                for (int i = 0 ; i < portalcount ; i++) {
                    dmap.Portals.Add(new MapPortal() {
                        Location = br.ReadPoint(),
                        [B]PortalType = br.ReadInt32()[/B]
                    });
                }
This change is necessary if you want to actually use the correct method for linking up portals, so if your using an official database for example, otherwise its possible you will become confused about what values mean what with relation to the database.

Code:
                Int32 portalcount = br.ReadInt32();
                dmap.Portals = new List<MapPortal>();
                for (int i = 0 ; i < portalcount ; i++) {
                    dmap.Portals.Add(new MapPortal() {
                        Location = br.ReadPoint(),
                        [B]PortalID = br.ReadInt32()[/B]
                    });
                }
06/12/2012 21:32 Zeroxelli#9
Quote:
Originally Posted by Korvacs View Post
Just a few corrections on Sparkie's code for the structure.

The value that Sparkie stores is actually a checksum for that row.

Code:
                for (int j = 0 ; j < dmap.Bounds.Height ; j++) {
                    for (int i = 0 ; i < dmap.Bounds.Width ; i++) {
                        dmap.Cells[i, j] = new MapCell() {
                            Access = br.ReadInt16(),
                            Surface = br.ReadInt16(),
                            Height = br.ReadInt16()
                        };
                    }
                    [B]dmap.Cells.OffsetArray.Add(br.ReadInt32());[/B]
                }
So here is TQ's checksum calculation in C++.

Code:
                for (int j = 0 ; j < dmap.Bounds.Height ; j++) {
                    for (int i = 0 ; i < dmap.Bounds.Width ; i++) {
                        dmap.Cells[i, j] = new MapCell() {
                            Access = br.ReadInt16(),
                            Surface = br.ReadInt16(),
                            Height = br.ReadInt16()
                        };
                        [B]//dwCheckData += pLayerInfo->usMask * (pLayerInfo->usTerrain+j+1) + (pLayerInfo->sAltitude+2)*(i+1+pLayerInfo->usTerrain);
                    }
                    int ActualCheckSum = br.ReadInt32();
                    if (RowCheckSum != ActualCheckSum)
                        return;[/B]
                }
The value labelled PortalType, is actually the PortalsID within that map.

Code:
                Int32 portalcount = br.ReadInt32();
                dmap.Portals = new List<MapPortal>();
                for (int i = 0 ; i < portalcount ; i++) {
                    dmap.Portals.Add(new MapPortal() {
                        Location = br.ReadPoint(),
                        [B]PortalType = br.ReadInt32()[/B]
                    });
                }
This change is necessary if you want to actually use the correct method for linking up portals, so if your using an official database for example, otherwise its possible you will become confused about what values mean what with relation to the database.

Code:
                Int32 portalcount = br.ReadInt32();
                dmap.Portals = new List<MapPortal>();
                for (int i = 0 ; i < portalcount ; i++) {
                    dmap.Portals.Add(new MapPortal() {
                        Location = br.ReadPoint(),
                        [B]PortalID = br.ReadInt32()[/B]
                    });
                }
Thanks :) I kinda figured it had something to do with verification, didn't expect a checksum though.
06/12/2012 23:46 CptSky#10
There is also an implementation in my CO2_CORE_DLL which is entirely based on the one in the EO server source. But with Sparkie's implementation and Korvacs rectification, it's pretty much the full format.
06/13/2012 03:02 Zeroxelli#11
Quote:
Originally Posted by CptSky View Post
There is also an implementation in my CO2_CORE_DLL which is entirely based on the one in the EO server source. But with Sparkie's implementation and Korvacs rectification, it's pretty much the full format.
Yeah, I've been considering using your DLL for some of my projects lately, as it does look to be quite useful.