/* * Copyright 2020 faddenSoft * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System; using System.Collections.Generic; using System.Collections.ObjectModel; using PluginCommon; namespace RuntimeData.Nintendo { /// /// Visualization generators for Nintendo Entertainment System graphics. /// /// The full PPU pattern table grid is pretty straightforward. The way the tiles are /// combined into sprites and background is not. This presents a "tile grid" that /// shows a simple MxN grid of tiles in row-major order, but reality seems to be /// more complex than that and may be game-specific. /// /// To simplify things, the CHR ROM section must be labeled "CHR_ROM", and should have /// a unique address. /// public class VisNES : MarshalByRefObject, IPlugin, IPlugin_SymbolList, IPlugin_Visualizer { // IPlugin public string Identifier { get { return "Nintendo Entertainment System Graphic Visualizer"; } } private IApplication mAppRef; private byte[] mFileData; private AddressTranslate mAddrTrans; private const string CHR_ROM = "CHR_ROM"; private int mChrRomOffset = -1; // Visualization identifiers; DO NOT change or projects that use them will break. private const string VIS_CHR_ROM = "nes-chr-rom"; private const string VIS_TILE_GRID = "nes-tile-grid"; private const string P_OFFSET = "offset"; private const string P_WIDTH = "width"; private const string P_HEIGHT = "height"; private const string P_COLOR_PALETTE = "colorPalette"; private const string P_SHOW_LABELS = "showLabels"; private const string P_FLIP_RIGHT = "flipRight"; private const string P_RIGHT_TABLE = "useRightTable"; private const int TileWidth = 8; private const int TileHeight = 8; private const int BytesPerTile = 16; // Visualization descriptors. private VisDescr[] mDescriptors = new VisDescr[] { new VisDescr(VIS_CHR_ROM, "NES CHR ROM Pattern Tables", VisDescr.VisType.Bitmap, new VisParamDescr[] { //new VisParamDescr("File offset (hex)", // P_OFFSET, typeof(int), 0, 0x00ffffff, VisParamDescr.SpecialMode.Offset, 0), // TODO: either make this an enum, or just provide 4 slots that take a color // (add a SpecialMode.Color and accept six-digit #123456 inputs; // no need to restrict to NES limitations) new VisParamDescr("Color palette", P_COLOR_PALETTE, typeof(int), 0, 2, 0, 0), new VisParamDescr("Show labels", P_SHOW_LABELS, typeof(bool), 0, 0, 0, true), }), new VisDescr(VIS_TILE_GRID, "NES Tile Grid", VisDescr.VisType.Bitmap, new VisParamDescr[] { new VisParamDescr("File offset (hex)", P_OFFSET, typeof(int), 0, 0x00ffffff, VisParamDescr.SpecialMode.Offset, 0), new VisParamDescr("Color palette", P_COLOR_PALETTE, typeof(int), 0, 2, 0, 0), new VisParamDescr("Width (in tiles)", P_WIDTH, typeof(int), 1, 256, 0, 1), new VisParamDescr("Height (in tiles)", P_HEIGHT, typeof(int), 1, 256, 0, 1), // Flips the pixels of the tiles on the right side. This handles a common // case, but in practice a sprite can be an arbitrary mix of flipped and // normal tiles. new VisParamDescr("Horiz-flip right side", P_FLIP_RIGHT, typeof(bool), 0, 0, 0, false), new VisParamDescr("Use right table", P_RIGHT_TABLE, typeof(bool), 0, 0, 0, false), }), }; // IPlugin public void Prepare(IApplication appRef, byte[] fileData, AddressTranslate addrTrans) { mAppRef = appRef; mFileData = fileData; mAddrTrans = addrTrans; } // IPlugin public void Unprepare() { mAppRef = null; mFileData = null; mAddrTrans = null; } // IPlugin_SymbolList public void UpdateSymbolList(List plSyms) { // reset this every time, in case they remove the symbol mChrRomOffset = -1; foreach (PlSymbol sym in plSyms) { if (sym.Label == CHR_ROM) { mChrRomOffset = sym.Offset; break; } } mAppRef.DebugLog(CHR_ROM + " @ +" + mChrRomOffset.ToString("x6")); } // IPlugin_SymbolList public bool IsLabelSignificant(string beforeLabel, string afterLabel) { return beforeLabel == CHR_ROM || afterLabel == CHR_ROM; } // IPlugin_Visualizer public VisDescr[] GetVisGenDescrs() { // We're using a static set, but it could be generated based on file contents. // Confirm that we're prepared. if (mFileData == null) { return null; } return mDescriptors; } // IPlugin_Visualizer public IVisualization2d Generate2d(VisDescr descr, ReadOnlyDictionary parms) { switch (descr.Ident) { case VIS_CHR_ROM: return GenerateRomChart(parms); case VIS_TILE_GRID: return GenerateTileGrid(parms); default: mAppRef.ReportError("Unknown ident " + descr.Ident); return null; } } private IVisualization2d GenerateRomChart(ReadOnlyDictionary parms) { int paletteNum = Util.GetFromObjDict(parms, P_COLOR_PALETTE, 0); bool showLabels = Util.GetFromObjDict(parms, P_SHOW_LABELS, true); if (mChrRomOffset < 0) { mAppRef.ReportError("CHR_ROM symbol not found"); return null; } if (mChrRomOffset + 8192 > mFileData.Length) { mAppRef.ReportError("8KB CHR ROM runs off end of file"); return null; } const int spacing = 1; const int tWidth = TileWidth + spacing; const int tHeight = TileHeight + spacing; const int gap = 4 + spacing * 2; int labelSpacing = showLabels ? 9 : 0; VisBitmap8 vb = new VisBitmap8(tWidth * 16 * 2 + gap + labelSpacing * 2 + 1, tHeight * 16 + labelSpacing + 1); SetPalette(vb, (Palette)paletteNum); if (showLabels) { for (int i = 0; i < 16; i++) { char ch = (i < 10) ? (char)('0' + i) : (char)('A' + i - 10); VisBitmap8.DrawChar(vb, ch, (i + 1) * tWidth + 1, 1, (byte)Color.Black, (byte)Color.White); VisBitmap8.DrawChar(vb, ch, (i + 16 + 1) * tWidth + gap + 1, 1, (byte)Color.Black, (byte)Color.White); VisBitmap8.DrawChar(vb, ch, 1, (i + 1) * tHeight + 1, (byte)Color.Black, (byte)Color.White); VisBitmap8.DrawChar(vb, ch, (1 + 16 + 16) * tWidth + gap + 1, (i + 1) * tHeight + 1, (byte)Color.Black, (byte)Color.White); } } for (int idx = 0; idx < 512; idx++) { int xshift = idx < 256 ? 0 : tWidth * 16 + gap; int xc = (idx & 0x0f) * tWidth + xshift + labelSpacing + 1; int yc = ((idx & 0xff) >> 4) * tHeight + labelSpacing + 1; RenderTile(idx, vb, xc, yc, false); } return vb; } private IVisualization2d GenerateTileGrid(ReadOnlyDictionary parms) { int offset = Util.GetFromObjDict(parms, P_OFFSET, 0); int paletteNum = Util.GetFromObjDict(parms, P_COLOR_PALETTE, 0); int width = Util.GetFromObjDict(parms, P_WIDTH, 1); int height = Util.GetFromObjDict(parms, P_HEIGHT, 1); bool flipRight = Util.GetFromObjDict(parms, P_FLIP_RIGHT, false); bool useRightTable = Util.GetFromObjDict(parms, P_RIGHT_TABLE, false); if (mChrRomOffset < 0) { mAppRef.ReportError("CHR_ROM symbol not found"); return null; } if (offset < 0 || offset >= mFileData.Length) { mAppRef.ReportError("Invalid parameter"); return null; } if (offset + width * height > mFileData.Length) { mAppRef.ReportError("Data runs off end of file"); return null; } VisBitmap8 vb = new VisBitmap8(TileWidth * width, TileHeight * height); SetPalette(vb, (Palette)paletteNum); for (int row = 0; row < height; row++) { for (int col = 0; col < width; col++) { int tileNum = mFileData[offset + row * width + col] + (useRightTable ? 256 : 0); RenderTile(tileNum, vb, TileWidth * col, TileHeight * row, flipRight && col >= (width + 1) / 2); } } return vb; } /// /// Renders a tile from the PPU pattern table. /// /// Tile number (0-511). /// Bitmap to render to. /// X coordinate for upper-left coordinate. /// Y coordinate for upper-left coordinate. /// Flip pixels horizontally private void RenderTile(int tileNum, VisBitmap8 vb, int xc, int yc, bool flipHoriz) { int tileOff = mChrRomOffset + tileNum * BytesPerTile; for (int row = 0; row < 8; row++) { byte part0 = mFileData[tileOff]; byte part1 = mFileData[tileOff + 8]; for (int bit = 7; bit >= 0; bit--) { int val = ((part0 >> bit) & 0x01) | (((part1 >> bit) & 0x01) << 1); vb.SetPixelIndex(xc + (flipHoriz ? bit : 7 - bit), yc, (byte)((byte)Color.Color0 + val)); } tileOff++; yc++; } } private enum Color : byte { Transparent = 0, Black = 1, White = 2, Color0 = 3, Color1 = 4, Color2 = 5, Color3 = 6 } private enum Palette : int { Greyscale = 0, Pinkish = 1, Greenish = 2, } private void SetPalette(VisBitmap8 vb, Palette pal) { vb.SetColor((byte)Color.Transparent, 0x00, 0x00, 0x00, 0x00); vb.SetColor((byte)Color.Black, 0xff, 0x00, 0x00, 0x00); vb.SetColor((byte)Color.White, 0xff, 0xff, 0xff, 0xff); switch (pal) { case Palette.Greyscale: default: vb.SetColor((byte)Color.Color0, 0xff, 0x00, 0x00, 0x00); // black vb.SetColor((byte)Color.Color1, 0xff, 0x80, 0x80, 0x80); // dark grey vb.SetColor((byte)Color.Color2, 0xff, 0xb0, 0xb0, 0xb0); // medium grey vb.SetColor((byte)Color.Color3, 0xff, 0xe0, 0xe0, 0xe0); // light grey break; case Palette.Pinkish: vb.SetColor((byte)Color.Color0, 0xff, 0x49, 0x99, 0xfe); // sky blue vb.SetColor((byte)Color.Color1, 0xff, 0xff, 0xbd, 0xaf); // pinkish vb.SetColor((byte)Color.Color2, 0xff, 0xcd, 0x50, 0x00); // dark orange vb.SetColor((byte)Color.Color3, 0xff, 0x00, 0x00, 0x00); // black break; case Palette.Greenish: vb.SetColor((byte)Color.Color0, 0xff, 0x49, 0x99, 0xfe); // sky blue vb.SetColor((byte)Color.Color1, 0xff, 0x00, 0xa4, 0x00); // medium green vb.SetColor((byte)Color.Color2, 0xff, 0xfc, 0xfc, 0xfc); // near white vb.SetColor((byte)Color.Color3, 0xff, 0xff, 0x99, 0x2b); // orange break; } } } }