Savegame AIR Editor

FXP Project (from tox): Dune Savegame Editor.rar

Flash Builder Project 4.0 by me, updated the above pack: Dune Savegame Editor v2.7z

Ready to use with Adobe AIR: DuneSGE_air.zip

Windows installer for it: DuneSGE.zip

From: http://gotoandlearnforum.com/viewtopic.php?f=35&t=29721

Written by tox:

hey folks,

ever wanted to edit the savegames of Dune? Well, I wanted to, but I couldn't find any editor... obviously no-one ever made one because the game uses an unusual compression method for the gamedata including the savegames. After a lot of research I found out how it works and made an editor for it. The classes I used follow. Mind you, even though I did a lot of testing I can't guarantee it's bug-free. Have fun and let me know if you do something with this.

Have fun!

DuneSavegame

Gives access to the savegames of Dune.

package de.webcodesign.dune

{

import flash.events.Event;

/**

* Gives access to the savegames of Dune.

* @author Tox

* @version 1.0

*/

public class DuneSavegame

{

public var sietches:Array2D = new Array2D(0x0B, 0x0C);

public var sietchNames:Array = [];

private var sc:SavegameCompression = new SavegameCompression(); // The compression-algorithm

private var noCompression:Boolean = false; // If set no decompression / compression is used

private var compressedSave:Array = []; // The compressed savegame

private var decompressedSave:Array = []; // The decompresssed savegame

private var saveSequences:Array = SietchSequences.compressed; // sequences used to identify the sietches (compressed, i.e. savegame )

private var exeSequences:Array = SietchSequences.uncompressed; // sequences used to identify the sietches (uncompressed, i.e. exe)

private var offsets:Array2D = new Array2D(0x0B, 0x0C); // offsets of the sietches / fortresses / palaces

private var sietchStartIndex:uint = 0; // beginning offset of the sietch-block

private var sietchEndIndex:uint = 0; // last offset of the sietch-block

public function DuneSavegame() { }

public function sietchExists(region:uint, subregion:uint):Boolean { return (sietches.read(subregion, region) == undefined) ? false : true; }

private function getSietchOffsets():void

{

var offsets1:Array = [], offsets2:Array = [];

var i:uint = 0, j:uint = 0, k:uint = 0, l:uint = saveSequences.length, m:int = 0;

var sietchFound:Boolean = false;

// Find all possible offsets and put them into the sOff Array2D

while (m < l)

{

if (m < l - 1) offsets.write(saveSequences[m][1], saveSequences[m][0], findSequence(decompressedSave, saveSequences[m]))

else offsets.write(saveSequences[m - 1][1] + 1, saveSequences[m - 1][0], findSequence(decompressedSave, saveSequences[m]));

m++;

}

// Test whether the found offset is connected to a sietch

l--;

while (i < l)

{

if (i+1 < 70)

{

offsets1 = offsets.read(saveSequences[i][1],saveSequences[i][0]);

offsets2 = offsets.read(saveSequences[i + 1][1], saveSequences[i + 1][0]);

}

else

{

offsets1 = offsets.read(saveSequences[i-1][1]+1,saveSequences[i-1][0]);

offsets2 = offsets.read(0x07, 0x0C);

}

if ((offsets2[0] - offsets1[0]) > 0x20 || (offsets2[0] - offsets1[0]) < 0x00)

{

for (j = 0; j < offsets2.length; j++)

{

for (k = 0; k < offsets1.length; k++)

{

if ((offsets2[j] - offsets1[k] <= 0x20) && (offsets2[j] - offsets1[k] > 0x00))

{

if (i + 1 < 70)

{

offsets.write(saveSequences[i][1], saveSequences[i][0], new Array([offsets1[k]]));

offsets.write(saveSequences[i + 1][1], saveSequences[i + 1][0], new Array([offsets2[j]]));

}

else

{

offsets.write(saveSequences[i - 1][1] + 1, saveSequences[i - 1][0], new Array([offsets1[k]]));

offsets.write(0x07, 0x0C, new Array([offsets2[j]]));

}

sietchFound = true;

}

if (sietchFound) break;

}

if (sietchFound) break;

}

sietchFound = false;

}

i++;

}

}

public function load(savegame:File):void

{

var sietchList:Array = [];

var offsets1:uint = 0;

var offsets2:uint = 0;

var region:uint = 0;

var subregion:uint = 0;

var i:uint = 0;

var l:uint = saveSequences.length-1;

var sietch:Sietch;

var fs:FileStream = new FileStream();

fs.open(savegame, FileMode.READ);

noCompression = (savegame.type.toLowerCase() == ".exe") ? true : false;

// Decompressing the savegame

decompressedSave = (noCompression) ? copyFileStreamToArray(fs) : sc.decompress(fs);

fs.close();

// Exchange sietch identification sequences if editing noCompression

if (noCompression) saveSequences = exeSequences;

// Finding all offsets for the sietches

getSietchOffsets();

// Reading begin and end offset of the sietch part

sietchStartIndex = offsets.read(0x01, 0x02)[0];

sietchEndIndex = offsets.read(0x07, 0x0C)[0];

// Getting the sietch part from the decompressed savegame

sietchList = decompressedSave.slice(sietchStartIndex, sietchEndIndex);

sietchList.splice(sietchEndIndex - sietchStartIndex);

if (!noCompression) sietchList = sc.removeF7ControlSequence(sietchList);

// Loading all sietches into a Array2D

while (i < l)

{

// Getting the ID of the sietch's region and subregion

if (i < 70)

{

region = saveSequences[i][0];

subregion = saveSequences[i][1];

}

else

{

region = saveSequences[i-1][0]+1;

subregion = saveSequences[i-1][1];

}

// Setting the offset bounds

offsets1 = i * 0x1C;

offsets2 = (i+1) * 0x1C;

// Creating a new sietch

sietch = new Sietch(sietchList.slice(offsets1, offsets2));

// Adding the sietchname to the list

(subregion >= 0x03) ? sietchNames.push({label:sietch.region+"-"+sietch.subregion, data:[region, subregion]}) : sietchNames.push( { label:sietch.subregion, data:[region, subregion] } );

// Adding the sietch to a Array2D

sietches.write(subregion, region, sietch);

i++;

}

}

public function save():void

{

var startPart:Array = []; // Beginning of save to beginning of sietch part

var sietchPart:Array = []; // Sietch part

var endPart:Array = []; // End of sietch part to save end

var savegame:Array = []; // The complete savegame

var fSave:File = new File(); // The savegame file

// Temporary array for the sietches

var sietchesTemp:Array = [];

var i:uint = 0, l:uint = saveSequences.length - 1, m:uint = 0;

startPart = decompressedSave.slice(0, sietchStartIndex);

endPart = decompressedSave.slice(sietchEndIndex);

for (i = 0; i < l; i++)

{

sietchesTemp = loadSietch(saveSequences[i][0], saveSequences[i][1]).toArray();

for (m = 0; m < sietchesTemp.length; m++) sietchPart.push(sietchesTemp[m]);

}

if (!noCompression)

{

startPart = sc.compressArray(startPart);

sietchPart = sc.insertF7ControlSequence(sietchPart);

sietchPart = sc.compressArray(sietchPart);

endPart = sc.compressArray(endPart);

}

for (i = 0; i < startPart.length; i++) savegame.push(startPart[i]);

for (i = 0; i < sietchPart.length; i++) savegame.push(sietchPart[i]);

for (i = 0; i < endPart.length; i++) savegame.push(endPart[i]);

compressedSave = savegame;

fSave.addEventListener

(

Event.SELECT,

function (e:Event):void

{

var fsSave:FileStream = new FileStream();

var fNew:File = e.target as File;

fsSave.open(fNew, FileMode.WRITE);

var i:uint = 0;

while (fsSave.position < compressedSave.length) fsSave.writeByte(compressedSave[i++]);

fsSave.close();

}

);

fSave.browseForSave("Save savegame...");

}

private function loadSietch(region:uint, subregion:uint):Sietch { return sietches.read(subregion, region); }

private function findSequence(source:Array, sequences:Array):Array

{

var b:Array = [];

var l:uint = source.length-sequences.length, c:uint = 0, j:uint = 0, v:uint = 0, i:uint = 0;

while (i < l)

{

c = source[i];

j = source[i + 1];

v = source[i + 2];

if ((c == sequences[0]) && (j == sequences[1]) && (v == sequences[2]))

{

b.push(i);

i += 3;

}

else i++;

}

return b;

}

private function copyFileStreamToArray(fs:FileStream):Array

{

fs.position = 0;

var l:uint = fs.bytesAvailable;

var a:Array = [];

while (fs.position < l) a.push(fs.readUnsignedByte());

return a;

}

}

}

