2019-11-25 22:27:38 +00:00
|
|
|
|
/*
|
2019-11-27 02:54:42 +00:00
|
|
|
|
* Copyright 2019 faddenSoft
|
2019-11-25 22:27:38 +00:00
|
|
|
|
*
|
|
|
|
|
* 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;
|
2019-12-04 23:50:19 +00:00
|
|
|
|
using System.Collections.ObjectModel;
|
2019-11-25 22:27:38 +00:00
|
|
|
|
|
|
|
|
|
using PluginCommon;
|
|
|
|
|
|
|
|
|
|
namespace RuntimeData.Apple {
|
2020-03-11 22:20:03 +00:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Visualizer for Apple II hi-res bitmaps.
|
|
|
|
|
/// </summary>
|
2019-11-27 02:54:42 +00:00
|
|
|
|
public class VisHiRes : MarshalByRefObject, IPlugin, IPlugin_Visualizer {
|
2019-12-01 01:52:33 +00:00
|
|
|
|
// IPlugin
|
2019-11-25 22:27:38 +00:00
|
|
|
|
public string Identifier {
|
|
|
|
|
get { return "Apple II Hi-Res Graphic Visualizer"; }
|
|
|
|
|
}
|
|
|
|
|
private IApplication mAppRef;
|
|
|
|
|
private byte[] mFileData;
|
|
|
|
|
private AddressTranslate mAddrTrans;
|
|
|
|
|
|
2019-12-03 00:38:32 +00:00
|
|
|
|
// Visualization identifiers; DO NOT change or projects that use them will break.
|
2019-11-27 02:54:42 +00:00
|
|
|
|
private const string VIS_GEN_BITMAP = "apple2-hi-res-bitmap";
|
2020-02-19 00:42:46 +00:00
|
|
|
|
private const string VIS_GEN_BITMAP_GRID = "apple2-hi-res-bitmap-grid";
|
2019-12-03 00:38:32 +00:00
|
|
|
|
private const string VIS_GEN_BITMAP_FONT = "apple2-hi-res-bitmap-font";
|
2019-12-05 02:05:17 +00:00
|
|
|
|
private const string VIS_GEN_HR_SCREEN = "apple2-hi-res-screen";
|
2019-11-27 02:54:42 +00:00
|
|
|
|
|
2019-12-01 01:52:33 +00:00
|
|
|
|
private const string P_OFFSET = "offset";
|
|
|
|
|
private const string P_BYTE_WIDTH = "byteWidth";
|
|
|
|
|
private const string P_HEIGHT = "height";
|
|
|
|
|
private const string P_COL_STRIDE = "colStride";
|
|
|
|
|
private const string P_ROW_STRIDE = "rowStride";
|
2020-02-19 00:42:46 +00:00
|
|
|
|
private const string P_CELL_STRIDE = "cellStride";
|
2019-12-01 01:52:33 +00:00
|
|
|
|
private const string P_IS_COLOR = "isColor";
|
|
|
|
|
private const string P_IS_FIRST_ODD = "isFirstOdd";
|
2020-02-19 00:42:46 +00:00
|
|
|
|
private const string P_IS_HIGH_BIT_FLIPPED = "isHighBitFlipped";
|
2019-12-05 21:48:18 +00:00
|
|
|
|
private const string P_COLOR_CONV_MODE = "colorConvMode";
|
2019-12-01 01:52:33 +00:00
|
|
|
|
private const string P_ITEM_BYTE_WIDTH = "itemByteWidth";
|
|
|
|
|
private const string P_ITEM_HEIGHT = "itemHeight";
|
|
|
|
|
private const string P_COUNT = "count";
|
|
|
|
|
|
|
|
|
|
private const int MAX_DIM = 4096;
|
|
|
|
|
|
2019-11-27 02:54:42 +00:00
|
|
|
|
// Visualization descriptors.
|
|
|
|
|
private VisDescr[] mDescriptors = new VisDescr[] {
|
|
|
|
|
new VisDescr(VIS_GEN_BITMAP, "Apple II Hi-Res Bitmap", VisDescr.VisType.Bitmap,
|
|
|
|
|
new VisParamDescr[] {
|
|
|
|
|
new VisParamDescr("File offset (hex)",
|
2019-12-03 00:38:32 +00:00
|
|
|
|
P_OFFSET, typeof(int), 0, 0x00ffffff, VisParamDescr.SpecialMode.Offset, 0),
|
2019-11-27 02:54:42 +00:00
|
|
|
|
new VisParamDescr("Width (in bytes)",
|
2020-02-19 00:42:46 +00:00
|
|
|
|
P_BYTE_WIDTH, typeof(int), 1, 256, 0, 1),
|
2019-11-27 02:54:42 +00:00
|
|
|
|
new VisParamDescr("Height",
|
2020-02-19 00:42:46 +00:00
|
|
|
|
P_HEIGHT, typeof(int), 1, 1024, 0, 1),
|
2019-11-27 02:54:42 +00:00
|
|
|
|
new VisParamDescr("Column stride (bytes)",
|
2021-05-18 00:28:13 +00:00
|
|
|
|
P_COL_STRIDE, typeof(int), 0, 4096, 0, 0),
|
2019-11-27 02:54:42 +00:00
|
|
|
|
new VisParamDescr("Row stride (bytes)",
|
2021-05-18 00:28:13 +00:00
|
|
|
|
P_ROW_STRIDE, typeof(int), 0, 4096, 0, 0),
|
2019-11-27 02:54:42 +00:00
|
|
|
|
new VisParamDescr("Color",
|
2019-12-01 01:52:33 +00:00
|
|
|
|
P_IS_COLOR, typeof(bool), 0, 0, 0, true),
|
|
|
|
|
new VisParamDescr("First col odd",
|
|
|
|
|
P_IS_FIRST_ODD, typeof(bool), 0, 0, 0, false),
|
2020-02-19 00:42:46 +00:00
|
|
|
|
new VisParamDescr("High bit flipped",
|
|
|
|
|
P_IS_HIGH_BIT_FLIPPED, typeof(bool), 0, 0, 0, false),
|
2019-12-05 21:48:18 +00:00
|
|
|
|
//new VisParamDescr("Color conv mode",
|
|
|
|
|
// P_COLOR_CONV_MODE, typeof(int), (int)ColorMode.SimpleColor,
|
|
|
|
|
// (int)ColorMode.IIgsRGB, 0, (int)ColorMode.SimpleColor),
|
2019-12-04 23:50:19 +00:00
|
|
|
|
//new VisParamDescr("Test Float",
|
|
|
|
|
// "floaty", typeof(float), -5.0f, 5.0f, 0, 0.1f),
|
2019-11-27 02:54:42 +00:00
|
|
|
|
}),
|
2020-02-19 00:42:46 +00:00
|
|
|
|
new VisDescr(VIS_GEN_BITMAP_GRID, "Apple II Hi-Res Sprite Sheet", VisDescr.VisType.Bitmap,
|
|
|
|
|
new VisParamDescr[] {
|
|
|
|
|
new VisParamDescr("File offset (hex)",
|
|
|
|
|
P_OFFSET, typeof(int), 0, 0x00ffffff, VisParamDescr.SpecialMode.Offset, 0),
|
|
|
|
|
new VisParamDescr("Cell width (in bytes)",
|
|
|
|
|
P_ITEM_BYTE_WIDTH, typeof(int), 1, 40, 0, 1),
|
|
|
|
|
new VisParamDescr("Cell height",
|
|
|
|
|
P_ITEM_HEIGHT, typeof(int), 1, 192, 0, 1),
|
|
|
|
|
new VisParamDescr("Column stride (bytes)",
|
2021-05-18 00:28:13 +00:00
|
|
|
|
P_COL_STRIDE, typeof(int), 0, 4096, 0, 0),
|
2020-02-19 00:42:46 +00:00
|
|
|
|
new VisParamDescr("Row stride (bytes)",
|
2021-05-18 00:28:13 +00:00
|
|
|
|
P_ROW_STRIDE, typeof(int), 0, 4096, 0, 0),
|
2020-02-19 00:42:46 +00:00
|
|
|
|
new VisParamDescr("Cell stride (bytes)",
|
|
|
|
|
P_CELL_STRIDE, typeof(int), 0, 4096, 0, 0),
|
|
|
|
|
new VisParamDescr("Number of items",
|
2021-05-18 00:28:13 +00:00
|
|
|
|
P_COUNT, typeof(int), 1, 4096, 0, 64),
|
2020-02-19 00:42:46 +00:00
|
|
|
|
new VisParamDescr("Color",
|
|
|
|
|
P_IS_COLOR, typeof(bool), 0, 0, 0, true),
|
|
|
|
|
new VisParamDescr("First col odd",
|
|
|
|
|
P_IS_FIRST_ODD, typeof(bool), 0, 0, 0, false),
|
|
|
|
|
new VisParamDescr("High bit flipped",
|
|
|
|
|
P_IS_HIGH_BIT_FLIPPED, typeof(bool), 0, 0, 0, false),
|
|
|
|
|
}),
|
2019-12-03 00:38:32 +00:00
|
|
|
|
new VisDescr(VIS_GEN_BITMAP_FONT, "Apple II Hi-Res Bitmap Font", VisDescr.VisType.Bitmap,
|
2019-11-27 02:54:42 +00:00
|
|
|
|
new VisParamDescr[] {
|
|
|
|
|
new VisParamDescr("File offset (hex)",
|
2019-12-03 00:38:32 +00:00
|
|
|
|
P_OFFSET, typeof(int), 0, 0x00ffffff, VisParamDescr.SpecialMode.Offset, 0),
|
2019-11-27 02:54:42 +00:00
|
|
|
|
new VisParamDescr("Item width (in bytes)",
|
2019-12-01 01:52:33 +00:00
|
|
|
|
P_ITEM_BYTE_WIDTH, typeof(int), 1, 40, 0, 1),
|
2019-11-27 02:54:42 +00:00
|
|
|
|
new VisParamDescr("Item height",
|
2019-12-01 01:52:33 +00:00
|
|
|
|
P_ITEM_HEIGHT, typeof(int), 1, 192, 0, 8),
|
2019-11-27 02:54:42 +00:00
|
|
|
|
new VisParamDescr("Number of items",
|
2019-12-05 02:05:17 +00:00
|
|
|
|
P_COUNT, typeof(int), 1, 256, 0, 96),
|
|
|
|
|
}),
|
|
|
|
|
new VisDescr(VIS_GEN_HR_SCREEN, "Apple II Hi-Res Screen Image", VisDescr.VisType.Bitmap,
|
|
|
|
|
new VisParamDescr[] {
|
|
|
|
|
new VisParamDescr("File offset (hex)",
|
|
|
|
|
P_OFFSET, typeof(int), 0, 0x00ffffff, VisParamDescr.SpecialMode.Offset, 0),
|
|
|
|
|
new VisParamDescr("Color",
|
|
|
|
|
P_IS_COLOR, typeof(bool), 0, 0, 0, true),
|
2019-11-27 02:54:42 +00:00
|
|
|
|
}),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
2019-12-01 01:52:33 +00:00
|
|
|
|
// IPlugin
|
2019-11-25 22:27:38 +00:00
|
|
|
|
public void Prepare(IApplication appRef, byte[] fileData, AddressTranslate addrTrans) {
|
|
|
|
|
mAppRef = appRef;
|
|
|
|
|
mFileData = fileData;
|
|
|
|
|
mAddrTrans = addrTrans;
|
|
|
|
|
}
|
|
|
|
|
|
2019-12-01 01:52:33 +00:00
|
|
|
|
// IPlugin
|
|
|
|
|
public void Unprepare() {
|
|
|
|
|
mAppRef = null;
|
|
|
|
|
mFileData = null;
|
|
|
|
|
mAddrTrans = null;
|
|
|
|
|
}
|
|
|
|
|
|
2019-11-27 02:54:42 +00:00
|
|
|
|
// IPlugin_Visualizer
|
|
|
|
|
public VisDescr[] GetVisGenDescrs() {
|
2019-12-05 18:21:07 +00:00
|
|
|
|
// 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;
|
|
|
|
|
}
|
2019-11-27 02:54:42 +00:00
|
|
|
|
return mDescriptors;
|
2019-11-25 22:27:38 +00:00
|
|
|
|
}
|
|
|
|
|
|
2019-11-27 02:54:42 +00:00
|
|
|
|
// IPlugin_Visualizer
|
|
|
|
|
public IVisualization2d Generate2d(VisDescr descr,
|
2019-12-04 23:50:19 +00:00
|
|
|
|
ReadOnlyDictionary<string, object> parms) {
|
2019-12-01 01:52:33 +00:00
|
|
|
|
switch (descr.Ident) {
|
|
|
|
|
case VIS_GEN_BITMAP:
|
|
|
|
|
return GenerateBitmap(parms);
|
2020-02-19 00:42:46 +00:00
|
|
|
|
case VIS_GEN_BITMAP_GRID:
|
|
|
|
|
return GenerateBitmapGrid(parms);
|
2019-12-03 00:38:32 +00:00
|
|
|
|
case VIS_GEN_BITMAP_FONT:
|
2019-12-05 02:05:17 +00:00
|
|
|
|
return GenerateBitmapFont(parms);
|
|
|
|
|
case VIS_GEN_HR_SCREEN:
|
|
|
|
|
return GenerateScreen(parms);
|
2019-12-01 01:52:33 +00:00
|
|
|
|
default:
|
2019-12-05 18:21:07 +00:00
|
|
|
|
mAppRef.ReportError("Unknown ident " + descr.Ident);
|
2019-12-01 01:52:33 +00:00
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-12-04 23:50:19 +00:00
|
|
|
|
private IVisualization2d GenerateBitmap(ReadOnlyDictionary<string, object> parms) {
|
2019-12-05 02:05:17 +00:00
|
|
|
|
int offset = Util.GetFromObjDict(parms, P_OFFSET, 0);
|
|
|
|
|
int byteWidth = Util.GetFromObjDict(parms, P_BYTE_WIDTH, 1); // width ignoring colStride
|
|
|
|
|
int height = Util.GetFromObjDict(parms, P_HEIGHT, 1);
|
|
|
|
|
int colStride = Util.GetFromObjDict(parms, P_COL_STRIDE, 0);
|
|
|
|
|
int rowStride = Util.GetFromObjDict(parms, P_ROW_STRIDE, 0);
|
|
|
|
|
bool isColor = Util.GetFromObjDict(parms, P_IS_COLOR, true);
|
|
|
|
|
bool isFirstOdd = Util.GetFromObjDict(parms, P_IS_FIRST_ODD, false);
|
2020-02-19 00:42:46 +00:00
|
|
|
|
bool isHighBitFlipped = Util.GetFromObjDict(parms, P_IS_HIGH_BIT_FLIPPED, false);
|
2019-12-05 21:48:18 +00:00
|
|
|
|
int colorConvMode = !isColor ? (int)ColorMode.Mono :
|
|
|
|
|
Util.GetFromObjDict(parms, P_COLOR_CONV_MODE, (int)ColorMode.SimpleColor);
|
2019-12-01 01:52:33 +00:00
|
|
|
|
|
|
|
|
|
// We allow the stride entries to be zero to indicate a "dense" bitmap.
|
|
|
|
|
if (colStride == 0) {
|
|
|
|
|
colStride = 1;
|
|
|
|
|
}
|
|
|
|
|
if (rowStride == 0) {
|
|
|
|
|
rowStride = byteWidth * colStride;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (offset < 0 || offset >= mFileData.Length ||
|
|
|
|
|
byteWidth <= 0 || byteWidth > MAX_DIM ||
|
2019-12-03 00:38:32 +00:00
|
|
|
|
height <= 0 || height > MAX_DIM) {
|
|
|
|
|
// the UI should flag these based on range (and ideally wouldn't have called us)
|
2019-12-05 18:21:07 +00:00
|
|
|
|
mAppRef.ReportError("Invalid parameter");
|
2019-12-01 01:52:33 +00:00
|
|
|
|
return null;
|
|
|
|
|
}
|
2019-12-03 00:38:32 +00:00
|
|
|
|
if (colStride <= 0 || colStride > MAX_DIM) {
|
2019-12-05 18:21:07 +00:00
|
|
|
|
mAppRef.ReportError("Invalid column stride");
|
2019-12-03 00:38:32 +00:00
|
|
|
|
return null;
|
|
|
|
|
}
|
2020-08-18 22:10:10 +00:00
|
|
|
|
// This used to be limited to rowStride >= byteWidth * colStride - (colStride - 1),
|
|
|
|
|
// but that doesn't allow for a 2x8 bitmap in column-major order. In that case
|
|
|
|
|
// you want a row stride of 1 (because each row starts 1 byte farther on) and a
|
|
|
|
|
// column stride of 8 (because each column is separated by 8 bytes).
|
|
|
|
|
if (rowStride < 1 || rowStride > MAX_DIM) {
|
2019-12-05 18:21:07 +00:00
|
|
|
|
mAppRef.ReportError("Invalid row stride");
|
2019-12-03 00:38:32 +00:00
|
|
|
|
return null;
|
|
|
|
|
}
|
2019-12-01 01:52:33 +00:00
|
|
|
|
|
2021-05-18 00:28:13 +00:00
|
|
|
|
int lastOffset = offset + rowStride * (height - 1) +
|
|
|
|
|
colStride * (byteWidth - 1);
|
2019-12-01 01:52:33 +00:00
|
|
|
|
if (lastOffset >= mFileData.Length) {
|
2019-12-05 18:21:07 +00:00
|
|
|
|
mAppRef.ReportError("Bitmap runs off end of file (last offset +" +
|
2019-12-03 00:38:32 +00:00
|
|
|
|
lastOffset.ToString("x6") + ")");
|
2019-12-01 01:52:33 +00:00
|
|
|
|
return null;
|
2021-05-18 00:28:13 +00:00
|
|
|
|
//} else {
|
|
|
|
|
// mAppRef.DebugLog("last offset=+" + lastOffset.ToString("x6"));
|
2019-12-01 01:52:33 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
VisBitmap8 vb = new VisBitmap8(byteWidth * 7, height);
|
|
|
|
|
SetHiResPalette(vb);
|
|
|
|
|
|
2019-12-05 02:05:17 +00:00
|
|
|
|
RenderBitmap(mFileData, offset, byteWidth, height, colStride, rowStride,
|
2020-02-19 00:42:46 +00:00
|
|
|
|
(ColorMode)colorConvMode, isFirstOdd, isHighBitFlipped, vb, 0, 0);
|
2019-12-04 23:50:19 +00:00
|
|
|
|
return vb;
|
|
|
|
|
}
|
|
|
|
|
|
2020-02-19 00:42:46 +00:00
|
|
|
|
private IVisualization2d GenerateBitmapGrid(ReadOnlyDictionary<string, object> parms) {
|
|
|
|
|
int offset = Util.GetFromObjDict(parms, P_OFFSET, 0);
|
|
|
|
|
int itemByteWidth = Util.GetFromObjDict(parms, P_ITEM_BYTE_WIDTH, 1);
|
|
|
|
|
int itemHeight = Util.GetFromObjDict(parms, P_ITEM_HEIGHT, 8);
|
|
|
|
|
int colStride = Util.GetFromObjDict(parms, P_COL_STRIDE, 0);
|
|
|
|
|
int rowStride = Util.GetFromObjDict(parms, P_ROW_STRIDE, 0);
|
|
|
|
|
int cellStride = Util.GetFromObjDict(parms, P_CELL_STRIDE, 0);
|
|
|
|
|
int count = Util.GetFromObjDict(parms, P_COUNT, 96);
|
|
|
|
|
bool isColor = Util.GetFromObjDict(parms, P_IS_COLOR, true);
|
|
|
|
|
bool isFirstOdd = Util.GetFromObjDict(parms, P_IS_FIRST_ODD, false);
|
|
|
|
|
bool isHighBitFlipped = Util.GetFromObjDict(parms, P_IS_HIGH_BIT_FLIPPED, false);
|
|
|
|
|
|
|
|
|
|
ColorMode colorConvMode = !isColor ? ColorMode.Mono :
|
|
|
|
|
(ColorMode)Util.GetFromObjDict(parms, P_COLOR_CONV_MODE, (int)ColorMode.SimpleColor);
|
|
|
|
|
|
|
|
|
|
return GenerateGrid(offset, itemByteWidth, itemHeight, colStride, rowStride,
|
|
|
|
|
cellStride, count, colorConvMode, isFirstOdd, isHighBitFlipped);
|
|
|
|
|
}
|
|
|
|
|
|
2019-12-05 02:05:17 +00:00
|
|
|
|
private IVisualization2d GenerateBitmapFont(ReadOnlyDictionary<string, object> parms) {
|
|
|
|
|
int offset = Util.GetFromObjDict(parms, P_OFFSET, 0);
|
|
|
|
|
int itemByteWidth = Util.GetFromObjDict(parms, P_ITEM_BYTE_WIDTH, 1);
|
|
|
|
|
int itemHeight = Util.GetFromObjDict(parms, P_ITEM_HEIGHT, 8);
|
|
|
|
|
int count = Util.GetFromObjDict(parms, P_COUNT, 96);
|
|
|
|
|
|
2020-02-19 00:42:46 +00:00
|
|
|
|
return GenerateGrid(offset, itemByteWidth, itemHeight, 0, 0, 0, count,
|
|
|
|
|
ColorMode.Mono, false, false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private IVisualization2d GenerateGrid(int offset, int itemByteWidth, int itemHeight,
|
|
|
|
|
int colStride, int rowStride, int cellStride, int count,
|
|
|
|
|
ColorMode colorConvMode, bool isFirstOdd, bool isHighBitFlipped) {
|
|
|
|
|
|
|
|
|
|
// We allow the stride entries to be zero to indicate a "dense" bitmap.
|
|
|
|
|
if (colStride == 0) {
|
|
|
|
|
colStride = 1;
|
|
|
|
|
}
|
|
|
|
|
if (rowStride == 0) {
|
|
|
|
|
rowStride = itemByteWidth * colStride;
|
|
|
|
|
}
|
|
|
|
|
if (cellStride == 0) {
|
|
|
|
|
cellStride = rowStride * itemHeight;
|
|
|
|
|
}
|
|
|
|
|
|
2019-12-05 02:05:17 +00:00
|
|
|
|
if (offset < 0 || offset >= mFileData.Length ||
|
|
|
|
|
itemByteWidth <= 0 || itemByteWidth > MAX_DIM ||
|
2020-02-19 00:42:46 +00:00
|
|
|
|
itemHeight <= 0 || itemHeight > MAX_DIM ||
|
|
|
|
|
count <= 0 || count > MAX_DIM) {
|
2019-12-05 02:05:17 +00:00
|
|
|
|
// should be caught by editor
|
2019-12-05 18:21:07 +00:00
|
|
|
|
mAppRef.ReportError("Invalid parameter");
|
2019-12-05 02:05:17 +00:00
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2020-02-19 00:42:46 +00:00
|
|
|
|
if (colStride <= 0 || colStride > MAX_DIM) {
|
|
|
|
|
mAppRef.ReportError("Invalid column stride");
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
if (rowStride < itemByteWidth * colStride - (colStride - 1) || rowStride > MAX_DIM) {
|
|
|
|
|
mAppRef.ReportError("Invalid row stride");
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int lastOffset = offset + (cellStride * (count - 1)) +
|
2021-05-18 00:28:13 +00:00
|
|
|
|
rowStride * (itemHeight - 1) +
|
|
|
|
|
colStride * (itemByteWidth - 1);
|
2019-12-05 02:05:17 +00:00
|
|
|
|
if (lastOffset >= mFileData.Length) {
|
2019-12-05 18:21:07 +00:00
|
|
|
|
mAppRef.ReportError("Bitmap runs off end of file (last offset +" +
|
2019-12-05 02:05:17 +00:00
|
|
|
|
lastOffset.ToString("x6") + ")");
|
|
|
|
|
return null;
|
2021-05-18 00:28:13 +00:00
|
|
|
|
//} else {
|
|
|
|
|
// mAppRef.DebugLog("lastOffset=+" + lastOffset.ToString("x6") +
|
|
|
|
|
// ", len=" + mFileData.Length.ToString("x6"));
|
2019-12-05 02:05:17 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-08-18 22:10:10 +00:00
|
|
|
|
// Set the number of horizontal cells. For small counts we try to make it square,
|
|
|
|
|
// for larger counts we use a reasonable power of 2.
|
2019-12-05 02:05:17 +00:00
|
|
|
|
int hcells;
|
|
|
|
|
if (count > 128) {
|
|
|
|
|
hcells = 32;
|
2020-08-22 03:01:41 +00:00
|
|
|
|
} else if (count > 64) {
|
2019-12-05 02:05:17 +00:00
|
|
|
|
hcells = 16;
|
2020-08-18 22:10:10 +00:00
|
|
|
|
} else if (count >= 32) {
|
|
|
|
|
hcells = 8;
|
|
|
|
|
} else {
|
|
|
|
|
hcells = (int)Math.Sqrt(count + 1);
|
2019-12-05 02:05:17 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int vcells = (count + hcells - 1) / hcells;
|
|
|
|
|
|
|
|
|
|
// Create a bitmap with room for each cell, plus a 1-pixel transparent boundary
|
|
|
|
|
// between them and around the edges.
|
|
|
|
|
VisBitmap8 vb = new VisBitmap8(1 + hcells * itemByteWidth * 7 + hcells,
|
|
|
|
|
1 + vcells * itemHeight + vcells);
|
|
|
|
|
SetHiResPalette(vb);
|
|
|
|
|
|
|
|
|
|
int cellx = 1;
|
|
|
|
|
int celly = 1;
|
|
|
|
|
for (int idx = 0; idx < count; idx++) {
|
2020-02-19 00:42:46 +00:00
|
|
|
|
RenderBitmap(mFileData, offset + idx * cellStride,
|
|
|
|
|
itemByteWidth, itemHeight, colStride, rowStride,
|
|
|
|
|
colorConvMode, isFirstOdd, isHighBitFlipped,
|
2019-12-05 02:05:17 +00:00
|
|
|
|
vb, cellx, celly);
|
|
|
|
|
|
|
|
|
|
cellx += itemByteWidth * 7 + 1;
|
|
|
|
|
if (cellx == vb.Width) {
|
|
|
|
|
cellx = 1;
|
|
|
|
|
celly += itemHeight + 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return vb;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private IVisualization2d GenerateScreen(ReadOnlyDictionary<string, object> parms) {
|
2019-12-06 22:49:35 +00:00
|
|
|
|
//const int RAW_IMAGE_SIZE = 0x1ff8;
|
2019-12-05 02:05:17 +00:00
|
|
|
|
const int HR_WIDTH = 280;
|
|
|
|
|
const int HR_BYTE_WIDTH = HR_WIDTH / 7;
|
|
|
|
|
const int HR_HEIGHT = 192;
|
|
|
|
|
|
|
|
|
|
int offset = Util.GetFromObjDict(parms, P_OFFSET, 0);
|
|
|
|
|
bool isColor = Util.GetFromObjDict(parms, P_IS_COLOR, true);
|
|
|
|
|
|
|
|
|
|
if (offset < 0 || offset >= mFileData.Length) {
|
|
|
|
|
// should be caught by editor
|
2019-12-05 18:21:07 +00:00
|
|
|
|
mAppRef.ReportError("Invalid parameter");
|
2019-12-05 02:05:17 +00:00
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2019-12-06 22:49:35 +00:00
|
|
|
|
//int lastOffset = offset + RAW_IMAGE_SIZE - 1;
|
|
|
|
|
//if (lastOffset >= mFileData.Length) {
|
|
|
|
|
// mAppRef.ReportError("Bitmap runs off end of file (last offset +" +
|
|
|
|
|
// lastOffset.ToString("x6") + ")");
|
|
|
|
|
// return null;
|
|
|
|
|
//}
|
|
|
|
|
|
|
|
|
|
// Linearize the data. To handle programs that move themselves around before
|
|
|
|
|
// executing we use the address translator (e.g. the title screen in Space Eggs
|
|
|
|
|
// is contiguous in memory but split in half in the file). This is slower, but
|
|
|
|
|
// mAddrTrans is a local (not proxy) object, so it's not too bad.
|
2019-12-05 02:05:17 +00:00
|
|
|
|
byte[] buf = new byte[HR_BYTE_WIDTH * HR_HEIGHT];
|
|
|
|
|
int outIdx = 0;
|
2019-12-06 22:49:35 +00:00
|
|
|
|
int baseAddr = mAddrTrans.OffsetToAddress(offset);
|
2019-12-05 02:05:17 +00:00
|
|
|
|
for (int row = 0; row < HR_HEIGHT; row++) {
|
2019-12-06 22:49:35 +00:00
|
|
|
|
// If row is ABCDEFGH, we want pppFGHCD EABAB000 (where p would be $20/$40).
|
2019-12-05 02:05:17 +00:00
|
|
|
|
int low = ((row & 0xc0) >> 1) | ((row & 0xc0) >> 3) | ((row & 0x08) << 4);
|
|
|
|
|
int high = ((row & 0x07) << 2) | ((row & 0x30) >> 4);
|
2019-12-06 22:49:35 +00:00
|
|
|
|
int rowAddr = baseAddr + ((high << 8) | low);
|
2019-12-05 02:05:17 +00:00
|
|
|
|
|
2019-12-18 00:40:27 +00:00
|
|
|
|
// Not expecting the data to wrap around, but it's possible.
|
|
|
|
|
rowAddr = (baseAddr & 0xff0000) | (rowAddr & 0xffff);
|
|
|
|
|
|
2019-12-05 02:05:17 +00:00
|
|
|
|
for (int col = 0; col < HR_BYTE_WIDTH; col++) {
|
2019-12-06 22:49:35 +00:00
|
|
|
|
int srcOffset = mAddrTrans.AddressToOffset(offset, rowAddr + col);
|
|
|
|
|
if (srcOffset < 0) {
|
|
|
|
|
mAppRef.ReportError("Address $" + (rowAddr + col).ToString("x4") +
|
|
|
|
|
" is outside of file");
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
buf[outIdx++] = mFileData[srcOffset];
|
2019-12-05 02:05:17 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
VisBitmap8 vb = new VisBitmap8(HR_WIDTH, HR_HEIGHT);
|
|
|
|
|
SetHiResPalette(vb);
|
|
|
|
|
RenderBitmap(buf, 0, HR_BYTE_WIDTH, HR_HEIGHT, 1, HR_BYTE_WIDTH,
|
2020-02-19 00:42:46 +00:00
|
|
|
|
isColor ? ColorMode.SimpleColor : ColorMode.Mono, false, false,
|
2019-12-05 02:05:17 +00:00
|
|
|
|
vb, 0, 0);
|
|
|
|
|
return vb;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2019-12-05 21:48:18 +00:00
|
|
|
|
private enum ColorMode { Mono, SimpleColor, IIgsRGB };
|
2019-12-04 23:50:19 +00:00
|
|
|
|
|
2019-12-07 17:11:25 +00:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Renders bitmap data.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="data">Data source, typically the file data.</param>
|
|
|
|
|
/// <param name="offset">Offset into data of the first byte.</param>
|
|
|
|
|
/// <param name="byteWidth">Width, in bytes, of the data to render. Each byte
|
|
|
|
|
/// represents 7 pixels in the output (more or less).</param>
|
|
|
|
|
/// <param name="height">Height, in lines, of the data to render.</param>
|
|
|
|
|
/// <param name="colStride">Column stride. The number of bytes used to hold each
|
|
|
|
|
/// byte of data. Must be >= 1.</param>
|
|
|
|
|
/// <param name="rowStride">Row stride. The number of bytes used to hold each row
|
|
|
|
|
/// of data. Must be >= (colStride * byteWidth - (colStride - 1)).</param>
|
|
|
|
|
/// <param name="colorMode">Color conversion mode.</param>
|
|
|
|
|
/// <param name="isFirstOdd">If true, render as if we're starting on an odd column.
|
|
|
|
|
/// This affects the colors.</param>
|
2020-02-19 00:42:46 +00:00
|
|
|
|
/// <param name="isHighBitFlipped">If true, render as if the high bit has the
|
|
|
|
|
/// opposite value. This affects the colors.</param>
|
2019-12-07 17:11:25 +00:00
|
|
|
|
/// <param name="vb">Output bitmap object.</param>
|
|
|
|
|
/// <param name="xstart">Initial X position in the output.</param>
|
|
|
|
|
/// <param name="ystart">Initial Y position in the output.</param>
|
2019-12-05 02:05:17 +00:00
|
|
|
|
private void RenderBitmap(byte[] data, int offset, int byteWidth, int height,
|
|
|
|
|
int colStride, int rowStride, ColorMode colorMode, bool isFirstOdd,
|
2020-02-19 00:42:46 +00:00
|
|
|
|
bool isHighBitFlipped, VisBitmap8 vb, int xstart, int ystart) {
|
2019-12-05 02:05:17 +00:00
|
|
|
|
int bx = xstart;
|
|
|
|
|
int by = ystart;
|
2019-12-04 23:50:19 +00:00
|
|
|
|
switch (colorMode) {
|
|
|
|
|
case ColorMode.Mono: {
|
|
|
|
|
// Since we're not displaying this we don't need to worry about
|
|
|
|
|
// half-pixel shifts, and can just convert 7 bits to pixels.
|
|
|
|
|
for (int row = 0; row < height; row++) {
|
|
|
|
|
int colIdx = 0;
|
|
|
|
|
for (int col = 0; col < byteWidth; col++) {
|
2019-12-05 02:05:17 +00:00
|
|
|
|
byte val = data[offset + colIdx];
|
2019-12-04 23:50:19 +00:00
|
|
|
|
for (int bit = 0; bit < 7; bit++) {
|
|
|
|
|
if ((val & 0x01) == 0) {
|
|
|
|
|
vb.SetPixelIndex(bx, by, (int)HiResColors.Black0);
|
|
|
|
|
} else {
|
|
|
|
|
vb.SetPixelIndex(bx, by, (int)HiResColors.White0);
|
|
|
|
|
}
|
|
|
|
|
val >>= 1;
|
|
|
|
|
bx++;
|
|
|
|
|
}
|
|
|
|
|
colIdx += colStride;
|
|
|
|
|
}
|
2019-12-05 02:05:17 +00:00
|
|
|
|
bx = xstart;
|
2019-12-04 23:50:19 +00:00
|
|
|
|
by++;
|
|
|
|
|
offset += rowStride;
|
2019-12-01 01:52:33 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
2019-12-04 23:50:19 +00:00
|
|
|
|
break;
|
|
|
|
|
case ColorMode.SimpleColor: {
|
|
|
|
|
// Straightforward conversion, with no funky border effects. This
|
|
|
|
|
// represents an idealized version of the hardware.
|
|
|
|
|
|
|
|
|
|
// Bits for every byte, plus a couple of "fake" bits on the ends so
|
|
|
|
|
// we don't have to throw range-checks everywhere.
|
|
|
|
|
const int OVER = 2;
|
|
|
|
|
bool[] lineBits = new bool[OVER + byteWidth * 7 + OVER];
|
|
|
|
|
bool[] hiFlags = new bool[OVER + byteWidth * 7 + OVER];
|
|
|
|
|
for (int row = 0; row < height; row++) {
|
|
|
|
|
// Unravel the bits. Note we do each byte "backwards", i.e. the
|
|
|
|
|
// low bit (which is generally considered to be on the right) is
|
|
|
|
|
// the leftmost pixel.
|
|
|
|
|
int idx = OVER; // start past "fake" bits
|
|
|
|
|
int colIdx = 0;
|
|
|
|
|
for (int col = 0; col < byteWidth; col++) {
|
2019-12-05 02:05:17 +00:00
|
|
|
|
byte val = data[offset + colIdx];
|
2019-12-04 23:50:19 +00:00
|
|
|
|
bool hiBitSet = (val & 0x80) != 0;
|
|
|
|
|
|
|
|
|
|
for (int bit = 0; bit < 7; bit++) {
|
2020-02-19 00:42:46 +00:00
|
|
|
|
hiFlags[idx] = hiBitSet ^ isHighBitFlipped;
|
2019-12-04 23:50:19 +00:00
|
|
|
|
lineBits[idx] = (val & 0x01) != 0;
|
|
|
|
|
idx++;
|
|
|
|
|
val >>= 1;
|
|
|
|
|
}
|
|
|
|
|
colIdx += colStride;
|
|
|
|
|
}
|
2019-12-01 01:52:33 +00:00
|
|
|
|
|
2019-12-04 23:50:19 +00:00
|
|
|
|
// Convert to color.
|
|
|
|
|
int lastBit = byteWidth * 7;
|
|
|
|
|
for (idx = OVER; idx < lastBit + OVER; idx++) {
|
|
|
|
|
int colorShift = hiFlags[idx] ? 2 : 0;
|
|
|
|
|
if (lineBits[idx] && (lineBits[idx - 1] || lineBits[idx + 1])) {
|
|
|
|
|
// [X]11 or [1]1X; two 1s in a row is always white
|
|
|
|
|
vb.SetPixelIndex(bx++, by, (byte)HiResColors.White0);
|
|
|
|
|
} else if (lineBits[idx]) {
|
|
|
|
|
// [0]10, color pixel
|
|
|
|
|
bool isOdd = ((idx & 0x01) != 0) ^ isFirstOdd;
|
|
|
|
|
if (isOdd) {
|
|
|
|
|
vb.SetPixelIndex(bx++, by,
|
|
|
|
|
(byte)((int)HiResColors.Green + colorShift));
|
|
|
|
|
} else {
|
|
|
|
|
vb.SetPixelIndex(bx++, by,
|
|
|
|
|
(byte)((int)HiResColors.Purple + colorShift));
|
|
|
|
|
}
|
|
|
|
|
} else if (lineBits[idx - 1] && lineBits[idx + 1]) {
|
|
|
|
|
// [1]01, keep color going
|
|
|
|
|
bool isOdd = ((idx & 0x01) != 0) ^ isFirstOdd;
|
|
|
|
|
if (isOdd) {
|
|
|
|
|
vb.SetPixelIndex(bx++, by,
|
|
|
|
|
(byte)((int)HiResColors.Purple + colorShift));
|
|
|
|
|
} else {
|
|
|
|
|
vb.SetPixelIndex(bx++, by,
|
|
|
|
|
(byte)((int)HiResColors.Green + colorShift));
|
|
|
|
|
}
|
2019-12-01 01:52:33 +00:00
|
|
|
|
} else {
|
2019-12-04 23:50:19 +00:00
|
|
|
|
// [0]0X or [X]01
|
|
|
|
|
vb.SetPixelIndex(bx++, by, (byte)HiResColors.Black0);
|
2019-12-01 01:52:33 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-12-04 23:50:19 +00:00
|
|
|
|
// move to next row
|
2019-12-05 02:05:17 +00:00
|
|
|
|
bx = xstart;
|
2019-12-04 23:50:19 +00:00
|
|
|
|
by++;
|
|
|
|
|
offset += rowStride;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
case ColorMode.IIgsRGB: {
|
|
|
|
|
// Color conversion similar to what CiderPress does, but without the
|
|
|
|
|
// half-pixel shift (we're trying to create a 1:1 bitmap, not 1:2).
|
|
|
|
|
//
|
|
|
|
|
// This replicates some of the oddness in Apple IIgs RGB monitor output,
|
|
|
|
|
// but it's not quite right though. For example:
|
|
|
|
|
//
|
|
|
|
|
// observed generated
|
|
|
|
|
// d5 2a: blue [dk blue] purple ... black ...
|
|
|
|
|
// aa 55: orange [yellow] green ... white ...
|
|
|
|
|
// 55 aa: purple [lt blue] blue ... black ...
|
|
|
|
|
// 2a d5: green [brown] orange ... black ...
|
|
|
|
|
//
|
|
|
|
|
// KEGS doesn't seem to try to model this; it shows solid colors with no
|
|
|
|
|
// wackiness. AppleWin in "Color TV" mode shows similar effects, but is
|
|
|
|
|
// much blurrier (by design).
|
|
|
|
|
bool[] lineBits = new bool[byteWidth * 7];
|
|
|
|
|
bool[] hiFlags = new bool[byteWidth * 7]; // overkill, but simpler
|
|
|
|
|
int[] colorBuf = new int[byteWidth * 7];
|
|
|
|
|
for (int row = 0; row < height; row++) {
|
|
|
|
|
// Unravel the bits.
|
|
|
|
|
int idx = 0;
|
|
|
|
|
int colIdx = 0;
|
|
|
|
|
for (int col = 0; col < byteWidth; col++) {
|
2019-12-05 02:05:17 +00:00
|
|
|
|
byte val = data[offset + colIdx];
|
2019-12-04 23:50:19 +00:00
|
|
|
|
bool hiBitSet = (val & 0x80) != 0;
|
|
|
|
|
|
|
|
|
|
for (int bit = 0; bit < 7; bit++) {
|
2020-02-19 00:42:46 +00:00
|
|
|
|
hiFlags[idx] = hiBitSet ^ isHighBitFlipped;
|
2019-12-04 23:50:19 +00:00
|
|
|
|
lineBits[idx] = (val & 0x01) != 0;
|
|
|
|
|
idx++;
|
|
|
|
|
val >>= 1;
|
|
|
|
|
}
|
|
|
|
|
colIdx += colStride;
|
|
|
|
|
}
|
2019-12-01 01:52:33 +00:00
|
|
|
|
|
2019-12-04 23:50:19 +00:00
|
|
|
|
// Convert to color.
|
|
|
|
|
int lastBit = byteWidth * 7;
|
|
|
|
|
for (idx = 0; idx < lastBit; idx++) {
|
|
|
|
|
int colorShift = hiFlags[idx] ? 2 : 0;
|
|
|
|
|
if (!lineBits[idx]) {
|
|
|
|
|
// Bit not set, set pixel to black.
|
|
|
|
|
colorBuf[idx] = (int)HiResColors.Black0;
|
|
|
|
|
} else {
|
|
|
|
|
// Bit set, set pixel to white or color.
|
|
|
|
|
if (idx > 0 && colorBuf[idx - 1] != (int)HiResColors.Black0) {
|
|
|
|
|
// previous bit was also set, this is white
|
|
|
|
|
colorBuf[idx] = (int)HiResColors.White0;
|
|
|
|
|
|
|
|
|
|
// the previous pixel is part of a run of white
|
|
|
|
|
colorBuf[idx - 1] = (int)HiResColors.White0;
|
|
|
|
|
} else {
|
|
|
|
|
// previous bit not set *or* was first pixel in line;
|
|
|
|
|
// set color based on whether this is even or odd pixel col
|
|
|
|
|
bool isOdd = ((idx & 0x01) != 0) ^ isFirstOdd;
|
|
|
|
|
if (isOdd) {
|
|
|
|
|
colorBuf[idx] = (int)HiResColors.Green + colorShift;
|
|
|
|
|
} else {
|
|
|
|
|
colorBuf[idx] = (int)HiResColors.Purple + colorShift;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Do we have a run of the same color? If so, smooth the
|
|
|
|
|
// color out. Note that white blends smoothly with everything.
|
|
|
|
|
if (idx > 1 && (colorBuf[idx - 2] == colorBuf[idx] ||
|
|
|
|
|
colorBuf[idx - 2] == (int)HiResColors.White0)) {
|
|
|
|
|
colorBuf[idx - 1] = colorBuf[idx];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-12-01 01:52:33 +00:00
|
|
|
|
|
2019-12-04 23:50:19 +00:00
|
|
|
|
// Write to bitmap.
|
|
|
|
|
for (idx = 0; idx < lastBit; idx++) {
|
|
|
|
|
vb.SetPixelIndex(bx++, by, (byte)colorBuf[idx]);
|
2019-12-01 01:52:33 +00:00
|
|
|
|
}
|
|
|
|
|
|
2019-12-04 23:50:19 +00:00
|
|
|
|
// move to next row
|
2019-12-05 02:05:17 +00:00
|
|
|
|
bx = xstart;
|
2019-12-04 23:50:19 +00:00
|
|
|
|
by++;
|
|
|
|
|
offset += rowStride;
|
|
|
|
|
}
|
2019-12-01 01:52:33 +00:00
|
|
|
|
}
|
2019-12-04 23:50:19 +00:00
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
// just leave the bitmap empty
|
2019-12-05 18:21:07 +00:00
|
|
|
|
mAppRef.ReportError("Unknown ColorMode " + colorMode);
|
2019-12-04 23:50:19 +00:00
|
|
|
|
break;
|
2019-11-28 01:12:26 +00:00
|
|
|
|
}
|
2019-11-25 22:27:38 +00:00
|
|
|
|
}
|
2019-12-01 01:52:33 +00:00
|
|
|
|
|
2019-12-04 23:50:19 +00:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Map hi-res colors to palette entries.
|
|
|
|
|
/// </summary>
|
|
|
|
|
private enum HiResColors : byte {
|
|
|
|
|
Black0 = 1,
|
|
|
|
|
Green = 3,
|
|
|
|
|
Purple = 4,
|
|
|
|
|
White0 = 2,
|
|
|
|
|
Black1 = 1,
|
2019-12-01 01:52:33 +00:00
|
|
|
|
Orange = 5,
|
|
|
|
|
Blue = 6,
|
2019-12-04 23:50:19 +00:00
|
|
|
|
White1 = 2
|
2019-12-01 01:52:33 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void SetHiResPalette(VisBitmap8 vb) {
|
|
|
|
|
// These don't match directly to hi-res color numbers because we want to
|
2019-12-04 23:50:19 +00:00
|
|
|
|
// avoid adding black/white twice. The colors correspond to Apple IIgs RGB
|
|
|
|
|
// monitor output.
|
2019-12-03 00:38:32 +00:00
|
|
|
|
vb.AddColor(0, 0, 0, 0); // 0=transparent
|
2019-12-01 01:52:33 +00:00
|
|
|
|
vb.AddColor(0xff, 0x00, 0x00, 0x00); // 1=black0/black1
|
|
|
|
|
vb.AddColor(0xff, 0xff, 0xff, 0xff); // 2=white0/white1
|
|
|
|
|
vb.AddColor(0xff, 0x11, 0xdd, 0x00); // 3=green
|
|
|
|
|
vb.AddColor(0xff, 0xdd, 0x22, 0xdd); // 4=purple
|
|
|
|
|
vb.AddColor(0xff, 0xff, 0x66, 0x00); // 5=orange
|
|
|
|
|
vb.AddColor(0xff, 0x22, 0x22, 0xff); // 6=blue
|
|
|
|
|
}
|
2019-11-25 22:27:38 +00:00
|
|
|
|
}
|
|
|
|
|
}
|