Location: PHPKode > scripts > ff4aSave > ff4asave/ff4asave.php
<?php
/***************************************************************************************
**	Final Fantasy IV Advance Savegame control class
**	Description:
**		This is a class which allows for the retrieval and modification of savegames from
**		the GBA port of Final Fantasy IV
**
**	Public functions:
**		__construct($filename) -- instantiates the class.  if $filename is nonzero, loads that savegame
**		lastError() -- returns last error tossed by the class instance (if debugging is 0)
**		lastAction() -- returns last action of the class instance (if debugging level is 0 or 1)
**		int setDebugLevel($level) -- sets or returns debug level of class instance.  Omit $level to get.
**		Open($filename) -- load in an FF4A savegame
**		Store($filename) -- saves an FF4A savegame.  Omit $filename to use original name
**		Save($save) -- Selects a save bank from the FF4A savegame.  Can be 0-2.  Quicksave not yet supported.
**			If $save is omitted, returns the currently selected save bank
**		Char($char) -- Selects a character to work on.  Can be 0-12 or any default playable character name.
**		Stats($data) -- Sets or retrieves current status for selected character.  Takes an associative array.
**			Pass with $data omitted to get all data (and to see the keys of the needed array)
**			Does not require all entries to be filled in $data; will overwrite existing entries.
**		Inventory($data) -- Sets or retrieves inventory data.  Covers on-hand stuff, items stored with the
**			Fat Chocobo, and the party's gil.  Call without parameters to get existing inventory, and to 
**			get the structure of the needed array.
**		Item($item) -- Returns the item name for numeric values of $item, and returns the numerical value of 
**			the closest match for textual entries
**		Delete() -- clears the selected save bank.  Once done, you need the game to generate a new save.

*/
class ff4aSave {
	private $data;
	private $error;
	private $action;
	private $debug;
	private $filename;
	private $saveOffset;
	private $charOffset;
	private $nameOffset;
	private static function dataChars() {
		return Array("cecil","kain","rosa","rydia","cid","tellah","edward","yang","palom","porom","edge","fusoya","golbez");
	}
	private static function dataItems() {
		return Array(
			"__Nothing__","Fire Claw","Ice Claw","Thunder Claw","Fairy Claw","Hell Claw","Cat Claw","Rod",
			"Ice Rod","Flame Rod","Thunder Rod","Rod of Change","Fairy Rod","Stardust Rod","Rod of Lilith","Staff",
			"Healing Staff","Mythril Staff","Power Staff","Kinesis Staff","Sage's Staff","Rune Staff","Dark Sword","Shadow Sword",
			"Deathbringer","Sword of Legend","Light Sword","Excalibur","Flame Sword","Ice Brand","Defender","Blood Sword",
			"Ancient Sword","Sleep Blade","Gorgon Blade","Spear","Wind Spear","Fire Lance","Ice Lance","Wyvern Lance",
			"Holy Lance","Blood Lance","Gungnir","Kunai","Ashura","Kotetsu","Kikuichimonji","Mursame",
			"Masamune","Assassin Dagger","Mage Masher","Whip","Chain Whip","Blitz Whip","Fire Whip","Dragon's Whisker",
			"Hand Axe","Dwarf Axe","Ogrekiller","Mythril Knife","Dancing Dagger","Mythril Sword","Knife","Ragnarok",
			"Shuriken","Fuma Shuriken","Boomerang","Full Moon","Dreamer's Harp","Lamia Harp","None","Poison Axe",
			"Rune Axe","Mythril Hammer","Gaia Hammer","Wooden Hammer","Avenger","Bow","Crossbow","Great Bow",
			"Killer Bow","Elven Bow","Yoichi's Bow","Artemis's Bow","Iron Arrow","Holy Arrow","Fire Arrow","Ice Arrow",
			"Thunder Arrow","Dark Arrow","Poison Arrow","Mute Arrow","Angel Arrow","Yoichi Arrow","Medusa Arrow","Artemis Arrow",
			"None","Iron Shield","Dark Shield","Demon Shield","Light Shield","Mythril Shield","Fire Shield","Ice Shield",
			"Diamond Shield","Aegis Shield","Genji Shield","Dragon Shield","Crystal Shield","Iron Helm","Dark Helm","Hades Helm",
			"Demon Helm","Light Helm","Mythril Helm","Diamond Helm","Genji Helm","Dragon Helm","Crystal Helm","Leather Cap",
			"Feathered Cap","Wizard's Hat","Sage's Miter","Gold Hairpin","Ribbon","Twist Headband","Green Beret","Black Cloud",
			"Glass Mask","Iron Armor","Dark Armor","Hades Armor","Demon Armor","Knight's Armor","Mythril Armor","Flame Mail",
			"Ice Armor","Diamond Armor","Genji Armor","Dragon Mail","Crystal Mail","Clothes","Leather Garb","Gaia Gear",
			"Sage's Surplice","Black Robe","Light Robe","White Robe","Power sash","Minerva's Plate","Prisoner's Wear","Bard's Tunic",
			"Kenpogi","Black Belt","Adamant Armor","Black Garb","Iron Gloves","Dark Gloves","Hades Gloves","Demon Gloves",
			"Gauntlets","Mythril Gloves","Diamond Gloves","Giant's Gloves","Genji Gloves","Dragon Gloves","Crystal Gloves","Iron Ring",
			"Ruby Ring","Silver Armlet","Power Armlet","Rune Armlet","Crystal Ring","Diamond Armlet","Protect Ring","Cursed Ring",
			"Bomb Fragment","Bomb Arm","Anarctic Wind","Arctic Wind","Rage of Zeus","Rage of the Gods","Stardust","Kiss of Lilith",
			"Vampire Fang","Bacchus's Cider","Hermes' Shoes","Hourglass","Silver Hourglass","Gold Hourglass","Spider's Silk","Decoy",
			"Red Fang","White Fang","Blue Fang","Light Curtain","Bomb Core","Lunar Curtain","Bell of Silence","Gaia Drum",
			"Crystal","Coeurl's Whisker","Grimoire","Beastiary","Alarm Clock","Unicorn Horn","Potion","Hi-Potion",
			"X-Potion","Ether","Dry Ether","Elixr","Phoenix Down","Gold Needle","Maiden's Kiss","Mallet",
			"Diet Food","Echo Herbs","Eye Drops","Antidote","Cross","Remedy","Siren","Golden Apple",
			"Silver Apple","Soma Drop","Tent","Cottage","Magazine","Emergency Exit","Dwarven Bread","Goblin",
			"Bomb","Cockatrice","Mind Flayer","Gysahl Greens","Membership Pass","Gysahl Whistle","Bomb Ring","Baron Key",
			"Sand Ruby","Earth Crystal","Magma Rock","Luca's Necklace","Twin Harp","Dark Crystal","Rat Tail","Adamantite",
			"Frying Pan","Pink Tail","Dr. Lugae's Key","Dark Matter","Hand of the Gods","Apollo's Harp","Triton's Dagger","Seraphim's Mace",
			"Thor's Hammer","Lightbringer","Flandango","Caliburn","Abel's Lance","Firey Hammer","Dragoon Gloves","Hanzo Gloves",
			"Discipline Armlet","White Ring","Mist Ring","Harmonious Ring","Twin Stars","Grimoire LO","Grimoire LL","Grimoire LA", 
			"Grimoire LS","Grimoire LI","Grimoire LR","Grimoire LT","Grimoire LB","Grimoire LD","Courageous Suit","Red Jacket","Sage's Robe",
			"Lord's Robe","Grand Armor","Funny Mask","Red Cap","Coronet","Cat Hood","Grand Helm","Nirvana","Asura's Rod","Sasuke's Katana",
			"Mutsonokami","Mist Whip","Perseus's Bow","Perseus Arrow","Tiger Fang","Dragon Claw","Loki's Lute","Rising Sun","Assassin Dagger",
			"Gigant Axe","Piggy's Stick","Hero's Shield","Rainbow Robe","White Dress","Chocobo Suit","Tabby Suit","Maximilian",
			"Caesar's Plate","Dragoon Plate","Assassin Vest","Battle Gear","Vishnu Vest","Scrap Metal","Clear Water","Muddy Water","Honey",
			"Firewood","Torch","Doll","Raggedy Doll","Key","Megalixr","Bld-Skd Lance","Requiem Harp"
		);
	}
	private static function dataFF4Sig() {
		return 	"\x83\x74\x83\x40\x83\x43\x83\x69\x83\x8b\x83\x74\x83\x40\x83\x93".
				"\x83\x5e\x83\x57\x81\x5b\x87\x57\x82\x60\x82\x66\x82\x61\x81\x49";
	}
	private static function dataCharMap() {
		return Array(	
			0xa3=>0x41,0xa4=>0x42,0xa5=>0x43,0xa6=>0x44,0xa7=>0x45,0xa8=>0x46,
			0xa9=>0x47,0xaa=>0x48,0xab=>0x49,0xac=>0x4a,0xad=>0x4b,0xae=>0x4c,
			0xaf=>0x4d,0xb0=>0x4e,0xb1=>0x4f,0xb2=>0x50,0xb3=>0x51,0xb4=>0x52,
			0xb5=>0x53,0xb6=>0x54,0xb7=>0x55,0xb8=>0x56,0xb9=>0x57,0xba=>0x58,
			0xbb=>0x59,0xbc=>0x5a,0xbd=>0x61,0xbe=>0x62,0xbf=>0x63,0xc0=>0x64,
			0xc1=>0x65,0xc2=>0x66,0xc3=>0x67,0xc4=>0x68,0xc5=>0x69,0xc6=>0x6a,
			0xc7=>0x6b,0xc8=>0x6c,0xc9=>0x6d,0xca=>0x6e,0xcb=>0x6f,0xcc=>0x70,
			0xcd=>0x71,0xce=>0x72,0xcf=>0x73,0xd0=>0x74,0xd1=>0x75,0xd2=>0x76,
			0xd3=>0x77,0xd4=>0x78,0xd5=>0x79,0xd6=>0x7a,0xd7=>0x30,0xd8=>0x31,
			0xd9=>0x32,0xda=>0x33,0xdb=>0x34,0xdc=>0x35,0xdd=>0x36,0xde=>0x37,
			0xdf=>0x38,0xe0=>0x39,0xe1=>0x21,0xe2=>0x3f,0xe3=>0x2d,0xe4=>0x2b,
			0xe5=>0x2f,0xe6=>0x25,0xe7=>0x2e,0xe8=>0x3a,0xe9=>0xb0,0xea=>0x23,0xff=>0x20
		);

	}
	private function toss($err) { 
		$this->error=$err; 
		if ($this->debug>0) echo $err."\n";
		return false; 
	}
	private function act($action) {
		$this->action=$action; 
		if ($this->debug>1) echo $action."\n";
		return true;
	}
	private function calcSaveOffset($save) {
		if (!$this->checkLoaded()) return false;
		if ($save==3) return $this->toss("Quicksave not yet supported");
		if (strtolower(substr($save,0,1))=="q") return $this->toss("Quicksave not yet supported");//$save=3;
		if (!is_numeric($save)) return $this->toss("Passed non-numeric save number that was not 'quick'\n");
		if (($save<0)||($save>3)) return $this->toss("Passed save number ($save) is out of legal range (0-3)\n");
		$offset=0x40+0x1138*$save;
		$this->act("Calculated save offset: $offset");
		return $offset;
	}
	private function findChar($ch) {
		$chars=$this->dataChars();
		$char=$ch;
		if (!is_numeric($char)) $char=array_search(strtolower($char), $chars);
		if (($char<0)||($char>12)||($char===false)) return $this->toss("Invalid character argument passed: $ch.");
		$this->act("Chose character $char");
		return $char;
	}
	private function calcCharOffset($char) {
		if (!$this->checkLoaded()) return false;
		$char=$this->findChar($char);
		$ofs=0x38+0x48*$char;
		$this->act("Calculated intrasave offset ($ofs) for character $char.");
		return $ofs;
	}
	private function Data($offset, $length,$num=false) {
		$mul=1;
		$sum=0;
		if ($num===false)
			for ($i=$offset; $i<$offset+$length; $i++) {
				$sum+=ord($this->data[$i])*$mul;
				$mul*=256;
			}
		else{
			for ($i=$offset; $i<$offset+$length; $i++) {
				$this->data[$i]=chr(($num/$mul)&($mul*256-1));
				$mul*=256;
			}
			$sum=$num;
		}
		return $sum;
	}
	private function checkLoaded() {
		if (strlen($this->data)!=32768) return $this->toss("Please load a Final Fantasy IV Advance savegame first");
		return true;
	}
	private function getChecksum() {
		if ($this->saveOffset===false) return $this->toss("You must select a savegame first");
		if (!$this->checkloaded()) return false;
		$off=$this->saveOffset;
		$sum=0;
		$len=0x1138;
		$start=0x22;
		if ($off==0x33E8) { $len=0x1138; $start=0x22; }		
		$this->act("Adding data from offset 0x".dechex($off+0x22)."-0x".dechex($off+$len));
		for ($i=$start; $i<$len; $i++) {
			$sum+=ord($this->data[$off+$i]);
		}
		$sum1=$this->Data($off+0x20,2);
		$this->act("Calculated checksum: ".($sum%61129)."\nSaved Checksum: $sum1\nWould-be Checksum: $sum");
		return Array($sum%61129, $sum1, $sum);
	}
	private function fixChecksum() {
		if (!$this->checkloaded()) return false;
		$x=$this->getChecksum();
		$off=$this->saveOffset;
		$this->Data($off+0x20,2,$x[0]);
		return ($x[0]==$x[1]);
	}
	private function Name($name=false) {
		if (!$this->checkLoaded()) return false;
		if ($this->saveOffset===false) $this->toss("You must select a savegame first");
		if ($this->nameOffset===false) $this->toss("You must select a character first");
		$off=$this->saveOffset;
		$choff=$this->nameOffset;
		$this->act("Summed name offset: ".($off+$choff));
		$chars=$this->dataCharMap();
		if ($name===false) {
			$cname="";
			for ($i=0; $i<6; $i++) {
				$out=-1;
				$ch=ord($this->data[$off+$choff+$i]);
				if (!empty($chars[$ch])) $out=chr($chars[$ch]);
				if ($out==-1) $out="[".dechex($ch)."]";
				$cname.=$out;
			}
			$this->act("Retrieved name for selected character.");
			return trim($cname);
		} else {
			$name=str_pad(substr($name,0,6),6);
			for ($i=0; $i<6; $i++) {
				$ch=ord($name[$i]);
				$x=array_search($ch,$chars);
				if ($x!==false) $this->data[$off+$choff+$i]=chr($x);
			}
			$this->act("Changed name for selected character.");
			return $name;
		}
	}
	function __construct($fn=false) {
		if ($fn!==false) $this->Open($fn);
		$this->debugLevel(0);
		return $this;
	}
	public function lasterror() { 
		// Returns the last error tossed by this class instance
		return $this->error; 
	}
	public function lastaction() { 
		// Returns the last action performed by this class instance
		return $this->action; 
	}
	public function debugLevel($level=false) { 
		// Sets or retrieves curent debug level (default: 0)
		// returns current debug level if no argument given
		// 0: no messages
		// 1: Errors only
		// 2: Errors and all actions.  Very chatty.
		if ($level!==false) $this->debug=$level; 
		return $this->debug;
	}
	public function Open($fn) {
		//Loads $fn in and makes checks against whether the file is an ff7 savegame
		if (!file_exists($fn)) return $this->toss("File \"$fn\" does not exist.");
		$f=@fopen($fn,"r");
		if ($f==false) return $this->toss("Could not open \"$fn\". Check permissions.");
		$this->data=@fread($f,32768);
		@fclose($f);
		$this->filename=$fn;
		$this->saveOffset=false;
		$this->charOffset=0;
		return $this->act("Loaded \"$fn\".");
	}
	public function Store($fn=false) {
		//Stores savegame in $fn (or in the file it was loaded as, if argument is omitted)
		if ($fn===false) $fn=$this->filename;
		if (!$this->checkloaded()) return false;
		$f=fopen($fn,"w");
		fwrite($f,$this->data);
		fclose($f);
		return $this->act("Saved ".strlen($this->data)." bytes to \"$fn\".");
	}
	public function Save($save=false) {
		// Selects or retrieves save slot
		// no argument means retrieve
		if (!$this->checkLoaded()) return false;
		if ($save!==false) {
			$this->saveOffset=$this->calcSaveOffset($save);
			if (!$this->saveOffset) return false;
			return $this->act("Selected save number $save");
		}else {
			if (!$this->saveOffset) return false;
			return ($this->saveOffset-0x40)/0x1138;
		}
	}
	public function Char($char=false) {
		// Selects or retrieves character slot
		// no argment means retrieve
		if (!$this->checkLoaded()) return false;
		if ($char!==false) {
			$this->charOffset=$this->calcCharOffset($char);
			if (!$this->charOffset) return false;
			$this->nameOffset=0x398+0x8*$this->findChar($char);
			$this->act("Name offset at {$this->nameOffset}.");
			return $this->act("Selected character $char");
		} else {
			if (!$this->charOffset) return false;
			return ($this->charOffset-0x38)/0x48;
		}
	}
	public function Stats($data=false) {
		// Set or retrieve selected character's stats
		// no argument means retrieve
		// look to code for parameters of associative array.
		if (!$this->checkLoaded()) return false;
		if ($this->saveOffset===false) $this->toss("You must select a savegame first");
		if ($this->nameOffset===false) $this->toss("You must select a character first");
		if ($this->charOffset===false) $this->toss("You must select a character first.  \nActually, if it got this far, there's probably a problem with MY code.");
		$off=$this->saveOffset;
		$choff=$this->charOffset;
		if ($data===false) {
			$stats["Name"]=$this->Name();
			$stats["HP"]=$this->Data($off+$choff+0x00,2);
			$stats["HP max"]=$this->Data($off+$choff+0x02,2);
			$stats["MP"]=$this->Data($off+$choff+0x04,2);
			$stats["MP max"]=$this->Data($off+$choff+0x06,2);
			$stats["Strength"]=$this->Data($off+$choff+0x14,1);
			$stats["Agility"]=$this->Data($off+$choff+0x15,1);
			$stats["Stamina"]=$this->Data($off+$choff+0x16,1);
			$stats["Intellect"]=$this->Data($off+$choff+0x17,1);
			$stats["Spirit"]=$this->Data($off+$choff+0x18,1);
			$stats["XP"]=$this->Data($off+$choff+0x40,4);
			$this->act("Retrieved data for selected character.");
			return $stats;
		} else {
			$stats=$data;
			if (!empty($stats["Name"])) $this->Name($stats["Name"]);
			if (!empty($stats["HP"])) $this->Data($off+$choff+0x00,2,$stats["HP"]);
			if (!empty($stats["HP max"])) $this->Data($off+$choff+0x02,2,$stats["HP max"]);
			if (!empty($stats["MP"])) $this->Data($off+$choff+0x04,2,$stats["MP"]);
			if (!empty($stats["MP max"])) $this->Data($off+$choff+0x06,2,$stats["MP max"]);
			if (!empty($stats["Strength"])) $this->Data($off+$choff+0x14,1,$stats["Strength"]);
			if (!empty($stats["Agility"])) $this->Data($off+$choff+0x15,1,$stats["Agility"]);
			if (!empty($stats["Stamina"])) $this->Data($off+$choff+0x16,1,$stats["Stamina"]);
			if (!empty($stats["Intellect"])) $this->Data($off+$choff+0x17,1,$stats["Intellect"]);
			if (!empty($stats["Spirit"])) $this->Data($off+$choff+0x18,1,$stats["Spirit"]);
			if (!empty($stats["XP"])) $this->Data($off+$choff+0x40,4,$stats["XP"]);
			$this->fixChecksum();
			$this->act("Changed stats for selected character.");
			return $stats;
		}
	}
	public function Inventory($Inv=false) {
		if (!$this->checkLoaded()) return false;
		if ($this->saveOffset===false) $this->toss("You must select a savegame first");
		$off=$this->saveOffset;
		if ($Inv===false) {
			$Inv=Array();
			for ($i=0; $i<0xAE; $i++) {
				$iofs=0x524+0x04*$i;
				$citem=Array(
					"ItemID"=>$this->Data($off+$iofs,2),
					"Count"=>$this->Data($off+$iofs+2,1)
				);
				if ($i<0x30) $Inv["On Hand"][]=$citem;
				else $Inv["Fat Chocobo"][]=$citem;
			}
			$Inv["Gil"]=$this->Data($off+0x8FC,4);
			$this->act("Retrieved inventory for selected save.");
			return $Inv;
		} else {
			$broke=0;
			for ($i=0; $i<0xAE; $i++) {
				$iofs=0x524+0x04*$i;
				if ($i<0x30) {
					if (empty($Inv["On Hand"][$i])) continue;
					$item=$Inv["On Hand"][$i];
					$this->act("Set on-hand item ".$i." to ".$item["Count"]." ".$this->Item($item["ItemID"])."(s) (".$item["ItemID"].")");
				} else {
					if (empty($Inv["Fat Chocobo"][$i-0x30])) continue;
					$item=$Inv["Fat Chocobo"][$i-0x30];
					$this->act("Set fat chocobo item ".($i-0x30)." to ".$item["Count"]." ".$this->Item($item["ItemID"])."(s) (".$item["ItemID"].")");
				}
				$this->Data($off+$iofs,2,$item["ItemID"]);
				$this->Data($off+$iofs+2,1,$item["Count"]);
			}
			if (!empty($Inv["Gil"])) {
				$this->Data($off+0x8FC,4,$Inv["Gil"]);
				$this->act("Set Gil to {$Inv["Gil"]}");
			}
			$this->fixChecksum();
			$this->act("Modified inventory for selected save.");
		}
	}
	public static function Item($item) {
		// If $item is numeric, will return the item's name
		// Otherwise, will return closest match to passed text
		$items=ff4Save::dataItems();
		if (is_numeric($item)) return $items[$item];
		$x=explode(" ",$item);
		$m="/(";
		for ($i=0; $i<sizeof($x); $i++) {
			if ($i!=0) $m.="|";
			$m.=preg_quote($x[$i]);
		}
		$m.=")/i";
		$high=0; $highi=-1;
		for ($i=0; $i<sizeof($items); $i++) {
			$iiq=strtolower($items[$i]);
			$x=preg_match_all($m,$iiq,$r);
			unset($r);
			if ($x>$high) { $high=$x; $highi=$i; }
		}
		if ($highi!=-1) return $highi;
		return false;		
	}
	public function Delete() {
		// deletes the selected savegame
		if (!$this->checkLoaded()) return false;
		if ($this->saveOffset===false) return $this->toss("You must select a savegame first");
		for ($i=$this->saveOffset; $i<$this->saveOffset+0x1138; $i++) $data[$i]=chr(0);
		$this->act("Deleted selected save");		
		return true;
	}
}

?>
Return current item: ff4aSave