SavegameCompression

Algorithm for compressing / decompressing savegames used by the game Dune.

package de.webcodesign.dune

{

/**

* Algorithm for compressing / decompressing savegames used by the game Dune.

* @author Tox

* @version 1.0

*/

public class SavegameCompression

{

public function SavegameCompression() { }

public function decompress(fs:FileStream):Array

{

fs.position = 0;

var a:Array = [], b:Array = [];

var i:uint = 0, l:uint = fs.bytesAvailable;

while (fs.position < l) a.push(fs.readUnsignedByte());

b = decompressArray(a);

return b;

}

public function compress(fs:FileStream):Array

{

fs.position = 0;

var a:Array = [], b:Array = [];

var i:uint = 0, l:uint = fs.bytesAvailable;

while (fs.position < l) a.push(fs.readUnsignedByte);

b = compressArray(a);

return b;

}

public function decompressArray(a:Array):Array

{

var b:Array = [];

var l:uint = a.length, c:uint = 0, i:uint = 0, j:uint = 0, k:uint = 0, v:uint = 0;

while (i < l)

{

c = a[i];

j = a[i + 1];

v = a[i + 2];

if (isControlSequence(c, j, v))

{

b.push(c);

b.push(j);

b.push(v);

i += 3;

}

else if (isReplaceSequence(c, j, v))

{

k = 0;

while (k < j) { b.push(v); k++; }

i += 3;

}

else

{

b.push(c);

i++;

}

}

return b;

}

public function compressArray(a:Array):Array

{

var b:Array = [];

var l:uint = a.length, c:uint = 0, v:uint = 0, count:uint = 0, i:uint = 0, j:uint = 0;

while (i < l)

{

c = a[i];

j = a[i + 1];

v = a[i + 2];

if (isControlSequence(c, j, v))

{

b.push(c);

b.push(j);

b.push(v);

i += 3;

}

else

{

count = 1;

j = i+1;

while (j < l)

{

v = a[j];

(v == c) ? count++ : j = l;

j++;

}

if (count >= 0x03)

{

b.push(0xF7);

b.push(count);

b.push(c);

i += count;

}

else

{

b.push(c);

i++;

}

}

}

return b;

}

private function isControlSequence(a:uint, b:uint, c:uint):Boolean { return ((a == 0xF7)&&((b == 0x01 && c == a) || (b == 0xFF && c == 0x00))); }

private function isReplaceSequence(a:uint, b:uint, c:uint):Boolean { return (a == 0xF7 && b > 0x02 && b < 0xFF); }

public function removeF7ControlSequence(a:Array):Array

{

var b:Array = [];

var i:uint = 0, l:uint = a.length, c:uint = 0, j:uint = 0, v:uint = 0;

while (i < l)

{

c = a[i];

j = a[i + 1];

v = a[i + 2];

if ((c == 0xF7) && (j == 0x01) && (v == 0xF7))

{

b.push(c);

i += 3;

}

else

{

b.push(c);

i++;

}

}

return b;

}

public function insertF7ControlSequence(a:Array):Array

{

var b:Array = [];

var i:uint = 0, l:uint = a.length;

while (i < l)

{

if (a[i] == 0xF7)

{

b.push(0xF7);

b.push(0x01);

b.push(0xF7);

}

else b.push(a[i]);

i++;

}

return b;

}

}

}

