.art files, decoding frames

Discussion in 'Modding and Scripting Support' started by foobie42, Sep 19, 2011.

Remove all ads!
Support Terra-Arcanum:

GOG.com

PayPal - The safer, easier way to pay online!
  1. foobie42

    foobie42 New Member

    Messages:
    20
    Likes Received:
    0
    Joined:
    Sep 19, 2011
    Hey,

    So far I've been able to decode .art metadata. However, there seems to be no documentation or source code on how the actual frame data is encoded, other than that it seems RLE'd.

    Could you guys help me a bit?
     
  2. foobie42

    foobie42 New Member

    Messages:
    20
    Likes Received:
    0
    Joined:
    Sep 19, 2011
    Thanks for nothing. Here's the code:

    Code:
            public ArcanumArt(Stream stream)
            {
                Palettes = new ArcanumRGB[4,256];
                stream.Position = 0;
                UInt32 flags = stream.ReadLittleUInt32();
                if ((flags & MagicCritter) != 0)
                {
                    Critter = true;
                }
                Directions = (flags & MagicStatic) != 0 ? 1 : 8;
                Framerate = stream.ReadLittleUInt32();
                UInt32 rotationCount = stream.ReadLittleUInt32();
                int paletteCount = 0;
                for (int i = 0; i < 4; i++)
                {
                    int paletteCheck = stream.ReadInt32();
                    if (paletteCheck != 0)
                    {
                        paletteCount++;
                    }
                }
                ActionFrame = stream.ReadLittleUInt32();
                UInt32 frameCount = stream.ReadLittleUInt32();
                for (int i = 0; i < 24; i++)
                {
                    stream.ReadInt32();
                }
                for (int i = 0; i < paletteCount; i++)
                {
                    for (int j = 0; j < 256; j++)
                    {
                        Palettes[i, j].Red = stream.ReadByteOrDie();
                        Palettes[i, j].Green = stream.ReadByteOrDie();
                        Palettes[i, j].Blue = stream.ReadByteOrDie();
                        stream.ReadByteOrDie();
                    }
                }
                _frames = new ArcanumFrame[rotationCount,frameCount];
                var sizes = new UInt32[rotationCount,frameCount];
                for (int rot = 0; rot < rotationCount; rot++)
                {
                    for (int frcnt = 0; frcnt < frameCount; frcnt++)
                    {
                        UInt32 width = stream.ReadLittleUInt32();
                        UInt32 height = stream.ReadLittleUInt32();
                        UInt32 size = stream.ReadLittleUInt32();
                        sizes[rot, frcnt] = size;
                        Int32 offX = stream.ReadLittleInt32();
                        Int32 offY = stream.ReadLittleInt32();
                        Int32 dx = stream.ReadLittleInt32();
                        Int32 dy = stream.ReadLittleInt32();
                        _frames[rot, frcnt] = new ArcanumFrame(this, width, height, offX, offY, dx, dy);
                    }
                }
                for (int rot = 0; rot < rotationCount; rot++)
                {
                    for (int frcnt = 0; frcnt < frameCount; frcnt++)
                    {
                        long size = sizes[rot, frcnt];
                        ArcanumFrame foo = _frames[rot, frcnt];
                        uint width = foo.Width;
                        uint height = foo.Height;
                        byte[,] frameData = foo.FrameData;
                        var data = new byte[width*height];
                        if (width*height == size)
                        {
                            for (int i = 0; i < size; i++)
                            {
                                data[i] = stream.ReadByteOrDie();
                            }
                        }
                        else
                        {
                            int dataPos = 0;
                            long endPos = stream.Position + size;
                            while (stream.Position < endPos)
                            {
                                byte repeat = stream.ReadByteOrDie();
                                bool flag = (repeat & 0x80) == 0;
                                var cnt = (byte) (repeat & 0x7f);
                                if (flag)
                                {
                                    byte c = stream.ReadByteOrDie();
                                    for (int x = 0; x < cnt; x++)
                                    {
                                        data[dataPos++] = c;
                                    }
                                }
                                else
                                {
                                    for (int x = 0; x < cnt; x++)
                                    {
                                        data[dataPos++] = stream.ReadByteOrDie();
                                    }
                                }
                            }
                        }
                        int k = 0;
                        for (int y = 0; y < height; y++)
                        {
                            for (int x = 0; x < width; x++)
                            {
                                frameData[x, y] = data[k++];
                            }
                        }
                    }
                }
            }
    [/code]
     
  3. Jojobobo

    Jojobobo Well-Known Member

    Messages:
    3,031
    Likes Received:
    122
    Joined:
    May 29, 2011
    You only waited a day for a comment, I don't know how active you presumed these forums would be for modding and scripting but maybe next time wait a little longer for a useful response.
     
  4. foobie42

    foobie42 New Member

    Messages:
    20
    Likes Received:
    0
    Joined:
    Sep 19, 2011
    Yeah, I was kinda' desperate. My bad.

    When the next person stumbles with the same problem stumbles upon this thread, take note: the original code contains ERRORS,

    1) Colors are in BGR format.
    2) "rotationCount" should be set to "Directions". It's the same thing. Otherwise tiles are renderred incorrectly or an exception occurs.

    Edit: Yo, check this out: http://img1.uploadscreenshot.com/images ... 7-orig.jpg
     
  5. TheDavisChanger

    TheDavisChanger Well-Known Member

    Messages:
    1,845
    Likes Received:
    13
    Joined:
    Feb 6, 2009
    With a name like foobie42, I had expected an advertisement for smart phones so his impatience actually exceeded my expectations.
     
  6. Crypton

    Crypton Member

    Messages:
    589
    Likes Received:
    2
    Joined:
    May 22, 2008
    Wow, that screenshot... you weren't kidding, you are really making a new engine. Is this one-man project, or rather community project?

    Btw, there are other projects "similar" to yours, you might want to check them out, before you continue with the development. Here is link to project list: http://arcanum.game-alive.com/forums/vi ... 29&start=0

    Hope it helps :)
    Good luck!
     
  7. foobie42

    foobie42 New Member

    Messages:
    20
    Likes Received:
    0
    Joined:
    Sep 19, 2011
    > Is this one-man project, or rather community project?

    It's a strictly one-man project, save for some libraries I'll need, such as lidgren or xdelta3.

    > Btw, there are other projects "similar" to yours, you might want to check them out, before you continue with the development

    Thanks, I'll see if there's some advice I could take before continuing.

    Right now I'm worried about one thing - biome generation and how it relates to tilesets. I'd like to "connect", say, desert to dirt, and there's no tile that makes a smooth transition. It's going to look like crap. Worse yet, it's the same with water and other important things. Anyone has a solution for that?

    Also, check out this movie. The movement is jerky, but I believe it was the same in the original game. Feel free to prove me wrong: http://www.youtube.com/watch?v=A6UmYowFE-U

    Of course, big thanks go to Crypton for describing a large part of needed file formats as well as pointing me to artView Pascal source code.
     
  8. Crypton

    Crypton Member

    Messages:
    589
    Likes Received:
    2
    Joined:
    May 22, 2008
    AFAIK, there are tilesets for such terrain transitions... but I'm not sure about it's smoothness. I don't remember seeing any glitches there, and I think that the terrain graphics looks quite good. I suggest you to take a look at WorldEd, since you can see the all the transitions there, easily.

    If it's not enough, you could also try to generate new terrain graphics, by using perlin noise, and set of textures, for example. Then you can use some color matching algorithm, and replace colors in newly generated tiles with colors from original terrain, so the the new terrain will fit in, if you know what I mean. Those color palettes can be found here:
    http://arcanum.game-alive.com/forums/vi ... f=23&t=103

    It looks quite choppy :) Are you using the frameRate variable from the .art file header? Also, there are "delta" coords, which are used especially for critter's movement, are you using that to adjust the frame's position on screen? You can also have bug in world-to-screen conversion algorithm... the tile's image dimension is 78x40 pixels, but AFAIR, the Arcanum's renderer is using 80x40 dimension, to ensure smooth movement across the grid.
     
  9. foobie42

    foobie42 New Member

    Messages:
    20
    Likes Received:
    0
    Joined:
    Sep 19, 2011
    > Are you using the frameRate variable from the .art file header?

    Yes, I generate a new "tick" every N milliseconds, where N is 1k divided by the framerate.

    > Also, there are "delta" coords, which are used especially for critter's movement

    I use exclusively "delta[xy]" for critter's movement. But I noticed that "offset" is non-zero as well. What is it for?

    My code:

    Code:
            private void ClientCritterFrameChanged(object sender, EventArgs e)
            {
                Bitmap = _frameCache[AnimationName, Direction, FrameNo, PaletteNo];
                ArcanumFrame frame = Animation[(uint) Direction, (uint) FrameNo];
                if (Animation.Critter)
                {
                    PosX = _prevOffX + frame.DeltaX;
                    PosY = _prevOffY + frame.DeltaY;
                }
                else
                {
                    PosX = frame.OffsetX;
                    PosY = frame.OffsetY;
                }
                _prevOffX = PosX;
                _prevOffY = PosY;
            }
    Once again, thank you for all the help. About your project that's being currently written in C++ - do you need any help so that I might return the favor?

    Also, for now I have no tile-to-screen coordinates. It's just animation being played on a blank screen. But I once did the same for Fallout, with roofs, scenery, scripted doors etc. (check out http://www.youtube.com/watch?v=ie0Il8UYU6I ) so it won't be hard to recreate it despite a hard-drive crash that destroyed my work. It's now properly backed-up at a remote location, and my disk is WD. I've never seen one fail.

    Check out this: http://www.youtube.com/watch?v=Zgh8rIy4YsA Seems like the animation(s) look much better when played at half speed.

    Edit: I've been able to seemingly correctly draw a facade, take a look: http://img1.uploadscreenshot.com/images ... 1-orig.jpg

    If you're interested in the project, be sure to follow my blog: http://foobie42.blogspot.com

    -sh
     
  10. Crypton

    Crypton Member

    Messages:
    589
    Likes Received:
    2
    Joined:
    May 22, 2008
    That's where the problem is, because you're also supposed to use the offset variable for the movement as well. All the critter animations were (AFAIR) rendered into 640x480 images, where characters were placed in the middle, and to save some space, the devs decided to crop the images by calculating bounding boxes around the characters. The offset coord is just difference vector between the old and new top-left corner of the image. Same thing for other objects with animations, but without the delta coord.

    Thanks, but OpenArcanum is no longer under development, and can be considered as abandoned project. But beer will do... :thumbup:

    I'm sorry to hear that, no way to restore it? I'm quite paranoid about loosing any of my project's data, they are priceless, and that's why I'm using RAID 1, plus making a DVD backups since the first day of the development. Also, I just switched to SSDs, so I hope that I won't experience any disk crashes in the future.

    Will do, looks interesting already. I just cloned the SVN repo, and checked out your code... well, it looks quite unoptimized, especially the .art loading algorithm :) For example, you're reading palette colors from input stream per byte, no file/frame headers, reading unwated variables instead of skipping them, etc.

    Just saying, because from my experience, the art loading can be a main bottleneck when pre-loading 3x3 sector grid. In OpenArcanum, it takes 2-5 sec to load one sector. That's slow, and I did fully optimized the .art loading algorithm (rewritten like 50 times), but I'm caching everything I can, using SSE4 and have dedicated thread for it. Still it's slow... :thumbup: I wonder about your speed, expecially because you're using XNA.
     
  11. TheDavisChanger

    TheDavisChanger Well-Known Member

    Messages:
    1,845
    Likes Received:
    13
    Joined:
    Feb 6, 2009
    Now that foobie has the Val Kilmer avatar, I can begin to take him seriously.
     
  12. foobie42

    foobie42 New Member

    Messages:
    20
    Likes Received:
    0
    Joined:
    Sep 19, 2011
    > I'm sorry to hear that, no way to restore it?

    I fiddled with AHCI, plugging the power off and on for 10 times until I could transmit work data. It was actually important stuff.

    Then I placed the disk on a cupboard and considered the AHCI ordeal again. But then my cat jumped on the cupboard and threw the disk off it.

    > well, it looks quite unoptimized, especially the .art loading algorithm
    > For example, you're reading palette colors from input stream per
    > byte, no file/frame headers

    It's a buffered stream, I believe. Also, there is caching, for now last 1024 art files to debug the caching algorithm until I run into any thrashing.

    > reading unwated variables instead of skipping them, etc.

    I agree. But let's not care about it before it actually becomes a bottleneck. I did it mostly for clarity but also due to laziness :)

    > I wonder about your speed, expecially because you're using XNA.

    Now it's going to be 50x50 tiles, as I doubt now or in the foreseeable future anyone will have any bigger displays than 4000 pixels :)

    I'll add a bigger cache when biome-generation is "done", also just-loaded sectors that aren't displayed already can be loaded in the background, only waiting on the background thread if they become visible on the actual screen really fast.

    Also I don't plan on using too many facades because the content will be procedurally generated or built by players. For now there won't be any castles etcetera :)

    I'll worry about it when it's time, for now I have to parse what you wrote about deltas and offsets.

    Edit: Think it's done. Here's the code:

    Code:
            private void ClientCritterFrameChanged(object sender, EventArgs e)
            {
                Bitmap = _frameCache[AnimationName, Direction, FrameNo, PaletteNo];
                ArcanumFrame frame = Animation[Direction, (UInt32) FrameNo];
                if (Animation.Critter)
                {
                    _deltaX += frame.DeltaX;
                    _deltaY += frame.DeltaY;
                    PosX = _deltaX - frame.OffsetX;
                    PosY = _deltaY - frame.OffsetY;
                }
                else
                {
                    PosX = -frame.OffsetX;
                    PosY = -frame.OffsetY;
                }
            }
    Does it make sense now?
     
  13. Crypton

    Crypton Member

    Messages:
    589
    Likes Received:
    2
    Joined:
    May 22, 2008
    Even if it's buffered or memory stream, there is a lot of calculations going on, plus calling class method is expensive operation by itself. You could optimize it a lot, e.g. you can read all palettes in one read call, instead of calling read method 4*256*4 times, which is exactly what you're doing right now. Also you're using readByteOrDie, which is way slower than readByte method, since it calls checkAdvance everytime, and can throw an exception...

    Code:
    inline void checkAdvance(int advance) {
    		if ((m_pos + advance) <0> m_size) {
                if (m_canGrow) {
                    enableAdvance(advance);
                } else {
                    throw JNIComException("passed end of stream in the marshalling layer");
                }
            }
        }
    Why? When you can just test if input stream is big enough once at the beginning.

    You'll notice the slowness of the loading very soon, when debugging and testing new features. :) When you compile the app, you'll have to wait tens of seconds before it actually runs and loads all the data, and after your game gets more complex, it'll take much more time to load, thus you'll waste a lot of time waiting. Just saying, from my experience... so I think that it's better to make loading fast from the very beginning.

    Oh my, do you even know how big the Arcanum is? The main map is 2000x2000 sectors big, and one sector has 64x64 tiles :) Plus it's diamond shaped map, not staggered, so it's even bigger. If you're making MMO, the new map shouldn't be smaller, but even bigger, I think.

    Remember that player can move very fastly, there are spells for teleporation. You'll have to cache at least 9 sectors (3x3 grid), that's 36864 tiles... not even close to your 50 :)

    I suppose... does it work? Is the animation still choppy?:) Please post video or something.
     
  14. foobie42

    foobie42 New Member

    Messages:
    20
    Likes Received:
    0
    Joined:
    Sep 19, 2011
    > calling class method is expensive operation by itself

    You mean prediction miss due to a virtual call or just a function call overhead?

    > Also you're using readByteOrDie

    Yes, good point. It's a prototype. The API is (seemingly) stable for the class, but the actual implementation can change. No need to fret about it. I'll change it when it becomes a priority, i.e. I implement much-needed features such as smooth isometric scrolling, or biome generation which is now in progress.

    > Just saying, from my experience... so I think that it's better to make loading fast from the very beginning.

    Thanks for valuable info. I'll change it once loading takes more than just a few seconds on my i7.

    > Oh my, do you even know how big the Arcanum is? The main map is 2000x2000 sectors big, and one sector has 64x64 tiles Plus it's diamond shaped map, not staggered, so it's even bigger. If you're making MMO, the new map shouldn't be smaller, but even bigger, I think.

    For testing purposes with biome generation the map is presently 2500x2500. I know it isn't at all impressive, but right now due to the lack of any database setup I have to load it all into memory, and even that takes over 1 GB due to .NET overhead like runtime types.

    > I suppose... does it work? Is the animation still choppy? Please post video or something.

    It's not, I hope :) Here's a video: http://www.youtube.com/watch?v=9koj6LsS3uk

    > Also you're using readByteOrDie, which is way slower than readByte method

    It's C#, not Java. "ReadByteOrDie" is an extension method written by me.

    I made major speedups to the renderer loop. It's over 10x times more efficient. The caching mechanism was at fault. Replaced it with a circular buffer. Now time to finish biome generation.

    Edit: Some basic biome generation is in progress. See http://img1.uploadscreenshot.com/images ... 4-orig.jpg

    Can generate an "island" with fancy jagged sea-line. All that's left for that part is to figure out the tiles to connect them to land without such ugliness.

    Also, when run standalone in release mode, I get stable 1k FPS. Isn't bad, especially with some basic caching done. If you want you can check out the latest revision. Be warned if running it, though, as a data store for biome generation still isn't implemented. I'll leave it for last when it comes to map generation.

    Update 2:

    http://img1.uploadscreenshot.com/images ... 4-orig.jpg

    All of it is generated algorithmically without an editor of any sort. FPS is low. Profiling doesn't show an easy-to-blame suspect. Perhaps fullscreen mode could be better, but I'd rather avoid that for now. It's also possible that it's texture switching overhead.

    Update 3: http://img1.uploadscreenshot.com/images ... 2-orig.jpg

    Unless I manage to do some blending for "connecting" tiles that's the final look.

    Update 4:

    Final or not, I managed to improve it. :)

    http://img1.uploadscreenshot.com/images ... 3-orig.jpg
     
Our Host!