summary refs log tree commit diff stats
path: root/tools/sprite_dumper/sprite_dumper.cpp
blob: 392f3458300e6a0785f3e3b184cce84423fc8323 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
.highlight .hll { background-color: #ffffcc }
.highlight .c { color: #888888 } /* Comment */
.highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */
.highlight .k { color: #008800; font-weight: bold } /* Keyword */
.highlight .ch { color: #888888 } /* Comment.Hashbang */
.highlight .cm { color: #888888 } /* Comment.Multiline */
.highlight .cp { color: #cc0000; font-weight: bold } /* Comment.Preproc */
.highlight .cpf { color: #888888 } /* Comment.PreprocFile */
.highlight .c1 { color: #888888 } /* Comment.Single */
.highlight .cs { color: #cc0000; font-weight: bold; background-color: #fff0f0 } /* Comment.Special */
.highlight .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */
.highlight .ge { font-style: italic } /* Generic.Emph */
.highlight .gr { color: #aa0000 } /* Generic.Error */
.highlight .gh { color: #333333 } /* Generic.Heading */
.highlight .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */
.highlight .go { color: #888888 } /* Generic.Output */
.highlight .gp { color: #555555 } /* Generic.Prompt */
.highlight .gs { font-weight: bold } /* Generic.Strong */
.highlight .gu { color: #666666 } /* Generic.Subheading */
.highlight .gt { color: #aa0000 } /* Generic.Traceback */
.highlight .kc { color: #008800; font-weight: bold } /* Keyword.Constant */
.highlight .kd { color: #008800; font-weight: bold } /* Keyword.Declaration */
.highlight .kn { color: #008800; font-weight: bold } /* Keyword.Namespace */
.highlight .kp { color: #008800 } /* Keyword.Pseudo */
.highlight .kr { color: #008800; font-weight: bold } /* Keyword.Reserved */
.highlight .kt { color: #888888; font-weight: bold } /* Keyword.Type */
.highlight .m { color: #0000DD; font-weight: bold } /* Literal.Number */
.highlight .s { color: #dd2200; background-color: #fff0f0 } /* Literal.String */
.highlight .na { color: #336699 } /* Name.Attribute */
.highlight .nb { color: #003388 } /* Name.Builtin */
.highlight .nc { color: #bb0066; font-weight: bold } /* Name.Class */
.highlight .no { color: #003366; font-weight: bold } /* Name.Constant */
.highlight .nd { color: #555555 } /* Name.Decorator */
.highlight .ne { color: #bb0066; font-weight: bold } /* Name.Exception */
.highlight .nf { color: #0066bb; font-weight: bold } /* Name.Function */
.highlight .nl { color: #336699; font-style: italic } /* Name.Label */
.highlight .nn { color: #bb0066; font-weight: bold } /* Name.Namespace */
.highlight .py { color: #336699; font-weight: bold } /* Name.Property */
.highlight .nt { color: #bb0066; font-weight: bold } /* Name.Tag */
.highlight .nv { color: #336699 } /* Name.Variable */
.highlight .ow { color: #008800 } /* Operator.Word */
.highlight .w { color: #bbbbbb } /* Text.Whitespace */
.highlight .mb { color: #0000DD; font-weight: bold } /* Literal.Number.Bin */
.highlight .mf { color: #0000DD; font-weight: bold } /* Literal.Number.Float */
.highlight .mh { color: #0000DD; font-weight: bold } /* Literal.Number.Hex */
.highlight .mi { color: #0000DD; font-weight: bold } /* Literal.Number.Integer */
.highlight .mo { color: #0000DD; font-weight: bold } /* Literal.Number.Oct */
.highlight .sa { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Affix */
.highlight .sb { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Backtick */
.highlight .sc { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Char */
.highlight .dl { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Delimiter */
.highlight .sd { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Doc */
.highlight .s2 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Double */
.highlight .se { color: #0044dd; background-color: #fff0f0 } /* Literal.String.Escape */
.highlight .sh { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Heredoc */
.highlight .si { color: #3333bb; background-color: #fff0f0 } /* Literal.String.Interpol */
.highlight .sx { color: #22bb22; background-color: #f0fff0 } /* Literal.String.Other */
.highlight .sr { color: #008800; background-color: #fff0ff } /* Literal.String.Regex */
.highlight .s1 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Single */
.highlight .ss { color: #aa6600; background-color: #fff0f0 } /* Literal.String.Symbol */
.highlight .bp { color: #003388 } /* Name.Builtin.Pseudo */
.highlight .fm { color: #0066bb; font-weight: bold } /* Name.Function.Magic */
.highlight .vc { color: #336699 } /* Name.Variable.Class */
.highlight .vg { color: #dd7700 } /* Name.Variable.Global */
.highlight .vi { color: #3333bb } /* Name.Variable.Instance */
.highlight .vm { color: #336699 } /* Name.Variable.Magic */
.highlight .il { color: #0000DD; font-weight: bold } /* Literal.Number.Integer.Long */
#include "mixer.h"

void Mixer::playSound(std::string_view filename) {
  Mix_Chunk* chunk = getChunkByFilename(std::string(filename));

  if (Mix_PlayChannel(-1, chunk, 0) == -1) {
    throw mix_error();
  }
}

int Mixer::loopSound(std::string_view filename) {
  Mix_Chunk* chunk = getChunkByFilename(std::string(filename));

  int ret = Mix_PlayChannel(-1, chunk, -1);
  if (ret == -1) {
    throw mix_error();
  }

  return ret;
}

void Mixer::stopChannel(int channel) {
  Mix_HaltChannel(channel);
}

void Mixer::playMusic(std::string_view name, int ms) {
  Mix_Music* song = getMusicByName(name);
  int ret;

  if (ms .highlight .hll { background-color: #ffffcc }
.highlight .c { color: #888888 } /* Comment */
.highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */
.highlight .k { color: #008800; font-weight: bold } /* Keyword */
.highlight .ch { color: #888888 } /* Comment.Hashbang */
.highlight .cm { color: #888888 } /* Comment.Multiline */
.highlight .cp { color: #cc0000; font-weight: bold } /* Comment.Preproc */
.highlight .cpf { color: #888888 } /* Comment.PreprocFile */
.highlight .c1 { color: #888888 } /* Comment.Single */
.highlight .cs { color: #cc0000; font-weight: bold; background-color: #fff0f0 } /* Comment.Special */
.highlight .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */
.highlight .ge { font-style: italic } /* Generic.Emph */
.highlight .gr { color: #aa0000 } /* Generic.Error */
.highlight .gh { color: #333333 } /* Generic.Heading */
.highlight .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */
.highlight .go { color: #888888 } /* Generic.Output */
.highlight .gp { color: #555555 } /* Generic.Prompt */
.highlight .gs { font-weight: bold } /* Generic.Strong */
.highlight .gu { color: #666666 } /* Generic.Subheading */
.highlight .gt { color: #aa0000 } /* Generic.Traceback */
.highlight .kc { color: #008800; font-weight: bold } /* Keyword.Constant */
.highlight .kd { color: #008800; font-weight: bold } /* Keyword.Declaration */
.highlight .kn { color: #008800; font-weight: bold } /* Keyword.Namespace */
.highlight .kp { color: #008800 } /* Keyword.Pseudo */
.highlight .kr { color: #008800; font-weight: bold } /* Keyword.Reserved */
.highlight .kt { color: #888888; font-weight: bold } /* Keyword.Type */
.highlight .m { color: #0000DD; font-weight: bold } /* Literal.Number */
.highlight .s { color: #dd2200; background-color: #fff0f0 } /* Literal.String */
.highlight .na { color: #336699 } /* Name.Attribute */
.highlight .nb { color: #003388 } /* Name.Builtin */
.highlight .nc { color: #bb0066; font-weight: bold } /* Name.Class */
.highlight .no { color: #003366; font-weight: bold } /* Name.Constant */
.highlight .nd { color: #555555 } /* Name.Decorator */
.highlight .ne { color: #bb0066; font-weight: bold } /* Name.Exception */
.highlight .nf { color: #0066bb; font-weight: bold } /* Name.Function */
.highlight .nl { color: #336699; font-style: italic } /* Name.Label */
.highlight .nn { color: #bb0066; font-weight: bold } /* Name.Namespace */
.highlight .py { color: #336699; font-weight: bold } /* Name.Property */
.highlight .nt { color: #bb0066; font-weight: bold } /* Name.Tag */
.highlight .nv { color: #336699 } /* Name.Variable */
.highlight .ow { color: #008800 } /* Operator.Word */
.highlight .w { color: #bbbbbb } /* Text.Whitespace */
.highlight .mb { color: #0000DD; font-weight: bold } /* Literal.Number.Bin */
.highlight .mf { color: #0000DD; font-weight: bold } /* Literal.Number.Float */
.highlight .mh { color: #0000DD; font-weight: bold } /* Literal.Number.Hex */
.highlight .mi { color: #0000DD; font-weight: bold } /* Literal.Number.Integer */
.highlight .mo { color: #0000DD; font-weight: bold } /* Literal.Number.Oct */
.highlight .sa { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Affix */
.highlight .sb { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Backtick */
.highlight .sc { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Char */
.highlight .dl { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Delimiter */
.highlight .sd { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Doc */
.highlight .s2 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Double */
.highlight .se { color: #0044dd; background-color: #fff0f0 } /* Literal.String.Escape */
.highlight .sh { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Heredoc */
.highlight .si { color: #3333bb; background-color: #fff0f0 } /* Literal.String.Interpol */
.highlight .sx { color: #22bb22; background-color: #f0fff0 } /* Literal.String.Other */
.highlight .sr { color: #008800; background-color: #fff0ff } /* Literal.String.Regex */
.highlight .s1 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Single */
.highlight .ss { color: #aa6600; background-color: #fff0f0 } /* Literal.String.Symbol */
.highlight .bp { color: #003388 } /* Name.Builtin.Pseudo */
.highlight .fm { color: #0066bb; font-weight: bold } /* Name.Function.Magic */
.highlight .vc { color: #336699 } /* Name.Variable.Class */
.highlight .vg { color: #dd7700 } /* Name.Variable.Global */
.highlight .vi { color: #3333bb } /* Name.Variable.Instance */
.highlight .vm { color: #336699 } /* Name.Variable.Magic */
.highlight .il { color: #0000DD; font-weight: bold } /* Literal.Number.Integer.Long */
// Inspired a lot by https://github.com/jeffman/MOTHER-3-Funland

#include <fstream>
#include <string_view>
#include <stdexcept>
#include <string>
#include <vector>
#include <iostream>
#include <Magick++.h>
#include <sstream>
#include "common.h"

class PaletteSet {
public:
  PaletteSet(BufferView m3) : m3_(m3) {
    for (int i=0; i<16; i++) {
      defaultPals_.emplace_back(m3, GetPointer(0) + (i << 5));
    }
  }

  const Palette& GetPalette(int spriteindex, int palnum = -1) {
    int a = GetPointer(spriteindex);

    if ((spriteindex > 0xFF) && (spriteindex < 0x26C)) {
      specialPals_[spriteindex] = Palette(m3_, a);
      return specialPals_[spriteindex];
    } else {
      if (palnum == -1) {
        palnum = static_cast<unsigned char>(m3_.ReadByte(0x1433D7C + 1 + ((spriteindex + 1) * 12))) & 0xF;
      }

      return defaultPals_.at(palnum);
    }
  }

private:

  int GetPointer(int spriteindex) {
    const int ADDR = 0x1A41548;

    int index = 0;

    if ((spriteindex > 0xFF) && (spriteindex < 0x26C)) {
      // These sprites use the (spriteindex - 0xFF)th entry
      index = spriteindex - 0xff;
    }

    int a = m3_.ReadFourBytes(ADDR + 4 + (index << 2));
    if (a == -1) return -1;
    return ADDR + a;
  }

  BufferView m3_;
  std::vector<Palette> defaultPals_;
  std::map<int, Palette> specialPals_;
};

struct Subsprite {
  int x;
  int y;
  int tile;
  bool flipH;
  bool flipV;
  int objSize;
  int objShape;

  int Width() const { return WIDTHS[objShape][objSize]; }

  int Height() const { return HEIGHTS[objShape][objSize]; }

private:

  static constexpr int WIDTHS[3][4] = { { 8, 16, 32, 64 }, { 16, 32, 32, 64 }, { 8, 8, 16, 32 } };
  static constexpr int HEIGHTS[3][4] = { { 8, 16, 32, 64 }, { 8, 8, 16, 32 }, { 16, 32, 32, 64 } };
};

struct FrameOutput {
  Magick::Image image;
  int width;
  int height;
  int centerX;
  int centerY;
};

struct Sprite {
  std::vector<Subsprite> subsprites;

  FrameOutput render(BufferView m3, const Palette& palette, const int gfxPtr) const {
    FrameOutput output;

    if (subsprites.empty()) return output;

    int minX = subsprites[0].x;
    int minY = subsprites[0].y;
    int maxX = minX + subsprites[0].Width();
    int maxY = minY + subsprites[0].Height();

    for (int j=1; j<subsprites.size(); j++) {
      const Subsprite& o = subsprites[j];

      if (o.x < minX) minX = o.x;
      if (o.y < minY) minY = o.y;
      if ((o.x + o.Width()) > maxX) maxX = o.x + o.Width();
      if ((o.y + o.Height()) > maxY) maxY = o.y + o.Height();
    }

    int width = maxX - minX;
    int height = maxY - minY;
    int centerX = -minX;
    int centerY = -minY;

    output.width = width;
    output.height = height;
    output.centerX = centerX;
    output.centerY = centerY;
    output.image = Magick::Image(Magick::Geometry(width, height), "transparent");
    output.image.modifyImage();

    Magick::Pixels view(output.image);

    for (const Subsprite& o : subsprites) {
      int tilePointer = o.tile << 5;
      for (int y=0; y<o.Height(); y += 8) {
        int actualY = o.flipV ? (o.Height() - y - 8) : y;

        for (int x=0; x<o.Width(); x += 8) {
          int tileData[8][8];

          int tileSrcOffset = gfxPtr + tilePointer;
          for (int ty=0; ty<8; ty++) {
            for (int tx=0; tx<4; tx++) {
              unsigned char ch = m3.ReadByte(tileSrcOffset++);
              tileData[tx*2][ty] = static_cast<unsigned char>(ch & 0xF);
              tileData[tx*2+1][ty] = static_cast<unsigned char>((ch >> 4) & 0xF);
            }
          }

          tilePointer += 0x20;

          int actualX = o.flipH ? (o.Width() - x - 8) : x;

          int destX = o.x + centerX + actualX;
          int destY = o.y + centerY + actualY;

          Magick::PixelPacket* pixels = view.get(destX,destY,8,8);

          for (int ty=0; ty<8; ty++) {
            int actualTy = o.flipV ? (7-ty) : ty;

            for (int tx=0; tx<8; tx++) {
              int actualTx = o.flipH ? (7-tx) : tx;

              if (tileData[actualTx][actualTy] != 0) {
                //auto& c = palette.Colors().at(tileData[actualTx][actualTy]);
                //std::cout << c.redQuantum() << "," << c.greenQuantum() << "," << c.blueQuantum() << std::endl;
                *pixels = palette.Colors().at(tileData[actualTx][actualTy]);
                //std::cout << tileData[actualTx][actualTy] << std::endl;
              }
              pixels++;
            }
          }

          view.sync();
        }
      }
    }

    return output;
  }
};

struct SpriteSheet {
  int spriteindex = 0;
  int gfxPtr;
  std::vector<Sprite> sprites;

  void render(BufferView m3, PaletteSet& palettes, std::vector<FrameOutput>& frames) const {
    const Palette& palette = palettes.GetPalette(spriteindex);
    for (int i=0; i<sprites.size(); i++) {
      FrameOutput f = sprites[i].render(m3, palette, gfxPtr);
      frames.push_back(std::move(f));
    }
  }
};

class Bank {
public:

  Bank(BufferView m3, const int baseAddr, const int gfxAddr) : baseAddr_(baseAddr), gfxAddr_(gfxAddr) {
    int numEntries = m3.ReadFourBytes(baseAddr);
    std::cout << numEntries << std::endl;

    for (int i = 0; i < numEntries; i++) {
      int sheetAddr = GetPointerToSheet(m3, i);
      if (sheetAddr == -1) continue;

      SpriteSheet ss;
      ss.spriteindex = i;
      ss.gfxPtr = GetGfxPointer(m3, i);

      m3.Seek(sheetAddr);
      m3.SeekAdd(8);

      unsigned short spriteCount = m3.ReadNextTwoBytes();
      //std::cout << i << ":" << spriteCount << " at " << std::hex << sheetAddr << std::endl;
      for (int j=0; j<spriteCount; j++) {
        Sprite sprite;

        unsigned short subspriteCount = m3.ReadNextTwoBytes();
        for (int k=0; k<subspriteCount; k++) {
          Subsprite subsprite;
          subsprite.y = static_cast<char>(m3.ReadNextByte());
          subsprite.x = static_cast<char>(m3.ReadNextByte());

          unsigned short ch = m3.ReadNextTwoBytes();
          subsprite.tile = static_cast<unsigned short>(ch & 0x3FF);
          subsprite.flipH = (ch & 0x400) != 0;
          subsprite.flipV = (ch & 0x800) != 0;
          subsprite.objSize = static_cast<unsigned char>((ch >> 12) & 3);
          subsprite.objShape = static_cast<unsigned char>((ch >> 14) & 3);

          /*std::cout << subsprite.y << std::endl;
          std::cout << subsprite.x << std::endl;
          std::cout << subsprite.flipH << "," << subsprite.flipV << std::endl;*/

          sprite.subsprites.push_back(std::move(subsprite));
        }

        m3.SeekAdd(2);

        ss.sprites.push_back(std::move(sprite));
      }

      spritesheets_[i] = std::move(ss);
      //if (i == 15) return;
    }
  }

  const std::map<int, SpriteSheet>& SpriteSheets() const { return spritesheets_; }

private:

  int GetPointerToSheet(BufferView m3, int index) {
    int readAt = baseAddr_ + 4 + (index << 2);
    int a = m3.ReadFourBytes(readAt);
    //std::cout << readAt << " :: " << a << std::hex << std::endl;
    if (a == 0) return -1;
    return a + baseAddr_;
  }

  int GetGfxPointer(BufferView m3, int index) {
    return m3.ReadFourBytes(gfxAddr_ + 4 + (index << 2)) + gfxAddr_;
  }

  int baseAddr_;
  int gfxAddr_;
  std::map<int, SpriteSheet> spritesheets_;
};

int main(int argc, char** argv) {
  if (argc < 3) {
    std::cout << "Usage: ./sprite_dumper [path to rom] {sheet IDs}" << std::endl;
    std::cout << "sheet IDs should be space separated references to the sheets to concatenate" << std::endl;
    std::cout << "the format of the ID is BankNum.SheetNum" << std::endl;
    return -1;
  }

  Magick::InitializeMagick(nullptr);

  Rom m3(argv[1]);
  PaletteSet palettes(m3.buffer());
  Bank b1(m3.buffer(), 0x1A442A4, 0x14383E4);
  Bank b2(m3.buffer(), 0x1AE0638, 0x194BC30);
  Bank b3(m3.buffer(), 0x1AEE4C4, 0x1A012B8);
  Bank b4(m3.buffer(), 0x1AF1ED0, 0x1A36AA0);

  std::vector<FrameOutput> frames;

  for (int i=2; i<argc; i++) {
    std::stringstream argfmt;
    argfmt << argv[i];

    int bankNum;
    char ch;
    int sheetNum;
    argfmt >> bankNum;
    argfmt >> ch; //.
    argfmt >> sheetNum;

    const Bank& bankToRead = [&](){
      switch (bankNum) {
        case 0: return b1;
        case 1: return b2;
        case 2: return b3;
        case 3: return b4;
        default: throw std::invalid_argument("Invalid bank num: " + std::to_string(bankNum));
      }
    }();

    bankToRead.SpriteSheets().at(sheetNum).render(m3.buffer(), palettes, frames);
  }

  int maxWidth = 0;
  int maxHeight = 0;
  for (const FrameOutput& f : frames) {
    if (f.width > maxWidth) maxWidth = f.width;
    if (f.height > maxHeight) maxHeight = f.height;
  }

  const int FRAMES_PER_ROW = 10;
  int sheetWidth;
  int sheetHeight;

  std::ofstream datafile("out.txt");
  datafile << maxWidth << "," << maxHeight << " cell size" << std::endl;
  datafile << FRAMES_PER_ROW << " frames per row" << std::endl;
  datafile << frames.size() << " frames" << std::endl;
  datafile << std::endl;

  if (frames.size() < FRAMES_PER_ROW) {
    sheetWidth = frames.size() * maxWidth;
    sheetHeight = maxHeight;
  } else {
    sheetWidth = FRAMES_PER_ROW * maxWidth;
    sheetHeight = (frames.size() / FRAMES_PER_ROW + 1) * maxHeight;
  }

  Magick::Image sheet(Magick::Geometry(sheetWidth, sheetHeight), "transparent");
  for (int i=0; i<frames.size(); i++) {
    const FrameOutput& f = frames.at(i);
    sheet.composite(f.image, (i%FRAMES_PER_ROW)*maxWidth, (i/FRAMES_PER_ROW)*maxHeight, Magick::OverCompositeOp);
    datafile << f.width << "," << f.height << "," << f.centerX << "," << f.centerY << std::endl;
  }

  //Magick::Image im = b2.SpriteSheets().at(3).render(m3, palettes);
  sheet.magick("png");
  sheet.write("out.png");

  return 0;
}