Regions

Use this class to get the names of sietches.

package de.webcodesign.dune

{

/**

* Use this class to get the names of sietches.

* @author Tox

* @version 1.0

*/

public class Regions

{

public static function region(ID:uint):String

{

var res:String = "";

switch (ID)

{

case 0x01: res = "Arrakeen"; break;

case 0x02: res = "Carthag"; break;

case 0x03: res = "Tuono"; break;

case 0x04: res = "Habbanya"; break;

case 0x05: res = "Oxtyn"; break;

case 0x06: res = "Tsympo"; break;

case 0x07: res = "Bledan"; break;

case 0x08: res = "Ergsun"; break;

case 0x09: res = "Haga"; break;

case 0x0A: res = "Cielago"; break;

case 0x0B: res = "Sihaya"; break;

case 0x0C: res = "Celimyn"; break;

}

return res;

}

public static function subregion(ID:uint):String

{

var res:String = "";

switch (ID)

{

case 0x1: res = "Atreides Palace"; break;

case 0x2: res = "Harkonnen Palace"; break;

case 0x3: res = "Tabr"; break;

case 0x4: res = "Timin"; break;

case 0x5: res = "Tuek"; break;

case 0x6: res = "Harg"; break;

case 0x7: res = "Clam"; break;

case 0x8: res = "Tsymyn"; break;

case 0x9: res = "Siet"; break;

case 0xA: res = "Pyons"; break;

case 0xB: res = "Pyort"; break;

}

return res;

}

}

}

