1145 lines
30 KiB
D
1145 lines
30 KiB
D
/+
|
|
+ peripheral/diskii.d
|
|
+
|
|
+ Copyright: 2007 Gerald Stocker
|
|
+
|
|
+ This file is part of Twoapple.
|
|
+
|
|
+ Twoapple is free software; you can redistribute it and/or modify
|
|
+ it under the terms of the GNU General Public License as published by
|
|
+ the Free Software Foundation; either version 2 of the License, or
|
|
+ (at your option) any later version.
|
|
+
|
|
+ Twoapple is distributed in the hope that it will be useful,
|
|
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
+ GNU General Public License for more details.
|
|
+
|
|
+ You should have received a copy of the GNU General Public License
|
|
+ along with Twoapple; if not, write to the Free Software
|
|
+ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
|
+/
|
|
|
|
module peripheral.diskii;
|
|
|
|
import peripheral.base;
|
|
import device.base;
|
|
import memory;
|
|
import timer;
|
|
|
|
import ui.mainwindow;
|
|
import ui.sound;
|
|
|
|
import std.stream;
|
|
|
|
ubyte[256] controllerRom = [
|
|
0xa2, 0x20, 0xa0, 0x00, 0xa2, 0x03, 0x86, 0x3c,
|
|
0x8a, 0x0a, 0x24, 0x3c, 0xf0, 0x10, 0x05, 0x3c,
|
|
0x49, 0xff, 0x29, 0x7e, 0xb0, 0x08, 0x4a, 0xd0,
|
|
0xfb, 0x98, 0x9d, 0x56, 0x03, 0xc8, 0xe8, 0x10,
|
|
0xe5, 0x20, 0x58, 0xff, 0xba, 0xbd, 0x00, 0x01,
|
|
0x0a, 0x0a, 0x0a, 0x0a, 0x85, 0x2b, 0xaa, 0xbd,
|
|
0x8e, 0xc0, 0xbd, 0x8c, 0xc0, 0xbd, 0x8a, 0xc0,
|
|
0xbd, 0x89, 0xc0, 0xa0, 0x50, 0xbd, 0x80, 0xc0,
|
|
0x98, 0x29, 0x03, 0x0a, 0x05, 0x2b, 0xaa, 0xbd,
|
|
0x81, 0xc0, 0xa9, 0x56, 0x20, 0xa8, 0xfc, 0x88,
|
|
0x10, 0xeb, 0x85, 0x26, 0x85, 0x3d, 0x85, 0x41,
|
|
0xa9, 0x08, 0x85, 0x27, 0x18, 0x08, 0xbd, 0x8c,
|
|
0xc0, 0x10, 0xfb, 0x49, 0xd5, 0xd0, 0xf7, 0xbd,
|
|
0x8c, 0xc0, 0x10, 0xfb, 0xc9, 0xaa, 0xd0, 0xf3,
|
|
0xea, 0xbd, 0x8c, 0xc0, 0x10, 0xfb, 0xc9, 0x96,
|
|
0xf0, 0x09, 0x28, 0x90, 0xdf, 0x49, 0xad, 0xf0,
|
|
0x25, 0xd0, 0xd9, 0xa0, 0x03, 0x85, 0x40, 0xbd,
|
|
0x8c, 0xc0, 0x10, 0xfb, 0x2a, 0x85, 0x3c, 0xbd,
|
|
0x8c, 0xc0, 0x10, 0xfb, 0x25, 0x3c, 0x88, 0xd0,
|
|
0xec, 0x28, 0xc5, 0x3d, 0xd0, 0xbe, 0xa5, 0x40,
|
|
0xc5, 0x41, 0xd0, 0xb8, 0xb0, 0xb7, 0xa0, 0x56,
|
|
0x84, 0x3c, 0xbc, 0x8c, 0xc0, 0x10, 0xfb, 0x59,
|
|
0xd6, 0x02, 0xa4, 0x3c, 0x88, 0x99, 0x00, 0x03,
|
|
0xd0, 0xee, 0x84, 0x3c, 0xbc, 0x8c, 0xc0, 0x10,
|
|
0xfb, 0x59, 0xd6, 0x02, 0xa4, 0x3c, 0x91, 0x26,
|
|
0xc8, 0xd0, 0xef, 0xbc, 0x8c, 0xc0, 0x10, 0xfb,
|
|
0x59, 0xd6, 0x02, 0xd0, 0x87, 0xa0, 0x00, 0xa2,
|
|
0x56, 0xca, 0x30, 0xfb, 0xb1, 0x26, 0x5e, 0x00,
|
|
0x03, 0x2a, 0x5e, 0x00, 0x03, 0x2a, 0x91, 0x26,
|
|
0xc8, 0xd0, 0xee, 0xe6, 0x27, 0xe6, 0x3d, 0xa5,
|
|
0x3d, 0xcd, 0x00, 0x08, 0xa6, 0x2b, 0x90, 0xdb,
|
|
0x4c, 0x01, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00
|
|
];
|
|
|
|
class StopTimer
|
|
{
|
|
Timer timer;
|
|
Timer.Counter stopCounter;
|
|
void delegate() notifyExpired;
|
|
|
|
void startCountdown()
|
|
{
|
|
if (stopCounter is null)
|
|
stopCounter = timer.new Counter(1_020_484, &expire);
|
|
}
|
|
|
|
void stopCountdown()
|
|
{
|
|
if (stopCounter !is null)
|
|
{
|
|
stopCounter.discard();
|
|
stopCounter = null;
|
|
}
|
|
}
|
|
|
|
bool expire()
|
|
{
|
|
notifyExpired();
|
|
stopCounter = null;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
class Controller : Peripheral
|
|
{
|
|
import gtk.VBox;
|
|
|
|
Drive[2] drives;
|
|
|
|
int activeDrive;
|
|
bool writeMode;
|
|
bool loadRegister;
|
|
bool delegate() checkFinalCycle;
|
|
ubyte dataLatch;
|
|
bool isOn;
|
|
StopTimer drivesOffDelay;
|
|
|
|
this()
|
|
{
|
|
ioSelectROM = controllerRom;
|
|
statusBox = new VBox(false, 3);
|
|
drives[0] = new Drive(1);
|
|
statusBox.packStart(drives[0].status.display, false, false, 0);
|
|
drives[1] = new Drive(2);
|
|
statusBox.packStart(drives[1].status.display, false, false, 0);
|
|
}
|
|
|
|
void reset()
|
|
{
|
|
drives[0].deactivate();
|
|
drives[1].deactivate();
|
|
activeDrive = 0;
|
|
}
|
|
|
|
void initTimer(Timer timer)
|
|
{
|
|
drivesOffDelay = new StopTimer();
|
|
drivesOffDelay.timer = timer;
|
|
drivesOffDelay.notifyExpired = &doDrivesOff;
|
|
}
|
|
|
|
void doDrivesOff()
|
|
{
|
|
drives[activeDrive].deactivate();
|
|
isOn = false;
|
|
}
|
|
|
|
void changeActiveDrive(int newActive)
|
|
{
|
|
drives[activeDrive].deactivate();
|
|
if (isOn) drives[newActive].activate();
|
|
activeDrive = newActive;
|
|
}
|
|
|
|
ubyte checkWriteProtect()
|
|
{
|
|
if (drives[activeDrive].writeProtected ||
|
|
drives[activeDrive].magnets[1])
|
|
return 0x80;
|
|
else
|
|
return 0x00;
|
|
}
|
|
|
|
ubyte phaseOff(ushort addr)
|
|
{
|
|
int phase = (addr >> 1) & 3;
|
|
drives[activeDrive].setPhase(phase, false);
|
|
return 0xFF;
|
|
}
|
|
|
|
void phaseOn(ushort addr)
|
|
{
|
|
int phase = (addr >> 1) & 3;
|
|
drives[activeDrive].setPhase(phase, true);
|
|
}
|
|
|
|
ubyte drive_0_select()
|
|
{
|
|
if (activeDrive != 0) changeActiveDrive(0);
|
|
return 0xFF;
|
|
}
|
|
|
|
void drive_1_select()
|
|
{
|
|
if (activeDrive != 1) changeActiveDrive(1);
|
|
}
|
|
|
|
ubyte drivesOff()
|
|
{
|
|
drivesOffDelay.startCountdown();
|
|
return 0xFF;
|
|
}
|
|
|
|
void drivesOn()
|
|
{
|
|
isOn = true;
|
|
drivesOffDelay.stopCountdown();
|
|
drives[activeDrive].activate();
|
|
}
|
|
|
|
void readQ6H()
|
|
{
|
|
loadRegister = true;
|
|
}
|
|
|
|
ubyte Q6L()
|
|
{
|
|
loadRegister = false;
|
|
if (isOn && checkFinalCycle())
|
|
{
|
|
if (writeMode)
|
|
{
|
|
drives[activeDrive].write(dataLatch);
|
|
return dataLatch;
|
|
}
|
|
else
|
|
{
|
|
return drives[activeDrive].read();
|
|
}
|
|
}
|
|
return 0x00;
|
|
}
|
|
|
|
void writeQ6H(ubyte val)
|
|
{
|
|
loadRegister = true;
|
|
dataLatch = val;
|
|
}
|
|
|
|
ubyte Q7L()
|
|
{
|
|
writeMode = false;
|
|
if (loadRegister) return checkWriteProtect();
|
|
else return 0xFF;
|
|
}
|
|
|
|
void readQ7H()
|
|
{
|
|
writeMode = true;
|
|
}
|
|
|
|
void writeQ7H(ubyte val)
|
|
{
|
|
writeMode = true;
|
|
dataLatch = val;
|
|
}
|
|
|
|
mixin(InitSwitches("", [
|
|
mixin(MakeSwitch([0xC080, 0xC082, 0xC084, 0xC086],
|
|
"RW", "phaseOff(addr)")),
|
|
mixin(MakeSwitch([0xC081, 0xC083, 0xC085, 0xC087],
|
|
"R0W", "phaseOn(addr)")),
|
|
mixin(MakeSwitch([0xC088], "RW", "drivesOff")),
|
|
mixin(MakeSwitch([0xC089], "R0W", "drivesOn")),
|
|
mixin(MakeSwitch([0xC08A], "RW", "drive_0_select")),
|
|
mixin(MakeSwitch([0xC08B], "R0W", "drive_1_select")),
|
|
mixin(MakeSwitch([0xC08C], "RW", "Q6L")),
|
|
mixin(MakeSwitch([0xC08D], "R0", "readQ6H")),
|
|
mixin(MakeSwitch([0xC08D], "W", "writeQ6H(val)")),
|
|
mixin(MakeSwitch([0xC08E], "RW", "Q7L")),
|
|
mixin(MakeSwitch([0xC08F], "R0", "readQ7H")),
|
|
mixin(MakeSwitch([0xC08F], "W", "writeQ7H(val)"))
|
|
]));
|
|
}
|
|
|
|
class DriveStatus
|
|
{
|
|
import gtk.HBox;
|
|
import gtk.MenuBar;
|
|
import gtk.MenuItem;
|
|
import gtk.ProgressBar;
|
|
import gtk.CheckButton;
|
|
import gtk.ToggleButton;
|
|
import gtk.Label;
|
|
import gtkc.pangotypes;
|
|
|
|
import std.string;
|
|
|
|
Drive drive;
|
|
HBox display;
|
|
ProgressBar activity;
|
|
string statusClean, statusDirty;
|
|
CheckButton wpButton;
|
|
Label imgName;
|
|
MenuItem openItem;
|
|
MenuItem saveItem;
|
|
MenuItem ejectItem;
|
|
|
|
this(Drive drive_, int driveNum)
|
|
{
|
|
drive = drive_;
|
|
|
|
statusClean = "Drive " ~ std.string.toString(driveNum);
|
|
statusDirty = "( " ~ statusClean ~ " )";
|
|
activity = new ProgressBar();
|
|
activity.setText(statusClean);
|
|
|
|
auto menuBar = new MenuBar();
|
|
auto menu = menuBar.append("Image");
|
|
openItem = new MenuItem(&onOpenImage, "Open", "img.open", false);
|
|
saveItem = new MenuItem(&onSaveImage, "Save", "img.save", false);
|
|
ejectItem = new MenuItem(&onEjectImage, "Eject", "img.eject", false);
|
|
menu.append(openItem);
|
|
menu.append(saveItem);
|
|
menu.append(ejectItem);
|
|
saveItem.setSensitive(false);
|
|
ejectItem.setSensitive(false);
|
|
|
|
wpButton = new CheckButton("WP", false);
|
|
wpButton.addOnToggled(&onWPToggle);
|
|
wpButton.setSensitive(false);
|
|
|
|
imgName = new Label("No disk");
|
|
imgName.setMaxWidthChars(35);
|
|
imgName.setEllipsize(PangoEllipsizeMode.START);
|
|
|
|
display = new HBox(false, 3);
|
|
display.packStart(activity, false, false, 0);
|
|
display.packStart(wpButton, false, false, 0);
|
|
display.packStart(imgName, true, true, 0);
|
|
display.packStart(menuBar, false, false, 0);
|
|
}
|
|
|
|
void setActive(bool isActive)
|
|
{
|
|
soundCard.speedyMode = isActive;
|
|
activity.setFraction(isActive ? 1.0 : 0.0);
|
|
}
|
|
|
|
void setDirty(bool isDirty)
|
|
{
|
|
if (isDirty)
|
|
activity.setText(statusDirty);
|
|
else
|
|
activity.setText(statusClean);
|
|
saveItem.setSensitive(isDirty);
|
|
}
|
|
|
|
void onWPToggle(ToggleButton b)
|
|
{
|
|
drive.writeProtected = (b.getActive() != 0);
|
|
}
|
|
|
|
void onOpenImage(MenuItem m)
|
|
{
|
|
TwoappleFile file = TwoappleFilePicker.open("image file",
|
|
&ExternalImage.isValidImage);
|
|
if (file is null) return;
|
|
if (!drive.loadImage(file)) return;
|
|
|
|
if (!file.canWrite())
|
|
{
|
|
wpButton.setActive(true);
|
|
wpButton.setSensitive(false);
|
|
}
|
|
else
|
|
wpButton.setSensitive(true);
|
|
|
|
setDirty(false);
|
|
ejectItem.setSensitive(true);
|
|
imgName.setText(file.fileName);
|
|
}
|
|
|
|
void onEjectImage(MenuItem m)
|
|
{
|
|
if (!drive.ejectImage()) return;
|
|
setDirty(false);
|
|
wpButton.setActive(false);
|
|
wpButton.setSensitive(true);
|
|
ejectItem.setSensitive(false);
|
|
imgName.setText("No disk");
|
|
}
|
|
|
|
void onSaveImage(MenuItem m)
|
|
{
|
|
if (!drive.saveImage()) return;
|
|
setDirty(false);
|
|
}
|
|
}
|
|
|
|
class Drive
|
|
{
|
|
InternalImage imgData;
|
|
ExternalImage imgFile;
|
|
DriveStatus status;
|
|
bool writeProtected;
|
|
int headPos, maxHeadPos, track;
|
|
bool[4] magnets;
|
|
|
|
this(int driveNum)
|
|
{
|
|
maxHeadPos = (InternalImage.NUM_TRACKS - 1) * 4;
|
|
imgData = new NonImage();
|
|
status = new DriveStatus(this, driveNum);
|
|
}
|
|
|
|
void setPhase(int phase, bool newState)
|
|
{
|
|
//if (magnets[phase] == newState) return;
|
|
magnets[phase] = newState;
|
|
|
|
// Find the number of active magnets and their
|
|
// distances from any cog.
|
|
int totalOn = 0, delta;
|
|
for (int p = 0; p < 4; ++p)
|
|
{
|
|
if (magnets[p])
|
|
{
|
|
++totalOn;
|
|
delta = (p * 2) - (headPos % 8);
|
|
}
|
|
}
|
|
|
|
// Do not move the head if more than one magnet
|
|
// is on (which precludes quarter-tracking).
|
|
if (totalOn != 1) return;
|
|
|
|
// Do not move the head if the active magnet is
|
|
// equidistant from the two nearest cogs, or if
|
|
// it is already in line with a cog.
|
|
if ((delta == -4) || (delta == 4) || (delta == 0)) return;
|
|
|
|
// Pull the nearest cog to the magnet.
|
|
if (delta > 4) delta -= 8;
|
|
else if (delta < -4) delta += 8;
|
|
headPos += delta;
|
|
|
|
if (headPos < 0) headPos = 0; // make a noise?
|
|
//else if (headPos > 136) headPos = 136;
|
|
else if (headPos > maxHeadPos) headPos = maxHeadPos;
|
|
track = (headPos & -4) / 4;
|
|
}
|
|
|
|
void activate()
|
|
{
|
|
status.setActive(true);
|
|
}
|
|
|
|
void deactivate()
|
|
{
|
|
status.setActive(false);
|
|
magnets[0..4] = false;
|
|
}
|
|
|
|
ubyte read()
|
|
{
|
|
return imgData.peek(track);
|
|
}
|
|
|
|
void write(ubyte val)
|
|
{
|
|
if (writeProtected) return;
|
|
bool wasDirty = imgData.isDirty;
|
|
imgData.poke(track, val);
|
|
if (imgData.isDirty && !wasDirty) status.setDirty(true);
|
|
}
|
|
|
|
bool ejectImage()
|
|
{
|
|
if (imgData.isDirty)
|
|
{
|
|
int response = TwoappleDialog.run("Warning",
|
|
"Save image currently in drive?",
|
|
["Save", "Don't save", "Cancel"]);
|
|
if (response == 2) return false;
|
|
if (response == 0)
|
|
{
|
|
if (!saveImage(true)) return false;
|
|
}
|
|
}
|
|
|
|
imgData = new NonImage();
|
|
imgFile = null;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool loadImage(TwoappleFile file)
|
|
{
|
|
if (imgData.isDirty)
|
|
{
|
|
int response = TwoappleDialog.run("Warning",
|
|
"Save image currently in drive?",
|
|
["Save", "Don't save", "Cancel"]);
|
|
if (response == 2) return false;
|
|
if (response == 0)
|
|
{
|
|
if (!saveImage(true)) return false;
|
|
}
|
|
}
|
|
|
|
imgFile = ExternalImage.loadImage(file);
|
|
assert(imgFile !is null);
|
|
imgData = imgFile.imgData;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool saveImage(bool indirect = false)
|
|
{
|
|
bool retry, success;
|
|
|
|
void chooseAction(string msg, void delegate() firstAction = null,
|
|
string firstButton = null)
|
|
{
|
|
bool chooseAgain;
|
|
void delegate()[] actions;
|
|
string[] buttons;
|
|
|
|
void delegate() saveAsNib =
|
|
{
|
|
TwoappleFile file = TwoappleFilePicker.saveAs("Image file",
|
|
imgFile.imgFile.folder(),
|
|
imgFile.imgFile.baseName() ~ ".nib");
|
|
if (file is null) chooseAgain = true;
|
|
else
|
|
{
|
|
imgFile = new NIBImage(file, imgData);
|
|
retry = true;
|
|
}
|
|
};
|
|
void delegate() noSave = { success = true; };
|
|
void delegate() cancel = {};
|
|
|
|
actions.length = buttons.length = 2 +
|
|
(indirect ? 1 : 0) +
|
|
((firstAction !is null) ? 1 : 0);
|
|
actions[length - 1] = cancel;
|
|
buttons[length - 1] = "Cancel";
|
|
if (indirect)
|
|
{
|
|
actions[length - 2] = noSave;
|
|
buttons[length - 2] = "Don't save";
|
|
actions[length - 3] = saveAsNib;
|
|
buttons[length - 3] = "Save as NIB";
|
|
}
|
|
else
|
|
{
|
|
actions[length - 2] = saveAsNib;
|
|
buttons[length - 2] = "Save as NIB";
|
|
}
|
|
if (firstAction !is null)
|
|
{
|
|
actions[0] = firstAction;
|
|
buttons[0] = firstButton;
|
|
}
|
|
|
|
do
|
|
{
|
|
int action = TwoappleDialog.run("Warning", msg, buttons);
|
|
actions[action]();
|
|
} while (chooseAgain);
|
|
}
|
|
|
|
do
|
|
{
|
|
success = retry = false;
|
|
try
|
|
{
|
|
imgFile.save();
|
|
imgData.isDirty = false;
|
|
success = true;
|
|
}
|
|
catch (DSKImage.VolumeException e)
|
|
{
|
|
chooseAction("Disk volume is not 254",
|
|
{ e.ignoreVolume(); retry = true; },
|
|
"Ignore and save");
|
|
}
|
|
catch (DSKImage.DSKException e)
|
|
{
|
|
chooseAction("Image cannot be saved in DSK format");
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
TwoappleError.show(e.msg);
|
|
}
|
|
} while (retry);
|
|
|
|
return success;
|
|
}
|
|
}
|
|
|
|
class InternalImage
|
|
{
|
|
bool isDirty;
|
|
static const NUM_TRACKS = 35;
|
|
static const TRACK_LENGTH = 6656; // XXX No DOS 3.2
|
|
abstract ubyte peek(uint track);
|
|
abstract void poke(uint track, ubyte val);
|
|
}
|
|
|
|
class NonImage : InternalImage
|
|
{
|
|
ubyte peek(uint track) { return 0; }
|
|
void poke(uint track, ubyte val) {}
|
|
}
|
|
|
|
class RealImage : InternalImage
|
|
{
|
|
ubyte[][] trackData;
|
|
uint currentPos;
|
|
|
|
this()
|
|
{
|
|
trackData = new ubyte[][NUM_TRACKS];
|
|
for (uint t = 0; t < NUM_TRACKS; ++t)
|
|
trackData[t] = new ubyte[TRACK_LENGTH];
|
|
}
|
|
|
|
ubyte peek(uint track)
|
|
{
|
|
currentPos %= TRACK_LENGTH;
|
|
return trackData[track][currentPos++];
|
|
}
|
|
|
|
void poke(uint track, ubyte val)
|
|
{
|
|
currentPos %= TRACK_LENGTH;
|
|
trackData[track][currentPos++] = val;
|
|
isDirty = true;
|
|
}
|
|
}
|
|
|
|
class ExternalImage
|
|
{
|
|
TwoappleFile imgFile;
|
|
RealImage imgData;
|
|
static const string invalidMessage = "Invalid image file";
|
|
|
|
class ReadOnlyException : Exception
|
|
{
|
|
this() { super("File is read-only"); }
|
|
}
|
|
|
|
static string isValidImage(TwoappleFile checkFile)
|
|
{
|
|
uint fsize = checkFile.fileSize();
|
|
return (isDSKImage(fsize) || isNIBImage(fsize)) ?
|
|
null : invalidMessage;
|
|
}
|
|
|
|
static ExternalImage loadImage(TwoappleFile checkFile)
|
|
{
|
|
uint fsize = checkFile.fileSize();
|
|
if (isDSKImage(fsize)) return new DSKImage(checkFile);
|
|
if (isNIBImage(fsize)) return new NIBImage(checkFile);
|
|
return null;
|
|
}
|
|
|
|
static bool isDSKImage(uint fsize)
|
|
{
|
|
return (fsize == 143488) ||
|
|
((fsize >= 143358) && (fsize <= 143363));
|
|
}
|
|
|
|
static bool isNIBImage(uint fsize)
|
|
{
|
|
return (fsize == 232960);
|
|
}
|
|
|
|
this(TwoappleFile checkFile)
|
|
{
|
|
imgFile = checkFile;
|
|
imgData = new RealImage();
|
|
load();
|
|
}
|
|
|
|
this(TwoappleFile checkFile, InternalImage data)
|
|
{
|
|
imgFile = checkFile;
|
|
imgData = cast(RealImage)data;
|
|
}
|
|
|
|
abstract void load();
|
|
abstract void writeOut(File stream);
|
|
|
|
void save()
|
|
{
|
|
//if (!imgFile.canWrite()) throw new ReadOnlyException();
|
|
File stream = new File(imgFile.fileName, FileMode.Out);
|
|
scope(exit)
|
|
{
|
|
if (stream !is null) stream.close();
|
|
}
|
|
writeOut(stream);
|
|
}
|
|
}
|
|
|
|
class DSKImage : ExternalImage
|
|
{
|
|
static int[16] dosOrder = [
|
|
0x00, 0x07, 0x0E, 0x06, 0x0D, 0x05, 0x0C, 0x04,
|
|
0x0B, 0x03, 0x0A, 0x02, 0x09, 0x01, 0x08, 0x0F];
|
|
|
|
static int[16] prodosOrder = [
|
|
0x00, 0x08, 0x01, 0x09, 0x02, 0x0A, 0x03, 0x0B,
|
|
0x04, 0x0C, 0x05, 0x0D, 0x06, 0x0E, 0x07, 0x0F];
|
|
|
|
static ubyte[3] DATA_PROLOGUE = [0xD5, 0xAA, 0xAD];
|
|
static ubyte[3] ADDR_PROLOGUE = [0xD5, 0xAA, 0x96];
|
|
static ubyte[3] EPILOGUE = [0xDE, 0xAA, 0xEB];
|
|
|
|
static ubyte[0x40] diskByte = [
|
|
0x96, 0x97, 0x9A, 0x9B, 0x9D, 0x9E, 0x9F, 0xA6,
|
|
0xA7, 0xAB, 0xAC, 0xAD, 0xAE, 0xAF, 0xB2, 0xB3,
|
|
0xB4, 0xB5, 0xB6, 0xB7, 0xB9, 0xBA, 0xBB, 0xBC,
|
|
0xBD, 0xBE, 0xBF, 0xCB, 0xCD, 0xCE, 0xCF, 0xD3,
|
|
0xD6, 0xD7, 0xD9, 0xDA, 0xDB, 0xDC, 0xDD, 0xDE,
|
|
0xDF, 0xE5, 0xE6, 0xE7, 0xE9, 0xEA, 0xEB, 0xEC,
|
|
0xED, 0xEE, 0xEF, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6,
|
|
0xF7, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE, 0xFF
|
|
];
|
|
|
|
static int[0x70] memByte;
|
|
|
|
static const NUM_SECTORS = 16;
|
|
static const SECTOR_LENGTH = 256;
|
|
static const DATA_FIELD_LENGTH = 342;
|
|
|
|
static this()
|
|
{
|
|
memByte[0..0x70] = -1;
|
|
for (int i = 0; i < 0x40; ++i)
|
|
{
|
|
memByte[diskByte[i] - 0x96] = i;
|
|
}
|
|
}
|
|
|
|
class VolumeException : Exception
|
|
{
|
|
DSKImage img;
|
|
|
|
this()
|
|
{
|
|
super("");
|
|
img = this.outer;
|
|
}
|
|
|
|
void ignoreVolume()
|
|
{
|
|
img.preserveVolume = false;
|
|
}
|
|
}
|
|
|
|
class DSKException : Exception
|
|
{
|
|
this() { super(""); }
|
|
}
|
|
|
|
uint dataOffset;
|
|
ubyte[] mbHeader;
|
|
bool isProdos;
|
|
ubyte volume;
|
|
bool preserveVolume;
|
|
|
|
this(TwoappleFile checkFile)
|
|
{
|
|
super(checkFile);
|
|
volume = 0xFE;
|
|
preserveVolume = true;
|
|
}
|
|
|
|
this(TwoappleFile checkFile, InternalImage data)
|
|
{
|
|
super(checkFile, data);
|
|
}
|
|
|
|
void checkMacBinary(File stream)
|
|
{
|
|
if (imgFile.fileSize() == 143488)
|
|
{
|
|
dataOffset = 128;
|
|
mbHeader = new ubyte[128];
|
|
stream.read(mbHeader);
|
|
}
|
|
}
|
|
|
|
bool prodosOrderProdos(File stream)
|
|
{
|
|
ubyte checkLo, checkHi;
|
|
ushort check1, check2, match1, match2;
|
|
for (int s = 2; s <= 5; ++s)
|
|
{
|
|
stream.seekSet(s * 512 + dataOffset);
|
|
stream.read(checkLo); stream.read(checkHi);
|
|
check1 = (checkHi << 8) | checkLo;
|
|
stream.read(checkLo); stream.read(checkHi);
|
|
check2 = (checkHi << 8) | checkLo;
|
|
match1 = ((s == 2) ? 0 : (s - 1));
|
|
match2 = ((s == 5) ? 0 : (s + 1));
|
|
if ((check1 != match1) || (check2 != match2)) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool prodosOrderDos(File stream)
|
|
{
|
|
ubyte check;
|
|
for (int s = 5; s <= 13; ++s)
|
|
{
|
|
stream.seekSet(dataOffset + 0x11002 + (s * 256));
|
|
stream.read(check);
|
|
if (check != (14 - s)) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void loadTrack(File stream, int track)
|
|
{
|
|
uint offset;
|
|
ubyte[] trackData = imgData.trackData[track];
|
|
|
|
void loadBytes(ubyte[] data)
|
|
{
|
|
uint len = data.length;
|
|
trackData[offset..offset+len] = data;
|
|
offset += len;
|
|
}
|
|
|
|
void loadByte(ubyte data, uint len = 1)
|
|
{
|
|
trackData[offset..offset+len] = data;
|
|
offset += len;
|
|
}
|
|
|
|
void loadAddrField(ubyte sector)
|
|
{
|
|
void encode44(ubyte val)
|
|
{
|
|
loadByte((((val >> 1) & 0x55) | 0xAA));
|
|
loadByte(((val & 0x55) | 0xAA));
|
|
}
|
|
|
|
loadBytes(ADDR_PROLOGUE[0..3]);
|
|
|
|
encode44(0xFE); // volume
|
|
encode44(track);
|
|
encode44(sector);
|
|
encode44(0xFE ^ track ^ sector); // check byte
|
|
|
|
loadBytes(EPILOGUE[0..2]);
|
|
}
|
|
|
|
void loadDataField(ubyte sector)
|
|
{
|
|
loadBytes(DATA_PROLOGUE[0..3]);
|
|
loadBytes(loadSector(stream, track, sector));
|
|
loadBytes(EPILOGUE[0..3]);
|
|
}
|
|
|
|
loadByte(0xFF, 48); // Load gap 1
|
|
|
|
for (ubyte sector = 0; sector < NUM_SECTORS; ++sector)
|
|
{
|
|
loadAddrField(sector);
|
|
loadByte(0xFF, 6); // Load gap 2
|
|
loadDataField(sector);
|
|
loadByte(0xFF, 45); // Load gap 3
|
|
}
|
|
}
|
|
|
|
ubyte[] loadSector(File stream, int track, int sector)
|
|
{
|
|
ubyte[] sectorData = new ubyte[SECTOR_LENGTH];
|
|
ubyte[] trackData = new ubyte[DATA_FIELD_LENGTH + 1];
|
|
|
|
stream.seekSet(dataOffset + (track * (NUM_SECTORS * SECTOR_LENGTH)) +
|
|
(isProdos ? (prodosOrder[sector] * SECTOR_LENGTH) :
|
|
(dosOrder[sector] * SECTOR_LENGTH)));
|
|
stream.read(sectorData);
|
|
|
|
int x = 0x55;
|
|
ubyte y = 2;
|
|
uint val;
|
|
|
|
// Translate 256 bytes of data into 342 6-bit index values
|
|
|
|
while(true)
|
|
{
|
|
--y;
|
|
val = sectorData[y];
|
|
|
|
// index values 0 through 85 are composed of a combination
|
|
// of the two least significant bits from each data value
|
|
// index 85 from data 85, 171, 1
|
|
// index 84 from data 84, 170, 0
|
|
// index 83 from data 83, 169, 255
|
|
// ...
|
|
// index 0 from data 0, 86, 172
|
|
|
|
trackData[x] =
|
|
(trackData[x] << 2) |
|
|
(((val & 0x01) << 1) | ((val & 0x02) >> 1));
|
|
|
|
// index values 86 through 341 are composed of the six
|
|
// most significant bits of data values 0 through 255
|
|
|
|
trackData[y + 0x56] = (val >> 2);
|
|
|
|
--x;
|
|
if (x >= 0x00) continue;
|
|
x = 0x55;
|
|
if (y == 0) break;
|
|
}
|
|
|
|
// Translate the 342 index values into 343 disk bytes
|
|
// (where the 343rd disk byte is a check byte)
|
|
|
|
ubyte lastByte = 0, indexByte;
|
|
for (int i = 0; i < DATA_FIELD_LENGTH; ++i)
|
|
{
|
|
indexByte = trackData[i] & 0x3F;
|
|
trackData[i] = diskByte[lastByte ^ indexByte];
|
|
lastByte = indexByte;
|
|
}
|
|
trackData[DATA_FIELD_LENGTH] = diskByte[lastByte];
|
|
|
|
return trackData;
|
|
}
|
|
|
|
void load()
|
|
{
|
|
File stream = new File(imgFile.fileName);
|
|
|
|
checkMacBinary(stream);
|
|
isProdos = (prodosOrderDos(stream) || prodosOrderProdos(stream));
|
|
stream.seekSet(dataOffset);
|
|
|
|
for (int t = 0; t < InternalImage.NUM_TRACKS; ++t)
|
|
loadTrack(stream, t);
|
|
stream.close();
|
|
}
|
|
|
|
bool writeTrack(int track, ubyte[] saveData)
|
|
{
|
|
uint offset;
|
|
ubyte sector;
|
|
int expectedSector = -1;
|
|
bool sectorsWritten[] = new bool[NUM_SECTORS];
|
|
bool sectorsSeen[] = new bool[NUM_SECTORS];
|
|
ubyte sectorData[] = new ubyte[DATA_FIELD_LENGTH];
|
|
|
|
ubyte[] trackData = imgData.trackData[track];
|
|
|
|
ubyte nextData(uint delta)
|
|
{
|
|
return trackData[(offset + delta) % InternalImage.TRACK_LENGTH];
|
|
}
|
|
|
|
bool dataMatches(uint delta, ubyte[] data)
|
|
{
|
|
for (int b = 0; b < data.length; ++b)
|
|
{
|
|
if (nextData(delta + b) != data[b])
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool findAddressField()
|
|
{
|
|
ubyte decode44(ubyte first, ubyte second)
|
|
{
|
|
return ((first & 0x55) << 1) | (second & 0x55);
|
|
}
|
|
|
|
bool testAddressField()
|
|
{
|
|
ubyte check, storedVolume, storedTrack;
|
|
|
|
storedVolume = decode44(nextData(3), nextData(4));
|
|
if (storedVolume != 0xFE) volume = storedVolume;
|
|
|
|
storedTrack = decode44(nextData(5), nextData(6));
|
|
sector = decode44(nextData(7), nextData(8));
|
|
check = decode44(nextData(9), nextData(10));
|
|
|
|
if (expectedSector == -1)
|
|
expectedSector = sector;
|
|
else
|
|
expectedSector = (expectedSector + 1) % NUM_SECTORS;
|
|
|
|
offset += 13;
|
|
|
|
return (storedTrack == track) && (sector < NUM_SECTORS) &&
|
|
(expectedSector == sector) &&
|
|
(check == (storedVolume ^ storedTrack ^ sector)) &&
|
|
(!sectorsSeen[sector]);
|
|
}
|
|
|
|
while (offset < InternalImage.TRACK_LENGTH)
|
|
{
|
|
if (dataMatches(0, ADDR_PROLOGUE) &&
|
|
dataMatches(11, EPILOGUE[0..2]))
|
|
return testAddressField();
|
|
else
|
|
++offset;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool findDataField()
|
|
{
|
|
offset %= InternalImage.TRACK_LENGTH;
|
|
while (offset < InternalImage.TRACK_LENGTH)
|
|
{
|
|
if (dataMatches(0, DATA_PROLOGUE) &&
|
|
dataMatches(346, EPILOGUE))
|
|
{
|
|
offset += 3;
|
|
sectorsSeen[sector] = true;
|
|
return true;
|
|
}
|
|
else
|
|
++offset;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void writeDataBlock()
|
|
{
|
|
uint sectorFirst = (InternalImage.TRACK_LENGTH - offset);
|
|
if (sectorFirst > DATA_FIELD_LENGTH)
|
|
sectorFirst = DATA_FIELD_LENGTH;
|
|
uint sectorSecond = DATA_FIELD_LENGTH - sectorFirst;
|
|
|
|
sectorData[0..sectorFirst] = trackData[offset..offset+sectorFirst];
|
|
if (sectorSecond)
|
|
{
|
|
sectorData[sectorFirst..sectorFirst+sectorSecond] =
|
|
trackData[0..sectorSecond];
|
|
}
|
|
|
|
sectorsWritten[sector] =
|
|
writeSector(saveData, sectorData, sector);
|
|
}
|
|
|
|
while (true)
|
|
{
|
|
if (!findAddressField()) break;
|
|
if (!findDataField()) break;
|
|
writeDataBlock();
|
|
}
|
|
|
|
for (int sect = 0; sect < NUM_SECTORS; ++sect)
|
|
{
|
|
if (!sectorsWritten[sect]) return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool writeSector(ubyte[] saveData, ubyte[] sectorData, ubyte sector)
|
|
{
|
|
uint dskOffset = (isProdos ? (prodosOrder[sector] * SECTOR_LENGTH) :
|
|
(dosOrder[sector] * SECTOR_LENGTH));
|
|
ubyte[] dskData = saveData[dskOffset..dskOffset+SECTOR_LENGTH];
|
|
|
|
// Translate the 342 disk bytes into 6-bit index values
|
|
|
|
ubyte[] indexData = new ubyte[DATA_FIELD_LENGTH];
|
|
|
|
int lastByte = 0;
|
|
ubyte nibByte;
|
|
for (int i = 0; i < 0x156; ++i)
|
|
{
|
|
if (sectorData[i] < 0x96) return false;
|
|
nibByte = memByte[sectorData[i] - 0x96];
|
|
if (nibByte == -1) return false;
|
|
indexData[i] = lastByte ^ nibByte;
|
|
lastByte = indexData[i];
|
|
}
|
|
|
|
// TODO: verify the checksum
|
|
// Translate the 342 index values into 256 bytes
|
|
|
|
ubyte y = 0;
|
|
uint x = 0;
|
|
while(true)
|
|
{
|
|
// The lower two bits of each byte are taken from
|
|
// a pair of bits from index values 0 through 85
|
|
|
|
dskData[y] =
|
|
((indexData[x] & 0x01) << 1) | ((indexData[x] & 0x02) >> 1);
|
|
indexData[x] >>= 2;
|
|
|
|
// The upper six bits of bytes 0 through 255 are taken
|
|
// from the lower six bits of index values 86 through 342.
|
|
|
|
dskData[y] |= (indexData[y + 0x56] << 2);
|
|
++y;
|
|
if (y == 0) break;
|
|
++x;
|
|
if (x == 0x56) x = 0;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void writeOut(File stream)
|
|
{
|
|
ubyte[][] saveData;
|
|
saveData = new ubyte[][InternalImage.NUM_TRACKS];
|
|
|
|
for (int t = 0; t < InternalImage.NUM_TRACKS; ++t)
|
|
{
|
|
saveData[t] = new ubyte[NUM_SECTORS * SECTOR_LENGTH];
|
|
if (!(writeTrack(t, saveData[t])))
|
|
throw new DSKException();
|
|
}
|
|
if ((volume != 0xFE) && preserveVolume)
|
|
throw new VolumeException();
|
|
|
|
if (mbHeader.length != 0) stream.write(mbHeader);
|
|
for (int t = 0; t < InternalImage.NUM_TRACKS; ++t)
|
|
stream.write(saveData[t]);
|
|
}
|
|
}
|
|
|
|
class NIBImage : ExternalImage
|
|
{
|
|
this(TwoappleFile checkFile) { super(checkFile); }
|
|
|
|
this(TwoappleFile checkFile, InternalImage data)
|
|
{
|
|
super(checkFile, data);
|
|
}
|
|
|
|
void load()
|
|
{
|
|
File stream = new File(imgFile.fileName);
|
|
for (int t = 0; t < InternalImage.NUM_TRACKS; ++t)
|
|
{
|
|
stream.read(imgData.trackData[t]);
|
|
}
|
|
stream.close();
|
|
}
|
|
|
|
void writeOut(File stream)
|
|
{
|
|
for (int t = 0; t < InternalImage.NUM_TRACKS; ++t)
|
|
{
|
|
stream.write(imgData.trackData[t]);
|
|
}
|
|
}
|
|
}
|
|
|