Sietch

Represents a sietch. The Bitfield datatype can be found here: viewtopic.php?f=35&t=29714

package de.webcodesign.dune

{

import de.webcodesign.datatypes.Bitfield;

/**

* Represents a sietch.

* @author Tox

* @version 1.0

*/

public class Sietch

{

private var _region:uint = 0;

private var _subregion:uint = 0;

private var _status:Bitfield = new Bitfield(0);

public var desertAroundSietch:uint = 0;

public var mapPositionX:uint = 0;

public var mapPositionY:uint = 0;

public var unknown1:uint = 0;

public var positionX:uint = 0;

public var positionY:uint = 0;

public var type:uint = 0;

public var troop:uint = 0;

public var unknown2:uint = 0;

public var unknown3:uint = 0;

public var unknown4:uint = 0;

public var unknown5:uint = 0;

public var unknown6:uint = 0;

public var connectedArea:uint = 0;

public var amountSpice:uint = 0;

public var unknown7:uint = 0;

public var unknown8:uint = 0;

public var harvesters:uint = 0;

public var ornis:uint = 0;

public var krys:uint = 0;

public var laserguns:uint = 0;

public var weirdingModules:uint = 0;

public var atomics:uint = 0;

public var bulbs:uint = 0;

public var amountWater:uint = 0;

private var _tmpSietch:Array = [];

public function Sietch(sietch:Array)

{

_tmpSietch = sietch;

_region = getByte(0x00);

_subregion = getByte(0x01);

desertAroundSietch = getByte(0x02);

mapPositionX = getByte(0x03);

mapPositionY = getByte(0x04);

unknown1 = getByte(0x05);

positionX = getByte(0x06);

positionY = getByte(0x07);

type = getByte(0x08);

troop = getByte(0x09);

_status.bitfield = getByte(0x0A);

unknown2 = getByte(0x0B);

unknown3 = getByte(0x0C);

unknown4 = getByte(0x0D);

unknown5 = getByte(0x0E);

unknown6 = getByte(0x0F);

connectedArea = getByte(0x10);

amountSpice = getByte(0x11);

unknown7 = getByte(0x12);

unknown8 = getByte(0x13);

harvesters = getByte(0x14);

ornis = getByte(0x15);

krys = getByte(0x16);

laserguns = getByte(0x17);

weirdingModules = getByte(0x18);

atomics = getByte(0x19);

bulbs = getByte(0x1A);

amountWater = getByte(0x1B);

}

public function toArray():Array

{

setByte(0x00, _region);

setByte(0x01, _subregion);

setByte(0x02, desertAroundSietch);

setByte(0x03, mapPositionX);

setByte(0x04, mapPositionY);

setByte(0x05, unknown1);

setByte(0x06, positionX);

setByte(0x07, positionY);

setByte(0x08, type);

setByte(0x09, troop);

setByte(0x0A, _status.bitfield);

setByte(0x0B, unknown2);

setByte(0x0C, unknown3);

setByte(0x0D, unknown4);

setByte(0x0E, unknown5);

setByte(0x0F, unknown6);

setByte(0x10, connectedArea);

setByte(0x11, amountSpice);

setByte(0x12, unknown7);

setByte(0x13, unknown8);

setByte(0x14, harvesters);

setByte(0x15, ornis);

setByte(0x15, ornis);

setByte(0x16, krys);

setByte(0x17, laserguns);

setByte(0x18, weirdingModules);

setByte(0x19, atomics);

setByte(0x1A, bulbs);

setByte(0x1B, amountWater);

return _tmpSietch;

}

private function getByte(byte:uint):uint { return _tmpSietch[byte]; }

private function setByte(byte:uint, value:uint):void { _tmpSietch[byte] = value; }

public function get length():uint { return _tmpSietch.length; }

public function get region():String { return Regions.region(_region); }

public function get subregion():String { return Regions.subregion(_subregion); }

public function get hasVegetationBegun():Boolean { return _status.getBit(0); }

public function set hasVegetationBegun(value:Boolean):void { _status.setBit(0, value); }

public function get underAttack():Boolean { return _status.getBit(1); }

public function set underAttack(value:Boolean):void { _status.setBit(1, value); }

public function get sietchInfiltrated():Boolean { return _status.getBit(2); }

public function set sietchInfiltrated(value:Boolean):void { _status.setBit(2, value); }

public function get battleWon():Boolean { return _status.getBit(3); }

public function set battleWon(value:Boolean):void { _status.setBit(3, value); }

public function get fremenFound():Boolean { return _status.getBit(4); }

public function set fremenFound(value:Boolean):void { _status.setBit(4, value); }

public function get hasWindtrap():Boolean { return _status.getBit(5); }

public function set hasWindtrap(value:Boolean):void { _status.setBit(5, value); }

public function get spiceLocated():Boolean { return _status.getBit(6); }

public function set spiceLocated(value:Boolean):void { _status.setBit(6, value); }

public function get isHidden():Boolean { return _status.getBit(7); }

public function set isHidden(value:Boolean):void { _status.setBit(7, value); }

}

}

SietchSequences

Holds the sequences used to indentify sietches.

package de.webcodesign.dune

{

/**

* Holds the sequences used to indentify sietches.

* @author Tox

* @version 1.0

*/

public class SietchSequences

{

// use when opening a savegame

public static var compressed:Array = new Array

(

/* Palaces */

[0x02, 0x01, 0x15], [0x01, 0x02, 0xCF],

/* Arrakeen */

[0x01, 0x03, 0xFE], [0x01, 0x04, 0x38], [0x01, 0x05, 0x96], [0x01, 0x06, 0x37],

[0x01, 0x07, 0xD0], [0x01, 0x08, 0x0A], [0x01, 0x09, 0x89], [0x01, 0x0A, 0x6C],

/* Carthag */

[0x02, 0x03, 0x70], [0x02, 0x04, 0x4E], [0x02, 0x05, 0x55], [0x02, 0x06, 0x02],

[0x02, 0x07, 0x5D],

/* Tuono */

[0x03, 0x03, 0x12], [0x03, 0x04, 0x9A], [0x03, 0x05, 0x2F], [0x03, 0x06, 0x55],

[0x03, 0x07, 0x76], [0x03, 0x0A, 0x7E],

/* Habbanya (Harg & Clam are exchanged in the savegame...)*/

[0x04, 0x03, 0xC7], [0x04, 0x04, 0x47], [0x04, 0x05, 0xB1], [0x04, 0x07, 0xA1],

[0x04, 0x06, 0xD1],

/* Oxtyn */

[0x05, 0x03, 0xF7], [0x05, 0x04, 0x52], [0x05, 0x05, 0xA8], [0x05, 0x06, 0xA0],

[0x05, 0x0A, 0xC1],

/* Tsympo */

[0x06, 0x03, 0xF3], [0x06, 0x04, 0xF0], [0x06, 0x05, 0xC5], [0x06, 0x06, 0x27],

[0x06, 0x07, 0x29], [0x06, 0x08, 0x84], [0x06, 0x09, 0x46], [0x06, 0x0A, 0xFA],

[0x06, 0x0B, 0xCC],

/* Bledan */

[0x07, 0x03, 0x00], [0x07, 0x04, 0xE3], [0x07, 0x05, 0x4F], [0x07, 0x06, 0x6C],

/* Ergsun */

[0x08, 0x03, 0x61], [0x08, 0x04, 0x7F], [0x08, 0x05, 0x74], [0x08, 0x06, 0xC6],

[0x08, 0x07, 0x70], [0x08, 0x08, 0xF1],

/* Haga */

[0x09, 0x03, 0xE2], [0x09, 0x04, 0x32], [0x09, 0x05, 0x00], [0x09, 0x06, 0x18],

[0x09, 0x07, 0xE3], [0x09, 0x08, 0x14], [0x09, 0x09, 0xBE], [0x09, 0x0A, 0x03],

/* Cielago */

[0x0A, 0x03, 0xB5], [0x0A, 0x04, 0x95],

/* Sihaya */

[0x0B, 0x03, 0x4C], [0x0B, 0x04, 0xF3], [0x0B, 0x05, 0xDF], [0x0B, 0x06, 0xA2],

[0x0B, 0x07, 0xDE], [0x0B, 0x0A, 0xF0],

/* Celimyn */

[0x0C, 0x03, 0xB6], [0x0C, 0x04, 0x93], [0x0C, 0x05, 0xF6], [0x0C, 0x06, 0x00],

/* End of block delimiter */

[0xFF, 0xFF, 0x01]

);

// use when opening the game executable

public static var uncompressed:Array = new Array

(

/* Palaces */

[0x02, 0x01, 0x15], [0x01, 0x02, 0x98],

/* Arrakeen */

[0x01, 0x03, 0xFE], [0x01, 0x04, 0x1D], [0x01, 0x05, 0xAE], [0x01, 0x06, 0x37],

[0x01, 0x07, 0xD0], [0x01, 0x08, 0x0C], [0x01, 0x09, 0x89], [0x01, 0x0A, 0x19],

/* Carthag */

[0x02, 0x03, 0x48], [0x02, 0x04, 0x4E], [0x02, 0x05, 0x48], [0x02, 0x06, 0x1E],

[0x02, 0x07, 0x30],

/* Tuono */

[0x03, 0x03, 0xC8], [0x03, 0x04, 0x9A], [0x03, 0x05, 0x2F], [0x03, 0x06, 0x54],

[0x03, 0x07, 0x78], [0x03, 0x0A, 0x78],

/* Habbanya (Harg & Clam are exchanged in the savegame...)*/

[0x04, 0x03, 0xC7], [0x04, 0x04, 0x8D], [0x04, 0x05, 0xB1], [0x04, 0x07, 0xA8],

[0x04, 0x06, 0xD1],

/* Oxtyn */

[0x05, 0x03, 0xF7], [0x05, 0x04, 0x52], [0x05, 0x05, 0xCA], [0x05, 0x06, 0x9E],

[0x05, 0x0A, 0x75],

/* Tsympo */

[0x06, 0x03, 0xF3], [0x06, 0x04, 0xF0], [0x06, 0x05, 0xFB], [0x06, 0x06, 0x27],

[0x06, 0x07, 0x29], [0x06, 0x08, 0x7C], [0x06, 0x09, 0x46], [0x06, 0x0A, 0x2F],

[0x06, 0x0B, 0xCC],

/* Bledan */

[0x07, 0x03, 0x00], [0x07, 0x04, 0xE3], [0x07, 0x05, 0x4F], [0x07, 0x06, 0x6C],

/* Ergsun */

[0x08, 0x03, 0x61], [0x08, 0x04, 0x7F], [0x08, 0x05, 0x74], [0x08, 0x06, 0xC6],

[0x08, 0x07, 0x70], [0x08, 0x08, 0xF1],

/* Haga */

[0x09, 0x03, 0xE2], [0x09, 0x04, 0x32], [0x09, 0x05, 0x00], [0x09, 0x06, 0x18],

[0x09, 0x07, 0xE3], [0x09, 0x08, 0x14], [0x09, 0x09, 0xBE], [0x09, 0x0A, 0xF1],

/* Cielago */

[0x0A, 0x03, 0xB5], [0x0A, 0x04, 0x95],

/* Sihaya */

[0x0B, 0x03, 0x4C], [0x0B, 0x04, 0xF3], [0x0B, 0x05, 0xDF], [0x0B, 0x06, 0xB0],

[0x0B, 0x07, 0xDE], [0x0B, 0x0A, 0xAE],

/* Celimyn */

[0x0C, 0x03, 0xB6], [0x0C, 0x04, 0x93], [0x0C, 0x05, 0xF5], [0x0C, 0x06, 0x00],

/* End of block delimiter */

[0xFF, 0xFF, 0x01]

);

}

}